// // 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 System.Collections; using System.Collections.Generic; using System.Data; using Microsoft.Data.SqlClient; using Microsoft.SqlServer.Management.Common; using Microsoft.SqlServer.Management.Sdk.Sfc; using Microsoft.SqlServer.Management.Smo; using Microsoft.SqlTools.ServiceLayer.DisasterRecovery.Contracts; using Microsoft.SqlTools.ServiceLayer.Management; using Microsoft.SqlTools.ServiceLayer.TaskServices; namespace Microsoft.SqlTools.ServiceLayer.DisasterRecovery { /// /// This class implements backup operations /// public class BackupOperation : SmoScriptableTaskOperation, IBackupOperation { private CDataContainer dataContainer; private ServerConnection serverConnection; private CommonUtilities backupRestoreUtil = null; private Backup backup = null; /// /// Constants /// private const int constDeviceTypeFile = 2; private const int constDeviceTypeTape = 5; private const int constDeviceTypeMediaSet = 3; /// /// UI input values /// private BackupInfo backupInfo; private BackupComponent backupComponent; private BackupType backupType; private BackupDeviceType backupDeviceType; private BackupActionType backupActionType = BackupActionType.Database; private bool isBackupIncremental = false; private bool isLocalPrimaryReplica; /// this is used when the backup dialog is launched in the context of a backup device /// The InitialBackupDestination will be loaded in LoadData private string initialBackupDestination = string.Empty; // Helps in populating the properties of an Azure blob given its URI private class BlobProperties { private string containerName; public string ContainerName { get { return this.containerName; } } private string fileName; public string FileName { get { return this.fileName; } } public BlobProperties(Uri blobUri) { // Extracts the container name and the filename from URI of the strict format https://// // The input URI should be well formed (Current used context - URI is read from msdb - well formed) this.containerName = string.Empty; this.fileName = string.Empty; if (blobUri == null) { return; } string[] seg = blobUri.Segments; if (seg.Length >= 2) { this.containerName = seg[1].Replace("/", ""); } if (seg.Length >= 3) { this.fileName = seg[2].Replace("/", ""); } } }; #region ctors /// /// Ctor /// public BackupOperation() { } #endregion /// /// Initialize variables /// /// /// /// public void Initialize(CDataContainer dataContainer, SqlConnection sqlConnection) { this.dataContainer = dataContainer; this.serverConnection = new ServerConnection(sqlConnection); this.backupRestoreUtil = new CommonUtilities(this.dataContainer, this.serverConnection); } /// /// Set backup input properties /// /// public void SetBackupInput(BackupInfo input) { this.backupInfo = input; // convert the types this.backupComponent = (BackupComponent)input.BackupComponent; this.backupType = (BackupType)input.BackupType; this.backupDeviceType = (BackupDeviceType)input.BackupDeviceType; if (this.backupRestoreUtil.IsHADRDatabase(this.backupInfo.DatabaseName)) { this.isLocalPrimaryReplica = this.backupRestoreUtil.IsLocalPrimaryReplica(this.backupInfo.DatabaseName); } } /// /// Return backup configuration data /// /// /// public BackupConfigInfo CreateBackupConfigInfo(string databaseName) { BackupConfigInfo configInfo = new BackupConfigInfo(); configInfo.RecoveryModel = GetRecoveryModel(databaseName); configInfo.DefaultBackupFolder = CommonUtilities.GetDefaultBackupFolder(this.serverConnection); configInfo.BackupEncryptors = GetBackupEncryptors(); return configInfo; } /// /// The error occurred during backup operation /// public override string ErrorMessage { get { return string.Empty; } } public override Server Server { get { return this.dataContainer.Server; } } /// /// Execute backup /// public override void Execute() { // set the operation properties before using them to create backup obejct this.SetBackupProps(); this.backup = new Backup(); this.backup.Database = this.backupInfo.DatabaseName; this.backup.Action = this.backupActionType; this.backup.Incremental = this.isBackupIncremental; try { if (this.backup.Action == BackupActionType.Files) { IDictionaryEnumerator filegroupEnumerator = this.backupInfo.SelectedFileGroup.GetEnumerator(); filegroupEnumerator.Reset(); while (filegroupEnumerator.MoveNext()) { string currentKey = Convert.ToString(filegroupEnumerator.Key, System.Globalization.CultureInfo.InvariantCulture); string currentValue = Convert.ToString(filegroupEnumerator.Value, System.Globalization.CultureInfo.InvariantCulture); if (currentKey.IndexOf(",", StringComparison.Ordinal) < 0) { // is a file group this.backup.DatabaseFileGroups.Add(currentValue); } else { // is a file int idx = currentValue.IndexOf(".", StringComparison.Ordinal); currentValue = currentValue.Substring(idx + 1, currentValue.Length - idx - 1); this.backup.DatabaseFiles.Add(currentValue); } } } this.backup.BackupSetName = this.backupInfo.BackupsetName; for (int i = 0; i < this.backupInfo.BackupPathList.Count; i++) { string destName = Convert.ToString(this.backupInfo.BackupPathList[i], System.Globalization.CultureInfo.InvariantCulture); int deviceType = (int)(this.backupInfo.BackupPathDevices[destName]); int backupDeviceType = GetDeviceType(Convert.ToString(destName, System.Globalization.CultureInfo.InvariantCulture)); switch (deviceType) { case (int)DeviceType.LogicalDevice: if (this.backupDeviceType == BackupDeviceType.Disk && backupDeviceType == constDeviceTypeFile) { this.backup.Devices.AddDevice(destName, DeviceType.LogicalDevice); } break; case (int)DeviceType.File: if (this.backupDeviceType == BackupDeviceType.Disk) { this.backup.Devices.AddDevice(destName, DeviceType.File); } break; case (int)DeviceType.Url: if (this.backupDeviceType == BackupDeviceType.Url) { this.backup.Devices.AddDevice(destName, DeviceType.Url); } break; } } this.backup.CopyOnly = this.backupInfo.IsCopyOnly; this.backup.FormatMedia = this.backupInfo.FormatMedia; this.backup.Initialize = this.backupInfo.Initialize; this.backup.SkipTapeHeader = this.backupInfo.SkipTapeHeader; this.backup.Checksum = this.backupInfo.Checksum; this.backup.ContinueAfterError = this.backupInfo.ContinueAfterError; if (!string.IsNullOrEmpty(this.backupInfo.MediaName)) { this.backup.MediaName = this.backupInfo.MediaName; } if (!string.IsNullOrEmpty(this.backupInfo.MediaDescription)) { this.backup.MediaDescription = this.backupInfo.MediaDescription; } if (this.backupInfo.TailLogBackup && !this.backupRestoreUtil.IsHADRDatabase(this.backupInfo.DatabaseName) && !this.backupRestoreUtil.IsMirroringEnabled(this.backupInfo.DatabaseName)) { this.backup.NoRecovery = true; } if (this.backupInfo.LogTruncation) { this.backup.LogTruncation = BackupTruncateLogType.Truncate; } else { this.backup.LogTruncation = BackupTruncateLogType.NoTruncate; } if (!string.IsNullOrEmpty(this.backupInfo.BackupSetDescription)) { this.backup.BackupSetDescription = this.backupInfo.BackupSetDescription; } if (this.backupInfo.RetainDays >= 0) { this.backup.RetainDays = this.backupInfo.RetainDays; } else { this.backup.ExpirationDate = this.backupInfo.ExpirationDate; } this.backup.CompressionOption = (BackupCompressionOptions)this.backupInfo.CompressionOption; if (!string.IsNullOrEmpty(this.backupInfo.EncryptorName)) { this.backup.EncryptionOption = new BackupEncryptionOptions((BackupEncryptionAlgorithm)this.backupInfo.EncryptionAlgorithm, (BackupEncryptorType)this.backupInfo.EncryptorType, this.backupInfo.EncryptorName); } if (this.dataContainer.Server.ConnectionContext != null) { // Execute backup this.backup.SqlBackup(this.dataContainer.Server); // Verify backup if required if (this.backupInfo.VerifyBackupRequired) { Restore restore = new Restore(); restore.Devices.AddRange(this.backup.Devices); restore.Database = this.backup.Database; string errorMessage = null; restore.SqlVerifyLatest(this.dataContainer.Server, out errorMessage); if (errorMessage != null) { throw new DisasterRecoveryException(errorMessage); } } } } catch(Exception) { throw; } finally { if (this.serverConnection != null) { this.serverConnection.Disconnect(); } if(this.dataContainer != null) { this.dataContainer.Dispose(); } } } /// /// Cancel backup /// public override void Cancel() { if (this.backup != null) { this.backup.Abort(); } } #region Methods for UI logic /// /// Return recovery model of the database /// /// public string GetRecoveryModel(string databaseName) { RecoveryModel recoveryModel = this.backupRestoreUtil.GetRecoveryModel(databaseName); return recoveryModel.ToString(); } /// /// Return the latest backup locations /// /// public List GetLatestBackupLocations(string databaseName) { return this.backupRestoreUtil.GetLatestBackupLocations(databaseName); } /// /// Returns the certificates and asymmetric keys from master for encryption /// public List GetBackupEncryptors() { List encryptors = new List(); if (this.dataContainer.Server.Databases.Contains("master")) { CertificateCollection certificates = this.dataContainer.Server.Databases["master"].Certificates; DateTime currentUtcDateTime = DateTime.UtcNow; foreach (Certificate item in certificates) { if ((item.Name.StartsWith("##", StringComparison.InvariantCulture) && item.Name.EndsWith("##", StringComparison.InvariantCulture)) || DateTime.Compare(item.ExpirationDate, currentUtcDateTime) < 0) { continue; } encryptors.Add(new BackupEncryptor((int)BackupEncryptorType.ServerCertificate, item.Name)); } AsymmetricKeyCollection keys = this.dataContainer.Server.Databases["master"].AsymmetricKeys; foreach (AsymmetricKey item in keys) { if (item.KeyEncryptionAlgorithm == AsymmetricKeyEncryptionAlgorithm.CryptographicProviderDefined) { encryptors.Add(new BackupEncryptor((int)BackupEncryptorType.ServerAsymmetricKey, item.Name)); } } } return encryptors; } #endregion private void SetBackupProps() { switch (this.backupType) { case BackupType.Full: if (this.backupComponent == BackupComponent.Database) { this.backupActionType = BackupActionType.Database; } else if ((this.backupComponent == BackupComponent.Files) && (this.backupInfo.SelectedFileGroup != null) && (this.backupInfo.SelectedFileGroup.Count > 0)) { this.backupActionType = BackupActionType.Files; } this.isBackupIncremental = false; break; case BackupType.Differential: if ((this.backupComponent == BackupComponent.Files) && (this.backupInfo.SelectedFiles != null) && (this.backupInfo.SelectedFiles.Length > 0)) { this.backupActionType = BackupActionType.Files; this.isBackupIncremental = true; } else { this.backupActionType = BackupActionType.Database; this.isBackupIncremental = true; } break; case BackupType.TransactionLog: this.backupActionType = BackupActionType.Log; this.isBackupIncremental = false; break; default: break; } } private int GetDeviceType(string deviceName) { Enumerator enumerator = new Enumerator(); Request request = new Request(); DataSet dataset = new DataSet(); dataset.Locale = System.Globalization.CultureInfo.InvariantCulture; int result = -1; SqlExecutionModes executionMode = this.serverConnection.SqlExecutionModes; this.serverConnection.SqlExecutionModes = SqlExecutionModes.ExecuteSql; try { request.Urn = "Server/BackupDevice[@Name='" + Urn.EscapeString(deviceName) + "']"; request.Fields = new string[1]; request.Fields[0] = "BackupDeviceType"; dataset = enumerator.Process(this.serverConnection, request); if (dataset.Tables[0].Rows.Count > 0) { result = Convert.ToInt16(dataset.Tables[0].Rows[0]["BackupDeviceType"], System.Globalization.CultureInfo.InvariantCulture); } else { result = constDeviceTypeMediaSet; } } finally { this.serverConnection.SqlExecutionModes = executionMode; } return result; } } }