diff --git a/extensions/sql-migration/images/sqlVM.svg b/extensions/sql-migration/images/sqlVM.svg new file mode 100644 index 0000000000..2d3401dcb8 --- /dev/null +++ b/extensions/sql-migration/images/sqlVM.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/extensions/sql-migration/src/api/azure.ts b/extensions/sql-migration/src/api/azure.ts index 0d78849598..09de559d90 100644 --- a/extensions/sql-migration/src/api/azure.ts +++ b/extensions/sql-migration/src/api/azure.ts @@ -38,15 +38,14 @@ export async function getResourceGroups(account: azdata.Account, subscription: S export type SqlManagedInstance = AzureProduct; export async function getAvailableManagedInstanceProducts(account: azdata.Account, subscription: Subscription): Promise { const api = await getAzureCoreAPI(); - const result = await api.getSqlManagedInstances(account, [subscription], false); + sortResourceArrayByName(result.resources); return result.resources; } export type SqlServer = AzureProduct; export async function getAvailableSqlServers(account: azdata.Account, subscription: Subscription): Promise { const api = await getAzureCoreAPI(); - const result = await api.getSqlServers(account, [subscription], false); return result.resources; } @@ -54,9 +53,13 @@ export async function getAvailableSqlServers(account: azdata.Account, subscripti export type SqlVMServer = AzureProduct; export async function getAvailableSqlVMs(account: azdata.Account, subscription: Subscription): Promise { const api = await getAzureCoreAPI(); - - const result = await api.getSqlVMServers(account, [subscription], false); - return result.resources; + const path = `/subscriptions/${subscription.id}/providers/Microsoft.SqlVirtualMachine/sqlVirtualMachines?api-version=2017-03-01-preview`; + const response = await api.makeAzureRestRequest(account, subscription, path, azurecore.HttpRequestMethod.GET, undefined, true); + if (response.errors.length > 0) { + throw new Error(response.errors.toString()); + } + sortResourceArrayByName(response.response.data.value); + return response.response.data.value; } export type StorageAccount = AzureProduct; @@ -159,10 +162,10 @@ export async function getMigrationControllerMonitoringData(account: azdata.Accou return response.response.data; } -export async function startDatabaseMigration(account: azdata.Account, subscription: Subscription, resourceGroupName: string, regionName: string, managedInstance: string, targetDatabaseName: string, requestBody: StartDatabaseMigrationRequest): Promise { +export async function startDatabaseMigration(account: azdata.Account, subscription: Subscription, regionName: string, targetServer: SqlManagedInstance | SqlVMServer, targetDatabaseName: string, requestBody: StartDatabaseMigrationRequest): Promise { const api = await getAzureCoreAPI(); const host = `https://${regionName}.management.azure.com`; - const path = `/subscriptions/${subscription.id}/resourceGroups/${resourceGroupName}/providers/Microsoft.Sql/managedInstances/${managedInstance}/providers/Microsoft.DataMigration/databaseMigrations/${targetDatabaseName}?api-version=2020-09-01-preview`; + const path = `${targetServer.id}/providers/Microsoft.DataMigration/databaseMigrations/${targetDatabaseName}?api-version=2020-09-01-preview`; const response = await api.makeAzureRestRequest(account, subscription, path, azurecore.HttpRequestMethod.PUT, requestBody, true, host); if (response.errors.length > 0) { throw new Error(response.errors.toString()); diff --git a/extensions/sql-migration/src/constants/strings.ts b/extensions/sql-migration/src/constants/strings.ts index 6c885f7f35..12ecb9ce42 100644 --- a/extensions/sql-migration/src/constants/strings.ts +++ b/extensions/sql-migration/src/constants/strings.ts @@ -136,6 +136,7 @@ export const CONTROLLER_NOT_FOUND = localize('sql.migration.controller.not.found export const CONTROLLER_NOT_SETUP_ERROR = localize('sql.migration.controller.not.setup', "Please add a migration controller to proceed."); export const MANAGED_INSTANCE = localize('sql.migration.managed.instance', "Azure SQL managed instance"); export const NO_MANAGED_INSTANCE_FOUND = localize('sql.migration.no.managedInstance.found', "No managed instance found"); +export const NO_VIRTUAL_MACHINE_FOUND = localize('sql.migration.no.virtualMachine.found', "No virtual machine found"); export const TARGET_SELECTION_PAGE_TITLE = localize('sql.migration.target.page.title', "Choose the target Azure SQL"); // common strings diff --git a/extensions/sql-migration/src/models/product.ts b/extensions/sql-migration/src/models/product.ts index 39d2e1ef9d..e9189e002b 100644 --- a/extensions/sql-migration/src/models/product.ts +++ b/extensions/sql-migration/src/models/product.ts @@ -49,9 +49,11 @@ export const ProductLookupTable: { [key in MigrationProductType]: Product } = { 'AzureSQLMI': { type: 'AzureSQLMI', name: localize('sql.migration.products.azuresqlmi.name', 'Azure Managed Instance (Microsoft managed)'), + icon: 'sqlMI.svg' }, 'AzureSQLVM': { type: 'AzureSQLVM', name: localize('sql.migration.products.azuresqlvm.name', 'Azure SQL Virtual Machine (Customer managed)'), + icon: 'sqlVM.svg' } }; diff --git a/extensions/sql-migration/src/models/stateMachine.ts b/extensions/sql-migration/src/models/stateMachine.ts index e27abc279e..e8dcaf8d11 100644 --- a/extensions/sql-migration/src/models/stateMachine.ts +++ b/extensions/sql-migration/src/models/stateMachine.ts @@ -7,10 +7,12 @@ import * as azdata from 'azdata'; import { azureResource } from 'azureResource'; import * as vscode from 'vscode'; import * as mssql from '../../../mssql'; -import { getAvailableManagedInstanceProducts, getAvailableStorageAccounts, getBlobContainers, getFileShares, getMigrationControllers, getSubscriptions, SqlMigrationController, SqlManagedInstance, startDatabaseMigration, StartDatabaseMigrationRequest, StorageAccount } from '../api/azure'; +import { getAvailableManagedInstanceProducts, getAvailableStorageAccounts, getBlobContainers, getFileShares, getMigrationControllers, getSubscriptions, SqlMigrationController, SqlManagedInstance, startDatabaseMigration, StartDatabaseMigrationRequest, StorageAccount, getAvailableSqlVMs, SqlVMServer } from '../api/azure'; import { SKURecommendations } from './externalContract'; import * as constants from '../constants/strings'; import { MigrationLocalStorage } from './migrationLocalStorage'; +import * as nls from 'vscode-nls'; +const localize = nls.loadMessageBundle(); export enum State { INIT, @@ -82,8 +84,8 @@ export class MigrationStateModel implements Model, vscode.Disposable { public _targetSubscription!: azureResource.AzureResourceSubscription; public _targetManagedInstances!: SqlManagedInstance[]; - public _targetManagedInstance!: SqlManagedInstance; - + public _targetSqlVirtualMachines!: SqlVMServer[]; + public _targetServerInstance!: SqlManagedInstance; public _databaseBackup!: DatabaseBackupModel; public _migrationDbs: string[] = []; public _storageAccounts!: StorageAccount[]; @@ -266,6 +268,41 @@ export class MigrationStateModel implements Model, vscode.Disposable { return this._targetManagedInstances[index]; } + public async getSqlVirtualMachineValues(subscription: azureResource.AzureResourceSubscription): Promise { + let virtualMachineValues: azdata.CategoryValue[] = []; + try { + this._targetSqlVirtualMachines = await getAvailableSqlVMs(this._azureAccount, subscription); + virtualMachineValues = this._targetSqlVirtualMachines.map((virtualMachine) => { + return { + name: virtualMachine.id, + displayName: `${virtualMachine.name}` + }; + }); + + if (virtualMachineValues.length === 0) { + virtualMachineValues = [ + { + displayName: constants.NO_VIRTUAL_MACHINE_FOUND, + name: '' + } + ]; + } + } catch (e) { + console.log(e); + virtualMachineValues = [ + { + displayName: constants.NO_VIRTUAL_MACHINE_FOUND, + name: '' + } + ]; + } + return virtualMachineValues; + } + + public getVirtualMachine(index: number): SqlVMServer { + return this._targetSqlVirtualMachines[index]; + } + public async getStorageAccountValues(subscription: azureResource.AzureResourceSubscription): Promise { let storageAccountValues: azdata.CategoryValue[] = []; try { @@ -441,7 +478,7 @@ export class MigrationStateModel implements Model, vscode.Disposable { Username: currentConnection?.userName!, Password: connectionPassword.password }, - Scope: this._targetManagedInstance.id + Scope: this._targetServerInstance.id } }; @@ -452,10 +489,9 @@ export class MigrationStateModel implements Model, vscode.Disposable { const response = await startDatabaseMigration( this._azureAccount, this._targetSubscription, - this._targetManagedInstance.resourceGroup!, this._migrationController?.properties.location!, - this._targetManagedInstance.name, - currentConnection?.databaseName!, + this._targetServerInstance, + db, requestBody ); @@ -463,12 +499,12 @@ export class MigrationStateModel implements Model, vscode.Disposable { MigrationLocalStorage.saveMigration( currentConnection!, response.databaseMigration, - this._targetManagedInstance, + this._targetServerInstance, this._azureAccount, this._targetSubscription, this._migrationController ); - vscode.window.showInformationMessage(`Starting migration for database ${db} to ${this._targetManagedInstance.name}`); + vscode.window.showInformationMessage(localize("sql.migration.starting.migration.message", 'Starting migration for database {0} to {1}', db, this._targetServerInstance.name)); } } catch (e) { vscode.window.showInformationMessage(e); diff --git a/extensions/sql-migration/src/wizard/integrationRuntimePage.ts b/extensions/sql-migration/src/wizard/integrationRuntimePage.ts index 96fc8b49e7..59ab2bc86e 100644 --- a/extensions/sql-migration/src/wizard/integrationRuntimePage.ts +++ b/extensions/sql-migration/src/wizard/integrationRuntimePage.ts @@ -150,7 +150,7 @@ export class IntergrationRuntimePage extends MigrationWizardPage { public async populateMigrationController(): Promise { this.migrationControllerDropdown.loading = true; try { - this.migrationControllerDropdown.values = await this.migrationStateModel.getMigrationControllerValues(this.migrationStateModel._targetSubscription, this.migrationStateModel._targetManagedInstance); + this.migrationControllerDropdown.values = await this.migrationStateModel.getMigrationControllerValues(this.migrationStateModel._targetSubscription, this.migrationStateModel._targetServerInstance); if (this.migrationStateModel._migrationController) { this.migrationControllerDropdown.value = { name: this.migrationStateModel._migrationController.id, diff --git a/extensions/sql-migration/src/wizard/skuRecommendationPage.ts b/extensions/sql-migration/src/wizard/skuRecommendationPage.ts index ebe84dada8..7d493b7435 100644 --- a/extensions/sql-migration/src/wizard/skuRecommendationPage.ts +++ b/extensions/sql-migration/src/wizard/skuRecommendationPage.ts @@ -30,7 +30,8 @@ export class SKURecommendationPage extends MigrationWizardPage { private _chooseTargetComponent: azdata.FormComponent | undefined; private _azureSubscriptionText: azdata.FormComponent | undefined; private _managedInstanceSubscriptionDropdown!: azdata.DropDownComponent; - private _managedInstanceDropdown!: azdata.DropDownComponent; + private _resourceDropdownLabel!: azdata.TextComponent; + private _resourceDropdown!: azdata.DropDownComponent; private _view: azdata.ModelView | undefined; private _rbg!: azdata.RadioCardGroupComponent; @@ -48,20 +49,27 @@ export class SKURecommendationPage extends MigrationWizardPage { this._managedInstanceSubscriptionDropdown.onValueChanged((e) => { if (e.selected) { this.migrationStateModel._targetSubscription = this.migrationStateModel.getSubscription(e.index); - this.migrationStateModel._targetManagedInstance = undefined!; + this.migrationStateModel._targetServerInstance = undefined!; this.migrationStateModel._migrationController = undefined!; - this.populateManagedInstanceDropdown(); + this.populateResourceInstanceDropdown(); } }); - const managedInstanceDropdownLabel = view.modelBuilder.text().withProps({ + this._resourceDropdownLabel = view.modelBuilder.text().withProps({ value: constants.MANAGED_INSTANCE }).component(); - this._managedInstanceDropdown = view.modelBuilder.dropDown().component(); - this._managedInstanceDropdown.onValueChanged((e) => { - if (e.selected) { + this._resourceDropdown = view.modelBuilder.dropDown().component(); + this._resourceDropdown.onValueChanged((e) => { + if (e.selected && + e.selected !== constants.NO_MANAGED_INSTANCE_FOUND && + e.selected !== constants.NO_VIRTUAL_MACHINE_FOUND) { this.migrationStateModel._migrationControllers = undefined!; - this.migrationStateModel._targetManagedInstance = this.migrationStateModel.getManagedInstance(e.index); + if (this._rbg.selectedCardId === 'AzureSQLVM') { + this.migrationStateModel._targetServerInstance = this.migrationStateModel.getVirtualMachine(e.index); + } else { + this.migrationStateModel._targetServerInstance = this.migrationStateModel.getManagedInstance(e.index); + } + } }); @@ -69,8 +77,8 @@ export class SKURecommendationPage extends MigrationWizardPage { [ managedInstanceSubscriptionDropdownLabel, this._managedInstanceSubscriptionDropdown, - managedInstanceDropdownLabel, - this._managedInstanceDropdown + this._resourceDropdownLabel, + this._resourceDropdown ] ).withLayout({ flexFlow: 'column' @@ -151,41 +159,49 @@ export class SKURecommendationPage extends MigrationWizardPage { this._rbg = this._view!.modelBuilder.radioCardGroup().withProperties({ cards: [], cardWidth: '600px', - cardHeight: '60px', + cardHeight: '40px', orientation: azdata.Orientation.Vertical, iconHeight: '30px', iconWidth: '30px' }).component(); products.forEach((product) => { - const imagePath = path.resolve(this.migrationStateModel.getExtensionPath(), 'media', product.icon ?? 'ads.svg'); - let dbCount = 0; - if (product.type === 'AzureSQLVM') { - dbCount = 0; - } else { - dbCount = this.migrationStateModel._migrationDbs.length; - } + const imagePath = path.resolve(this.migrationStateModel.getExtensionPath(), 'images', product.icon ?? 'ads.svg'); + const dbCount = this.migrationStateModel._migrationDbs.length; const descriptions: azdata.RadioCardDescription[] = [ { textValue: product.name, - linkDisplayValue: 'Learn more', - displayLinkCodicon: true, textStyles: { - 'font-size': '1rem', - 'font-weight': 550, + 'font-size': '14px', + 'font-weight': 'bold', + 'line-height': '20px' }, + linkDisplayValue: 'Learn more', + linkStyles: { + 'font-size': '14px', + 'line-height': '20px' + }, + displayLinkCodicon: true, linkCodiconStyles: { - 'font-size': '1em', - 'color': 'royalblue' - } + 'font-size': '14px', + 'line-height': '20px' + }, }, { textValue: `${dbCount} databases will be migrated`, + textStyles: { + 'font-size': '13px', + 'line-height': '18px' + }, + linkStyles: { + 'font-size': '14px', + 'line-height': '20px' + }, linkDisplayValue: 'View/Change', displayLinkCodicon: true, linkCodiconStyles: { - 'font-size': '1em', - 'color': 'royalblue' + 'font-size': '13px', + 'line-height': '18px' } } ]; @@ -206,10 +222,7 @@ export class SKURecommendationPage extends MigrationWizardPage { }); this._rbg.onSelectionChanged((value) => { - if (value.cardId === 'AzureSQLVM') { - vscode.window.showInformationMessage('Feature coming soon'); - this._rbg.selectedCardId = 'AzureSQLMI'; - } + this.populateResourceInstanceDropdown(); }); this._rbg.selectedCardId = 'AzureSQLMI'; @@ -220,7 +233,10 @@ export class SKURecommendationPage extends MigrationWizardPage { private createAzureSubscriptionText(view: azdata.ModelView): azdata.FormComponent { const component = view.modelBuilder.text().withProperties({ value: 'Select an Azure subscription and an Azure SQL Managed Instance for your target.', //TODO: Localize - + CSSStyles: { + 'font-size': '13px', + 'line-height': '18px' + } }); return { @@ -232,7 +248,7 @@ export class SKURecommendationPage extends MigrationWizardPage { private async populateSubscriptionDropdown(): Promise { if (!this.migrationStateModel._targetSubscription) { this._managedInstanceSubscriptionDropdown.loading = true; - this._managedInstanceDropdown.loading = true; + this._resourceDropdown.loading = true; try { this._managedInstanceSubscriptionDropdown.values = await this.migrationStateModel.getSubscriptionsDropdownValues(); } catch (e) { @@ -243,16 +259,21 @@ export class SKURecommendationPage extends MigrationWizardPage { } } - private async populateManagedInstanceDropdown(): Promise { - if (!this.migrationStateModel._targetManagedInstance) { - this._managedInstanceDropdown.loading = true; - try { - this._managedInstanceDropdown.values = await this.migrationStateModel.getManagedInstanceValues(this.migrationStateModel._targetSubscription); - } catch (e) { - console.log(e); - } finally { - this._managedInstanceDropdown.loading = false; + private async populateResourceInstanceDropdown(): Promise { + this._resourceDropdown.loading = true; + try { + if (this._rbg.selectedCardId === 'AzureSQLVM') { + this._resourceDropdownLabel.value = constants.AZURE_SQL_DATABASE_VIRTUAL_MACHINE; + this._resourceDropdown.values = await this.migrationStateModel.getSqlVirtualMachineValues(this.migrationStateModel._targetSubscription); + + } else { + this._resourceDropdownLabel.value = constants.AZURE_SQL_DATABASE_MANAGED_INSTANCE; + this._resourceDropdown.values = await this.migrationStateModel.getManagedInstanceValues(this.migrationStateModel._targetSubscription); } + } catch (e) { + console.log(e); + } finally { + this._resourceDropdown.loading = false; } } @@ -278,7 +299,8 @@ export class SKURecommendationPage extends MigrationWizardPage { if ((this._managedInstanceSubscriptionDropdown.value).displayName === constants.NO_SUBSCRIPTIONS_FOUND) { errors.push(constants.INVALID_SUBSCRIPTION_ERROR); } - if ((this._managedInstanceDropdown.value).displayName === constants.NO_MANAGED_INSTANCE_FOUND) { + const resourceDropdownValue = (this._resourceDropdown.value).displayName; + if (resourceDropdownValue === constants.NO_MANAGED_INSTANCE_FOUND || resourceDropdownValue === constants.NO_VIRTUAL_MACHINE_FOUND) { errors.push(constants.INVALID_STORAGE_ACCOUNT_ERROR); } @@ -317,6 +339,8 @@ export class SKURecommendationPage extends MigrationWizardPage { }; const textValue: string = `${count} databases will be migrated`; this._rbg.cards[0].descriptions[1].textValue = textValue; + this._rbg.cards[1].descriptions[1].textValue = textValue; + this._rbg.updateProperties({ cards: this._rbg.cards }); diff --git a/extensions/sql-migration/src/wizard/summaryPage.ts b/extensions/sql-migration/src/wizard/summaryPage.ts index 816734cc02..e519a353cc 100644 --- a/extensions/sql-migration/src/wizard/summaryPage.ts +++ b/extensions/sql-migration/src/wizard/summaryPage.ts @@ -39,9 +39,9 @@ export class SummaryPage extends MigrationWizardPage { createHeadingTextComponent(this._view, constants.AZURE_ACCOUNT_LINKED), createHeadingTextComponent(this._view, this.migrationStateModel._azureAccount.displayInfo.displayName), createHeadingTextComponent(this._view, constants.MIGRATION_TARGET), - createInformationRow(this._view, constants.TYPE, constants.SUMMARY_MI_TYPE), + createInformationRow(this._view, constants.TYPE, (this.migrationStateModel._targetServerInstance.type === 'microsoft.compute/virtualmachines') ? constants.SUMMARY_VM_TYPE : constants.SUMMARY_MI_TYPE), createInformationRow(this._view, constants.SUBSCRIPTION, this.migrationStateModel._targetSubscription.name), - createInformationRow(this._view, constants.SUMMARY_MI_TYPE, this.migrationStateModel._targetManagedInstance.name), + createInformationRow(this._view, constants.SUMMARY_MI_TYPE, this.migrationStateModel._targetServerInstance.name), createInformationRow(this._view, constants.SUMMARY_DATABASE_COUNT_LABEL, this.migrationStateModel._migrationDbs.length.toString()), createHeadingTextComponent(this._view, constants.DATABASE_BACKUP_PAGE_TITLE), this.createNetworkContainerRows(), diff --git a/extensions/sql-migration/src/wizard/tempTargetSelectionPage.ts b/extensions/sql-migration/src/wizard/tempTargetSelectionPage.ts index 7a5cd8ab8a..677a0a6693 100644 --- a/extensions/sql-migration/src/wizard/tempTargetSelectionPage.ts +++ b/extensions/sql-migration/src/wizard/tempTargetSelectionPage.ts @@ -32,7 +32,7 @@ export class TempTargetSelectionPage extends MigrationWizardPage { this._managedInstanceSubscriptionDropdown.onValueChanged((e) => { if (e.selected) { this.migrationStateModel._targetSubscription = this.migrationStateModel.getSubscription(e.index); - this.migrationStateModel._targetManagedInstance = undefined!; + this.migrationStateModel._targetServerInstance = undefined!; this.migrationStateModel._migrationController = undefined!; this.populateManagedInstanceDropdown(); } @@ -49,7 +49,7 @@ export class TempTargetSelectionPage extends MigrationWizardPage { this._managedInstanceDropdown.onValueChanged((e) => { if (e.selected) { this.migrationStateModel._migrationControllers = undefined!; - this.migrationStateModel._targetManagedInstance = this.migrationStateModel.getManagedInstance(e.index); + this.migrationStateModel._targetServerInstance = this.migrationStateModel.getManagedInstance(e.index); } }); @@ -97,7 +97,7 @@ export class TempTargetSelectionPage extends MigrationWizardPage { } private async populateManagedInstanceDropdown(): Promise { - if (!this.migrationStateModel._targetManagedInstance) { + if (!this.migrationStateModel._targetServerInstance) { this._managedInstanceDropdown.loading = true; try { this._managedInstanceDropdown.values = await this.migrationStateModel.getManagedInstanceValues(this.migrationStateModel._targetSubscription);