From a0cf24424529ac0ae312df1b6888c35ed6d8d79b Mon Sep 17 00:00:00 2001 From: Raymond Truong Date: Mon, 4 Apr 2022 15:59:40 -0700 Subject: [PATCH] [SQL Migration] List resource groups based on available resources (#18846) * WIP - implemented logic to show resource groups as derived from list of resources, instead of directly listing all resource groups * Remove comments * Remove getResourceGroupByName and craft resource group object manually instead * Update subscription and location list when tenant is changed * Define Azure resource types locally instead of modifying azurecore * Add SQL VM scenario * Split getAzureResourceGroupDropdownValues into four separate functions * Refresh only subscription list when tenant is changed * Create new DMS dialog should show all resource groups * Remove unnecessary async code --- extensions/sql-migration/src/api/azure.ts | 12 +- .../sql-migration/src/models/stateMachine.ts | 207 +++++++++++++++++- .../src/wizard/databaseBackupPage.ts | 4 +- .../src/wizard/integrationRuntimePage.ts | 2 +- .../src/wizard/targetSelectionPage.ts | 12 +- 5 files changed, 222 insertions(+), 15 deletions(-) diff --git a/extensions/sql-migration/src/api/azure.ts b/extensions/sql-migration/src/api/azure.ts index 2b4f1b9d37..315714c11c 100644 --- a/extensions/sql-migration/src/api/azure.ts +++ b/extensions/sql-migration/src/api/azure.ts @@ -99,9 +99,9 @@ export type SqlVMServer = { tenantId: string, subscriptionId: string }; -export async function getAvailableSqlVMs(account: azdata.Account, subscription: Subscription, resourceGroup: azureResource.AzureResourceResourceGroup): Promise { +export async function getAvailableSqlVMs(account: azdata.Account, subscription: Subscription): Promise { const api = await getAzureCoreAPI(); - const path = encodeURI(`/subscriptions/${subscription.id}/resourceGroups/${resourceGroup.name}/providers/Microsoft.SqlVirtualMachine/sqlVirtualMachines?api-version=2017-03-01-preview`); + const path = encodeURI(`/subscriptions/${subscription.id}/providers/Microsoft.SqlVirtualMachine/sqlVirtualMachines?api-version=2021-11-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()); @@ -153,10 +153,10 @@ export async function getSqlMigrationService(account: azdata.Account, subscripti return response.response.data; } -export async function getSqlMigrationServices(account: azdata.Account, subscription: Subscription, resouceGroupName: string, sessionId: string): Promise { +export async function getSqlMigrationServices(account: azdata.Account, subscription: Subscription): Promise { const api = await getAzureCoreAPI(); - const path = encodeURI(`/subscriptions/${subscription.id}/resourceGroups/${resouceGroupName}/providers/Microsoft.DataMigration/sqlMigrationServices?api-version=2020-09-01-preview`); - const response = await api.makeAzureRestRequest(account, subscription, path, azurecore.HttpRequestMethod.GET, undefined, true, undefined, getSessionIdHeader(sessionId)); + const path = encodeURI(`/subscriptions/${subscription.id}/providers/Microsoft.DataMigration/sqlMigrationServices?api-version=2022-01-30-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()); } @@ -339,7 +339,7 @@ export async function getLocationDisplayName(location: string): Promise } type SortableAzureResources = AzureProduct | azureResource.FileShare | azureResource.BlobContainer | azureResource.Blob | azureResource.AzureResourceSubscription | SqlMigrationService; -function sortResourceArrayByName(resourceArray: SortableAzureResources[]): void { +export function sortResourceArrayByName(resourceArray: SortableAzureResources[]): void { if (!resourceArray) { return; } diff --git a/extensions/sql-migration/src/models/stateMachine.ts b/extensions/sql-migration/src/models/stateMachine.ts index 32bef1f814..bb03fd8e69 100644 --- a/extensions/sql-migration/src/models/stateMachine.ts +++ b/extensions/sql-migration/src/models/stateMachine.ts @@ -8,7 +8,7 @@ import { azureResource } from 'azureResource'; import * as azurecore from 'azurecore'; import * as vscode from 'vscode'; import * as mssql from 'mssql'; -import { getAvailableManagedInstanceProducts, getAvailableStorageAccounts, getBlobContainers, getFileShares, getSqlMigrationServices, getSubscriptions, SqlMigrationService, SqlManagedInstance, startDatabaseMigration, StartDatabaseMigrationRequest, StorageAccount, getAvailableSqlVMs, SqlVMServer, getLocations, getResourceGroups, getLocationDisplayName, getSqlManagedInstanceDatabases, getBlobs } from '../api/azure'; +import { getAvailableManagedInstanceProducts, getAvailableStorageAccounts, getBlobContainers, getFileShares, getSqlMigrationServices, getSubscriptions, SqlMigrationService, SqlManagedInstance, startDatabaseMigration, StartDatabaseMigrationRequest, StorageAccount, getAvailableSqlVMs, SqlVMServer, getLocations, getLocationDisplayName, getSqlManagedInstanceDatabases, getBlobs, sortResourceArrayByName, getFullResourceGroupFromId, getResourceGroupFromId, getResourceGroups } from '../api/azure'; import * as constants from '../constants/strings'; import { MigrationLocalStorage } from './migrationLocalStorage'; import * as nls from 'vscode-nls'; @@ -78,6 +78,7 @@ export enum PerformanceDataSourceOptions { CollectData = 'CollectData', OpenExisting = 'OpenExisting', } + export interface DatabaseBackupModel { migrationMode: MigrationMode; networkContainerType: NetworkContainerType; @@ -961,6 +962,203 @@ export class MigrationStateModel implements Model, vscode.Disposable { } else { this._resourceGroups = []; } + this._resourceGroups.forEach((rg) => { + resourceGroupValues.push({ + name: rg.id, + displayName: rg.name + }); + }); + if (resourceGroupValues.length === 0) { + resourceGroupValues = [ + { + displayName: constants.RESOURCE_GROUP_NOT_FOUND, + name: '' + } + ]; + } + } catch (e) { + console.log(e); + resourceGroupValues = [ + { + displayName: constants.RESOURCE_GROUP_NOT_FOUND, + name: '' + } + ]; + } + return resourceGroupValues; + } + + public async getAzureResourceGroupForManagedInstancesDropdownValues(subscription: azureResource.AzureResourceSubscription): Promise { + let resourceGroupValues: azdata.CategoryValue[] = []; + try { + if (this._azureAccount && subscription) { + let managedInstances = await getAvailableManagedInstanceProducts(this._azureAccount, subscription); + this._resourceGroups = managedInstances.map((mi) => { + return { + id: getFullResourceGroupFromId(mi.id), + name: getResourceGroupFromId(mi.id), + subscription: { + id: mi.subscriptionId + }, + tenant: mi.tenantId, + }; + }); + + // remove duplicates + this._resourceGroups = this._resourceGroups.filter((v, i, a) => a.findIndex(v2 => (v2.id === v.id)) === i); + sortResourceArrayByName(this._resourceGroups); + } else { + this._resourceGroups = []; + } + + this._resourceGroups.forEach((rg) => { + resourceGroupValues.push({ + name: rg.id, + displayName: rg.name + }); + }); + + if (resourceGroupValues.length === 0) { + resourceGroupValues = [ + { + displayName: constants.RESOURCE_GROUP_NOT_FOUND, + name: '' + } + ]; + } + } catch (e) { + console.log(e); + resourceGroupValues = [ + { + displayName: constants.RESOURCE_GROUP_NOT_FOUND, + name: '' + } + ]; + } + return resourceGroupValues; + } + + public async getAzureResourceGroupForVirtualMachinesDropdownValues(subscription: azureResource.AzureResourceSubscription): Promise { + let resourceGroupValues: azdata.CategoryValue[] = []; + try { + if (this._azureAccount && subscription) { + let virtualMachines = await getAvailableSqlVMs(this._azureAccount, subscription); + this._resourceGroups = virtualMachines.map((vm) => { + return { + id: getFullResourceGroupFromId(vm.id), + name: getResourceGroupFromId(vm.id), + subscription: { + id: vm.subscriptionId + }, + tenant: vm.tenantId, + }; + }); + + // remove duplicates + this._resourceGroups = this._resourceGroups.filter((v, i, a) => a.findIndex(v2 => (v2.id === v.id)) === i); + sortResourceArrayByName(this._resourceGroups); + } else { + this._resourceGroups = []; + } + + this._resourceGroups.forEach((rg) => { + resourceGroupValues.push({ + name: rg.id, + displayName: rg.name + }); + }); + + if (resourceGroupValues.length === 0) { + resourceGroupValues = [ + { + displayName: constants.RESOURCE_GROUP_NOT_FOUND, + name: '' + } + ]; + } + } catch (e) { + console.log(e); + resourceGroupValues = [ + { + displayName: constants.RESOURCE_GROUP_NOT_FOUND, + name: '' + } + ]; + } + return resourceGroupValues; + } + + public async getAzureResourceGroupForStorageAccountsDropdownValues(subscription: azureResource.AzureResourceSubscription): Promise { + let resourceGroupValues: azdata.CategoryValue[] = []; + try { + if (this._azureAccount && subscription) { + let storageAccounts = await getAvailableStorageAccounts(this._azureAccount, subscription); + this._resourceGroups = storageAccounts.map((sa) => { + return { + id: getFullResourceGroupFromId(sa.id), + name: getResourceGroupFromId(sa.id), + subscription: { + id: sa.subscriptionId + }, + tenant: sa.tenantId, + }; + }); + + // remove duplicates + this._resourceGroups = this._resourceGroups.filter((v, i, a) => a.findIndex(v2 => (v2.id === v.id)) === i); + sortResourceArrayByName(this._resourceGroups); + } else { + this._resourceGroups = []; + } + + this._resourceGroups.forEach((rg) => { + resourceGroupValues.push({ + name: rg.id, + displayName: rg.name + }); + }); + + if (resourceGroupValues.length === 0) { + resourceGroupValues = [ + { + displayName: constants.RESOURCE_GROUP_NOT_FOUND, + name: '' + } + ]; + } + } catch (e) { + console.log(e); + resourceGroupValues = [ + { + displayName: constants.RESOURCE_GROUP_NOT_FOUND, + name: '' + } + ]; + } + return resourceGroupValues; + } + + public async getAzureResourceGroupForSqlMigrationServicesDropdownValues(subscription: azureResource.AzureResourceSubscription): Promise { + let resourceGroupValues: azdata.CategoryValue[] = []; + try { + if (this._azureAccount && subscription) { + let dmsInstances = await getSqlMigrationServices(this._azureAccount, subscription); + this._resourceGroups = dmsInstances.map((dms) => { + return { + id: getFullResourceGroupFromId(dms.id), + name: getResourceGroupFromId(dms.id), + subscription: { + id: dms.properties.subscriptionId + } + }; + }); + + // remove duplicates + this._resourceGroups = this._resourceGroups.filter((v, i, a) => a.findIndex(v2 => (v2.id === v.id)) === i); + sortResourceArrayByName(this._resourceGroups); + } else { + this._resourceGroups = []; + } this._resourceGroups.forEach((rg) => { resourceGroupValues.push({ @@ -1059,8 +1257,8 @@ export class MigrationStateModel implements Model, vscode.Disposable { let virtualMachineValues: azdata.CategoryValue[] = []; try { if (this._azureAccount && subscription && location && resourceGroup) { - this._targetSqlVirtualMachines = (await getAvailableSqlVMs(this._azureAccount, subscription, resourceGroup)).filter((virtualMachine) => { - if (virtualMachine?.location?.toLowerCase() === location?.name?.toLowerCase()) { + this._targetSqlVirtualMachines = (await getAvailableSqlVMs(this._azureAccount, subscription)).filter((virtualMachine) => { + if (virtualMachine?.location?.toLowerCase() === location?.name?.toLowerCase() && getResourceGroupFromId(virtualMachine.id).toLowerCase() === resourceGroup?.name.toLowerCase()) { if (virtualMachine.properties.sqlImageOffer) { return virtualMachine.properties.sqlImageOffer.toLowerCase().includes('-ws'); //filtering out all non windows sql vms. } @@ -1068,6 +1266,7 @@ export class MigrationStateModel implements Model, vscode.Disposable { } return false; }); + virtualMachineValues = this._targetSqlVirtualMachines.map((virtualMachine) => { return { name: virtualMachine.id, @@ -1272,7 +1471,7 @@ export class MigrationStateModel implements Model, vscode.Disposable { let sqlMigrationServiceValues: azdata.CategoryValue[] = []; try { if (this._azureAccount && subscription && resourceGroupName && this._targetServerInstance) { - this._sqlMigrationServices = (await getSqlMigrationServices(this._azureAccount, subscription, resourceGroupName?.toLowerCase(), this._sessionId)).filter(sms => sms.location.toLowerCase() === this._targetServerInstance.location.toLowerCase()); + this._sqlMigrationServices = (await getSqlMigrationServices(this._azureAccount, subscription)).filter(sms => sms.location.toLowerCase() === this._targetServerInstance.location.toLowerCase() && sms.properties.resourceGroup.toLowerCase() === resourceGroupName.toLowerCase()); } else { this._sqlMigrationServices = []; } diff --git a/extensions/sql-migration/src/wizard/databaseBackupPage.ts b/extensions/sql-migration/src/wizard/databaseBackupPage.ts index 44d621e984..52598b84ab 100644 --- a/extensions/sql-migration/src/wizard/databaseBackupPage.ts +++ b/extensions/sql-migration/src/wizard/databaseBackupPage.ts @@ -1261,7 +1261,7 @@ export class DatabaseBackupPage extends MigrationWizardPage { private async loadNetworkStorageResourceGroup(): Promise { this._networkShareStorageAccountResourceGroupDropdown.loading = true; try { - this._networkShareStorageAccountResourceGroupDropdown.values = await this.migrationStateModel.getAzureResourceGroupDropdownValues(this.migrationStateModel._databaseBackup.subscription); + this._networkShareStorageAccountResourceGroupDropdown.values = await this.migrationStateModel.getAzureResourceGroupForStorageAccountsDropdownValues(this.migrationStateModel._databaseBackup.subscription); selectDefaultDropdownValue(this._networkShareStorageAccountResourceGroupDropdown, this.migrationStateModel._databaseBackup?.networkShares[0]?.resourceGroup?.id, false); } catch (error) { logError(TelemetryViews.DatabaseBackupPage, 'ErrorLoadingNetworkStorageResourceGroup', error); @@ -1288,7 +1288,7 @@ export class DatabaseBackupPage extends MigrationWizardPage { private async loadBlobResourceGroup(): Promise { this._blobContainerResourceGroupDropdowns.forEach(v => v.loading = true); try { - const resourceGroupValues = await this.migrationStateModel.getAzureResourceGroupDropdownValues(this.migrationStateModel._databaseBackup.subscription); + const resourceGroupValues = await this.migrationStateModel.getAzureResourceGroupForStorageAccountsDropdownValues(this.migrationStateModel._databaseBackup.subscription); this._blobContainerResourceGroupDropdowns.forEach((dropDown, index) => { dropDown.values = resourceGroupValues; selectDefaultDropdownValue(dropDown, this.migrationStateModel._databaseBackup?.blobs[index]?.resourceGroup?.id, false); diff --git a/extensions/sql-migration/src/wizard/integrationRuntimePage.ts b/extensions/sql-migration/src/wizard/integrationRuntimePage.ts index c16035af31..c91c5effe7 100644 --- a/extensions/sql-migration/src/wizard/integrationRuntimePage.ts +++ b/extensions/sql-migration/src/wizard/integrationRuntimePage.ts @@ -379,7 +379,7 @@ export class IntergrationRuntimePage extends MigrationWizardPage { this._resourceGroupDropdown.loading = true; this._dmsDropdown.loading = true; try { - this._resourceGroupDropdown.values = await this.migrationStateModel.getAzureResourceGroupDropdownValues(this.migrationStateModel._targetSubscription); + this._resourceGroupDropdown.values = await this.migrationStateModel.getAzureResourceGroupForSqlMigrationServicesDropdownValues(this.migrationStateModel._targetSubscription); const resourceGroup = (this.migrationStateModel._sqlMigrationService) ? getFullResourceGroupFromId(this.migrationStateModel._sqlMigrationService?.id) : undefined; diff --git a/extensions/sql-migration/src/wizard/targetSelectionPage.ts b/extensions/sql-migration/src/wizard/targetSelectionPage.ts index cc33302ba9..8c61089976 100644 --- a/extensions/sql-migration/src/wizard/targetSelectionPage.ts +++ b/extensions/sql-migration/src/wizard/targetSelectionPage.ts @@ -252,7 +252,7 @@ export class TargetSelectionPage extends MigrationWizardPage { fireOnTextChange: true, }).component(); - this._disposables.push(this._accountTenantDropdown.onValueChanged(value => { + this._disposables.push(this._accountTenantDropdown.onValueChanged(async (value) => { /** * Replacing all the tenants in azure account with the tenant user has selected. * All azure requests will only run on this tenant from now on @@ -263,6 +263,7 @@ export class TargetSelectionPage extends MigrationWizardPage { if (selectedIndex > -1) { this.migrationStateModel._azureAccount.properties.tenants = [this.migrationStateModel.getTenant(selectedIndex)]; } + await this.populateSubscriptionDropdown(); })); this._accountTenantFlexContainer = this._view.modelBuilder.flexContainer() @@ -480,7 +481,14 @@ export class TargetSelectionPage extends MigrationWizardPage { public async populateResourceGroupDropdown(): Promise { try { this.updateDropdownLoadingStatus(TargetDropDowns.ResourceGroup, true); - this._azureResourceGroupDropdown.values = await this.migrationStateModel.getAzureResourceGroupDropdownValues(this.migrationStateModel._targetSubscription); + switch (this.migrationStateModel._targetType) { + case MigrationTargetType.SQLMI: + this._azureResourceGroupDropdown.values = await this.migrationStateModel.getAzureResourceGroupForManagedInstancesDropdownValues(this.migrationStateModel._targetSubscription); + break; + case MigrationTargetType.SQLVM: + this._azureResourceGroupDropdown.values = await this.migrationStateModel.getAzureResourceGroupForVirtualMachinesDropdownValues(this.migrationStateModel._targetSubscription); + break; + } selectDefaultDropdownValue(this._azureResourceGroupDropdown, this.migrationStateModel._resourceGroup?.id, false); } catch (e) { console.log(e);