diff --git a/src/Microsoft.SqlTools.Hosting/Hosting/Contracts/DmpServerCapabilities.cs b/src/Microsoft.SqlTools.Hosting/Hosting/Contracts/DmpServerCapabilities.cs index 13ec99c391..9ae40cde1a 100644 --- a/src/Microsoft.SqlTools.Hosting/Hosting/Contracts/DmpServerCapabilities.cs +++ b/src/Microsoft.SqlTools.Hosting/Hosting/Contracts/DmpServerCapabilities.cs @@ -3,6 +3,8 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. // +using Microsoft.SqlTools.Hosting.Hosting.Contracts; + namespace Microsoft.SqlTools.Hosting.Contracts { /// @@ -19,5 +21,10 @@ public class DmpServerCapabilities public ConnectionProviderOptions ConnectionProvider { get; set; } public AdminServicesProviderOptions AdminServicesProvider { get; set; } + + /// + /// List of features + /// + public FeatureMetadataProvider[] Features { get; set; } } } diff --git a/src/Microsoft.SqlTools.Hosting/Hosting/Contracts/FeatureMetadataProvider.cs b/src/Microsoft.SqlTools.Hosting/Hosting/Contracts/FeatureMetadataProvider.cs new file mode 100644 index 0000000000..d0dc0a8a90 --- /dev/null +++ b/src/Microsoft.SqlTools.Hosting/Hosting/Contracts/FeatureMetadataProvider.cs @@ -0,0 +1,31 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using Microsoft.SqlTools.Hosting.Contracts; + +namespace Microsoft.SqlTools.Hosting.Hosting.Contracts +{ + /// + /// Includes the metadata for a feature + /// + public class FeatureMetadataProvider + { + /// + /// Indicates whether the feature is enabled + /// + public bool Enabled { get; set; } + + /// + /// Feature name + /// + public string FeatureName { get; set; } + + /// + /// The options metadata avaialble for this feature + /// + public ServiceOption[] OptionsMetadata { get; set; } + + } +} diff --git a/src/Microsoft.SqlTools.ServiceLayer/DisasterRecovery/Contracts/DatabaseFileInfo.cs b/src/Microsoft.SqlTools.ServiceLayer/DisasterRecovery/Contracts/DatabaseFileInfo.cs index 9321655c93..187a8263e8 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/DisasterRecovery/Contracts/DatabaseFileInfo.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/DisasterRecovery/Contracts/DatabaseFileInfo.cs @@ -29,6 +29,7 @@ public DatabaseFileInfo(LocalizedPropertyInfo[] properties) var idProperty = this.Properties.FirstOrDefault(x => x.PropertyName == IdPropertyName); Id = idProperty == null || idProperty.PropertyValue == null ? string.Empty : idProperty.PropertyValue.ToString(); } + IsSelected = true; } /// diff --git a/src/Microsoft.SqlTools.ServiceLayer/DisasterRecovery/Contracts/RestoreRequest.cs b/src/Microsoft.SqlTools.ServiceLayer/DisasterRecovery/Contracts/RestoreRequest.cs index be5f622b92..a328133355 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/DisasterRecovery/Contracts/RestoreRequest.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/DisasterRecovery/Contracts/RestoreRequest.cs @@ -14,15 +14,18 @@ namespace Microsoft.SqlTools.ServiceLayer.DisasterRecovery.Contracts /// public class RestoreParams : GeneralRequestDetails { - public string SessionId + /// + /// Restore session id. The parameter is optional and if passed, an existing plan will be used + /// + internal string SessionId { get { - return GetOptionValue("sessionId"); + return GetOptionValue(RestoreOptionsHelper.SessionId); } set { - SetOptionValue("sessionId", value); + SetOptionValue(RestoreOptionsHelper.SessionId, value); } } @@ -34,75 +37,75 @@ public string SessionId /// /// Comma delimited list of backup files /// - public string BackupFilePaths + internal string BackupFilePaths { get { - return GetOptionValue("backupFilePaths"); + return GetOptionValue(RestoreOptionsHelper.BackupFilePaths); } set { - SetOptionValue("backupFilePaths", value); + SetOptionValue(RestoreOptionsHelper.BackupFilePaths, value); } } /// /// Target Database name to restore to /// - public string TargetDatabaseName + internal string TargetDatabaseName { get { - return GetOptionValue("targetDatabaseName"); + return GetOptionValue(RestoreOptionsHelper.TargetDatabaseName); } set { - SetOptionValue("targetDatabaseName", value); + SetOptionValue(RestoreOptionsHelper.TargetDatabaseName, value); } } /// /// Source Database name to restore from /// - public string SourceDatabaseName + internal string SourceDatabaseName { get { - return GetOptionValue("sourceDatabaseName"); + return GetOptionValue(RestoreOptionsHelper.SourceDatabaseName); } set { - SetOptionValue("sourceDatabaseName", value); + SetOptionValue(RestoreOptionsHelper.SourceDatabaseName, value); } } /// /// If set to true, the db files will be relocated to default data location in the server /// - public bool RelocateDbFiles + internal bool RelocateDbFiles { get { - return GetOptionValue("relocateDbFiles"); + return GetOptionValue(RestoreOptionsHelper.RelocateDbFiles); } set { - SetOptionValue("relocateDbFiles", value); + SetOptionValue(RestoreOptionsHelper.RelocateDbFiles, value); } } /// /// Ids of the backup set to restore /// - public string[] SelectedBackupSets + internal string[] SelectedBackupSets { get { - return GetOptionValue("selectedBackupSets"); + return GetOptionValue(RestoreOptionsHelper.SelectedBackupSets); } set { - SetOptionValue("selectedBackupSets", value); + SetOptionValue(RestoreOptionsHelper.SelectedBackupSets, value); } } } @@ -160,8 +163,15 @@ public class RestoreDatabaseFileInfo /// public class RestorePlanResponse { - public string RestoreSessionId { get; set; } + /// + /// Restore session id, can be used in restore request to use an existing restore plan + /// + public string SessionId { get; set; } + + /// + /// The list of backup sets to restore + /// public DatabaseFileInfo[] BackupSetsToRestore { get; set; } /// @@ -185,30 +195,14 @@ public class RestorePlanResponse public string[] DatabaseNamesFromBackupSets { get; set; } /// - /// Server name - /// - public string ServerName { get; set; } - - /// - /// Database name to restore to - /// - public string DatabaseName { get; set; } - - /// - /// Indicates whether relocating the db files is required - /// because the original file paths are not valid in the target server - /// - public bool RelocateFilesNeeded { get; set; } - - /// - /// Default Data folder path in the target server + /// For testing purpose to verify the target database /// - public string DefaultDataFolder { get; set; } + internal string DatabaseName { get; set; } /// - /// Default log folder path in the target server + /// Plan details /// - public string DefaultLogFolder { get; set; } + public Dictionary PlanDetails { get; set; } } public class RestoreRequest diff --git a/src/Microsoft.SqlTools.ServiceLayer/DisasterRecovery/RestoreOperation/RestoreDatabaseHelper.cs b/src/Microsoft.SqlTools.ServiceLayer/DisasterRecovery/RestoreOperation/RestoreDatabaseHelper.cs index 43e0a40af6..97419b82b2 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/DisasterRecovery/RestoreOperation/RestoreDatabaseHelper.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/DisasterRecovery/RestoreOperation/RestoreDatabaseHelper.cs @@ -24,7 +24,7 @@ namespace Microsoft.SqlTools.ServiceLayer.DisasterRecovery.RestoreOperation /// public class RestoreDatabaseHelper { - + public const string LastBackupTaken = "lastBackupTaken"; private static RestoreDatabaseHelper instance = new RestoreDatabaseHelper(); private ConcurrentDictionary restoreSessions = new ConcurrentDictionary(); @@ -152,7 +152,8 @@ public RestorePlanResponse CreateRestorePlanResponse(RestoreDatabaseTaskDataObje { RestorePlanResponse response = new RestorePlanResponse() { - DatabaseName = restoreDataObject.RestoreParams.TargetDatabaseName + DatabaseName = restoreDataObject.RestoreParams.TargetDatabaseName, + PlanDetails = new System.Collections.Generic.Dictionary() }; try { @@ -162,7 +163,7 @@ public RestorePlanResponse CreateRestorePlanResponse(RestoreDatabaseTaskDataObje if (restoreDataObject != null && restoreDataObject.IsValid) { - response.RestoreSessionId = restoreDataObject.SessionId; + response.SessionId = restoreDataObject.SessionId; response.DatabaseName = restoreDataObject.TargetDatabase; response.DbFiles = restoreDataObject.DbFiles.Select(x => new RestoreDatabaseFileInfo { @@ -178,12 +179,25 @@ public RestorePlanResponse CreateRestorePlanResponse(RestoreDatabaseTaskDataObje response.ErrorMessage = SR.RestoreNotSupported; } + response.PlanDetails.Add(LastBackupTaken, restoreDataObject.GetLastBackupTaken()); + response.BackupSetsToRestore = restoreDataObject.GetBackupSetInfo().Select(x => new DatabaseFileInfo(x.ConvertPropertiesToArray())).ToArray(); var dbNames = restoreDataObject.GetSourceDbNames(); response.DatabaseNamesFromBackupSets = dbNames == null ? new string[] { } : dbNames.ToArray(); - response.RelocateFilesNeeded = !restoreDataObject.DbFilesLocationAreValid(); - response.DefaultDataFolder = restoreDataObject.DefaultDataFileFolder; - response.DefaultLogFolder = restoreDataObject.DefaultLogFileFolder; + + // Adding the default values for some of the options in the plan details + bool isTailLogBackupPossible = restoreDataObject.IsTailLogBackupPossible(restoreDataObject.RestorePlanner.DatabaseName); + // Default backup tail-log. It's true when tail-log backup is possible for the source database + response.PlanDetails.Add(RestoreOptionsHelper.DefaultBackupTailLog, isTailLogBackupPossible); + // Default backup file for tail-log bacup when Tail-Log bachup is set to true + response.PlanDetails.Add(RestoreOptionsHelper.DefaultTailLogBackupFile, + restoreDataObject.Util.GetDefaultTailLogbackupFile(restoreDataObject.RestorePlan.DatabaseName)); + // Default stand by file path for when RESTORE WITH STANDBY is selected + response.PlanDetails.Add(RestoreOptionsHelper.DefaultStandbyFile, restoreDataObject.Util.GetDefaultStandbyFile(restoreDataObject.RestorePlan.DatabaseName)); + // Default Data folder path in the target server + response.PlanDetails.Add(RestoreOptionsHelper.DefaultDataFileFolder, restoreDataObject.DefaultDataFileFolder); + // Default log folder path in the target server + response.PlanDetails.Add(RestoreOptionsHelper.DefaultLogFileFolder, restoreDataObject.DefaultLogFileFolder); } else { @@ -314,12 +328,29 @@ private void UpdateRestorePlan(RestoreDatabaseTaskDataObject restoreDataObject) restoreDataObject.RestorePlanner.DatabaseName = restoreDataObject.RestoreParams.SourceDatabaseName; } restoreDataObject.TargetDatabase = restoreDataObject.RestoreParams.TargetDatabaseName; - //TODO: used for other types of restore - /*bool isTailLogBackupPossible = restoreDataObject.RestorePlanner.IsTailLogBackupPossible(restoreDataObject.RestorePlanner.DatabaseName); - restoreDataObject.RestorePlanner.BackupTailLog = isTailLogBackupPossible; - restoreDataObject.TailLogBackupFile = restoreDataObject.Util.GetDefaultTailLogbackupFile(dbName); - restoreDataObject.RestorePlanner.TailLogBackupFile = restoreDataObject.TailLogBackupFile; - */ + + restoreDataObject.RestoreOptions.KeepReplication = restoreDataObject.RestoreParams.GetOptionValue(RestoreOptionsHelper.KeepReplication); + restoreDataObject.RestoreOptions.ReplaceDatabase = restoreDataObject.RestoreParams.GetOptionValue(RestoreOptionsHelper.ReplaceDatabase); + restoreDataObject.RestoreOptions.SetRestrictedUser = restoreDataObject.RestoreParams.GetOptionValue(RestoreOptionsHelper.SetRestrictedUser); + string recoveryState = restoreDataObject.RestoreParams.GetOptionValue(RestoreOptionsHelper.RecoveryState); + object databaseRecoveryState; + if (Enum.TryParse(typeof(DatabaseRecoveryState), recoveryState, out databaseRecoveryState)) + { + restoreDataObject.RestoreOptions.RecoveryState = (DatabaseRecoveryState)databaseRecoveryState; + } + bool isTailLogBackupPossible = restoreDataObject.IsTailLogBackupPossible(restoreDataObject.RestorePlanner.DatabaseName); + if (isTailLogBackupPossible) + { + restoreDataObject.RestorePlanner.BackupTailLog = restoreDataObject.RestoreParams.GetOptionValue(RestoreOptionsHelper.BackupTailLog); + restoreDataObject.TailLogBackupFile = restoreDataObject.RestoreParams.GetOptionValue(RestoreOptionsHelper.TailLogBackupFile); + restoreDataObject.TailLogWithNoRecovery = restoreDataObject.RestoreParams.GetOptionValue(RestoreOptionsHelper.TailLogWithNoRecovery); + } + else + { + restoreDataObject.RestorePlanner.BackupTailLog = false; + } + + restoreDataObject.CloseExistingConnections = restoreDataObject.RestoreParams.GetOptionValue(RestoreOptionsHelper.CloseExistingConnections); restoreDataObject.UpdateRestorePlan(restoreDataObject.RestoreParams.RelocateDbFiles); } @@ -340,6 +371,8 @@ public void ExecuteRestore(RestoreDatabaseTaskDataObject restoreDataObject, SqlT { restoreDataObject.SqlTask = sqlTask; restoreDataObject.Execute(); + RestoreDatabaseTaskDataObject cachedRestoreDataObject; + this.restoreSessions.TryRemove(restoreDataObject.SessionId, out cachedRestoreDataObject); } catch(Exception ex) { diff --git a/src/Microsoft.SqlTools.ServiceLayer/DisasterRecovery/RestoreOperation/RestoreDatabaseTaskDataObject.cs b/src/Microsoft.SqlTools.ServiceLayer/DisasterRecovery/RestoreOperation/RestoreDatabaseTaskDataObject.cs index d746e89a63..aba3e570ac 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/DisasterRecovery/RestoreOperation/RestoreDatabaseTaskDataObject.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/DisasterRecovery/RestoreOperation/RestoreDatabaseTaskDataObject.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.IO; using System.Linq; using Microsoft.SqlServer.Management.Smo; @@ -20,6 +21,9 @@ public class RestoreDatabaseTaskDataObject { private const char BackupMediaNameSeparator = ','; + private DatabaseRestorePlanner restorePlanner; + private string tailLogBackupFile; + public RestoreDatabaseTaskDataObject(Server server, String databaseName) { PlanUpdateRequired = true; @@ -147,6 +151,35 @@ public void RemoveFilteredBackupSets() } } + /// + /// Returns the last backup taken + /// + /// + public string GetLastBackupTaken() + { + string lastBackup = string.Empty; + int lastIndexSel = 0; //TODO: find the selected backup set + if (this.RestorePlanner.RestoreToLastBackup && + this.RestorePlan.RestoreOperations[lastIndexSel] != null && + this.RestorePlan.RestoreOperations.Count > 0 && + this.RestorePlan.RestoreOperations[lastIndexSel].BackupSet != null) + { + int lastIndex = this.RestorePlan.RestoreOperations.Count - 1; + DateTime backupTime = this.RestorePlan.RestoreOperations[lastIndexSel].BackupSet.BackupStartDate; + string backupTimeStr = backupTime.ToLongDateString() + " " + backupTime.ToLongTimeString(); + lastBackup = (lastIndexSel == lastIndex) ? + string.Format(CultureInfo.CurrentCulture, SR.TheLastBackupTaken, (backupTimeStr)) : backupTimeStr; + } + //TODO: find the selected one + else if (this.RestoreSelected[0] && !this.RestorePlanner.RestoreToLastBackup) + { + lastBackup = this.CurrentRestorePointInTime.Value.ToLongDateString() + + " " + this.CurrentRestorePointInTime.Value.ToLongTimeString(); + } + return lastBackup; + + } + /// /// Executes the restore operations /// @@ -170,9 +203,11 @@ public void Execute() } } + /// + /// Restore Util + /// public RestoreUtil Util { get; set; } - private DatabaseRestorePlanner restorePlanner; /// /// SMO database restore planner used to create a restore plan @@ -182,7 +217,6 @@ public DatabaseRestorePlanner RestorePlanner get { return restorePlanner; } } - private string tailLogBackupFile; public bool PlanUpdateRequired { get; private set; } /// @@ -303,6 +337,40 @@ public string LogFilesFolder } } + /// + /// Determines whether [is tail log backup possible]. + /// + /// + /// true if [is tail log backup possible]; otherwise, false. + /// + internal bool IsTailLogBackupPossible(string databaseName) + { + if (this.Server.Version.Major < 9 || String.IsNullOrEmpty(this.restorePlanner.DatabaseName)) + { + return false; + } + + Database db = this.Server.Databases[databaseName]; + if (db == null) + { + return false; + } + else + { + db.Refresh(); + } + + if (db.Status != DatabaseStatus.Normal && db.Status != DatabaseStatus.Suspect && db.Status != DatabaseStatus.EmergencyMode) + { + return false; + } + if (db.RecoveryModel == RecoveryModel.Full || db.RecoveryModel == RecoveryModel.BulkLogged) + { + return true; + } + return false; + } + /// /// Gets or sets a value indicating whether [prompt before each backup]. /// diff --git a/src/Microsoft.SqlTools.ServiceLayer/DisasterRecovery/RestoreOptionsHelper.cs b/src/Microsoft.SqlTools.ServiceLayer/DisasterRecovery/RestoreOptionsHelper.cs new file mode 100644 index 0000000000..ec133a1302 --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/DisasterRecovery/RestoreOptionsHelper.cs @@ -0,0 +1,186 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using Microsoft.SqlTools.Hosting.Contracts; + +namespace Microsoft.SqlTools.ServiceLayer.DisasterRecovery +{ + public class RestoreOptionsHelper + { + internal const string KeepReplication = "keepReplication"; + internal const string ReplaceDatabase = "replaceDatabase"; + internal const string SetRestrictedUser = "setRestrictedUser"; + internal const string RecoveryState = "eecoveryState"; + internal const string BackupTailLog = "backupTailLog"; + internal const string DefaultBackupTailLog = "defaultBackupTailLog"; + internal const string TailLogBackupFile = "tailLogBackupFile"; + internal const string DefaultTailLogBackupFile = "defaultTailLogBackupFile"; + internal const string TailLogWithNoRecovery = "tailLogWithNoRecovery"; + internal const string CloseExistingConnections = "closeExistingConnections"; + internal const string RelocateDbFiles = "relocateDbFiles"; + internal const string DataFileFolder = "dataFileFolder"; + internal const string DefaultDataFileFolder = "defaultDataFileFolder"; + internal const string LogFileFolder = "logFileFolder"; + internal const string DefaultLogFileFolder = "defaultLogFileFolder"; + internal const string SessionId = "sessionId"; + internal const string BackupFilePaths = "backupFilePaths"; + internal const string TargetDatabaseName = "targetDatabaseName"; + internal const string SourceDatabaseName = "sourceDatabaseName"; + internal const string SelectedBackupSets = "selectedBackupSets"; + internal const string StandbyFile = "standbyFile"; + internal const string DefaultStandbyFile = "defaultStandbyFile"; + + /// + /// Creates the options metadata available for restore operations + /// + /// + public static ServiceOption[] CreateRestoreOptions() + { + ServiceOption[] options = new ServiceOption[] + { + + new ServiceOption + { + Name = RestoreOptionsHelper.KeepReplication, + DisplayName = "Keep Replication", + Description = "Preserve the replication settings (WITH KEEP_REPLICATION)", + ValueType = ServiceOption.ValueTypeBoolean, + IsRequired = false, + GroupName = "Restore options" + }, + new ServiceOption + { + Name = RestoreOptionsHelper.ReplaceDatabase, + DisplayName = "ReplaceDatabase", + Description = "Overwrite the existing database (WITH REPLACE)", + ValueType = ServiceOption.ValueTypeBoolean, + IsRequired = false, + GroupName = "Restore options" + }, + new ServiceOption + { + Name = RestoreOptionsHelper.SetRestrictedUser, + DisplayName = "SetRestrictedUser", + Description = "Restrict access to the restored database (WITH RESTRICTED_USER)", + ValueType = ServiceOption.ValueTypeBoolean, + IsRequired = false, + GroupName = "Restore options" + }, + new ServiceOption + { + Name = RestoreOptionsHelper.RecoveryState, + DisplayName = "Recovery State", + Description = "Recovery State", + ValueType = ServiceOption.ValueTypeCategory, + IsRequired = false, + GroupName = "Restore options", + CategoryValues = new CategoryValue[] + { + new CategoryValue + { + Name = "WithRecovery", + DisplayName = "RESTORE WITH RECOVERTY" + }, + new CategoryValue + { + Name = "WithNoRecovery", + DisplayName = "RESTORE WITH NORECOVERTY" + }, + new CategoryValue + { + Name = "WithStandBy", + DisplayName = "RESTORE WITH STANDBY" + } + } + }, + new ServiceOption + { + Name = RestoreOptionsHelper.StandbyFile, + DisplayName = "Standby file", + Description = "Standby file", + ValueType = ServiceOption.ValueTypeString, + IsRequired = false, + GroupName = "Restore options" + }, + new ServiceOption + { + Name = RestoreOptionsHelper.BackupTailLog, + DisplayName = "Backup Tail Log", + Description = "Take tail-log backup before restore", + ValueType = ServiceOption.ValueTypeBoolean, + IsRequired = false, + DefaultValue = "true", + GroupName = "Tail-Log backup" + }, + new ServiceOption + { + Name = RestoreOptionsHelper.BackupTailLog, + DisplayName = "Backup Tail Log", + Description = "Take tail-log backup before restore", + ValueType = ServiceOption.ValueTypeBoolean, + IsRequired = false, + DefaultValue = "true", + GroupName = "Tail-Log backup" + }, + new ServiceOption + { + Name = RestoreOptionsHelper.TailLogBackupFile, + DisplayName = "Tail Log Backup File", + Description = "Tail Log Backup File", + ValueType = ServiceOption.ValueTypeString, + IsRequired = false, + GroupName = "Tail-Log backup" + }, + new ServiceOption + { + Name = RestoreOptionsHelper.TailLogWithNoRecovery, + DisplayName = "Tail Log With NoRecovery", + Description = "Leave source database in the restoring state(WITH NORECOVERTY)", + ValueType = ServiceOption.ValueTypeBoolean, + IsRequired = false, + GroupName = "Tail-Log backup" + }, + new ServiceOption + { + Name = RestoreOptionsHelper.CloseExistingConnections, + DisplayName = "Close Existing Connections", + Description = "Close existing connections to destination database", + ValueType = ServiceOption.ValueTypeBoolean, + IsRequired = false, + GroupName = "Server connections" + }, + new ServiceOption + { + Name = RestoreOptionsHelper.RelocateDbFiles, + DisplayName = "Relocate all files", + Description = "Relocate all files", + ValueType = ServiceOption.ValueTypeBoolean, + IsRequired = false, + GroupName = "Restore database files as" + }, + new ServiceOption + { + Name = RestoreOptionsHelper.DataFileFolder, + DisplayName = "Data file folder", + Description = "Data file folder", + ValueType = ServiceOption.ValueTypeString, + IsRequired = false, + GroupName = "Restore database files as" + }, + new ServiceOption + { + Name = RestoreOptionsHelper.LogFileFolder, + DisplayName = "Log file folder", + Description = "Log file folder", + ValueType = ServiceOption.ValueTypeString, + IsRequired = false, + GroupName = "Restore database files as" + } + }; + + return options; + } + } +} diff --git a/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.cs b/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.cs index 7491761c97..d5c0e055a4 100755 --- a/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.cs @@ -3477,6 +3477,14 @@ public static string RestoreBackupSetExpiration } } + public static string TheLastBackupTaken + { + get + { + return Keys.GetString(Keys.TheLastBackupTaken); + } + } + public static string ConnectionServiceListDbErrorNotConnected(string uri) { return Keys.GetString(Keys.ConnectionServiceListDbErrorNotConnected, uri); @@ -4890,6 +4898,9 @@ public class Keys public const string RestoreBackupSetExpiration = "RestoreBackupSetExpiration"; + public const string TheLastBackupTaken = "TheLastBackupTaken"; + + private Keys() { } diff --git a/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.resx b/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.resx index 5dcda5caaf..bfd644fa45 100755 --- a/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.resx +++ b/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.resx @@ -1911,4 +1911,8 @@ Expiration + + The last backup taken ({0}) + + diff --git a/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.strings b/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.strings index f9b8ca62ce..08c6e99b04 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.strings +++ b/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.strings @@ -835,4 +835,5 @@ RestoreBackupSetStartDate = Start Date RestoreBackupSetFinishDate = Finish Date RestoreBackupSetSize = Size RestoreBackupSetUserName = User Name -RestoreBackupSetExpiration = Expiration \ No newline at end of file +RestoreBackupSetExpiration = Expiration +TheLastBackupTaken = The last backup taken ({0}) \ No newline at end of file diff --git a/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.xlf b/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.xlf index d1fa9fab34..23be7182c5 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.xlf +++ b/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.xlf @@ -2239,6 +2239,11 @@ Name + + The last backup taken ({0}) + The last backup taken ({0}) + + \ No newline at end of file diff --git a/src/Microsoft.SqlTools.ServiceLayer/ServiceHost.cs b/src/Microsoft.SqlTools.ServiceLayer/ServiceHost.cs index 50f6baa92c..ce52672e72 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/ServiceHost.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/ServiceHost.cs @@ -16,6 +16,7 @@ using Microsoft.SqlTools.Utility; using Microsoft.SqlTools.ServiceLayer.Connection; using Microsoft.SqlTools.ServiceLayer.Admin; +using Microsoft.SqlTools.ServiceLayer.Utility; namespace Microsoft.SqlTools.ServiceLayer.Hosting { @@ -208,7 +209,8 @@ await requestContext.SendResult( ProviderName = ServiceHost.ProviderName, ProviderDisplayName = ServiceHost.ProviderDescription, ConnectionProvider = ConnectionProviderOptionsHelper.BuildConnectionProviderOptions(), - AdminServicesProvider = AdminServicesProviderOptionsHelper.BuildAdminServicesProviderOptions() + AdminServicesProvider = AdminServicesProviderOptionsHelper.BuildAdminServicesProviderOptions(), + Features = FeaturesMetadataProviderHelper.CreateFratureMetadataProviders() } } ); diff --git a/src/Microsoft.SqlTools.ServiceLayer/Utility/FeaturesMetadataProviderHelper.cs b/src/Microsoft.SqlTools.ServiceLayer/Utility/FeaturesMetadataProviderHelper.cs new file mode 100644 index 0000000000..2b2ccf5c15 --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/Utility/FeaturesMetadataProviderHelper.cs @@ -0,0 +1,28 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System.Collections.Generic; +using Microsoft.SqlTools.Hosting.Hosting.Contracts; +using Microsoft.SqlTools.ServiceLayer.DisasterRecovery; + +namespace Microsoft.SqlTools.ServiceLayer.Utility +{ + public class FeaturesMetadataProviderHelper + { + public static FeatureMetadataProvider[] CreateFratureMetadataProviders() + { + List featues = new List(); + + featues.Add(new FeatureMetadataProvider + { + FeatureName = "Restore", + Enabled = true, + OptionsMetadata = RestoreOptionsHelper.CreateRestoreOptions() + }); + + return featues.ToArray(); + } + } +} diff --git a/src/Microsoft.SqlTools.ServiceLayer/Utility/GeneralRequestDetails.cs b/src/Microsoft.SqlTools.ServiceLayer/Utility/GeneralRequestDetails.cs index af8a5bc46c..817117baa1 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/Utility/GeneralRequestDetails.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/Utility/GeneralRequestDetails.cs @@ -17,7 +17,7 @@ public GeneralRequestDetails() Options = new Dictionary(); } - protected T GetOptionValue(string name) + internal T GetOptionValue(string name) { T result = default(T); if (Options != null && Options.ContainsKey(name)) diff --git a/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/DisasterRecovery/RestoreDatabaseServiceTests.cs b/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/DisasterRecovery/RestoreDatabaseServiceTests.cs index 21d903292e..95a73c8b99 100644 --- a/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/DisasterRecovery/RestoreDatabaseServiceTests.cs +++ b/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/DisasterRecovery/RestoreDatabaseServiceTests.cs @@ -8,7 +8,6 @@ using System.Globalization; using System.IO; using System.Linq; -using System.Threading; using System.Threading.Tasks; using Microsoft.SqlServer.Management.Smo; using Microsoft.SqlTools.Extensibility; @@ -58,6 +57,52 @@ public async void RestorePlanShouldCreatedSuccessfullyForFullBackup() await VerifyRestore(fullBackUpDatabase, canRestore); } + [Fact] + public async void RestorePlanShouldCreatedSuccessfullyOnExistingDatabaseGivenReplaceOption() + { + SqlTestDb testDb = null; + try + { + testDb = await SqlTestDb.CreateNewAsync(TestServerType.OnPrem, false, null, null, "RestoreTest"); + //Create a backup from a test db but don't delete the database + await VerifyBackupFileCreated(); + bool canRestore = true; + Dictionary options = new Dictionary(); + options.Add(RestoreOptionsHelper.ReplaceDatabase, true); + + await VerifyRestore(new string[] { fullBackUpDatabase }, canRestore, true, testDb.DatabaseName, null, options); + } + finally + { + if (testDb != null) + { + testDb.Cleanup(); + } + } + } + + [Fact] + public async void RestorePlanShouldFailOnExistingDatabaseNotGivenReplaceOption() + { + SqlTestDb testDb = null; + try + { + testDb = await SqlTestDb.CreateNewAsync(TestServerType.OnPrem, false, null, null, "RestoreTest"); + //Create a backup from a test db but don't delete the database + await VerifyBackupFileCreated(); + bool canRestore = true; + + await VerifyRestore(new string[] { fullBackUpDatabase }, canRestore, false, testDb.DatabaseName, null, null); + } + finally + { + if (testDb != null) + { + testDb.Cleanup(); + } + } + } + [Fact] public async void RestoreShouldCreatedSuccessfullyGivenTwoBackupFiles() { @@ -228,7 +273,13 @@ private async Task VerifyRestore(string backupFileName, boo return await VerifyRestore(new string[] { backupFileName }, canRestore, execute, targetDatabase); } - private async Task VerifyRestore(string[] backupFileNames, bool canRestore, bool execute = false, string targetDatabase = null, string[] selectedBackupSets = null) + private async Task VerifyRestore( + string[] backupFileNames, + bool canRestore, + bool execute = false, + string targetDatabase = null, + string[] selectedBackupSets = null, + Dictionary options = null) { var filePaths = backupFileNames.Select(x => GetBackupFilePath(x)); string backUpFilePath = filePaths.Aggregate((current, next) => current + " ," + next); @@ -247,11 +298,22 @@ private async Task VerifyRestore(string[] backupFileNames, SelectedBackupSets = selectedBackupSets }; + if(options != null) + { + foreach (var item in options) + { + if (!request.Options.ContainsKey(item.Key)) + { + request.Options.Add(item.Key, item.Value); + } + } + } + var restoreDataObject = service.CreateRestoreDatabaseTaskDataObject(request); var response = service.CreateRestorePlanResponse(restoreDataObject); Assert.NotNull(response); - Assert.False(string.IsNullOrWhiteSpace(response.RestoreSessionId)); + Assert.False(string.IsNullOrWhiteSpace(response.SessionId)); Assert.Equal(response.CanRestore, canRestore); if (canRestore) { @@ -261,15 +323,23 @@ private async Task VerifyRestore(string[] backupFileNames, targetDatabase = response.DatabaseName; } Assert.Equal(response.DatabaseName, targetDatabase); + Assert.NotNull(response.PlanDetails); + Assert.True(response.PlanDetails.Any()); + Assert.NotNull(response.PlanDetails[RestoreOptionsHelper.DefaultBackupTailLog]); + Assert.NotNull(response.PlanDetails[RestoreOptionsHelper.DefaultTailLogBackupFile]); + Assert.NotNull(response.PlanDetails[RestoreOptionsHelper.DefaultDataFileFolder]); + Assert.NotNull(response.PlanDetails[RestoreOptionsHelper.DefaultLogFileFolder]); + Assert.NotNull(response.PlanDetails[RestoreOptionsHelper.DefaultStandbyFile]); + Assert.NotNull(response.PlanDetails[RestoreOptionsHelper.DefaultStandbyFile]); if(execute) { - request.SessionId = response.RestoreSessionId; + request.SessionId = response.SessionId; restoreDataObject = service.CreateRestoreDatabaseTaskDataObject(request); - Assert.Equal(response.RestoreSessionId, restoreDataObject.SessionId); - await DropDatabase(targetDatabase); - Thread.Sleep(2000); - request.RelocateDbFiles = response.RelocateFilesNeeded; + Assert.Equal(response.SessionId, restoreDataObject.SessionId); + //await DropDatabase(targetDatabase); + //Thread.Sleep(2000); + request.RelocateDbFiles = !restoreDataObject.DbFilesLocationAreValid(); service.ExecuteRestore(restoreDataObject); Assert.True(restoreDataObject.Server.Databases.Contains(targetDatabase)); if(selectedBackupSets != null)