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