SQL-Migration: add retry migration prompt (#22555)

* add retry migration prompt

* updating review comments

* update context menu postion to match toolbar
This commit is contained in:
brian-harris
2023-03-31 10:25:39 -07:00
committed by GitHub
parent e948b4e842
commit 887053c604
14 changed files with 412 additions and 205 deletions

View File

@@ -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<void> {
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<void> {
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;

View File

@@ -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',

View File

@@ -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

View File

@@ -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.');

View File

@@ -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<MigrationDetail
this.cutoverButton.enabled = canCutoverMigration(migration);
this.cancelButton.enabled = canCancelMigration(migration);
this.deleteButton.enabled = canDeleteMigration(migration);
this.retryButton.enabled = canRetryMigration(migration);
this.restartButton.enabled = canRestartMigrationWizard(migration);
} catch (e) {
await this.statusBar.showError(
loc.MIGRATION_STATUS_REFRESH_ERROR,
@@ -485,6 +485,8 @@ export class MigrationDetailsTab extends MigrationDetailsTabBase<MigrationDetail
.withProps({ width: '100%' })
.component();
await utils.updateControlDisplay(this.retryButton, false);
this.content = container;
} catch (e) {
logError(TelemetryViews.MigrationCutoverDialog, 'IntializingFailed', e);

View File

@@ -8,15 +8,16 @@ import * as vscode from 'vscode';
import { IconPathHelper } from '../constants/iconPathHelper';
import { MigrationServiceContext } from '../models/migrationLocalStorage';
import * as loc from '../constants/strings';
import { DatabaseMigration, deleteMigration } from '../api/azure';
import { DatabaseMigration, deleteMigration, getMigrationErrors, retryMigration } from '../api/azure';
import { TabBase } from './tabBase';
import { MigrationCutoverDialogModel } from '../dialog/migrationCutover/migrationCutoverDialogModel';
import { ConfirmCutoverDialog } from '../dialog/migrationCutover/confirmCutoverDialog';
import { RetryMigrationDialog } from '../dialog/retryMigration/retryMigrationDialog';
import { RestartMigrationDialog } from '../dialog/restartMigration/restartMigrationDialog';
import { DashboardStatusBar } from './DashboardStatusBar';
import { canDeleteMigration } from '../constants/helper';
import { canDeleteMigration, canRetryMigration } from '../constants/helper';
import { logError, TelemetryViews } from '../telemetry';
import { MenuCommands, MigrationTargetType } from '../api/utils';
import { openRetryMigrationDialog } from '../dialog/retryMigration/retryMigrationDialog';
export const infoFieldLgWidth: string = '330px';
export const infoFieldWidth: string = '250px';
@@ -48,6 +49,7 @@ export abstract class MigrationDetailsTabBase<T> extends TabBase<T> {
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<T> extends TabBase<T> {
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<T> extends TabBase<T> {
<azdata.ToolbarComponent>{ component: this.cancelButton },
<azdata.ToolbarComponent>{ component: this.deleteButton },
<azdata.ToolbarComponent>{ component: this.retryButton },
<azdata.ToolbarComponent>{ component: this.restartButton },
<azdata.ToolbarComponent>{ component: this.copyDatabaseMigrationDetails, toolbarSeparatorAfter: true },
<azdata.ToolbarComponent>{ component: this.newSupportRequest, toolbarSeparatorAfter: true },
<azdata.ToolbarComponent>{ component: this.refreshLoader },
@@ -493,7 +536,7 @@ export abstract class MigrationDetailsTabBase<T> extends TabBase<T> {
}
protected async showMigrationErrors(migration: DatabaseMigration): Promise<void> {
const errorMessage = this.getMigrationErrors(migration);
const errorMessage = getMigrationErrors(migration);
if (errorMessage?.length > 0) {
await this.statusBar.showError(
loc.MIGRATION_ERROR_DETAILS_TITLE,

View File

@@ -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<MigrationD
this.cancelButton.enabled = canCancelMigration(migration);
this.deleteButton.enabled = canDeleteMigration(migration);
this.retryButton.enabled = canRetryMigration(migration);
this.restartButton.enabled = canRestartMigrationWizard(migration);
}
private async _populateTableData(hashSet: loc.LookupTable<number> = {}): Promise<void> {

View File

@@ -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<MigrationsListTab> {
// "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<MigrationsListTab> {
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<MigrationsListTab> {
menuCommands.push(MenuCommands.DeleteMigration);
}
if (canRetryMigration(migration)) {
menuCommands.push(MenuCommands.RetryMigration);
}
menuCommands.push(MenuCommands.CopyMigration);
return menuCommands;
}

View File

@@ -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);
}
}));

View File

@@ -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<T> 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,

View File

@@ -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<ServiceContextChangeEvent>) {
}
private async createMigrationStateModel(
serviceContext: MigrationServiceContext,
migration: DatabaseMigration,
serverName: string,
migrationService: features.SqlMigrationService,
location: azureResource.AzureLocation): Promise<MigrationStateModel> {
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 = <features.SqlMigrationService>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);
}
}
}

View File

@@ -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>): void {
constructor(
private readonly _context: vscode.ExtensionContext,
private readonly _serviceContext: MigrationServiceContext,
private readonly _migration: DatabaseMigration,
private readonly _serviceContextChangedEvent: vscode.EventEmitter<ServiceContextChangeEvent>) {
}
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<MigrationStateModel> {
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 = <features.SqlMigrationService>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);
}

View File

@@ -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);

View File

@@ -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<void>[] = [];
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;
}