diff --git a/extensions/sql-migration/src/api/azure.ts b/extensions/sql-migration/src/api/azure.ts index aacaef56c5..44196cfe20 100644 --- a/extensions/sql-migration/src/api/azure.ts +++ b/extensions/sql-migration/src/api/azure.ts @@ -11,6 +11,7 @@ import { getSessionIdHeader } from './utils'; import { URL } from 'url'; import { MigrationSourceAuthenticationType, MigrationStateModel, NetworkShare } from '../models/stateMachine'; import { NetworkInterface } from './dataModels/azure/networkInterfaceModel'; +import { EOL } from 'os'; const ARM_MGMT_API_VERSION = '2021-04-01'; const SQL_VM_API_VERSION = '2021-11-01-preview'; @@ -628,6 +629,20 @@ export async function stopMigration(account: azdata.Account, subscription: Subsc } } +export async function retryMigration(account: azdata.Account, subscription: Subscription, migration: DatabaseMigration): Promise { + const api = await getAzureCoreAPI(); + const path = encodeURI(`${migration.id}/retry?api-version=${DMSV2_API_VERSION}`); + const requestBody = { migrationOperationId: migration.properties.migrationOperationId }; + const host = api.getProviderMetadataForAccount(account).settings.armResource?.endpoint; + const response = await api.makeAzureRestRequest(account, subscription, path, azurecore.HttpRequestMethod.POST, requestBody, true, host); + if (response.errors.length > 0) { + const message = response.errors + .map(err => err.message) + .join(', '); + throw new Error(message); + } +} + export async function deleteMigration(account: azdata.Account, subscription: Subscription, migrationId: string): Promise { const api = await getAzureCoreAPI(); const path = encodeURI(`${migrationId}?api-version=${DMSV2_API_VERSION}`); @@ -817,6 +832,27 @@ export function getBlobContainerId(resourceGroupId: string, storageAccountName: return `${resourceGroupId}/providers/Microsoft.Storage/storageAccounts/${storageAccountName}/blobServices/default/containers/${blobContainerName}`; } +export function getMigrationErrors(migration: DatabaseMigration): string { + const errors = []; + + if (migration?.properties) { + errors.push(migration.properties.provisioningError); + errors.push(migration.properties.migrationFailureError?.message); + errors.push(migration.properties.migrationStatusDetails?.fileUploadBlockingErrors ?? []); + errors.push(migration.properties.migrationStatusDetails?.restoreBlockingReason); + errors.push(migration.properties.migrationStatusDetails?.sqlDataCopyErrors); + errors.push(...migration.properties.migrationStatusDetails?.invalidFiles ?? []); + errors.push(migration.properties.migrationStatusWarnings?.completeRestoreErrorMessage); + errors.push(migration.properties.migrationStatusWarnings?.restoreBlockingReason); + errors.push(...migration.properties.migrationStatusDetails?.listOfCopyProgressDetails?.flatMap(cp => cp.errors) ?? []); + } + + // remove undefined and duplicate error entries + return errors + .filter((e, i, arr) => e !== undefined && i === arr.indexOf(e)) + .join(EOL); +} + export interface SqlMigrationServiceProperties { name: string; subscriptionId: string; diff --git a/extensions/sql-migration/src/api/utils.ts b/extensions/sql-migration/src/api/utils.ts index 0b488df038..2713154667 100644 --- a/extensions/sql-migration/src/api/utils.ts +++ b/extensions/sql-migration/src/api/utils.ts @@ -29,6 +29,7 @@ export const MenuCommands = { CancelMigration: 'sqlmigration.cancel.migration', DeleteMigration: 'sqlmigration.delete.migration', RetryMigration: 'sqlmigration.retry.migration', + RestartMigration: 'sqlmigration.restart.migration', StartMigration: 'sqlmigration.start', StartLoginMigration: 'sqlmigration.login.start', IssueReporter: 'workbench.action.openIssueReporter', diff --git a/extensions/sql-migration/src/constants/helper.ts b/extensions/sql-migration/src/constants/helper.ts index 7af3714c49..9ad7ea7fa8 100644 --- a/extensions/sql-migration/src/constants/helper.ts +++ b/extensions/sql-migration/src/constants/helper.ts @@ -264,6 +264,11 @@ export function canDeleteMigration(migration: DatabaseMigration | undefined): bo } export function canRetryMigration(migration: DatabaseMigration | undefined): boolean { + const status = getMigrationStatus(migration); + return status === loc.MigrationState.Retriable; +} + +export function canRestartMigrationWizard(migration: DatabaseMigration | undefined): boolean { const status = getMigrationStatus(migration); return status === loc.MigrationState.Canceled || status === loc.MigrationState.Retriable diff --git a/extensions/sql-migration/src/constants/strings.ts b/extensions/sql-migration/src/constants/strings.ts index fdcb69f676..505ecee698 100644 --- a/extensions/sql-migration/src/constants/strings.ts +++ b/extensions/sql-migration/src/constants/strings.ts @@ -1042,6 +1042,10 @@ export const DETAILS_COPIED = localize('sql.migration.details.copied', "Details export const CANCEL_MIGRATION_CONFIRMATION = localize('sql.cancel.migration.confirmation', "Are you sure you want to cancel this migration?"); export const DELETE_MIGRATION_CONFIRMATION = localize('sql.delete.migration.confirmation', "Are you sure you want to delete this migration?"); +export const RETRY_MIGRATION_TITLE = localize('sql.retry.migration.title', "The migration failed with the following errors:"); +export const RETRY_MIGRATION_SUMMARY = localize('sql.retry.migration.summary', "Please resolve any errors before retrying the migration."); +export const RETRY_MIGRATION_PROMPT = localize('sql.retry.migration.prompt', "Do you want to retry the failed table migrations?"); + export const YES = localize('sql.migration.yes', "Yes"); export const NO = localize('sql.migration.no', "No"); export const NA = localize('sql.migration.na', "N/A"); @@ -1361,6 +1365,11 @@ export const MIGRATION_CANNOT_RETRY = localize('sql.migration.cannot.retry', 'Mi export const RETRY_MIGRATION = localize('sql.migration.retry.migration', "Retry migration"); export const MIGRATION_RETRY_ERROR = localize('sql.migration.retry.migration.error', 'An error occurred while retrying the migration.'); +// Restart Migration +export const MIGRATION_CANNOT_RESTART = localize('sql.migration.cannot.retry', 'Migration cannot be restarted.'); +export const RESTART_MIGRATION_WIZARD = localize('sql.migration.restart.migration.wizard', "Restart migration wizard"); +export const MIGRATION_RESTART_ERROR = localize('sql.migration.retry.migration.error', 'An error occurred while restarting the migration.'); + export const INVALID_OWNER_URI = localize('sql.migration.invalid.owner.uri.error', 'Cannot connect to the database due to invalid OwnerUri (Parameter \'OwnerUri\')'); export const DATABASE_BACKUP_PAGE_LOAD_ERROR = localize('sql.migration.database.backup.load.error', 'An error occurred while accessing database details.'); diff --git a/extensions/sql-migration/src/dashboard/migrationDetailsTab.ts b/extensions/sql-migration/src/dashboard/migrationDetailsTab.ts index 7488619b25..f4265adc9b 100644 --- a/extensions/sql-migration/src/dashboard/migrationDetailsTab.ts +++ b/extensions/sql-migration/src/dashboard/migrationDetailsTab.ts @@ -10,7 +10,7 @@ import * as loc from '../constants/strings'; import { convertByteSizeToReadableUnit, convertIsoTimeToLocalTime, getMigrationStatusImage } from '../api/utils'; import { logError, TelemetryViews } from '../telemetry'; import * as styles from '../constants/styles'; -import { canCancelMigration, canCutoverMigration, canDeleteMigration, canRetryMigration, getMigrationStatusString, getMigrationTargetTypeEnum, isOfflineMigation, isShirMigration } from '../constants/helper'; +import { canCancelMigration, canCutoverMigration, canDeleteMigration, canRestartMigrationWizard, getMigrationStatusString, getMigrationTargetTypeEnum, isOfflineMigation, isShirMigration } from '../constants/helper'; import { AzureResourceKind, DatabaseMigration, getResourceName } from '../api/azure'; import * as utils from '../api/utils'; import * as helper from '../constants/helper'; @@ -291,7 +291,7 @@ export class MigrationDetailsTab extends MigrationDetailsTabBase extends TabBase { protected copyDatabaseMigrationDetails!: azdata.ButtonComponent; protected newSupportRequest!: azdata.ButtonComponent; protected retryButton!: azdata.ButtonComponent; + protected restartButton!: azdata.ButtonComponent; protected summaryTextComponent: azdata.TextComponent[] = []; public abstract create( @@ -241,14 +243,54 @@ export abstract class MigrationDetailsTabBase extends TabBase { this.disposables.push( this.retryButton.onDidClick( + async (e) => { + await this.statusBar.clearError(); + if (canRetryMigration(this.model.migration)) { + const errorMessage = getMigrationErrors(this.model.migration); + await openRetryMigrationDialog( + errorMessage, + async () => { + try { + await retryMigration( + this.serviceContext.azureAccount!, + this.serviceContext.subscription!, + this.model.migration); + await this.refresh(); + } catch (e) { + await this.statusBar.showError( + loc.MIGRATION_RETRY_ERROR, + loc.MIGRATION_RETRY_ERROR, + e.message); + logError(TelemetryViews.MigrationDetailsTab, MenuCommands.RetryMigration, e); + } + }); + } else { + await vscode.window.showInformationMessage(loc.MIGRATION_CANNOT_RETRY); + logError(TelemetryViews.MigrationDetailsTab, MenuCommands.RetryMigration, "cannot retry migration"); + } + } + )); + + this.restartButton = this.view.modelBuilder.button() + .withProps({ + label: loc.RESTART_MIGRATION_WIZARD, + iconPath: IconPathHelper.retry, + enabled: false, + iconHeight: '16px', + iconWidth: '16px', + height: buttonHeight, + }).component(); + + this.disposables.push( + this.restartButton.onDidClick( async (e) => { await this.refresh(); - const retryMigrationDialog = new RetryMigrationDialog( + const restartMigrationDialog = new RestartMigrationDialog( this.context, this.serviceContext, this.model.migration, this.serviceContextChangedEvent); - await retryMigrationDialog.openDialog(); + await restartMigrationDialog.openDialog(); } )); @@ -310,6 +352,7 @@ export abstract class MigrationDetailsTabBase extends TabBase { { component: this.cancelButton }, { component: this.deleteButton }, { component: this.retryButton }, + { component: this.restartButton }, { component: this.copyDatabaseMigrationDetails, toolbarSeparatorAfter: true }, { component: this.newSupportRequest, toolbarSeparatorAfter: true }, { component: this.refreshLoader }, @@ -493,7 +536,7 @@ export abstract class MigrationDetailsTabBase extends TabBase { } protected async showMigrationErrors(migration: DatabaseMigration): Promise { - const errorMessage = this.getMigrationErrors(migration); + const errorMessage = getMigrationErrors(migration); if (errorMessage?.length > 0) { await this.statusBar.showError( loc.MIGRATION_ERROR_DETAILS_TITLE, diff --git a/extensions/sql-migration/src/dashboard/migrationDetailsTableTab.ts b/extensions/sql-migration/src/dashboard/migrationDetailsTableTab.ts index f357afeda2..6a83465bbc 100644 --- a/extensions/sql-migration/src/dashboard/migrationDetailsTableTab.ts +++ b/extensions/sql-migration/src/dashboard/migrationDetailsTableTab.ts @@ -8,7 +8,7 @@ import * as vscode from 'vscode'; import * as loc from '../constants/strings'; import { getMigrationStatusImage, getPipelineStatusImage } from '../api/utils'; import { logError, TelemetryViews } from '../telemetry'; -import { canCancelMigration, canCutoverMigration, canDeleteMigration, canRetryMigration, formatDateTimeString, formatNumber, formatSizeBytes, formatSizeKb, formatTime, getMigrationStatusString, getMigrationTargetTypeEnum, isOfflineMigation, PipelineStatusCodes } from '../constants/helper'; +import { canCancelMigration, canCutoverMigration, canDeleteMigration, canRestartMigrationWizard, canRetryMigration, formatDateTimeString, formatNumber, formatSizeBytes, formatSizeKb, formatTime, getMigrationStatusString, getMigrationTargetTypeEnum, isOfflineMigation, PipelineStatusCodes } from '../constants/helper'; import { CopyProgressDetail, getResourceName } from '../api/azure'; import { InfoFieldSchema, MigrationDetailsTabBase, MigrationTargetTypeName } from './migrationDetailsTabBase'; import { IconPathHelper } from '../constants/iconPathHelper'; @@ -328,6 +328,7 @@ export class MigrationDetailsTableTab extends MigrationDetailsTabBase = {}): Promise { diff --git a/extensions/sql-migration/src/dashboard/migrationsListTab.ts b/extensions/sql-migration/src/dashboard/migrationsListTab.ts index 5f6da55b9e..86603b12dc 100644 --- a/extensions/sql-migration/src/dashboard/migrationsListTab.ts +++ b/extensions/sql-migration/src/dashboard/migrationsListTab.ts @@ -9,8 +9,8 @@ import { IconPathHelper } from '../constants/iconPathHelper'; import { getCurrentMigrations, getSelectedServiceStatus } from '../models/migrationLocalStorage'; import * as loc from '../constants/strings'; import { filterMigrations, getMigrationDuration, getMigrationStatusImage, getMigrationStatusWithErrors, getMigrationTime, MenuCommands } from '../api/utils'; -import { getMigrationTargetType, getMigrationMode, canCancelMigration, canCutoverMigration, canDeleteMigration } from '../constants/helper'; -import { DatabaseMigration, getResourceName } from '../api/azure'; +import { getMigrationTargetType, getMigrationMode, canCancelMigration, canCutoverMigration, canDeleteMigration, canRetryMigration } from '../constants/helper'; +import { DatabaseMigration, getMigrationErrors, getResourceName } from '../api/azure'; import { logError, TelemetryViews } from '../telemetry'; import { SelectMigrationServiceDialog } from '../dialog/selectMigrationService/selectMigrationServiceDialog'; import { AdsMigrationStatus, EmptySettingValue, ServiceContextChangeEvent, TabBase } from './tabBase'; @@ -565,7 +565,7 @@ export class MigrationsListTab extends TabBase { // "Migration status" column case 2: const statusMessage = loc.DATABASE_MIGRATION_STATUS_LABEL(getMigrationStatusWithErrors(migration)); - const errors = this.getMigrationErrors(migration!); + const errors = getMigrationErrors(migration!); this.showDialogMessage( loc.DATABASE_MIGRATION_STATUS_TITLE, @@ -592,8 +592,7 @@ export class MigrationsListTab extends TabBase { menuCommands.push(...[ MenuCommands.ViewDatabase, MenuCommands.ViewTarget, - MenuCommands.ViewService, - MenuCommands.CopyMigration]); + MenuCommands.ViewService]); if (canCancelMigration(migration)) { menuCommands.push(MenuCommands.CancelMigration); @@ -603,6 +602,12 @@ export class MigrationsListTab extends TabBase { menuCommands.push(MenuCommands.DeleteMigration); } + if (canRetryMigration(migration)) { + menuCommands.push(MenuCommands.RetryMigration); + } + + menuCommands.push(MenuCommands.CopyMigration); + return menuCommands; } diff --git a/extensions/sql-migration/src/dashboard/sqlServerDashboard.ts b/extensions/sql-migration/src/dashboard/sqlServerDashboard.ts index 3c8d7e1cd6..8abb384b1e 100644 --- a/extensions/sql-migration/src/dashboard/sqlServerDashboard.ts +++ b/extensions/sql-migration/src/dashboard/sqlServerDashboard.ts @@ -6,16 +6,16 @@ import * as azdata from 'azdata'; import * as vscode from 'vscode'; import { promises as fs } from 'fs'; -import { DatabaseMigration, deleteMigration, getMigrationDetails } from '../api/azure'; +import { DatabaseMigration, deleteMigration, getMigrationDetails, getMigrationErrors, retryMigration } from '../api/azure'; import { MenuCommands, SqlMigrationExtensionId } from '../api/utils'; -import { canCancelMigration, canCutoverMigration, canDeleteMigration, canRetryMigration } from '../constants/helper'; +import { canCancelMigration, canCutoverMigration, canDeleteMigration, canRestartMigrationWizard, canRetryMigration } from '../constants/helper'; import { IconPathHelper } from '../constants/iconPathHelper'; import { MigrationNotebookInfo, NotebookPathHelper } from '../constants/notebookPathHelper'; import * as loc from '../constants/strings'; import { SavedAssessmentDialog } from '../dialog/assessmentResults/savedAssessmentDialog'; import { ConfirmCutoverDialog } from '../dialog/migrationCutover/confirmCutoverDialog'; import { MigrationCutoverDialogModel } from '../dialog/migrationCutover/migrationCutoverDialogModel'; -import { RetryMigrationDialog } from '../dialog/retryMigration/retryMigrationDialog'; +import { RestartMigrationDialog } from '../dialog/restartMigration/restartMigrationDialog'; import { SqlMigrationServiceDetailsDialog } from '../dialog/sqlMigrationService/sqlMigrationServiceDetailsDialog'; import { MigrationLocalStorage } from '../models/migrationLocalStorage'; import { MigrationStateModel, SavedInfo } from '../models/stateMachine'; @@ -28,6 +28,7 @@ import { AdsMigrationStatus, MigrationDetailsEvent, ServiceContextChangeEvent } import { migrationServiceProvider } from '../service/provider'; import { ApiType, SqlMigrationService } from '../service/features'; import { getSourceConnectionId, getSourceConnectionProfile } from '../api/sqlUtils'; +import { openRetryMigrationDialog } from '../dialog/retryMigration/retryMigrationDialog'; export interface MenuCommandArgs { connectionId: string, @@ -354,28 +355,62 @@ export class DashboardWidget { this._context.subscriptions.push( vscode.commands.registerCommand( MenuCommands.RetryMigration, + async (args: MenuCommandArgs) => { + await this.clearError(args.connectionId); + const service = await MigrationLocalStorage.getMigrationServiceContext(); + const migration = await this._getMigrationById(args.migrationId, args.migrationOperationId); + if (service && migration && canRetryMigration(migration)) { + const errorMessage = getMigrationErrors(migration); + await openRetryMigrationDialog( + errorMessage, + async () => { + try { + await retryMigration( + service.azureAccount!, + service.subscription!, + migration); + await this._migrationsTab.refresh(); + } catch (e) { + await this.showError( + args.connectionId, + loc.MIGRATION_RETRY_ERROR, + loc.MIGRATION_RETRY_ERROR, + e.message); + logError(TelemetryViews.MigrationsTab, MenuCommands.RetryMigration, e); + } + }); + } + else { + await vscode.window.showInformationMessage(loc.MIGRATION_CANNOT_RETRY); + logError(TelemetryViews.MigrationsTab, MenuCommands.RetryMigration, "cannot retry migration"); + } + })); + + this._context.subscriptions.push( + vscode.commands.registerCommand( + MenuCommands.RestartMigration, async (args: MenuCommandArgs) => { try { await this.clearError(args.connectionId); const migration = await this._getMigrationById(args.migrationId, args.migrationOperationId); - if (migration && canRetryMigration(migration)) { - const retryMigrationDialog = new RetryMigrationDialog( + if (migration && canRestartMigrationWizard(migration)) { + const restartMigrationDialog = new RestartMigrationDialog( this._context, await MigrationLocalStorage.getMigrationServiceContext(), migration, this._onServiceContextChanged); - await retryMigrationDialog.openDialog(); + await restartMigrationDialog.openDialog(); } else { - await vscode.window.showInformationMessage(loc.MIGRATION_CANNOT_RETRY); + await vscode.window.showInformationMessage(loc.MIGRATION_CANNOT_RESTART); } } catch (e) { await this.showError( args.connectionId, - loc.MIGRATION_RETRY_ERROR, - loc.MIGRATION_RETRY_ERROR, + loc.MIGRATION_RESTART_ERROR, + loc.MIGRATION_RESTART_ERROR, e.message); - logError(TelemetryViews.MigrationsTab, MenuCommands.RetryMigration, e); + logError(TelemetryViews.MigrationsTab, MenuCommands.RestartMigration, e); } })); diff --git a/extensions/sql-migration/src/dashboard/tabBase.ts b/extensions/sql-migration/src/dashboard/tabBase.ts index b0082179bf..4d69b1651e 100644 --- a/extensions/sql-migration/src/dashboard/tabBase.ts +++ b/extensions/sql-migration/src/dashboard/tabBase.ts @@ -7,8 +7,6 @@ import * as azdata from 'azdata'; import * as vscode from 'vscode'; import * as loc from '../constants/strings'; import { IconPathHelper } from '../constants/iconPathHelper'; -import { EOL } from 'os'; -import { DatabaseMigration } from '../api/azure'; import { getSelectedServiceStatus } from '../models/migrationLocalStorage'; import { MenuCommands, SqlMigrationExtensionId } from '../api/utils'; import { DashboardStatusBar } from './DashboardStatusBar'; @@ -184,20 +182,6 @@ export abstract class TabBase implements azdata.Tab, vscode.Disposable { return feedbackButton; } - protected getMigrationErrors(migration: DatabaseMigration): string { - const errors = []; - errors.push(migration.properties.provisioningError); - errors.push(migration.properties.migrationFailureError?.message); - errors.push(migration.properties.migrationStatusDetails?.fileUploadBlockingErrors ?? []); - errors.push(migration.properties.migrationStatusDetails?.restoreBlockingReason); - errors.push(migration.properties.migrationStatusDetails?.sqlDataCopyErrors); - - // remove undefined and duplicate error entries - return errors - .filter((e, i, arr) => e !== undefined && i === arr.indexOf(e)) - .join(EOL); - } - protected showDialogMessage( title: string, statusMessage: string, diff --git a/extensions/sql-migration/src/dialog/restartMigration/restartMigrationDialog.ts b/extensions/sql-migration/src/dialog/restartMigration/restartMigrationDialog.ts new file mode 100644 index 0000000000..18e547fddc --- /dev/null +++ b/extensions/sql-migration/src/dialog/restartMigration/restartMigrationDialog.ts @@ -0,0 +1,177 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as azdata from 'azdata'; +import * as vscode from 'vscode'; +import * as features from '../../service/features'; +import { azureResource } from 'azurecore'; +import { getLocations, getResourceGroupFromId, getBlobContainerId, getFullResourceGroupFromId, getResourceName, DatabaseMigration, getMigrationTargetInstance } from '../../api/azure'; +import { MigrationMode, MigrationStateModel, NetworkContainerType, SavedInfo } from '../../models/stateMachine'; +import { MigrationServiceContext } from '../../models/migrationLocalStorage'; +import { WizardController } from '../../wizard/wizardController'; +import { getMigrationModeEnum, getMigrationTargetTypeEnum } from '../../constants/helper'; +import * as constants from '../../constants/strings'; +import { ServiceContextChangeEvent } from '../../dashboard/tabBase'; +import { migrationServiceProvider } from '../../service/provider'; +import { getSourceConnectionProfile } from '../../api/sqlUtils'; + +export class RestartMigrationDialog { + + constructor( + private readonly _context: vscode.ExtensionContext, + private readonly _serviceContext: MigrationServiceContext, + private readonly _migration: DatabaseMigration, + private readonly _serviceContextChangedEvent: vscode.EventEmitter) { + } + + private async createMigrationStateModel( + serviceContext: MigrationServiceContext, + migration: DatabaseMigration, + serverName: string, + migrationService: features.SqlMigrationService, + location: azureResource.AzureLocation): Promise { + + const stateModel = new MigrationStateModel(this._context, migrationService); + const sourceDatabaseName = migration.properties.sourceDatabaseName; + const savedInfo: SavedInfo = { + closedPage: 0, + + // DatabaseSelector + databaseAssessment: [sourceDatabaseName], + + // SKURecommendation + databaseList: [sourceDatabaseName], + databaseInfoList: [], + serverAssessment: null, + skuRecommendation: null, + migrationTargetType: getMigrationTargetTypeEnum(migration)!, + + // TargetSelection + azureAccount: serviceContext.azureAccount!, + azureTenant: serviceContext.azureAccount!.properties.tenants[0]!, + subscription: serviceContext.subscription!, + location: location, + resourceGroup: { + id: getFullResourceGroupFromId(migration.id), + name: getResourceGroupFromId(migration.id), + subscription: serviceContext.subscription!, + }, + targetServerInstance: await getMigrationTargetInstance( + serviceContext.azureAccount!, + serviceContext.subscription!, + migration), + + // MigrationMode + migrationMode: getMigrationModeEnum(migration), + + // DatabaseBackup + targetDatabaseNames: [migration.name], + networkContainerType: null, + networkShares: [], + blobs: [], + + // Integration Runtime + sqlMigrationService: serviceContext.migrationService, + }; + + const getStorageAccountResourceGroup = (storageAccountResourceId: string): azureResource.AzureResourceResourceGroup => { + return { + id: getFullResourceGroupFromId(storageAccountResourceId!), + name: getResourceGroupFromId(storageAccountResourceId!), + subscription: this._serviceContext.subscription! + }; + }; + const getStorageAccount = (storageAccountResourceId: string): azureResource.AzureGraphResource => { + const storageAccountName = getResourceName(storageAccountResourceId); + return { + type: 'microsoft.storage/storageaccounts', + id: storageAccountResourceId!, + tenantId: savedInfo.azureTenant?.id!, + subscriptionId: this._serviceContext.subscription?.id!, + name: storageAccountName, + location: savedInfo.location!.name, + }; + }; + + const sourceLocation = migration.properties.backupConfiguration?.sourceLocation; + if (sourceLocation?.fileShare) { + savedInfo.networkContainerType = NetworkContainerType.NETWORK_SHARE; + const storageAccountResourceId = migration.properties.backupConfiguration?.targetLocation?.storageAccountResourceId!; + savedInfo.networkShares = [ + { + password: '', + networkShareLocation: sourceLocation?.fileShare?.path!, + windowsUser: sourceLocation?.fileShare?.username!, + storageAccount: getStorageAccount(storageAccountResourceId!), + resourceGroup: getStorageAccountResourceGroup(storageAccountResourceId!), + storageKey: '' + } + ]; + } else if (sourceLocation?.azureBlob) { + savedInfo.networkContainerType = NetworkContainerType.BLOB_CONTAINER; + const storageAccountResourceId = sourceLocation?.azureBlob?.storageAccountResourceId!; + savedInfo.blobs = [ + { + blobContainer: { + id: getBlobContainerId(getFullResourceGroupFromId(storageAccountResourceId!), getResourceName(storageAccountResourceId!), sourceLocation?.azureBlob.blobContainerName), + name: sourceLocation?.azureBlob.blobContainerName, + subscription: this._serviceContext.subscription! + }, + lastBackupFile: getMigrationModeEnum(migration) === MigrationMode.OFFLINE ? migration.properties.offlineConfiguration?.lastBackupName! : undefined, + storageAccount: getStorageAccount(storageAccountResourceId!), + resourceGroup: getStorageAccountResourceGroup(storageAccountResourceId!), + storageKey: '' + } + ]; + } + + stateModel.restartMigration = true; + stateModel.savedInfo = savedInfo; + stateModel.serverName = serverName; + return stateModel; + } + + public async openDialog(dialogName?: string) { + const locations = await getLocations( + this._serviceContext.azureAccount!, + this._serviceContext.subscription!); + + const targetInstance = await getMigrationTargetInstance( + this._serviceContext.azureAccount!, + this._serviceContext.subscription!, + this._migration); + + let location: azureResource.AzureLocation; + locations.forEach(azureLocation => { + if (azureLocation.name === targetInstance.location) { + location = azureLocation; + } + }); + + const activeConnection = await getSourceConnectionProfile(); + let serverName: string = ''; + if (!activeConnection) { + const connection = await azdata.connection.openConnectionDialog(); + if (connection) { + serverName = connection.options.server; + } + } else { + serverName = activeConnection.serverName; + } + + const migrationService = await migrationServiceProvider.getService(features.ApiType.SqlMigrationProvider)!; + const stateModel = await this.createMigrationStateModel(this._serviceContext, this._migration, serverName, migrationService, location!); + + if (await stateModel.loadSavedInfo()) { + const wizardController = new WizardController( + this._context, + stateModel, + this._serviceContextChangedEvent); + await wizardController.openWizard(); + } else { + void vscode.window.showInformationMessage(constants.MIGRATION_CANNOT_RESTART); + } + } +} diff --git a/extensions/sql-migration/src/dialog/retryMigration/retryMigrationDialog.ts b/extensions/sql-migration/src/dialog/retryMigration/retryMigrationDialog.ts index 27fd313639..c173c20340 100644 --- a/extensions/sql-migration/src/dialog/retryMigration/retryMigrationDialog.ts +++ b/extensions/sql-migration/src/dialog/retryMigration/retryMigrationDialog.ts @@ -5,173 +5,82 @@ import * as azdata from 'azdata'; import * as vscode from 'vscode'; -import * as features from '../../service/features'; -import { azureResource } from 'azurecore'; -import { getLocations, getResourceGroupFromId, getBlobContainerId, getFullResourceGroupFromId, getResourceName, DatabaseMigration, getMigrationTargetInstance } from '../../api/azure'; -import { MigrationMode, MigrationStateModel, NetworkContainerType, SavedInfo } from '../../models/stateMachine'; -import { MigrationServiceContext } from '../../models/migrationLocalStorage'; -import { WizardController } from '../../wizard/wizardController'; -import { getMigrationModeEnum, getMigrationTargetTypeEnum } from '../../constants/helper'; import * as constants from '../../constants/strings'; -import { ServiceContextChangeEvent } from '../../dashboard/tabBase'; -import { migrationServiceProvider } from '../../service/provider'; -import { getSourceConnectionProfile } from '../../api/sqlUtils'; -export class RetryMigrationDialog { +export function openRetryMigrationDialog( + errorMessage: string, + onAcceptCallback: () => Promise): void { - constructor( - private readonly _context: vscode.ExtensionContext, - private readonly _serviceContext: MigrationServiceContext, - private readonly _migration: DatabaseMigration, - private readonly _serviceContextChangedEvent: vscode.EventEmitter) { - } + const disposables: vscode.Disposable[] = []; + const tab = azdata.window.createTab(''); - private async createMigrationStateModel( - serviceContext: MigrationServiceContext, - migration: DatabaseMigration, - serverName: string, - migrationService: features.SqlMigrationService, - location: azureResource.AzureLocation): Promise { + tab.registerContent(async (view) => { + disposables.push( + view.onClosed(e => + disposables.forEach( + d => { try { d.dispose(); } catch { } }))); - const stateModel = new MigrationStateModel(this._context, migrationService); - const sourceDatabaseName = migration.properties.sourceDatabaseName; - const savedInfo: SavedInfo = { - closedPage: 0, + const flex = view.modelBuilder.flexContainer() + .withItems([ + view.modelBuilder.text() + .withProps({ + value: constants.RETRY_MIGRATION_TITLE, + title: constants.RETRY_MIGRATION_TITLE, + CSSStyles: { 'font-weight': '600', 'margin': '10px 0px 5px 0px' }, + }) + .component(), + view.modelBuilder.inputBox() + .withProps({ + value: errorMessage, + title: errorMessage, + readOnly: true, + multiline: true, + inputType: 'text', + rows: 10, + CSSStyles: { 'overflow': 'hidden auto', 'margin': '0px 0px 10px 0px' }, + }) + .component(), + view.modelBuilder.text() + .withProps({ + value: constants.RETRY_MIGRATION_SUMMARY, + title: constants.RETRY_MIGRATION_SUMMARY, + CSSStyles: { 'margin': '0px 0px 10px 0px' }, + }) + .component(), + view.modelBuilder.text() + .withProps({ + value: constants.RETRY_MIGRATION_PROMPT, + title: constants.RETRY_MIGRATION_PROMPT, + CSSStyles: { 'margin': '0px 0px 5px 0px' }, + }) + .component(), + ]) + .withLayout({ flexFlow: 'column', }) + .withProps({ CSSStyles: { 'margin': '0 15px 0 15px' } }) + .component(); - // DatabaseSelector - databaseAssessment: [sourceDatabaseName], + await view.initializeModel(flex); + }); - // SKURecommendation - databaseList: [sourceDatabaseName], - databaseInfoList: [], - serverAssessment: null, - skuRecommendation: null, - migrationTargetType: getMigrationTargetTypeEnum(migration)!, + const dialog = azdata.window.createModelViewDialog( + '', + 'retryMigrationDialog', + 500, + 'normal', + undefined, + false); + dialog.content = [tab]; + dialog.okButton.label = constants.YES; + dialog.okButton.position = 'left'; + dialog.cancelButton.label = constants.NO; + dialog.cancelButton.position = 'left'; + dialog.cancelButton.focused = true; - // TargetSelection - azureAccount: serviceContext.azureAccount!, - azureTenant: serviceContext.azureAccount!.properties.tenants[0]!, - subscription: serviceContext.subscription!, - location: location, - resourceGroup: { - id: getFullResourceGroupFromId(migration.id), - name: getResourceGroupFromId(migration.id), - subscription: serviceContext.subscription!, - }, - targetServerInstance: await getMigrationTargetInstance( - serviceContext.azureAccount!, - serviceContext.subscription!, - migration), - // MigrationMode - migrationMode: getMigrationModeEnum(migration), - // DatabaseBackup - targetDatabaseNames: [migration.name], - networkContainerType: null, - networkShares: [], - blobs: [], + disposables.push( + dialog.okButton.onClick( + async e => await onAcceptCallback())); - // Integration Runtime - sqlMigrationService: serviceContext.migrationService, - }; - - const getStorageAccountResourceGroup = (storageAccountResourceId: string): azureResource.AzureResourceResourceGroup => { - return { - id: getFullResourceGroupFromId(storageAccountResourceId!), - name: getResourceGroupFromId(storageAccountResourceId!), - subscription: this._serviceContext.subscription! - }; - }; - const getStorageAccount = (storageAccountResourceId: string): azureResource.AzureGraphResource => { - const storageAccountName = getResourceName(storageAccountResourceId); - return { - type: 'microsoft.storage/storageaccounts', - id: storageAccountResourceId!, - tenantId: savedInfo.azureTenant?.id!, - subscriptionId: this._serviceContext.subscription?.id!, - name: storageAccountName, - location: savedInfo.location!.name, - }; - }; - - const sourceLocation = migration.properties.backupConfiguration?.sourceLocation; - if (sourceLocation?.fileShare) { - savedInfo.networkContainerType = NetworkContainerType.NETWORK_SHARE; - const storageAccountResourceId = migration.properties.backupConfiguration?.targetLocation?.storageAccountResourceId!; - savedInfo.networkShares = [ - { - password: '', - networkShareLocation: sourceLocation?.fileShare?.path!, - windowsUser: sourceLocation?.fileShare?.username!, - storageAccount: getStorageAccount(storageAccountResourceId!), - resourceGroup: getStorageAccountResourceGroup(storageAccountResourceId!), - storageKey: '' - } - ]; - } else if (sourceLocation?.azureBlob) { - savedInfo.networkContainerType = NetworkContainerType.BLOB_CONTAINER; - const storageAccountResourceId = sourceLocation?.azureBlob?.storageAccountResourceId!; - savedInfo.blobs = [ - { - blobContainer: { - id: getBlobContainerId(getFullResourceGroupFromId(storageAccountResourceId!), getResourceName(storageAccountResourceId!), sourceLocation?.azureBlob.blobContainerName), - name: sourceLocation?.azureBlob.blobContainerName, - subscription: this._serviceContext.subscription! - }, - lastBackupFile: getMigrationModeEnum(migration) === MigrationMode.OFFLINE ? migration.properties.offlineConfiguration?.lastBackupName! : undefined, - storageAccount: getStorageAccount(storageAccountResourceId!), - resourceGroup: getStorageAccountResourceGroup(storageAccountResourceId!), - storageKey: '' - } - ]; - } - - stateModel.retryMigration = true; - stateModel.savedInfo = savedInfo; - stateModel.serverName = serverName; - return stateModel; - } - - public async openDialog(dialogName?: string) { - const locations = await getLocations( - this._serviceContext.azureAccount!, - this._serviceContext.subscription!); - - const targetInstance = await getMigrationTargetInstance( - this._serviceContext.azureAccount!, - this._serviceContext.subscription!, - this._migration); - - let location: azureResource.AzureLocation; - locations.forEach(azureLocation => { - if (azureLocation.name === targetInstance.location) { - location = azureLocation; - } - }); - - const activeConnection = await getSourceConnectionProfile(); - let serverName: string = ''; - if (!activeConnection) { - const connection = await azdata.connection.openConnectionDialog(); - if (connection) { - serverName = connection.options.server; - } - } else { - serverName = activeConnection.serverName; - } - - const migrationService = await migrationServiceProvider.getService(features.ApiType.SqlMigrationProvider)!; - const stateModel = await this.createMigrationStateModel(this._serviceContext, this._migration, serverName, migrationService, location!); - - if (await stateModel.loadSavedInfo()) { - const wizardController = new WizardController( - this._context, - stateModel, - this._serviceContextChangedEvent); - await wizardController.openWizard(); - } else { - void vscode.window.showInformationMessage(constants.MIGRATION_CANNOT_RETRY); - } - } + azdata.window.openDialog(dialog); } diff --git a/extensions/sql-migration/src/models/stateMachine.ts b/extensions/sql-migration/src/models/stateMachine.ts index b6372af261..b9e19ce88c 100644 --- a/extensions/sql-migration/src/models/stateMachine.ts +++ b/extensions/sql-migration/src/models/stateMachine.ts @@ -95,7 +95,7 @@ export enum Page { export enum WizardEntryPoint { Default = 'Default', SaveAndClose = 'SaveAndClose', - RetryMigration = 'RetryMigration', + RestartMigration = 'RestartMigration', } export enum PerformanceDataSourceOptions { @@ -260,7 +260,7 @@ export class MigrationStateModel implements Model, vscode.Disposable { public _skuEnableElastic!: boolean; public refreshDatabaseBackupPage!: boolean; - public retryMigration!: boolean; + public restartMigration!: boolean; public resumeAssessment!: boolean; public savedInfo!: SavedInfo; public closedPage!: number; @@ -1121,8 +1121,8 @@ export class MigrationStateModel implements Model, vscode.Disposable { let wizardEntryPoint = WizardEntryPoint.Default; if (this.resumeAssessment) { wizardEntryPoint = WizardEntryPoint.SaveAndClose; - } else if (this.retryMigration) { - wizardEntryPoint = WizardEntryPoint.RetryMigration; + } else if (this.restartMigration) { + wizardEntryPoint = WizardEntryPoint.RestartMigration; } if (response.status === 201 || response.status === 200) { sendSqlMigrationActionEvent( @@ -1167,7 +1167,7 @@ export class MigrationStateModel implements Model, vscode.Disposable { finally { // kill existing data collection if user start migration await this.refreshPerfDataCollection(); - if ((!this.resumeAssessment || this.retryMigration) && this._perfDataCollectionIsCollecting) { + if ((!this.resumeAssessment || this.restartMigration) && this._perfDataCollectionIsCollecting) { void this.stopPerfDataCollection(); void vscode.window.showInformationMessage( constants.AZURE_RECOMMENDATION_STOP_POPUP); diff --git a/extensions/sql-migration/src/wizard/wizardController.ts b/extensions/sql-migration/src/wizard/wizardController.ts index b97d882dc3..754ece49c1 100644 --- a/extensions/sql-migration/src/wizard/wizardController.ts +++ b/extensions/sql-migration/src/wizard/wizardController.ts @@ -99,7 +99,7 @@ export class WizardController { // kill existing data collection if user relaunches the wizard via new migration or retry existing migration await this._model.refreshPerfDataCollection(); - if ((!this._model.resumeAssessment || this._model.retryMigration) && this._model._perfDataCollectionIsCollecting) { + if ((!this._model.resumeAssessment || this._model.restartMigration) && this._model._perfDataCollectionIsCollecting) { void this._model.stopPerfDataCollection(); void vscode.window.showInformationMessage(loc.AZURE_RECOMMENDATION_STOP_POPUP); } @@ -107,7 +107,7 @@ export class WizardController { const wizardSetupPromises: Thenable[] = []; wizardSetupPromises.push(...pages.map(p => p.registerWizardContent())); wizardSetupPromises.push(this._wizardObject.open()); - if (this._model.retryMigration || this._model.resumeAssessment) { + if (this._model.resumeAssessment || this._model.restartMigration) { if (this._model.savedInfo.closedPage >= Page.IntegrationRuntime) { this._model.refreshDatabaseBackupPage = true; }