diff --git a/extensions/sql-migration/src/constants/strings.ts b/extensions/sql-migration/src/constants/strings.ts index ff460e54e5..8493f52512 100644 --- a/extensions/sql-migration/src/constants/strings.ts +++ b/extensions/sql-migration/src/constants/strings.ts @@ -35,6 +35,9 @@ export const SKU_RECOMMENDATION_PAGE_TITLE = localize('sql.migration.wizard.sku. export const SKU_RECOMMENDATION_ALL_SUCCESSFUL = (databaseCount: number): string => { return localize('sql.migration.wizard.sku.all', "Based on the assessment results, all {0} of your database(s) in online state can be migrated to Azure SQL.", databaseCount); }; +export const SKU_RECOMMENDATION_ASSESSMENT_ERROR = (serverName: string): string => { + return localize('sql.migration.qizard.sku.assessment.error', "An error occurred while assessing the server '{0}'.", serverName); +}; export const SKU_RECOMMENDATION_SOME_SUCCESSFUL = (migratableCount: number, databaseCount: number): string => { return localize('sql.migration.wizard.sku.some', "Based on the results of our source configuration scans, {0} out of {1} of your databases can be migrated to Azure SQL.", migratableCount, databaseCount); }; diff --git a/extensions/sql-migration/src/models/stateMachine.ts b/extensions/sql-migration/src/models/stateMachine.ts index 20615a58f3..0e5b7c40bd 100644 --- a/extensions/sql-migration/src/models/stateMachine.ts +++ b/extensions/sql-migration/src/models/stateMachine.ts @@ -138,6 +138,7 @@ export class MigrationStateModel implements Model, vscode.Disposable { private _skuRecommendations: SKURecommendations | undefined; public _assessmentResults!: ServerAssessement; + public _runAssessments: boolean = true; private _assessmentApiResponse!: mssql.AssessmentResult; public _vmDbs: string[] = []; @@ -186,16 +187,27 @@ export class MigrationStateModel implements Model, vscode.Disposable { public async getDatabaseAssessments(): Promise { const ownerUri = await azdata.connection.getUriForConnection(this.sourceConnectionId); - this._assessmentApiResponse = (await this.migrationService.getAssessments(ownerUri, this._databaseAssessment))!; - this._assessmentResults = { - issues: this._assessmentApiResponse.assessmentResult.items, - databaseAssessments: this._assessmentApiResponse.assessmentResult.databases.map(d => { - return { - name: d.name, - issues: d.items - }; - }) - }; + try { + this._assessmentApiResponse = (await this.migrationService.getAssessments(ownerUri, this._databaseAssessment))!; + this._assessmentResults = { + issues: this._assessmentApiResponse.assessmentResult.items, + databaseAssessments: this._assessmentApiResponse.assessmentResult.databases.map(d => { + return { + name: d.name, + issues: d.items, + errors: d.errors + }; + }), + errors: this._assessmentApiResponse.errors + }; + } catch (error) { + this._assessmentResults = { + issues: [], + databaseAssessments: [], + errors: [], + assessmentError: error + }; + } // Generating all the telemetry asynchronously as we don't need to block the user for it. this.generateAssessmentTelemetry().catch(e => console.error(e)); @@ -528,7 +540,6 @@ export class MigrationStateModel implements Model, vscode.Disposable { return this._resourceGroups[index]; } - public async getManagedInstanceValues(subscription: azureResource.AzureResourceSubscription, location: azureResource.AzureLocation, resourceGroup: azureResource.AzureResourceResourceGroup): Promise { let managedInstanceValues: azdata.CategoryValue[] = []; if (!this._azureAccount) { @@ -536,7 +547,7 @@ export class MigrationStateModel implements Model, vscode.Disposable { } try { this._targetManagedInstances = (await getAvailableManagedInstanceProducts(this._azureAccount, subscription)).filter((mi) => { - if (mi.location.toLowerCase() === location.name.toLowerCase() && mi.resourceGroup?.toLowerCase() === resourceGroup.name.toLowerCase()) { + if (mi.location.toLowerCase() === location.name.toLowerCase() && mi.resourceGroup?.toLowerCase() === resourceGroup?.name.toLowerCase()) { return true; } return false; @@ -581,21 +592,25 @@ export class MigrationStateModel implements Model, vscode.Disposable { public async getSqlVirtualMachineValues(subscription: azureResource.AzureResourceSubscription, location: azureResource.AzureLocation, resourceGroup: azureResource.AzureResourceResourceGroup): Promise { let virtualMachineValues: azdata.CategoryValue[] = []; try { - this._targetSqlVirtualMachines = (await getAvailableSqlVMs(this._azureAccount, subscription, resourceGroup)).filter((virtualMachine) => { - if (virtualMachine.location === location.name) { - if (virtualMachine.properties.sqlImageOffer) { - return virtualMachine.properties.sqlImageOffer.toLowerCase().includes('-ws'); //filtering out all non windows sql vms. + if (this._azureAccount && subscription && resourceGroup) { + this._targetSqlVirtualMachines = (await getAvailableSqlVMs(this._azureAccount, subscription, resourceGroup)).filter((virtualMachine) => { + if (virtualMachine.location === location.name) { + if (virtualMachine.properties.sqlImageOffer) { + return virtualMachine.properties.sqlImageOffer.toLowerCase().includes('-ws'); //filtering out all non windows sql vms. + } + return true; // Returning all VMs that don't have this property as we don't want to accidentally skip valid vms. } - return true; // Returning all VMs that don't have this property as we don't want to accidentally skip valid vms. - } - return false; - }); - virtualMachineValues = this._targetSqlVirtualMachines.map((virtualMachine) => { - return { - name: virtualMachine.id, - displayName: `${virtualMachine.name}` - }; - }); + return false; + }); + virtualMachineValues = this._targetSqlVirtualMachines.map((virtualMachine) => { + return { + name: virtualMachine.id, + displayName: `${virtualMachine.name}` + }; + }); + } else { + this._targetSqlVirtualMachines = []; + } if (virtualMachineValues.length === 0) { virtualMachineValues = [ @@ -944,5 +959,8 @@ export interface ServerAssessement { databaseAssessments: { name: string; issues: mssql.SqlMigrationAssessmentResultItem[]; + errors?: mssql.ErrorModel[]; }[]; + errors?: mssql.ErrorModel[]; + assessmentError?: Error; } diff --git a/extensions/sql-migration/src/wizard/databaseSelectorPage.ts b/extensions/sql-migration/src/wizard/databaseSelectorPage.ts index 67e15a4733..2de1a658a2 100644 --- a/extensions/sql-migration/src/wizard/databaseSelectorPage.ts +++ b/extensions/sql-migration/src/wizard/databaseSelectorPage.ts @@ -81,7 +81,18 @@ export class DatabaseSelectorPage extends MigrationWizardPage { }); } public async onPageLeave(): Promise { - this.migrationStateModel._databaseAssessment = this.selectedDbs(); + const assessedDatabases = this.migrationStateModel._databaseAssessment ?? []; + const selectedDatabases = this.selectedDbs(); + // run assessment if + // * the prior assessment had an error or + // * the assessed databases list is different from the selected databases list + this.migrationStateModel._runAssessments = !!this.migrationStateModel._assessmentResults?.assessmentError + || assessedDatabases.length === 0 + || assessedDatabases.length !== selectedDatabases.length + || assessedDatabases.some(db => selectedDatabases.indexOf(db) < 0); + + this.migrationStateModel._databaseAssessment = selectedDatabases; + this.wizard.registerNavigationValidator((pageChangeInfo) => { return true; }); diff --git a/extensions/sql-migration/src/wizard/skuRecommendationPage.ts b/extensions/sql-migration/src/wizard/skuRecommendationPage.ts index 6fba41f99e..4aa688c635 100644 --- a/extensions/sql-migration/src/wizard/skuRecommendationPage.ts +++ b/extensions/sql-migration/src/wizard/skuRecommendationPage.ts @@ -62,7 +62,6 @@ export class SKURecommendationPage extends MigrationWizardPage { } ]; - constructor(wizard: azdata.window.Wizard, migrationStateModel: MigrationStateModel) { super(wizard, azdata.window.createWizardPage(constants.SKU_RECOMMENDATION_PAGE_TITLE), migrationStateModel); } @@ -287,7 +286,6 @@ export class SKURecommendationPage extends MigrationWizardPage { } }).component(); - const managedInstanceSubscriptionDropdownLabel = this._view.modelBuilder.text().withProps({ value: constants.SUBSCRIPTION, width: WIZARD_INPUT_COMPONENT_WIDTH, @@ -407,18 +405,27 @@ export class SKURecommendationPage extends MigrationWizardPage { } private changeTargetType(newTargetType: string) { + // remove assessed databases that have been removed from the source selection list + const miDbs = this.migrationStateModel._miDbs.filter( + db => this.migrationStateModel._databaseAssessment.findIndex( + dba => dba === db) >= 0); + + const vmDbs = this.migrationStateModel._vmDbs.filter( + db => this.migrationStateModel._databaseAssessment.findIndex( + dba => dba === db) >= 0); + if (newTargetType === MigrationTargetType.SQLMI) { this._viewAssessmentsHelperText.value = constants.SKU_RECOMMENDATION_VIEW_ASSESSMENT_MI; - this._databaseSelectedHelperText.value = constants.TOTAL_DATABASES_SELECTED(this.migrationStateModel._miDbs.length, this.migrationStateModel._databaseAssessment.length); + this._databaseSelectedHelperText.value = constants.TOTAL_DATABASES_SELECTED(miDbs.length, this.migrationStateModel._databaseAssessment.length); this.migrationStateModel._targetType = MigrationTargetType.SQLMI; this._azureSubscriptionText.value = constants.SELECT_AZURE_MI; - this.migrationStateModel._migrationDbs = this.migrationStateModel._miDbs; + this.migrationStateModel._migrationDbs = miDbs; } else { this._viewAssessmentsHelperText.value = constants.SKU_RECOMMENDATION_VIEW_ASSESSMENT_VM; - this._databaseSelectedHelperText.value = constants.TOTAL_DATABASES_SELECTED(this.migrationStateModel._vmDbs.length, this.migrationStateModel._databaseAssessment.length); + this._databaseSelectedHelperText.value = constants.TOTAL_DATABASES_SELECTED(vmDbs.length, this.migrationStateModel._databaseAssessment.length); this.migrationStateModel._targetType = MigrationTargetType.SQLVM; this._azureSubscriptionText.value = constants.SELECT_AZURE_VM; - this.migrationStateModel._migrationDbs = this.migrationStateModel._vmDbs; + this.migrationStateModel._migrationDbs = vmDbs; } this.migrationStateModel.refreshDatabaseBackupPage = true; this._targetContainer.display = (this.migrationStateModel._migrationDbs.length === 0) ? 'none' : 'inline'; @@ -426,18 +433,38 @@ export class SKURecommendationPage extends MigrationWizardPage { } private async constructDetails(): Promise { + this.wizard.message = { + text: '', + level: azdata.window.MessageLevel.Error + }; + this._assessmentComponent.updateCssStyles({ display: 'block' }); + this._formContainer.component().updateCssStyles({ display: 'none' }); + this._assessmentLoader.loading = true; const serverName = (await this.migrationStateModel.getSourceConnectionProfile()).serverName; this._igComponent.value = constants.ASSESSMENT_COMPLETED(serverName); try { await this.migrationStateModel.getDatabaseAssessments(); this._detailsComponent.value = constants.SKU_RECOMMENDATION_ALL_SUCCESSFUL(this.migrationStateModel._assessmentResults.databaseAssessments.length); + + const error = this.migrationStateModel._assessmentResults.assessmentError; + if (error) { + this.wizard.message = { + text: constants.SKU_RECOMMENDATION_ASSESSMENT_ERROR(serverName), + description: error.message + EOL + error.stack, + level: azdata.window.MessageLevel.Error + }; + } + + this.migrationStateModel._runAssessments = !!error; } catch (e) { console.log(e); } this.refreshCardText(); this._assessmentLoader.loading = false; + this._assessmentComponent.updateCssStyles({ display: 'none' }); + this._formContainer.component().updateCssStyles({ display: 'block' }); } private async populateSubscriptionDropdown(): Promise { @@ -475,13 +502,20 @@ export class SKURecommendationPage extends MigrationWizardPage { private async populateResourceInstanceDropdown(): Promise { try { this._resourceDropdown.loading = true; + if (this._rbg.selectedCardId === MigrationTargetType.SQLVM) { this._resourceDropdownLabel.value = constants.AZURE_SQL_DATABASE_VIRTUAL_MACHINE; - this._resourceDropdown.values = await this.migrationStateModel.getSqlVirtualMachineValues(this.migrationStateModel._targetSubscription, this.migrationStateModel._location, this.migrationStateModel._resourceGroup); + this._resourceDropdown.values = await this.migrationStateModel.getSqlVirtualMachineValues( + this.migrationStateModel._targetSubscription, + this.migrationStateModel._location, + this.migrationStateModel._resourceGroup); } else { this._resourceDropdownLabel.value = constants.AZURE_SQL_DATABASE_MANAGED_INSTANCE; - this._resourceDropdown.values = await this.migrationStateModel.getManagedInstanceValues(this.migrationStateModel._targetSubscription, this.migrationStateModel._location, this.migrationStateModel._resourceGroup); + this._resourceDropdown.values = await this.migrationStateModel.getManagedInstanceValues( + this.migrationStateModel._targetSubscription, + this.migrationStateModel._location, + this.migrationStateModel._resourceGroup); } selectDropDownIndex(this._resourceDropdown, 0); @@ -492,7 +526,6 @@ export class SKURecommendationPage extends MigrationWizardPage { } } - public async onPageEnter(pageChangeInfo: azdata.window.WizardPageChangeInfo): Promise { this.wizard.registerNavigationValidator((pageChangeInfo) => { const errors: string[] = []; @@ -538,7 +571,7 @@ export class SKURecommendationPage extends MigrationWizardPage { return true; }); this.wizard.nextButton.enabled = false; - if (!this.migrationStateModel._assessmentResults) { + if (this.migrationStateModel._runAssessments) { await this.constructDetails(); } this._assessmentComponent.updateCssStyles({ @@ -567,16 +600,16 @@ export class SKURecommendationPage extends MigrationWizardPage { } public refreshDatabaseCount(selectedDbs: string[]): void { + this.wizard.message = { + text: '', + level: azdata.window.MessageLevel.Error + }; this.migrationStateModel._migrationDbs = selectedDbs; this.refreshCardText(); } public refreshCardText(): void { this._rbgLoader.loading = true; - this.wizard.message = { - text: '', - level: azdata.window.MessageLevel.Error - }; if (this._rbg.selectedCardId === MigrationTargetType.SQLMI) { this.migrationStateModel._migrationDbs = this.migrationStateModel._miDbs; } else { @@ -586,7 +619,6 @@ export class SKURecommendationPage extends MigrationWizardPage { this._azureResourceGroupDropdown.display = (!this._rbg.selectedCardId) ? 'none' : 'inline'; this._targetContainer.display = (this.migrationStateModel._migrationDbs.length === 0) ? 'none' : 'inline'; - if (this.migrationStateModel._assessmentResults) { const dbCount = this.migrationStateModel._assessmentResults.databaseAssessments.length; @@ -608,6 +640,11 @@ export class SKURecommendationPage extends MigrationWizardPage { cards: this._rbg.cards }); } + + if (this._rbg.selectedCardId) { + this.changeTargetType(this._rbg.selectedCardId); + } + this._rbgLoader.loading = false; }