Add IR Migration configuration Validation to SQL Migration extension (#21386)

* re-factor and consolidate wizard pages

* validation WIP 11/10

* validate ir dialog

* navigation fixes

* bump version to 1.2.0

* add resource strings and fix navigatin issue

* map validation state to resource string clean up

* address review comments

* fix typos, address review comments

* address review feedback, readability

* fix res string, validation check, col width

* bug fixes, nav, sqldb migration

* fix nav/refresh/visibility issues

* fix nav issues, cancel pending validation items

* update error text / position

* fix localization bug
This commit is contained in:
brian-harris
2022-12-16 14:52:24 -08:00
committed by GitHub
parent 754d70d654
commit 2e240729af
29 changed files with 1993 additions and 692 deletions

View File

@@ -9,6 +9,7 @@ import * as azurecore from 'azurecore';
import * as constants from '../constants/strings';
import { getSessionIdHeader } from './utils';
import { URL } from 'url';
import { MigrationSourceAuthenticationType, MigrationStateModel, NetworkShare } from '../models/stateMachine';
const ARM_MGMT_API_VERSION = '2021-04-01';
const SQL_VM_API_VERSION = '2021-11-01-preview';
@@ -39,17 +40,18 @@ export async function getLocations(account: azdata.Account, subscription: Subscr
const path = `/subscriptions/${subscription.id}/providers/Microsoft.DataMigration?api-version=${ARM_MGMT_API_VERSION}`;
const host = api.getProviderMetadataForAccount(account).settings.armResource?.endpoint;
const dataMigrationResourceProvider = (await api.makeAzureRestRequest(account, subscription, path, azurecore.HttpRequestMethod.GET, undefined, true, host)).response.data;
const sqlMigratonResource = dataMigrationResourceProvider.resourceTypes.find((r: any) => r.resourceType === 'SqlMigrationServices');
const sqlMigrationResourceLocations = sqlMigratonResource.locations;
if (response.errors.length > 0) {
throw new Error(response.errors.toString());
const dataMigrationResourceProvider = (await api.makeAzureRestRequest(account, subscription, path, azurecore.HttpRequestMethod.GET, undefined, true, host))?.response?.data;
const sqlMigratonResource = dataMigrationResourceProvider?.resourceTypes?.find((r: any) => r.resourceType === 'SqlMigrationServices');
const sqlMigrationResourceLocations = sqlMigratonResource?.locations ?? [];
if (response.errors?.length > 0) {
const message = response.errors
.map(err => err.message)
.join(', ');
throw new Error(message);
}
const filteredLocations = response.locations
.filter(loc => sqlMigrationResourceLocations.includes(loc.displayName));
const filteredLocations = response?.locations?.filter(
loc => sqlMigrationResourceLocations.includes(loc.displayName));
sortResourceArrayByName(filteredLocations);
@@ -209,7 +211,10 @@ export async function getAvailableSqlDatabaseServers(account: azdata.Account, su
const host = api.getProviderMetadataForAccount(account).settings.armResource?.endpoint;
const response = await api.makeAzureRestRequest(account, subscription, path, azurecore.HttpRequestMethod.GET, undefined, true, host);
if (response.errors.length > 0) {
throw new Error(response.errors.toString());
const message = response.errors
.map(err => err.message)
.join(', ');
throw new Error(message);
}
sortResourceArrayByName(response.response.data.value);
return response.response.data.value;
@@ -221,7 +226,10 @@ export async function getAvailableSqlDatabases(account: azdata.Account, subscrip
const host = api.getProviderMetadataForAccount(account).settings.armResource?.endpoint;
const response = await api.makeAzureRestRequest(account, subscription, path, azurecore.HttpRequestMethod.GET, undefined, true, host);
if (response.errors.length > 0) {
throw new Error(response.errors.toString());
const message = response.errors
.map(err => err.message)
.join(', ');
throw new Error(message);
}
sortResourceArrayByName(response.response.data.value);
return response.response.data.value;
@@ -234,7 +242,10 @@ export async function getAvailableSqlVMs(account: azdata.Account, subscription:
const response = await api.makeAzureRestRequest(account, subscription, path, azurecore.HttpRequestMethod.GET, undefined, true, host);
if (response.errors.length > 0) {
throw new Error(response.errors.toString());
const message = response.errors
.map(err => err.message)
.join(', ');
throw new Error(message);
}
sortResourceArrayByName(response.response.data.value);
return response.response.data.value;
@@ -283,7 +294,10 @@ export async function getSqlMigrationServiceById(account: azdata.Account, subscr
const host = api.getProviderMetadataForAccount(account).settings.armResource?.endpoint;
const response = await api.makeAzureRestRequest(account, subscription, path, azurecore.HttpRequestMethod.GET, undefined, true, host);
if (response.errors.length > 0) {
throw new Error(response.errors.toString());
const message = response.errors
.map(err => err.message)
.join(', ');
throw new Error(message);
}
response.response.data.properties.resourceGroup = getResourceGroupFromId(response.response.data.id);
return response.response.data;
@@ -295,7 +309,10 @@ export async function getSqlMigrationServicesByResourceGroup(account: azdata.Acc
const host = api.getProviderMetadataForAccount(account).settings.armResource?.endpoint;
const response = await api.makeAzureRestRequest(account, subscription, path, azurecore.HttpRequestMethod.GET, undefined, true, host);
if (response.errors.length > 0) {
throw new Error(response.errors.toString());
const message = response.errors
.map(err => err.message)
.join(', ');
throw new Error(message);
}
sortResourceArrayByName(response.response.data.value);
response.response.data.value.forEach((sms: SqlMigrationService) => {
@@ -310,7 +327,10 @@ export async function getSqlMigrationServices(account: azdata.Account, subscript
const host = api.getProviderMetadataForAccount(account).settings.armResource?.endpoint;
const response = await api.makeAzureRestRequest(account, subscription, path, azurecore.HttpRequestMethod.GET, undefined, true, host);
if (response.errors.length > 0) {
throw new Error(response.errors.toString());
const message = response.errors
.map(err => err.message)
.join(', ');
throw new Error(message);
}
sortResourceArrayByName(response.response.data.value);
response.response.data.value.forEach((sms: SqlMigrationService) => {
@@ -328,7 +348,10 @@ export async function createSqlMigrationService(account: azdata.Account, subscri
};
const response = await api.makeAzureRestRequest(account, subscription, path, azurecore.HttpRequestMethod.PUT, requestBody, true, host, getSessionIdHeader(sessionId));
if (response.errors.length > 0) {
throw new Error(response.errors.toString());
const message = response.errors
.map(err => err.message)
.join(', ');
throw new Error(message);
}
const asyncUrl = response.response.headers['azure-asyncoperation'];
const asyncPath = asyncUrl.replace((new URL(asyncUrl)).origin + '/', ''); // path is everything after the hostname, e.g. the 'test' part of 'https://management.azure.com/test'
@@ -357,7 +380,10 @@ export async function getSqlMigrationServiceAuthKeys(account: azdata.Account, su
const host = api.getProviderMetadataForAccount(account).settings.armResource?.endpoint;
const response = await api.makeAzureRestRequest(account, subscription, path, azurecore.HttpRequestMethod.POST, undefined, true, host);
if (response.errors.length > 0) {
throw new Error(response.errors.toString());
const message = response.errors
.map(err => err.message)
.join(', ');
throw new Error(message);
}
return {
authKey1: response?.response?.data?.authKey1 ?? '',
@@ -375,7 +401,10 @@ export async function regenerateSqlMigrationServiceAuthKey(account: azdata.Accou
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) {
throw new Error(response.errors.toString());
const message = response.errors
.map(err => err.message)
.join(', ');
throw new Error(message);
}
return {
authKey1: response?.response?.data?.authKey1 ?? '',
@@ -387,7 +416,10 @@ export async function getStorageAccountAccessKeys(account: azdata.Account, subsc
const api = await getAzureCoreAPI();
const response = await api.getStorageAccountAccessKey(account, subscription, storageAccount, true);
if (response.errors.length > 0) {
throw new Error(response.errors.toString());
const message = response.errors
.map(err => err.message)
.join(', ');
throw new Error(message);
}
return {
keyName1: response?.keyName1,
@@ -401,7 +433,10 @@ export async function getSqlMigrationServiceMonitoringData(account: azdata.Accou
const host = api.getProviderMetadataForAccount(account).settings.armResource?.endpoint;
const response = await api.makeAzureRestRequest(account, subscription, path, azurecore.HttpRequestMethod.POST, undefined, true, host);
if (response.errors.length > 0) {
throw new Error(response.errors.toString());
const message = response.errors
.map(err => err.message)
.join(', ');
throw new Error(message);
}
return response.response.data;
}
@@ -420,7 +455,10 @@ export async function startDatabaseMigration(
const host = api.getProviderMetadataForAccount(account).settings.armResource?.endpoint;
const response = await api.makeAzureRestRequest(account, subscription, path, azurecore.HttpRequestMethod.PUT, requestBody, true, host, getSessionIdHeader(sessionId));
if (response.errors.length > 0) {
throw new Error(response.errors.toString());
const message = response.errors
.map(err => err.message)
.join(', ');
throw new Error(message);
}
const asyncUrl = response.response.headers['azure-asyncoperation'];
return {
@@ -440,7 +478,10 @@ export async function getMigrationDetails(account: azdata.Account, subscription:
const host = api.getProviderMetadataForAccount(account).settings.armResource?.endpoint;
const response = await api.makeAzureRestRequest(account, subscription, path, azurecore.HttpRequestMethod.GET, undefined, true, host);
if (response.errors.length > 0) {
throw new Error(response.errors.toString());
const message = response.errors
.map(err => err.message)
.join(', ');
throw new Error(message);
}
return response.response.data;
@@ -452,7 +493,10 @@ export async function getServiceMigrations(account: azdata.Account, subscription
const host = api.getProviderMetadataForAccount(account).settings.armResource?.endpoint;
const response = await api.makeAzureRestRequest(account, subscription, path, azurecore.HttpRequestMethod.GET, undefined, true, host);
if (response.errors.length > 0) {
throw new Error(response.errors.toString());
const message = response.errors
.map(err => err.message)
.join(', ');
throw new Error(message);
}
return response.response.data.value;
@@ -465,7 +509,10 @@ export async function getMigrationTargetInstance(account: azdata.Account, subscr
const host = api.getProviderMetadataForAccount(account).settings.armResource?.endpoint;
const response = await api.makeAzureRestRequest(account, subscription, path, azurecore.HttpRequestMethod.GET, undefined, true, host);
if (response.errors.length > 0) {
throw new Error(response.errors.toString());
const message = response.errors
.map(err => err.message)
.join(', ');
throw new Error(message);
}
return response.response.data;
@@ -477,7 +524,10 @@ export async function getMigrationAsyncOperationDetails(account: azdata.Account,
const host = api.getProviderMetadataForAccount(account).settings.armResource?.endpoint;
const response = await api.makeAzureRestRequest(account, subscription, path, azurecore.HttpRequestMethod.GET, undefined, true, host);
if (response.errors.length > 0) {
throw new Error(response.errors.toString());
const message = response.errors
.map(err => err.message)
.join(', ');
throw new Error(message);
}
return response.response.data;
}
@@ -489,7 +539,10 @@ export async function startMigrationCutover(account: azdata.Account, subscriptio
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) {
throw new Error(response.errors.toString());
const message = response.errors
.map(err => err.message)
.join(', ');
throw new Error(message);
}
return response.response.data.value;
}
@@ -501,7 +554,10 @@ export async function stopMigration(account: azdata.Account, subscription: Subsc
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) {
throw new Error(response.errors.toString());
const message = response.errors
.map(err => err.message)
.join(', ');
throw new Error(message);
}
}
@@ -510,16 +566,140 @@ export async function getLocationDisplayName(location: string): Promise<string>
return api.getRegionDisplayName(location);
}
export async function validateIrSqlDatabaseMigrationSettings(
migration: MigrationStateModel,
sourceServerName: string,
trustServerCertificate: boolean,
sourceDatabaseName: string,
targetDatabaseName: string,
testIrOnline: boolean = true,
testSourceConnectivity: boolean = true,
testTargetConnectivity: boolean = true): Promise<ValdiateIrDatabaseMigrationResponse> {
const api = await getAzureCoreAPI();
const account = migration._azureAccount;
const subscription = migration._targetSubscription;
const serviceId = migration._sqlMigrationService?.id;
const host = api.getProviderMetadataForAccount(account).settings.armResource?.endpoint;
const path = encodeURI(`${serviceId}/validateIr?api-version=${DMSV2_API_VERSION}`);
const targetDatabaseServer = migration._targetServerInstance as AzureSqlDatabaseServer;
const requestBody: ValidateIrSqlDatabaseMigrationRequest = {
sourceDatabaseName: sourceDatabaseName,
targetDatabaseName: targetDatabaseName,
kind: AzureResourceKind.SQLDB,
validateIntegrationRuntimeOnline: testIrOnline,
sourceSqlConnection: {
testConnectivity: testSourceConnectivity,
dataSource: sourceServerName,
userName: migration._sqlServerUsername,
password: migration._sqlServerPassword,
authentication: migration._authenticationType,
trustServerCertificate: trustServerCertificate,
// encryptConnection: true,
},
targetSqlConnection: {
testConnectivity: testTargetConnectivity,
dataSource: targetDatabaseServer.properties.fullyQualifiedDomainName,
userName: migration._targetUserName,
password: migration._targetPassword,
encryptConnection: true,
trustServerCertificate: false,
authentication: MigrationSourceAuthenticationType.Sql,
}
};
const response = await api.makeAzureRestRequest(
account,
subscription,
path,
azurecore.HttpRequestMethod.POST,
requestBody,
true,
host);
if (response.errors.length > 0) {
throw new Error(response.errors.map(e => e.message).join(','));
}
return response.response.data;
}
export async function validateIrDatabaseMigrationSettings(
migration: MigrationStateModel,
sourceServerName: string,
trustServerCertificate: boolean,
sourceDatabaseName: string,
networkShare: NetworkShare,
testIrOnline: boolean = true,
testSourceLocationConnectivity: boolean = true,
testSourceConnectivity: boolean = true,
testBlobConnectivity: boolean = true): Promise<ValdiateIrDatabaseMigrationResponse> {
const api = await getAzureCoreAPI();
const account = migration._azureAccount;
const subscription = migration._targetSubscription;
const serviceId = migration._sqlMigrationService?.id;
const host = api.getProviderMetadataForAccount(account).settings.armResource?.endpoint;
const path = encodeURI(`${serviceId}/validateIr?api-version=${DMSV2_API_VERSION}`);
const storage = await getStorageAccountAccessKeys(account, subscription, networkShare.storageAccount);
const requestBody: ValdiateIrDatabaseMigrationRequest = {
sourceDatabaseName: sourceDatabaseName ?? '',
kind: migration.isSqlMiTarget
? AzureResourceKind.SQLMI
: AzureResourceKind.SQLVM,
validateIntegrationRuntimeOnline: testIrOnline,
backupConfiguration: {
sourceLocation: {
testConnectivity: testSourceLocationConnectivity,
fileShare: {
path: networkShare.networkShareLocation,
username: networkShare.windowsUser,
password: networkShare.password,
},
},
targetLocation: {
testConnectivity: testBlobConnectivity,
accountKey: storage?.keyName1,
storageAccountResourceId: networkShare.storageAccount?.id
},
},
sourceSqlConnection: {
testConnectivity: testSourceConnectivity,
dataSource: sourceServerName,
userName: migration._sqlServerUsername,
password: migration._sqlServerPassword,
trustServerCertificate: trustServerCertificate,
encryptConnection: true,
authentication: migration._authenticationType,
}
};
const response = await api.makeAzureRestRequest(
account,
subscription,
path,
azurecore.HttpRequestMethod.POST,
requestBody,
true,
host);
if (response.errors.length > 0) {
throw new Error(response.errors.map(e => e.message).join(','));
}
return response.response.data;
}
type SortableAzureResources = AzureProduct | azurecore.azureResource.FileShare | azurecore.azureResource.BlobContainer | azurecore.azureResource.Blob | azurecore.azureResource.AzureResourceSubscription | SqlMigrationService;
export function sortResourceArrayByName(resourceArray: SortableAzureResources[]): void {
if (!resourceArray) {
return;
}
resourceArray.sort((a: SortableAzureResources, b: SortableAzureResources) => {
if (a.name.toLowerCase() < b.name.toLowerCase()) {
if (a?.name?.toLowerCase() < b?.name?.toLowerCase()) {
return -1;
}
if (a.name.toLowerCase() > b.name.toLowerCase()) {
if (a?.name?.toLowerCase() > b?.name?.toLowerCase()) {
return 1;
}
return 0;
@@ -640,8 +820,100 @@ export interface StartDatabaseMigrationRequest {
export interface StartDatabaseMigrationResponse {
status: number,
databaseMigration: DatabaseMigration
asyncUrl: string
databaseMigration: DatabaseMigration,
asyncUrl: string,
}
export enum AzureResourceKind {
SQLDB = 'SqlDb',
SQLMI = 'SqlMi',
SQLVM = 'SqlVm',
}
export interface ValidateIrSqlDatabaseMigrationRequest {
sourceDatabaseName: string,
targetDatabaseName: string,
kind: string,
validateIntegrationRuntimeOnline?: boolean,
sourceSqlConnection: {
testConnectivity?: boolean,
dataSource: string,
userName: string,
password: string,
encryptConnection?: boolean,
trustServerCertificate?: boolean,
authentication: string,
},
targetSqlConnection: {
testConnectivity?: boolean,
dataSource: string,
userName: string,
password: string,
encryptConnection?: boolean,
trustServerCertificate?: boolean,
authentication: string,
},
}
export interface ValdiateIrDatabaseMigrationRequest {
sourceDatabaseName: string,
kind: string,
validateIntegrationRuntimeOnline?: boolean,
backupConfiguration: {
targetLocation: {
testConnectivity?: boolean,
storageAccountResourceId?: string,
accountKey?: string,
},
sourceLocation: {
testConnectivity?: boolean,
fileShare: {
path: string,
username: string,
password: string,
}
}
},
sourceSqlConnection: {
testConnectivity?: boolean,
dataSource?: string,
userName?: string,
password?: string,
encryptConnection?: boolean,
trustServerCertificate?: boolean,
authentication?: string,
},
}
export interface ValidationError {
code: string,
message: string,
}
export interface ValdiateIrDatabaseMigrationResponse {
kind: string,
sourceDatabaseName: string,
sourceSqlConnection: {
testConnectivity: boolean,
encryptConnection: true,
trustServerCertificate: false,
dataSource: string,
},
backupConfiguration: {
sourceLocation: {
testConnectivity: boolean,
fileShare: {
path: string, //?
},
},
targetLocation: {
testConnectivity: boolean,
storageAccountResourceId: string,
},
},
succeeded: boolean,
errors: ValidationError[],
validateIntegrationRuntimeOnline: boolean,
}
export interface DatabaseMigration {

View File

@@ -99,14 +99,14 @@ function getSqlDbConnectionProfile(
databaseName: databaseName,
userName: userName,
password: password,
authenticationType: 'SqlLogin',
authenticationType: azdata.connection.AuthenticationType.SqlLogin,
savePassword: false,
saveProfile: false,
options: {
conectionName: '',
server: serverName,
database: databaseName,
authenticationType: 'SqlLogin',
authenticationType: azdata.connection.AuthenticationType.SqlLogin,
user: userName,
password: password,
connectionTimeout: 60,
@@ -137,7 +137,7 @@ function getConnectionProfile(
azureResourceId: azureResourceId,
userName: userName,
password: password,
authenticationType: 'SqlLogin', // TODO: use azdata.connection.AuthenticationType.SqlLogin after next ADS release
authenticationType: azdata.connection.AuthenticationType.SqlLogin,
savePassword: false,
groupFullName: connectId,
groupId: connectId,
@@ -146,7 +146,7 @@ function getConnectionProfile(
options: {
conectionName: connectId,
server: serverName,
authenticationType: 'SqlLogin',
authenticationType: azdata.connection.AuthenticationType.SqlLogin,
user: userName,
password: password,
connectionTimeout: 60,

View File

@@ -204,10 +204,11 @@ export function selectDefaultDropdownValue(dropDown: DropDownComponent, value?:
if (dropDown.values && dropDown.values.length > 0) {
let selectedIndex;
if (value) {
const searchValue = value.toLowerCase();
if (useDisplayName) {
selectedIndex = dropDown.values.findIndex((v: any) => (v as CategoryValue)?.displayName?.toLowerCase() === value.toLowerCase());
selectedIndex = dropDown.values.findIndex((v: any) => (v as CategoryValue)?.displayName?.toLowerCase() === searchValue);
} else {
selectedIndex = dropDown.values.findIndex((v: any) => (v as CategoryValue)?.name?.toLowerCase() === value.toLowerCase());
selectedIndex = dropDown.values.findIndex((v: any) => (v as CategoryValue)?.name?.toLowerCase() === searchValue);
}
} else {
selectedIndex = -1;
@@ -220,7 +221,7 @@ export function selectDefaultDropdownValue(dropDown: DropDownComponent, value?:
export function selectDropDownIndex(dropDown: DropDownComponent, index: number): void {
if (dropDown.values && dropDown.values.length > 0) {
if (index >= 0 && index <= dropDown.values.length - 1) {
if (index >= 0 && index < dropDown.values.length) {
dropDown.value = dropDown.values[index] as CategoryValue;
return;
}