// // Copyright (c) Microsoft. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. // #nullable disable using System; using Microsoft.Data.SqlClient; using System.Diagnostics; using System.Threading.Tasks; using Microsoft.SqlTools.Hosting.Protocol; using Microsoft.SqlTools.ServiceLayer.Admin; using Microsoft.SqlTools.ServiceLayer.Admin.Contracts; using Microsoft.SqlTools.ServiceLayer.Connection; using Microsoft.SqlTools.ServiceLayer.DisasterRecovery.Contracts; using Microsoft.SqlTools.ServiceLayer.DisasterRecovery.RestoreOperation; using Microsoft.SqlTools.ServiceLayer.FileBrowser; using Microsoft.SqlTools.ServiceLayer.Management; using Microsoft.SqlTools.ServiceLayer.TaskServices; using Microsoft.SqlTools.Utility; namespace Microsoft.SqlTools.ServiceLayer.DisasterRecovery { /// /// Service for Backup and Restore /// public class DisasterRecoveryService { private static readonly Lazy instance = new Lazy(() => new DisasterRecoveryService()); private static ConnectionService connectionService = null; private SqlTaskManager sqlTaskManagerInstance = null; private FileBrowserService fileBrowserService = null; private RestoreDatabaseHelper restoreDatabaseService = new RestoreDatabaseHelper(); /// /// Default, parameterless constructor. /// internal DisasterRecoveryService() { } /// /// Gets the singleton instance object /// public static DisasterRecoveryService Instance { get { return instance.Value; } } /// /// Internal for testing purposes only /// internal static ConnectionService ConnectionServiceInstance { get { connectionService ??= ConnectionService.Instance; return connectionService; } set { connectionService = value; } } private SqlTaskManager SqlTaskManagerInstance { get { sqlTaskManagerInstance ??= SqlTaskManager.Instance; return sqlTaskManagerInstance; } set { sqlTaskManagerInstance = value; } } /// /// Gets or sets the current filebrowser service instance /// internal FileBrowserService FileBrowserServiceInstance { get { fileBrowserService ??= FileBrowserService.Instance; return fileBrowserService; } set { fileBrowserService = value; } } /// /// Initializes the service instance /// public void InitializeService(IProtocolEndpoint serviceHost) { // Get database info serviceHost.SetRequestHandler(BackupConfigInfoRequest.Type, HandleBackupConfigInfoRequest, true); // Create backup serviceHost.SetRequestHandler(BackupRequest.Type, HandleBackupRequest, true); // Create restore task serviceHost.SetRequestHandler(RestoreRequest.Type, HandleRestoreRequest, true); // Create restore plan serviceHost.SetRequestHandler(RestorePlanRequest.Type, HandleRestorePlanRequest, true); // Cancel restore plan serviceHost.SetRequestHandler(CancelRestorePlanRequest.Type, HandleCancelRestorePlanRequest, true); // Create restore config serviceHost.SetRequestHandler(RestoreConfigInfoRequest.Type, HandleRestoreConfigInfoRequest, true); // Register file path validation callbacks FileBrowserServiceInstance.RegisterValidatePathsCallback(FileValidationServiceConstants.Backup, DisasterRecoveryFileValidator.ValidatePaths); FileBrowserServiceInstance.RegisterValidatePathsCallback(FileValidationServiceConstants.Restore, DisasterRecoveryFileValidator.ValidatePaths); } /// /// Handle request to get backup configuration info /// /// /// /// internal async Task HandleBackupConfigInfoRequest( DefaultDatabaseInfoParams optionsParams, RequestContext requestContext) { var response = new BackupConfigInfoResponse(); ConnectionInfo connInfo; DisasterRecoveryService.ConnectionServiceInstance.TryFindConnection( optionsParams.OwnerUri, out connInfo); if (connInfo != null) { using (DatabaseTaskHelper helper = AdminService.CreateDatabaseTaskHelper(connInfo, databaseExists: true)) { using (SqlConnection sqlConn = ConnectionService.OpenSqlConnection(connInfo, "Backup")) { if (sqlConn != null && !connInfo.IsCloud) { BackupConfigInfo backupConfigInfo = this.GetBackupConfigInfo(helper.DataContainer, sqlConn, sqlConn.Database); response.BackupConfigInfo = backupConfigInfo; } } } } await requestContext.SendResult(response); } /// /// Handles a restore request /// internal async Task HandleCancelRestorePlanRequest( RestoreParams restoreParams, RequestContext requestContext) { bool result = false; try { result = this.restoreDatabaseService.CancelRestorePlan(restoreParams); await requestContext.SendResult(result); } catch (Exception ex) { Logger.Write(TraceEventType.Error, "Failed to cancel restore session. error: " + ex.Message); await requestContext.SendResult(result); } } /// /// Handles a restore request /// internal async Task HandleRestorePlanRequest( RestoreParams restoreParams, RequestContext requestContext) { RestorePlanResponse response = new RestorePlanResponse(); try { ConnectionInfo connInfo; bool supported = IsBackupRestoreOperationSupported(restoreParams.OwnerUri, out connInfo); if (restoreParams.OverwriteTargetDatabase) { restoreParams.TargetDatabaseName = restoreParams.SourceDatabaseName; } if (supported && connInfo != null) { RestoreDatabaseTaskDataObject restoreDataObject = this.restoreDatabaseService.CreateRestoreDatabaseTaskDataObject(restoreParams); restoreDataObject.OverwriteTargetDatabase = restoreParams.OverwriteTargetDatabase; response = this.restoreDatabaseService.CreateRestorePlanResponse(restoreDataObject); } else { response.CanRestore = false; response.ErrorMessage = SR.RestoreNotSupported; } await requestContext.SendResult(response); } catch (Exception ex) { response.CanRestore = false; response.ErrorMessage = ex.Message; await requestContext.SendResult(response); } } /// /// Handles a restore config info request /// internal async Task HandleRestoreConfigInfoRequest( RestoreConfigInfoRequestParams restoreConfigInfoParams, RequestContext requestContext) { RestoreConfigInfoResponse response = new RestoreConfigInfoResponse(); try { ConnectionInfo connInfo; bool supported = IsBackupRestoreOperationSupported(restoreConfigInfoParams.OwnerUri, out connInfo); if (supported && connInfo != null) { response = this.restoreDatabaseService.CreateConfigInfoResponse(restoreConfigInfoParams); } else { response.ErrorMessage = SR.RestoreNotSupported; } await requestContext.SendResult(response); } catch (Exception ex) { response.ErrorMessage = ex.Message; await requestContext.SendResult(response); } } /// /// Handles a restore request /// internal async Task HandleRestoreRequest( RestoreParams restoreParams, RequestContext requestContext) { RestoreResponse response = new RestoreResponse(); try { ConnectionInfo connInfo; bool supported = IsBackupRestoreOperationSupported(restoreParams.OwnerUri, out connInfo); if (supported && connInfo != null) { try { RestoreDatabaseTaskDataObject restoreDataObject = this.restoreDatabaseService.CreateRestoreDatabaseTaskDataObject(restoreParams, connInfo); if (restoreDataObject != null) { restoreDataObject.LockedDatabaseManager = ConnectionServiceInstance.LockedDatabaseManager; // create task metadata TaskMetadata metadata = TaskMetadata.Create(restoreParams, SR.RestoreTaskName, restoreDataObject, ConnectionServiceInstance); metadata.DatabaseName = restoreParams.TargetDatabaseName; // create restore task and perform SqlTask sqlTask = SqlTaskManagerInstance.CreateAndRun(metadata); response.TaskId = sqlTask.TaskId.ToString(); } else { response.ErrorMessage = SR.RestorePlanFailed; } } catch (Exception ex) { response.ErrorMessage = ex.Message; } } else { response.ErrorMessage = SR.RestoreNotSupported; } await requestContext.SendResult(response); } catch (Exception ex) { response.Result = false; response.ErrorMessage = ex.Message; await requestContext.SendResult(response); } } /// /// Handles a backup request /// internal async Task HandleBackupRequest( BackupParams backupParams, RequestContext requestContext) { BackupResponse response = new BackupResponse(); ConnectionInfo connInfo; bool supported = IsBackupRestoreOperationSupported(backupParams.OwnerUri, out connInfo); if (supported && connInfo != null) { DatabaseTaskHelper helper = AdminService.CreateDatabaseTaskHelper(connInfo, databaseExists: true); // Open a new connection to use for the backup, which will be closed when the backup task is completed // (or an error occurs) SqlConnection sqlConn = ConnectionService.OpenSqlConnection(connInfo, "Backup"); try { BackupOperation backupOperation = CreateBackupOperation(helper.DataContainer, sqlConn, backupParams.BackupInfo); // create task metadata TaskMetadata metadata = TaskMetadata.Create(backupParams, SR.BackupTaskName, backupOperation, ConnectionServiceInstance); SqlTask sqlTask = SqlTaskManagerInstance.CreateAndRun(metadata); sqlTask.StatusChanged += (object sender, TaskEventArgs e) => { SqlTask sqlTask = e.SqlTask; if (sqlTask != null && sqlTask.IsCompleted) { sqlConn.Dispose(); } }; } catch { // Ensure that the connection is closed if any error occurs while starting up the task sqlConn.Dispose(); throw; } } else { response.Result = false; } await requestContext.SendResult(response); } private bool IsBackupRestoreOperationSupported(string ownerUri, out ConnectionInfo connectionInfo) { SqlConnection sqlConn = null; try { ConnectionInfo connInfo; DisasterRecoveryService.ConnectionServiceInstance.TryFindConnection( ownerUri, out connInfo); if (connInfo != null) { using (sqlConn = ConnectionService.OpenSqlConnection(connInfo, "DisasterRecovery")) { if (sqlConn != null && !connInfo.IsCloud) { connectionInfo = connInfo; return true; } } } } catch { if (sqlConn != null && sqlConn.State == System.Data.ConnectionState.Open) { sqlConn.Close(); } } connectionInfo = null; return false; } private BackupOperation CreateBackupOperation(CDataContainer dataContainer, SqlConnection sqlConnection) { BackupOperation backupOperation = new BackupOperation(); backupOperation.Initialize(dataContainer, sqlConnection); return backupOperation; } internal BackupOperation CreateBackupOperation(CDataContainer dataContainer, SqlConnection sqlConnection, BackupInfo input) { BackupOperation backupOperation = CreateBackupOperation(dataContainer, sqlConnection); backupOperation.SetBackupInput(input); return backupOperation; } internal BackupConfigInfo GetBackupConfigInfo(CDataContainer dataContainer, SqlConnection sqlConnection, string databaseName) { BackupOperation backupOperation = CreateBackupOperation(dataContainer, sqlConnection); return backupOperation.CreateBackupConfigInfo(databaseName); } /// /// For testing purpose only /// internal void PerformBackup(BackupOperation backupOperation) { backupOperation.Execute(TaskExecutionMode.ExecuteAndScript); } /// /// For testing purpose only /// internal void ScriptBackup(BackupOperation backupOperation) { backupOperation.Execute(TaskExecutionMode.Script); } } }