From 64e671ca2a7232ef01a2cef017c3099028aae567 Mon Sep 17 00:00:00 2001 From: Leila Lali Date: Wed, 12 Jul 2017 10:20:47 -0700 Subject: [PATCH] added restore errors to resx (#407) * added restore errors to resx * including the restore error messages in the result * added more error handling and tests --- .../Contracts/RestoreRequest.cs | 3 +- .../DisasterRecoveryService.cs | 9 +- ...aseService.cs => RestoreDatabaseHelper.cs} | 83 ++++++++++++++----- .../RestoreDatabaseTaskDataObject.cs | 2 +- .../Localization/sr.cs | 66 +++++++++++++++ .../Localization/sr.resx | 24 ++++++ .../Localization/sr.strings | 1 + .../Localization/sr.xlf | 30 +++++++ .../TaskServices/SqlTaskManager.cs | 2 +- .../RestoreDatabaseServiceTests.cs | 49 +++++++++-- 10 files changed, 233 insertions(+), 36 deletions(-) rename src/Microsoft.SqlTools.ServiceLayer/DisasterRecovery/RestoreOperation/{RestoreDatabaseService.cs => RestoreDatabaseHelper.cs} (70%) diff --git a/src/Microsoft.SqlTools.ServiceLayer/DisasterRecovery/Contracts/RestoreRequest.cs b/src/Microsoft.SqlTools.ServiceLayer/DisasterRecovery/Contracts/RestoreRequest.cs index 33ce0d9e..a058b95e 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/DisasterRecovery/Contracts/RestoreRequest.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/DisasterRecovery/Contracts/RestoreRequest.cs @@ -24,8 +24,7 @@ namespace Microsoft.SqlTools.ServiceLayer.DisasterRecovery.Contracts public string BackupFilePath { get; set; } /// - /// Database name to restore from (either the back file path or database name can be used for restore operation, - /// If the backup file is set, the database name will be ignored) + /// Target Database name to restore to /// public string DatabaseName { get; set; } diff --git a/src/Microsoft.SqlTools.ServiceLayer/DisasterRecovery/DisasterRecoveryService.cs b/src/Microsoft.SqlTools.ServiceLayer/DisasterRecovery/DisasterRecoveryService.cs index 61f9ab50..8afacc2a 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/DisasterRecovery/DisasterRecoveryService.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/DisasterRecovery/DisasterRecoveryService.cs @@ -128,7 +128,7 @@ namespace Microsoft.SqlTools.ServiceLayer.DisasterRecovery else { response.CanRestore = false; - response.ErrorMessage = "Restore is not supported"; //TOOD: have a better error message + response.ErrorMessage = SR.RestoreNotSupported; } await requestContext.SendResult(response); @@ -157,18 +157,17 @@ namespace Microsoft.SqlTools.ServiceLayer.DisasterRecovery TaskMetadata metadata = new TaskMetadata(); metadata.ServerName = connInfo.ConnectionDetails.ServerName; metadata.DatabaseName = connInfo.ConnectionDetails.DatabaseName; - metadata.Name = SR.Backup_TaskName; + metadata.Name = SR.RestoreTaskName; metadata.IsCancelable = true; metadata.Data = restoreDataObject; - // create restore task and perform SqlTask sqlTask = SqlTaskManager.Instance.CreateAndRun(metadata, this.restoreDatabaseService.RestoreTaskAsync, restoreDatabaseService.CancelTaskAsync); response.TaskId = sqlTask.TaskId.ToString(); } else { - response.ErrorMessage = "Failed to create restore task"; + response.ErrorMessage = SR.RestorePlanFailed; } } catch (Exception ex) @@ -178,7 +177,7 @@ namespace Microsoft.SqlTools.ServiceLayer.DisasterRecovery } else { - response.ErrorMessage = "Restore database is not supported"; //TOOD: have a better error message + response.ErrorMessage = SR.RestoreNotSupported; } await requestContext.SendResult(response); diff --git a/src/Microsoft.SqlTools.ServiceLayer/DisasterRecovery/RestoreOperation/RestoreDatabaseService.cs b/src/Microsoft.SqlTools.ServiceLayer/DisasterRecovery/RestoreOperation/RestoreDatabaseHelper.cs similarity index 70% rename from src/Microsoft.SqlTools.ServiceLayer/DisasterRecovery/RestoreOperation/RestoreDatabaseService.cs rename to src/Microsoft.SqlTools.ServiceLayer/DisasterRecovery/RestoreOperation/RestoreDatabaseHelper.cs index c7f9f329..38da7613 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/DisasterRecovery/RestoreOperation/RestoreDatabaseService.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/DisasterRecovery/RestoreOperation/RestoreDatabaseHelper.cs @@ -39,8 +39,23 @@ namespace Microsoft.SqlTools.ServiceLayer.DisasterRecovery.RestoreOperation TaskResult result = new TaskResult(); try { - ExecuteRestore(restoreDataObject); - result.TaskStatus = SqlTaskStatus.Succeeded; + if (restoreDataObject.IsValid) + { + ExecuteRestore(restoreDataObject); + result.TaskStatus = SqlTaskStatus.Succeeded; + } + else + { + result.TaskStatus = SqlTaskStatus.Failed; + if (restoreDataObject.ActiveException != null) + { + result.ErrorMessage = restoreDataObject.ActiveException.Message; + } + else + { + result.ErrorMessage = SR.RestoreNotSupported; + } + } } catch (Exception ex) { @@ -48,7 +63,11 @@ namespace Microsoft.SqlTools.ServiceLayer.DisasterRecovery.RestoreOperation result.ErrorMessage = ex.Message; if (ex.InnerException != null) { - result.ErrorMessage += System.Environment.NewLine + ex.InnerException.Message; + result.ErrorMessage += Environment.NewLine + ex.InnerException.Message; + } + if (restoreDataObject != null && restoreDataObject.ActiveException != null) + { + result.ErrorMessage += Environment.NewLine + restoreDataObject.ActiveException.Message; } } return result; @@ -114,34 +133,54 @@ namespace Microsoft.SqlTools.ServiceLayer.DisasterRecovery.RestoreOperation { DatabaseName = restoreDataObject.RestoreParams.DatabaseName }; - if (restoreDataObject != null && restoreDataObject.IsValid) + try { - UpdateRestorePlan(restoreDataObject); - if (restoreDataObject != null && restoreDataObject.IsValid) { - response.DatabaseName = restoreDataObject.RestorePlanner.DatabaseName; - response.DbFiles = restoreDataObject.DbFiles.Select(x => x.PhysicalName); - response.CanRestore = CanRestore(restoreDataObject); + UpdateRestorePlan(restoreDataObject); - if (!response.CanRestore) + if (restoreDataObject != null && restoreDataObject.IsValid) { - response.ErrorMessage = "Backup not supported."; - } + response.DatabaseName = restoreDataObject.RestorePlanner.DatabaseName; + response.DbFiles = restoreDataObject.DbFiles.Select(x => x.PhysicalName); + response.CanRestore = CanRestore(restoreDataObject); - response.RelocateFilesNeeded = !restoreDataObject.DbFilesLocationAreValid(); - response.DefaultDataFolder = restoreDataObject.DefaultDataFileFolder; - response.DefaultLogFolder = restoreDataObject.DefaultLogFileFolder; + if (!response.CanRestore) + { + response.ErrorMessage = SR.RestoreNotSupported; + } + + response.RelocateFilesNeeded = !restoreDataObject.DbFilesLocationAreValid(); + response.DefaultDataFolder = restoreDataObject.DefaultDataFileFolder; + response.DefaultLogFolder = restoreDataObject.DefaultLogFileFolder; + } + else + { + if (restoreDataObject.ActiveException != null) + { + response.ErrorMessage = restoreDataObject.ActiveException.Message; + } + else + { + response.ErrorMessage = SR.RestorePlanFailed; + } + response.CanRestore = false; + } } else { - response.ErrorMessage = "Failed to create restore plan"; - response.CanRestore = false; + response.ErrorMessage = SR.RestorePlanFailed; } } - else + catch(Exception ex) { - response.ErrorMessage = "Failed to create restore database plan"; + response.ErrorMessage = ex.Message; + + if (ex.InnerException != null) + { + response.ErrorMessage += Environment.NewLine; + response.ErrorMessage += ex.InnerException.Message; + } } return response; @@ -190,8 +229,6 @@ namespace Microsoft.SqlTools.ServiceLayer.DisasterRecovery.RestoreOperation /// private void UpdateRestorePlan(RestoreDatabaseTaskDataObject restoreDataObject) { - // Server server = new Server(new ServerConnection(connInfo.ConnectionDetails.ServerName)); - //RestoreDatabaseTaskDataObject restoreDataObject = new RestoreDatabaseTaskDataObject(server, requestParam.DatabaseName); if (!string.IsNullOrEmpty(restoreDataObject.RestoreParams.BackupFilePath)) { restoreDataObject.AddFile(restoreDataObject.RestoreParams.BackupFilePath); @@ -215,6 +252,10 @@ namespace Microsoft.SqlTools.ServiceLayer.DisasterRecovery.RestoreOperation { restoreDataObject.RestorePlan.Execute(); } + else + { + throw new InvalidOperationException(SR.RestoreNotSupported); + } } } } diff --git a/src/Microsoft.SqlTools.ServiceLayer/DisasterRecovery/RestoreOperation/RestoreDatabaseTaskDataObject.cs b/src/Microsoft.SqlTools.ServiceLayer/DisasterRecovery/RestoreOperation/RestoreDatabaseTaskDataObject.cs index f1ea2067..9471ce92 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/DisasterRecovery/RestoreOperation/RestoreDatabaseTaskDataObject.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/DisasterRecovery/RestoreOperation/RestoreDatabaseTaskDataObject.cs @@ -43,7 +43,7 @@ namespace Microsoft.SqlTools.ServiceLayer.DisasterRecovery.RestoreOperation { get { - return this.Server != null && this.RestorePlanner != null; + return this.Server != null && this.RestorePlanner != null && ActiveException == null; } } diff --git a/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.cs b/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.cs index 2f5ae684..5a2221ea 100755 --- a/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.cs @@ -3301,6 +3301,54 @@ namespace Microsoft.SqlTools.ServiceLayer } } + public static string ConflictWithNoRecovery + { + get + { + return Keys.GetString(Keys.ConflictWithNoRecovery); + } + } + + public static string InvalidPathForDatabaseFile + { + get + { + return Keys.GetString(Keys.InvalidPathForDatabaseFile); + } + } + + public static string Log + { + get + { + return Keys.GetString(Keys.Log); + } + } + + public static string RestorePlanFailed + { + get + { + return Keys.GetString(Keys.RestorePlanFailed); + } + } + + public static string RestoreNotSupported + { + get + { + return Keys.GetString(Keys.RestoreNotSupported); + } + } + + public static string RestoreTaskName + { + get + { + return Keys.GetString(Keys.RestoreTaskName); + } + } + public static string ConnectionServiceListDbErrorNotConnected(string uri) { return Keys.GetString(Keys.ConnectionServiceListDbErrorNotConnected, uri); @@ -4648,6 +4696,24 @@ namespace Microsoft.SqlTools.ServiceLayer public const string Task_Completed = "Task_Completed"; + public const string ConflictWithNoRecovery = "ConflictWithNoRecovery"; + + + public const string InvalidPathForDatabaseFile = "InvalidPathForDatabaseFile"; + + + public const string Log = "Log"; + + + public const string RestorePlanFailed = "RestorePlanFailed"; + + + public const string RestoreNotSupported = "RestoreNotSupported"; + + + public const string RestoreTaskName = "RestoreTaskName"; + + private Keys() { } diff --git a/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.resx b/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.resx index bfc67676..15c5345c 100755 --- a/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.resx +++ b/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.resx @@ -1823,4 +1823,28 @@ Completed + + Specifying this option when restoring a backup with the NORECOVERY option is not permitted. + + + + Invalid path for database file: '{0}' + + + + Log + + + + Failed to create restore plan + + + + Restore database is not supported + + + + Restore Database + + diff --git a/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.strings b/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.strings index 9fb6f4f9..ba5bf52e 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.strings +++ b/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.strings @@ -820,3 +820,4 @@ InvalidPathForDatabaseFile = Invalid path for database file: '{0}' Log = Log RestorePlanFailed = Failed to create restore plan RestoreNotSupported = Restore database is not supported +RestoreTaskName = Restore Database \ 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 3c6bd12d..066273ab 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.xlf +++ b/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.xlf @@ -2129,6 +2129,36 @@ Parameterization + + Specifying this option when restoring a backup with the NORECOVERY option is not permitted. + Specifying this option when restoring a backup with the NORECOVERY option is not permitted. + + + + Invalid path for database file: '{0}' + Invalid path for database file: '{0}' + + + + Log + Log + + + + Failed to create restore plan + Failed to create restore plan + + + + Restore database is not supported + Restore database is not supported + + + + Restore Database + Restore Database + + \ No newline at end of file diff --git a/src/Microsoft.SqlTools.ServiceLayer/TaskServices/SqlTaskManager.cs b/src/Microsoft.SqlTools.ServiceLayer/TaskServices/SqlTaskManager.cs index 9aae187a..b1d2198d 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/TaskServices/SqlTaskManager.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/TaskServices/SqlTaskManager.cs @@ -118,7 +118,7 @@ namespace Microsoft.SqlTools.ServiceLayer.TaskServices /// public SqlTask CreateAndRun(TaskMetadata taskMetadata, Func> taskToRun, Func> taskToCancel) { - var sqlTask = CreateTask(taskMetadata, taskToRun, null); + var sqlTask = CreateTask(taskMetadata, taskToRun, taskToCancel); sqlTask.Run(); return sqlTask; } diff --git a/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/DisasterRecovery/RestoreDatabaseServiceTests.cs b/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/DisasterRecovery/RestoreDatabaseServiceTests.cs index 074ace74..b9483d4a 100644 --- a/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/DisasterRecovery/RestoreDatabaseServiceTests.cs +++ b/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/DisasterRecovery/RestoreDatabaseServiceTests.cs @@ -53,6 +53,14 @@ namespace Microsoft.SqlTools.ServiceLayer.IntegrationTests.DisasterRecovery var restorePlan = await VerifyRestore(backupFileName, canRestore, true); } + [Fact] + public async void RestoreToAnotherDatabaseShouldExecuteSuccessfullyForFullBackup() + { + string backupFileName = "FullBackup.bak"; + bool canRestore = true; + var restorePlan = await VerifyRestore(backupFileName, canRestore, true, "NewRestoredDatabase"); + } + [Fact] public async void RestorePlanShouldFailForDiffBackup() { @@ -101,7 +109,7 @@ namespace Microsoft.SqlTools.ServiceLayer.IntegrationTests.DisasterRecovery { TestConnectionResult connectionResult = await LiveConnectionHelper.InitLiveConnectionInfoAsync("master", queryTempFile.FilePath); - string filePath = GetBackupFilePath("FullBackup.bak"); + string filePath = GetBackupFilePath("SomeFile.bak"); RestoreParams restoreParams = new RestoreParams { @@ -121,6 +129,31 @@ namespace Microsoft.SqlTools.ServiceLayer.IntegrationTests.DisasterRecovery } } + [Fact] + public async Task RestorePlanRequestShouldReturnErrorMessageGivenInvalidFilePath() + { + using (SelfCleaningTempFile queryTempFile = new SelfCleaningTempFile()) + { + TestConnectionResult connectionResult = await LiveConnectionHelper.InitLiveConnectionInfoAsync("master", queryTempFile.FilePath); + + string filePath = GetBackupFilePath("InvalidFilePath"); + + RestoreParams restoreParams = new RestoreParams + { + BackupFilePath = filePath, + OwnerUri = queryTempFile.FilePath + }; + + await RunAndVerify( + test: (requestContext) => service.HandleRestorePlanRequest(restoreParams, requestContext), + verify: ((result) => + { + Assert.False(string.IsNullOrEmpty(result.ErrorMessage)); + Assert.False(result.CanRestore); + })); + } + } + private async Task DropDatabase(string databaseName) { string dropDatabaseQuery = string.Format(CultureInfo.InvariantCulture, @@ -129,7 +162,7 @@ namespace Microsoft.SqlTools.ServiceLayer.IntegrationTests.DisasterRecovery await TestServiceProvider.Instance.RunQueryAsync(TestServerType.OnPrem, "master", dropDatabaseQuery); } - private async Task VerifyRestore(string backupFileName, bool canRestore, bool execute = false) + private async Task VerifyRestore(string backupFileName, bool canRestore, bool execute = false, string targetDatabase = null) { string filePath = GetBackupFilePath(backupFileName); using (SelfCleaningTempFile queryTempFile = new SelfCleaningTempFile()) @@ -140,7 +173,7 @@ namespace Microsoft.SqlTools.ServiceLayer.IntegrationTests.DisasterRecovery var request = new RestoreParams { BackupFilePath = filePath, - DatabaseName = string.Empty, + DatabaseName = targetDatabase, OwnerUri = queryTempFile.FilePath }; @@ -153,14 +186,18 @@ namespace Microsoft.SqlTools.ServiceLayer.IntegrationTests.DisasterRecovery { Assert.True(response.DbFiles.Any()); Assert.Equal(response.DatabaseName, "BackupTestDb"); + if (string.IsNullOrEmpty(targetDatabase)) + { + targetDatabase = response.DatabaseName; + } if(execute) { - await DropDatabase(response.DatabaseName); + await DropDatabase(targetDatabase); Thread.Sleep(2000); request.RelocateDbFiles = response.RelocateFilesNeeded; service.ExecuteRestore(restoreDataObject); - Assert.True(restoreDataObject.Server.Databases.Contains(response.DatabaseName)); - await DropDatabase(response.DatabaseName); + Assert.True(restoreDataObject.Server.Databases.Contains(targetDatabase)); + await DropDatabase(targetDatabase); } }