mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-01-23 01:25:38 -05:00
Fixing bugs for migration extension private preview 1. (#14872)
* Fixing Database backup page target layout * Filtering out Azure sql db issues from assessment results Correcting the database count for issued databases in sku rec page. * Adding copy migration details button to migration status * Adding start migration button to toolbar * Fixing a syntax error in package.json * Adding rg and location to target selection page Filtering storage account by target location. * Fixing dashboard title to azure sql migration * Not making assessment targets selected by default. * Adding tooltip for database and instance table items. * Fixing duplicate task widget * Some fixes mentioned in the PR Localizing button text renaming a var changing null to undefined. * Adding enum for Migration target types * Fixing a critical multi db migration bug because of unhandled race condition * Adding Azure location api to azure core * Adding source database info in status
This commit is contained in:
@@ -26,6 +26,20 @@ export async function getSubscriptions(account: azdata.Account): Promise<Subscri
|
||||
return subscriptions.subscriptions;
|
||||
}
|
||||
|
||||
export async function getLocations(account: azdata.Account, subscription: Subscription): Promise<azureResource.AzureLocation[]> {
|
||||
const api = await getAzureCoreAPI();
|
||||
const response = await api.getLocations(account, subscription, true);
|
||||
if (response.errors.length > 0) {
|
||||
throw new Error(response.errors.toString());
|
||||
}
|
||||
sortResourceArrayByName(response.locations);
|
||||
const supportedLocations = ['eastus2', 'eastus2euap'];
|
||||
const filteredLocations = response.locations.filter(loc => {
|
||||
return supportedLocations.includes(loc.name);
|
||||
});
|
||||
return filteredLocations;
|
||||
}
|
||||
|
||||
export type AzureProduct = azureResource.AzureGraphResource;
|
||||
|
||||
export async function getResourceGroups(account: azdata.Account, subscription: Subscription): Promise<azureResource.AzureResourceResourceGroup[]> {
|
||||
@@ -65,9 +79,9 @@ export type SqlVMServer = {
|
||||
tenantId: string,
|
||||
subscriptionId: string
|
||||
};
|
||||
export async function getAvailableSqlVMs(account: azdata.Account, subscription: Subscription): Promise<SqlVMServer[]> {
|
||||
export async function getAvailableSqlVMs(account: azdata.Account, subscription: Subscription, resourceGroup: azureResource.AzureResourceResourceGroup): Promise<SqlVMServer[]> {
|
||||
const api = await getAzureCoreAPI();
|
||||
const path = `/subscriptions/${subscription.id}/providers/Microsoft.SqlVirtualMachine/sqlVirtualMachines?api-version=2017-03-01-preview`;
|
||||
const path = `/subscriptions/${subscription.id}/resourceGroups/${resourceGroup.name}/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());
|
||||
|
||||
@@ -87,10 +87,12 @@ export const DATABASE_BACKUP_MIGRATION_MODE_OFFLINE_LABEL = localize('sql.migrat
|
||||
export const DATABASE_BACKUP_EMAIL_NOTIFICATION_LABEL = localize('sql.migration.database.backup.email.notification.label', "Email notifications");
|
||||
export const DATABASE_BACKUP_EMAIL_NOTIFICATION_CHECKBOX_LABEL = localize('sql.migration.database.backup.email.notification.checkbox.label', "Notify me when migration is complete");
|
||||
export const NO_SUBSCRIPTIONS_FOUND = localize('sql.migration.no.subscription.found', "No subscription found");
|
||||
export const NO_LOCATION_FOUND = localize('sql.migration.no.location.found', "No location found");
|
||||
export const NO_STORAGE_ACCOUNT_FOUND = localize('sql.migration.no.storageAccount.found', "No storage account found");
|
||||
export const NO_FILESHARES_FOUND = localize('sql.migration.no.fileShares.found', "No file shares found");
|
||||
export const NO_BLOBCONTAINERS_FOUND = localize('sql.migration.no.blobContainers.found', "No blob containers found");
|
||||
export const INVALID_SUBSCRIPTION_ERROR = localize('sql.migration.invalid.subscription.error', "Please select a valid subscription to proceed.");
|
||||
export const INVALID_LOCATION_ERROR = localize('sql.migration.invalid.location.error', "Please select a valid location to proceed.");
|
||||
export const INVALID_STORAGE_ACCOUNT_ERROR = localize('sql.migration.invalid.storageAccount.error', "Please select a valid storage account to proceed.");
|
||||
export const INVALID_FILESHARE_ERROR = localize('sql.migration.invalid.fileShare.error', "Please select a valid file share to proceed.");
|
||||
export const INVALID_BLOBCONTAINER_ERROR = localize('sql.migration.invalid.blobContainer.error', "Please select a valid blob container to proceed.");
|
||||
@@ -108,7 +110,7 @@ export function TARGET_FILE_SHARE(dbName: string): string {
|
||||
export function TARGET_BLOB_CONTAINER(dbName: string): string {
|
||||
return localize('sql.migration.blob.container', "Select the container that contains the backup files for ‘{0}’", dbName);
|
||||
}
|
||||
export const ENTER_NETWORK_SHARE_INFORMATION = localize('sql.migration.enter.network.share.information', "Enter network share path information for selected databases");
|
||||
export const ENTER_NETWORK_SHARE_INFORMATION = localize('sql.migration.enter.network.share.information', "Enter target names for selected databases");
|
||||
export const ENTER_BLOB_CONTAINER_INFORMATION = localize('sql.migration.blob.container.information', "Enter the target name and select the blob container location for selected databases");
|
||||
export const ENTER_FILE_SHARE_INFORMATION = localize('sql.migration.enter.file.share.information', "Enter the target name and select the file share location of selected databases");
|
||||
|
||||
@@ -152,7 +154,7 @@ export function SERVICE_NOT_READY(serviceName: string): string {
|
||||
export function SERVICE_READY(serviceName: string, host: string): string {
|
||||
return localize('sql.migration.service.ready', "Azure Data Migration Service '{0}' is connected to self-hosted Integration Runtime running on the node - {1}", serviceName, host);
|
||||
}
|
||||
export const RESOURCE_GROUP_NOT_FOUND = localize('sql.migration.resource.group.not.found', "No resource Groups found");
|
||||
export const RESOURCE_GROUP_NOT_FOUND = localize('sql.migration.resource.group.not.found', "No resource groups found");
|
||||
export const INVALID_RESOURCE_GROUP_ERROR = localize('sql.migration.invalid.resourceGroup.error', "Please select a valid resource group to proceed.");
|
||||
export const INVALID_REGION_ERROR = localize('sql.migration.invalid.region.error', "Please select a valid region to proceed.");
|
||||
export const INVALID_SERVICE_NAME_ERROR = localize('sql.migration.invalid.service.name.error', "Please enter a valid name for the Migration Service.");
|
||||
@@ -230,6 +232,7 @@ export const EASTUS2EUAP = localize('sql.migration.eastus2euap', 'East US 2 EUAP
|
||||
|
||||
//Migration cutover dialog
|
||||
export const MIGRATION_CUTOVER = localize('sql.migration.cutover', "Migration cutover");
|
||||
export const SOURCE_DATABASE = localize('sql.migration.source.database', "Source database");
|
||||
export const SOURCE_SERVER = localize('sql.migration.source.server', "Source server");
|
||||
export const SOURCE_VERSION = localize('sql.migration.source.version', "Source version");
|
||||
export const TARGET_SERVER = localize('sql.migration.target.server', "Target server");
|
||||
@@ -255,7 +258,7 @@ export function ACTIVE_BACKUP_FILES_ITEMS(fileCount: number) {
|
||||
return localize('sql.migration.active.backup.files.multiple.items', "Active Backup files ({0} items)", fileCount);
|
||||
}
|
||||
}
|
||||
|
||||
export const COPY_MIGRATION_DETAILS = localize('sql.migration.copy.migration.details', "Copy Migration Details");
|
||||
|
||||
//Migration status dialog
|
||||
export const SEARCH_FOR_MIGRATIONS = localize('sql.migration.search.for.migration', "Search for migrations");
|
||||
|
||||
@@ -166,14 +166,14 @@ export class SqlDatabaseTree {
|
||||
{
|
||||
displayName: constants.INSTANCE,
|
||||
valueType: azdata.DeclarativeDataType.component,
|
||||
width: 150,
|
||||
width: 130,
|
||||
isReadOnly: true,
|
||||
headerCssStyles: headerLeft
|
||||
},
|
||||
{
|
||||
displayName: constants.WARNINGS,
|
||||
valueType: azdata.DeclarativeDataType.string,
|
||||
width: 50,
|
||||
width: 30,
|
||||
isReadOnly: true,
|
||||
headerCssStyles: headerRight
|
||||
}
|
||||
@@ -595,9 +595,9 @@ export class SqlDatabaseTree {
|
||||
|
||||
public selectedDbs(): string[] {
|
||||
let result: string[] = [];
|
||||
this._databaseTable.dataValues?.forEach((arr) => {
|
||||
this._databaseTable.dataValues?.forEach((arr, index) => {
|
||||
if (arr[0].value === true) {
|
||||
result.push(arr[1].value.toString());
|
||||
result.push(this._dbNames[index]);
|
||||
}
|
||||
});
|
||||
return result;
|
||||
@@ -615,8 +615,6 @@ export class SqlDatabaseTree {
|
||||
);
|
||||
});
|
||||
this._assessmentResultsTable.dataValues = assessmentResults;
|
||||
this._selectedIssue = this._activeIssues[0];
|
||||
this.refreshAssessmentDetails();
|
||||
}
|
||||
|
||||
public refreshAssessmentDetails(): void {
|
||||
@@ -734,14 +732,8 @@ export class SqlDatabaseTree {
|
||||
);
|
||||
});
|
||||
}
|
||||
this._dbName.value = this._serverName;
|
||||
this._instanceTable.dataValues = instanceTableValues;
|
||||
this._databaseTable.dataValues = databaseTableValues;
|
||||
if (this._targetType === MigrationTargetType.SQLMI) {
|
||||
this._activeIssues = this._model._assessmentResults.issues;
|
||||
this._selectedIssue = this._model._assessmentResults?.issues[0];
|
||||
this.refreshResults();
|
||||
}
|
||||
}
|
||||
|
||||
private createIconTextCell(icon: IconPath, text: string): azdata.FlexContainer {
|
||||
@@ -755,8 +747,10 @@ export class SqlDatabaseTree {
|
||||
}).component();
|
||||
const textComponent = this._view.modelBuilder.text().withProps({
|
||||
value: text,
|
||||
title: text,
|
||||
CSSStyles: {
|
||||
'margin': '0px'
|
||||
'margin': '0px',
|
||||
'width': '110px'
|
||||
}
|
||||
}).component();
|
||||
|
||||
|
||||
@@ -10,6 +10,8 @@ import { MigrationCutoverDialogModel } from './migrationCutoverDialogModel';
|
||||
import * as loc from '../../constants/strings';
|
||||
import { getSqlServerName } from '../../api/utils';
|
||||
import { EOL } from 'os';
|
||||
import * as vscode from 'vscode';
|
||||
|
||||
export class MigrationCutoverDialog {
|
||||
private _dialogObject!: azdata.window.Dialog;
|
||||
private _view!: azdata.ModelView;
|
||||
@@ -20,9 +22,11 @@ export class MigrationCutoverDialog {
|
||||
private _refreshButton!: azdata.ButtonComponent;
|
||||
private _cancelButton!: azdata.ButtonComponent;
|
||||
private _refreshLoader!: azdata.LoadingComponent;
|
||||
private _copyDatabaseMigrationDetails!: azdata.ButtonComponent;
|
||||
|
||||
private _serverName!: azdata.TextComponent;
|
||||
private _serverVersion!: azdata.TextComponent;
|
||||
private _sourceDatabase!: azdata.TextComponent;
|
||||
private _targetServer!: azdata.TextComponent;
|
||||
private _targetVersion!: azdata.TextComponent;
|
||||
private _migrationStatus!: azdata.TextComponent;
|
||||
@@ -46,9 +50,11 @@ export class MigrationCutoverDialog {
|
||||
let tab = azdata.window.createTab('');
|
||||
tab.registerContent(async (view: azdata.ModelView) => {
|
||||
this._view = view;
|
||||
const sourceDatabase = this.createInfoField(loc.SOURCE_DATABASE, '');
|
||||
const sourceDetails = this.createInfoField(loc.SOURCE_SERVER, '');
|
||||
const sourceVersion = this.createInfoField(loc.SOURCE_VERSION, '');
|
||||
|
||||
this._sourceDatabase = sourceDatabase.text;
|
||||
this._serverName = sourceDetails.text;
|
||||
this._serverVersion = sourceVersion.text;
|
||||
|
||||
@@ -56,6 +62,11 @@ export class MigrationCutoverDialog {
|
||||
flexFlow: 'column'
|
||||
}).component();
|
||||
|
||||
flexServer.addItem(sourceDatabase.flexContainer, {
|
||||
CSSStyles: {
|
||||
'width': '150px'
|
||||
}
|
||||
});
|
||||
flexServer.addItem(sourceDetails.flexContainer, {
|
||||
CSSStyles: {
|
||||
'width': '150px'
|
||||
@@ -336,6 +347,27 @@ export class MigrationCutoverDialog {
|
||||
}
|
||||
});
|
||||
|
||||
this._copyDatabaseMigrationDetails = this._view.modelBuilder.button().withProps({
|
||||
iconPath: IconPathHelper.copy,
|
||||
iconHeight: '16px',
|
||||
iconWidth: '16px',
|
||||
label: loc.COPY_MIGRATION_DETAILS,
|
||||
height: '55px',
|
||||
width: '100px'
|
||||
}).component();
|
||||
|
||||
this._copyDatabaseMigrationDetails.onDidClick(async (e) => {
|
||||
await this.refreshStatus();
|
||||
vscode.env.clipboard.writeText(JSON.stringify(this._model.migrationStatus, undefined, 2));
|
||||
});
|
||||
|
||||
header.addItem(this._copyDatabaseMigrationDetails, {
|
||||
flex: '0',
|
||||
CSSStyles: {
|
||||
'width': '100px'
|
||||
}
|
||||
});
|
||||
|
||||
this._refreshLoader = this._view.modelBuilder.loadingComponent().withProps({
|
||||
loading: false,
|
||||
height: '55px'
|
||||
@@ -344,6 +376,7 @@ export class MigrationCutoverDialog {
|
||||
header.addItem(this._refreshLoader, {
|
||||
flex: '0'
|
||||
});
|
||||
|
||||
return header;
|
||||
}
|
||||
|
||||
@@ -363,6 +396,7 @@ export class MigrationCutoverDialog {
|
||||
};
|
||||
const sqlServerInfo = await azdata.connection.getServerInfo(this._model._migration.sourceConnectionProfile.connectionId);
|
||||
const sqlServerName = this._model._migration.sourceConnectionProfile.serverName;
|
||||
const sourceDatabaseName = this._model._migration.migrationContext.properties.sourceDatabaseName;
|
||||
const versionName = getSqlServerName(sqlServerInfo.serverMajorVersion!);
|
||||
const sqlServerVersion = versionName ? versionName : sqlServerInfo.serverVersion;
|
||||
const targetServerName = this._model._migration.targetManagedInstance.name;
|
||||
@@ -402,6 +436,7 @@ export class MigrationCutoverDialog {
|
||||
}
|
||||
});
|
||||
|
||||
this._sourceDatabase.value = sourceDatabaseName;
|
||||
this._serverName.value = sqlServerName;
|
||||
this._serverVersion.value = `${sqlServerVersion}
|
||||
${sqlServerInfo.serverVersion}`;
|
||||
@@ -414,7 +449,7 @@ export class MigrationCutoverDialog {
|
||||
|
||||
this._lastAppliedLSN.value = lastAppliedSSN!;
|
||||
this._lastAppliedBackupFile.value = this._model.migrationStatus.properties.migrationStatusDetails?.lastRestoredFilename;
|
||||
this._lastAppliedBackupTakenOn.value = new Date(lastAppliedBackupFileTakenOn!).toLocaleString();
|
||||
this._lastAppliedBackupTakenOn.value = lastAppliedBackupFileTakenOn! ? new Date(lastAppliedBackupFileTakenOn).toLocaleString() : '';
|
||||
|
||||
this._fileCount.value = loc.ACTIVE_BACKUP_FILES_ITEMS(tableData.length);
|
||||
|
||||
|
||||
@@ -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 } from '../api/azure';
|
||||
import { getAvailableManagedInstanceProducts, getAvailableStorageAccounts, getBlobContainers, getFileShares, getSqlMigrationServices, getSubscriptions, SqlMigrationService, SqlManagedInstance, startDatabaseMigration, StartDatabaseMigrationRequest, StorageAccount, getAvailableSqlVMs, SqlVMServer, getLocations, getResourceGroups } from '../api/azure';
|
||||
import { SKURecommendations } from './externalContract';
|
||||
import * as constants from '../constants/strings';
|
||||
import { MigrationLocalStorage } from './migrationLocalStorage';
|
||||
@@ -34,8 +34,9 @@ export enum State {
|
||||
}
|
||||
|
||||
export enum MigrationTargetType {
|
||||
SQLVM = 'sqlvm',
|
||||
SQLMI = 'sqlmi'
|
||||
SQLVM = 'AzureSqlVirtualMachine',
|
||||
SQLMI = 'AzureSqlManagedInstance',
|
||||
SQLDB = 'AzureSqlDatabase'
|
||||
}
|
||||
|
||||
export enum MigrationSourceAuthenticationType {
|
||||
@@ -63,7 +64,7 @@ export interface NetworkShare {
|
||||
export interface DatabaseBackupModel {
|
||||
migrationCutover: MigrationCutover;
|
||||
networkContainerType: NetworkContainerType;
|
||||
networkShareLocations: string[];
|
||||
networkShareLocation: string;
|
||||
windowsUser: string;
|
||||
password: string;
|
||||
subscription: azureResource.AzureResourceSubscription;
|
||||
@@ -100,6 +101,10 @@ export class MigrationStateModel implements Model, vscode.Disposable {
|
||||
public _subscriptions!: azureResource.AzureResourceSubscription[];
|
||||
|
||||
public _targetSubscription!: azureResource.AzureResourceSubscription;
|
||||
public _locations!: azureResource.AzureLocation[];
|
||||
public _location!: azureResource.AzureLocation;
|
||||
public _resourceGroups!: azureResource.AzureResourceResourceGroup[];
|
||||
public _resourceGroup!: azureResource.AzureResourceResourceGroup;
|
||||
public _targetManagedInstances!: SqlManagedInstance[];
|
||||
public _targetSqlVirtualMachines!: SqlVMServer[];
|
||||
public _targetServerInstance!: SqlManagedInstance;
|
||||
@@ -173,11 +178,13 @@ export class MigrationStateModel implements Model, vscode.Disposable {
|
||||
});
|
||||
|
||||
assessmentResults?.items.forEach((item) => {
|
||||
const dbIndex = serverDatabases.indexOf(item.databaseName);
|
||||
if (dbIndex === -1) {
|
||||
serverLevelAssessments.push(item);
|
||||
} else {
|
||||
databaseLevelAssessments[dbIndex].issues.push(item);
|
||||
if (item.appliesToMigrationTargetPlatform === MigrationTargetType.SQLMI) {
|
||||
const dbIndex = serverDatabases.indexOf(item.databaseName);
|
||||
if (dbIndex === -1) {
|
||||
serverLevelAssessments.push(item);
|
||||
} else {
|
||||
databaseLevelAssessments[dbIndex].issues.push(item);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -313,10 +320,88 @@ export class MigrationStateModel implements Model, vscode.Disposable {
|
||||
return this._subscriptions[index];
|
||||
}
|
||||
|
||||
public async getManagedInstanceValues(subscription: azureResource.AzureResourceSubscription): Promise<azdata.CategoryValue[]> {
|
||||
public async getAzureLocationDropdownValues(subscription: azureResource.AzureResourceSubscription): Promise<azdata.CategoryValue[]> {
|
||||
let locationValues: azdata.CategoryValue[] = [];
|
||||
try {
|
||||
this._locations = await getLocations(this._azureAccount, subscription);
|
||||
this._locations.forEach((loc) => {
|
||||
locationValues.push({
|
||||
name: loc.name,
|
||||
displayName: loc.displayName
|
||||
});
|
||||
});
|
||||
|
||||
if (locationValues.length === 0) {
|
||||
locationValues = [
|
||||
{
|
||||
displayName: constants.INVALID_LOCATION_ERROR,
|
||||
name: ''
|
||||
}
|
||||
];
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
locationValues = [
|
||||
{
|
||||
displayName: constants.INVALID_LOCATION_ERROR,
|
||||
name: ''
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
return locationValues;
|
||||
}
|
||||
|
||||
public getLocation(index: number): azureResource.AzureLocation {
|
||||
return this._locations[index];
|
||||
}
|
||||
|
||||
public async getAzureResourceGroupDropdownValues(subscription: azureResource.AzureResourceSubscription): Promise<azdata.CategoryValue[]> {
|
||||
let resourceGroupValues: azdata.CategoryValue[] = [];
|
||||
try {
|
||||
this._resourceGroups = await getResourceGroups(this._azureAccount, subscription);
|
||||
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 getAzureResourceGroup(index: number): azureResource.AzureResourceResourceGroup {
|
||||
return this._resourceGroups[index];
|
||||
}
|
||||
|
||||
|
||||
public async getManagedInstanceValues(subscription: azureResource.AzureResourceSubscription, location: azureResource.AzureLocation, resourceGroup: azureResource.AzureResourceResourceGroup): Promise<azdata.CategoryValue[]> {
|
||||
let managedInstanceValues: azdata.CategoryValue[] = [];
|
||||
try {
|
||||
this._targetManagedInstances = await getAvailableManagedInstanceProducts(this._azureAccount, subscription);
|
||||
this._targetManagedInstances = (await getAvailableManagedInstanceProducts(this._azureAccount, subscription)).filter((mi) => {
|
||||
if (mi.location === location.name && mi.resourceGroup?.toLocaleLowerCase() === resourceGroup.name.toLowerCase()) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
this._targetManagedInstances.forEach((managedInstance) => {
|
||||
managedInstanceValues.push({
|
||||
name: managedInstance.id,
|
||||
@@ -348,13 +433,19 @@ export class MigrationStateModel implements Model, vscode.Disposable {
|
||||
return this._targetManagedInstances[index];
|
||||
}
|
||||
|
||||
public async getSqlVirtualMachineValues(subscription: azureResource.AzureResourceSubscription): Promise<azdata.CategoryValue[]> {
|
||||
public async getSqlVirtualMachineValues(subscription: azureResource.AzureResourceSubscription, location: azureResource.AzureLocation, resourceGroup: azureResource.AzureResourceResourceGroup): Promise<azdata.CategoryValue[]> {
|
||||
let virtualMachineValues: azdata.CategoryValue[] = [];
|
||||
try {
|
||||
this._targetSqlVirtualMachines = await getAvailableSqlVMs(this._azureAccount, subscription);
|
||||
virtualMachineValues = this._targetSqlVirtualMachines.filter((virtualMachine) => {
|
||||
return virtualMachine.properties.sqlImageOffer.toLowerCase().includes('-ws'); //filtering out all non windows sql vms.
|
||||
}).map((virtualMachine) => {
|
||||
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 false;
|
||||
});
|
||||
virtualMachineValues = this._targetSqlVirtualMachines.map((virtualMachine) => {
|
||||
return {
|
||||
name: virtualMachine.id,
|
||||
displayName: `${virtualMachine.name}`
|
||||
@@ -388,7 +479,7 @@ export class MigrationStateModel implements Model, vscode.Disposable {
|
||||
public async getStorageAccountValues(subscription: azureResource.AzureResourceSubscription): Promise<azdata.CategoryValue[]> {
|
||||
let storageAccountValues: azdata.CategoryValue[] = [];
|
||||
try {
|
||||
this._storageAccounts = await getAvailableStorageAccounts(this._azureAccount, subscription);
|
||||
this._storageAccounts = (await getAvailableStorageAccounts(this._azureAccount, subscription)).filter(sa => sa.location === this._targetServerInstance.location);
|
||||
this._storageAccounts.forEach((storageAccount) => {
|
||||
storageAccountValues.push({
|
||||
name: storageAccount.id,
|
||||
@@ -564,17 +655,16 @@ export class MigrationStateModel implements Model, vscode.Disposable {
|
||||
}
|
||||
};
|
||||
|
||||
this._migrationDbs.forEach(async (db, index) => {
|
||||
|
||||
requestBody.properties.sourceDatabaseName = db;
|
||||
for (let i = 0; i < this._migrationDbs.length; i++) {
|
||||
requestBody.properties.sourceDatabaseName = this._migrationDbs[i];
|
||||
try {
|
||||
requestBody.properties.backupConfiguration.sourceLocation.fileShare!.path = this._databaseBackup.networkShareLocations[index];
|
||||
requestBody.properties.backupConfiguration.sourceLocation.fileShare!.path = this._databaseBackup.networkShareLocation;
|
||||
const response = await startDatabaseMigration(
|
||||
this._azureAccount,
|
||||
this._targetSubscription,
|
||||
this._sqlMigrationService?.properties.location!,
|
||||
this._targetServerInstance,
|
||||
this._targetDatabaseNames[index],
|
||||
this._targetDatabaseNames[i],
|
||||
requestBody
|
||||
);
|
||||
if (response.status === 201) {
|
||||
@@ -586,14 +676,13 @@ export class MigrationStateModel implements Model, vscode.Disposable {
|
||||
this._targetSubscription,
|
||||
this._sqlMigrationService
|
||||
);
|
||||
vscode.window.showInformationMessage(localize("sql.migration.starting.migration.message", 'Starting migration for database {0} to {1} - {2}', db, this._targetServerInstance.name, this._targetDatabaseNames[index]));
|
||||
vscode.window.showInformationMessage(localize("sql.migration.starting.migration.message", 'Starting migration for database {0} to {1} - {2}', this._migrationDbs[i], this._targetServerInstance.name, this._targetDatabaseNames[i]));
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
vscode.window.showInformationMessage(e);
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -332,12 +332,32 @@ export class DatabaseBackupPage extends MigrationWizardPage {
|
||||
this.migrationStateModel._databaseBackup.storageAccount = this.migrationStateModel.getStorageAccount(value.index);
|
||||
}
|
||||
});
|
||||
|
||||
const networkLocationInputBoxLabel = this._view.modelBuilder.text().withProps({
|
||||
value: constants.DATABASE_BACKUP_NETWORK_SHARE_LOCATION_LABEL,
|
||||
requiredIndicator: true
|
||||
}).component();
|
||||
const networkLocationInputBox = this._view.modelBuilder.inputBox().withProps({
|
||||
placeHolder: '\\\\Servername.domainname.com\\Backupfolder',
|
||||
required: true,
|
||||
validationErrorMessage: constants.INVALID_NETWORK_SHARE_LOCATION
|
||||
}).withValidation((component) => {
|
||||
if (this.migrationStateModel._databaseBackup.networkContainerType === NetworkContainerType.NETWORK_SHARE) {
|
||||
if (component.value) {
|
||||
if (!/(?<=\\\\)[^\\]*/.test(component.value)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}).component();
|
||||
networkLocationInputBox.onTextChanged((value) => {
|
||||
this.validateFields();
|
||||
this.migrationStateModel._databaseBackup.networkShareLocation = value;
|
||||
});
|
||||
|
||||
const networkShareDatabaseConfigHeader = view.modelBuilder.text().withProps({
|
||||
value: constants.ENTER_NETWORK_SHARE_INFORMATION
|
||||
}).component();
|
||||
|
||||
this._networkShareDatabaseConfigContainer = view.modelBuilder.flexContainer().withLayout({
|
||||
flexFlow: 'column'
|
||||
}).component();
|
||||
@@ -347,14 +367,16 @@ export class DatabaseBackupPage extends MigrationWizardPage {
|
||||
[
|
||||
azureAccountHelpText,
|
||||
networkShareHelpText,
|
||||
subscriptionLabel,
|
||||
this._networkShareContainerSubscriptionDropdown,
|
||||
storageAccountLabel,
|
||||
this._networkShareContainerStorageAccountDropdown,
|
||||
networkLocationInputBoxLabel,
|
||||
networkLocationInputBox,
|
||||
windowsUserAccountLabel,
|
||||
this._windowsUserAccountText,
|
||||
passwordLabel,
|
||||
this._passwordText,
|
||||
subscriptionLabel,
|
||||
this._networkShareContainerSubscriptionDropdown,
|
||||
storageAccountLabel,
|
||||
this._networkShareContainerStorageAccountDropdown,
|
||||
networkShareDatabaseConfigHeader,
|
||||
this._networkShareDatabaseConfigContainer
|
||||
]
|
||||
@@ -374,7 +396,6 @@ export class DatabaseBackupPage extends MigrationWizardPage {
|
||||
this._fileShareDropdowns = [];
|
||||
this._blobContainerDropdowns = [];
|
||||
this.migrationStateModel._targetDatabaseNames = [];
|
||||
this.migrationStateModel._databaseBackup.networkShareLocations = [];
|
||||
this.migrationStateModel._databaseBackup.fileShares = [];
|
||||
this.migrationStateModel._databaseBackup.blobContainers = [];
|
||||
this._networkShareDatabaseConfigContainer.clearItems();
|
||||
@@ -389,42 +410,16 @@ export class DatabaseBackupPage extends MigrationWizardPage {
|
||||
requiredIndicator: true
|
||||
}).component();
|
||||
const targetNameNetworkInputBox = this._view.modelBuilder.inputBox().withProps({
|
||||
required: true
|
||||
required: true,
|
||||
value: db
|
||||
}).component();
|
||||
targetNameNetworkInputBox.onTextChanged((value) => {
|
||||
this.migrationStateModel._targetDatabaseNames[index] = value;
|
||||
});
|
||||
|
||||
const networkLocationInputBoxLabel = this._view.modelBuilder.text().withProps({
|
||||
value: constants.TARGET_NETWORK_SHARE_LOCATION(db),
|
||||
requiredIndicator: true
|
||||
}).component();
|
||||
const networkLocationInputBox = this._view.modelBuilder.inputBox().withProps({
|
||||
placeHolder: '\\\\Servername.domainname.com\\Backupfolder',
|
||||
required: true,
|
||||
validationErrorMessage: constants.INVALID_NETWORK_SHARE_LOCATION
|
||||
}).withValidation((component) => {
|
||||
if (this.migrationStateModel._databaseBackup.networkContainerType === NetworkContainerType.NETWORK_SHARE) {
|
||||
if (component.value) {
|
||||
if (!/(?<=\\\\)[^\\]*/.test(component.value)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}).component();
|
||||
networkLocationInputBox.onTextChanged((value) => {
|
||||
this.validateFields();
|
||||
this.migrationStateModel._databaseBackup.networkShareLocations[index] = value;
|
||||
});
|
||||
this.migrationStateModel._databaseBackup.networkShareLocations.push(undefined!);
|
||||
this._networkShareLocations.push(networkLocationInputBox);
|
||||
this._networkShareDatabaseConfigContainer.addItems(
|
||||
[
|
||||
targetNameNetworkInputBoxLabel,
|
||||
targetNameNetworkInputBox,
|
||||
networkLocationInputBoxLabel,
|
||||
networkLocationInputBox
|
||||
targetNameNetworkInputBox
|
||||
]
|
||||
);
|
||||
|
||||
|
||||
@@ -26,6 +26,8 @@ export class SKURecommendationPage extends MigrationWizardPage {
|
||||
private _chooseTargetComponent!: azdata.DivContainer;
|
||||
private _azureSubscriptionText!: azdata.TextComponent;
|
||||
private _managedInstanceSubscriptionDropdown!: azdata.DropDownComponent;
|
||||
private _azureLocationDropdown!: azdata.DropDownComponent;
|
||||
private _azureResourceGroupDropdown!: azdata.DropDownComponent;
|
||||
private _resourceDropdownLabel!: azdata.TextComponent;
|
||||
private _resourceDropdown!: azdata.DropDownComponent;
|
||||
private _rbg!: azdata.RadioCardGroupComponent;
|
||||
@@ -66,6 +68,35 @@ export class SKURecommendationPage extends MigrationWizardPage {
|
||||
this.migrationStateModel._targetSubscription = this.migrationStateModel.getSubscription(e.index);
|
||||
this.migrationStateModel._targetServerInstance = undefined!;
|
||||
this.migrationStateModel._sqlMigrationService = undefined!;
|
||||
this.populateLocationAndResourceGroupDropdown();
|
||||
}
|
||||
});
|
||||
this._resourceDropdownLabel = view.modelBuilder.text().withProps({
|
||||
value: constants.MANAGED_INSTANCE
|
||||
}).component();
|
||||
|
||||
const azureLocationLabel = view.modelBuilder.text().withProps({
|
||||
value: constants.LOCATION
|
||||
}).component();
|
||||
this._azureLocationDropdown = view.modelBuilder.dropDown().component();
|
||||
this._azureLocationDropdown.onValueChanged((e) => {
|
||||
if (e.selected) {
|
||||
this.migrationStateModel._location = this.migrationStateModel.getLocation(e.index);
|
||||
this.populateResourceInstanceDropdown();
|
||||
}
|
||||
});
|
||||
this._resourceDropdownLabel = view.modelBuilder.text().withProps({
|
||||
value: constants.MANAGED_INSTANCE
|
||||
}).component();
|
||||
|
||||
|
||||
const azureResourceGroupLabel = view.modelBuilder.text().withProps({
|
||||
value: constants.RESOURCE_GROUP
|
||||
}).component();
|
||||
this._azureResourceGroupDropdown = view.modelBuilder.dropDown().component();
|
||||
this._azureResourceGroupDropdown.onValueChanged((e) => {
|
||||
if (e.selected) {
|
||||
this.migrationStateModel._resourceGroup = this.migrationStateModel.getAzureResourceGroup(e.index);
|
||||
this.populateResourceInstanceDropdown();
|
||||
}
|
||||
});
|
||||
@@ -91,6 +122,10 @@ export class SKURecommendationPage extends MigrationWizardPage {
|
||||
[
|
||||
managedInstanceSubscriptionDropdownLabel,
|
||||
this._managedInstanceSubscriptionDropdown,
|
||||
azureLocationLabel,
|
||||
this._azureLocationDropdown,
|
||||
azureResourceGroupLabel,
|
||||
this._azureResourceGroupDropdown,
|
||||
this._resourceDropdownLabel,
|
||||
this._resourceDropdown
|
||||
]
|
||||
@@ -284,16 +319,30 @@ export class SKURecommendationPage extends MigrationWizardPage {
|
||||
}
|
||||
}
|
||||
|
||||
public async populateLocationAndResourceGroupDropdown(): Promise<void> {
|
||||
this._azureResourceGroupDropdown.loading = true;
|
||||
this._azureLocationDropdown.loading = true;
|
||||
try {
|
||||
this._azureResourceGroupDropdown.values = await this.migrationStateModel.getAzureResourceGroupDropdownValues(this.migrationStateModel._targetSubscription);
|
||||
this._azureLocationDropdown.values = await this.migrationStateModel.getAzureLocationDropdownValues(this.migrationStateModel._targetSubscription);
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
} finally {
|
||||
this._azureResourceGroupDropdown.loading = false;
|
||||
this._azureLocationDropdown.loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async populateResourceInstanceDropdown(): Promise<void> {
|
||||
this._resourceDropdown.loading = true;
|
||||
try {
|
||||
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._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._resourceDropdown.values = await this.migrationStateModel.getManagedInstanceValues(this.migrationStateModel._targetSubscription, this.migrationStateModel._location, this.migrationStateModel._resourceGroup);
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
@@ -322,6 +371,13 @@ export class SKURecommendationPage extends MigrationWizardPage {
|
||||
if ((<azdata.CategoryValue>this._managedInstanceSubscriptionDropdown.value).displayName === constants.NO_SUBSCRIPTIONS_FOUND) {
|
||||
errors.push(constants.INVALID_SUBSCRIPTION_ERROR);
|
||||
}
|
||||
if ((<azdata.CategoryValue>this._azureLocationDropdown.value).displayName === constants.NO_LOCATION_FOUND) {
|
||||
errors.push(constants.INVALID_LOCATION_ERROR);
|
||||
}
|
||||
|
||||
if ((<azdata.CategoryValue>this._managedInstanceSubscriptionDropdown.value).displayName === constants.RESOURCE_GROUP_NOT_FOUND) {
|
||||
errors.push(constants.INVALID_RESOURCE_GROUP_ERROR);
|
||||
}
|
||||
const resourceDropdownValue = (<azdata.CategoryValue>this._resourceDropdown.value).displayName;
|
||||
if (resourceDropdownValue === constants.NO_MANAGED_INSTANCE_FOUND) {
|
||||
errors.push(constants.NO_MANAGED_INSTANCE_FOUND);
|
||||
@@ -377,8 +433,8 @@ export class SKURecommendationPage extends MigrationWizardPage {
|
||||
if (this.migrationStateModel._assessmentResults) {
|
||||
const dbCount = this.migrationStateModel._assessmentResults.databaseAssessments.length;
|
||||
|
||||
const dbWithIssuesCount = this.migrationStateModel._assessmentResults.databaseAssessments.filter(db => db.issues.length > 0).length;
|
||||
const miCardText = `${dbWithIssuesCount} out of ${dbCount} databases can be migrated (${this.migrationStateModel._miDbs.length} selected)`;
|
||||
const dbWithoutIssuesCount = this.migrationStateModel._assessmentResults.databaseAssessments.filter(db => db.issues.length === 0).length;
|
||||
const miCardText = `${dbWithoutIssuesCount} out of ${dbCount} databases can be migrated (${this.migrationStateModel._miDbs.length} selected)`;
|
||||
this._rbg.cards[0].descriptions[1].textValue = miCardText;
|
||||
|
||||
const vmCardText = `${dbCount} out of ${dbCount} databases can be migrated (${this.migrationStateModel._vmDbs.length} selected)`;
|
||||
|
||||
@@ -72,6 +72,7 @@ export class SummaryPage extends MigrationWizardPage {
|
||||
flexContainer.addItems(
|
||||
[
|
||||
createInformationRow(this._view, constants.TYPE, constants.NETWORK_SHARE),
|
||||
createInformationRow(this._view, constants.DATABASE_BACKUP_NETWORK_SHARE_LOCATION_LABEL, this.migrationStateModel._databaseBackup.networkShareLocation),
|
||||
createInformationRow(this._view, constants.USER_ACCOUNT, this.migrationStateModel._databaseBackup.windowsUser),
|
||||
createInformationRow(this._view, constants.SUMMARY_AZURE_STORAGE_SUBSCRIPTION, this.migrationStateModel._databaseBackup.subscription.name),
|
||||
createInformationRow(this._view, constants.SUMMARY_AZURE_STORAGE, this.migrationStateModel._databaseBackup.storageAccount.name),
|
||||
@@ -80,7 +81,6 @@ export class SummaryPage extends MigrationWizardPage {
|
||||
);
|
||||
this.migrationStateModel._migrationDbs.forEach((db, index) => {
|
||||
flexContainer.addItem(createInformationRow(this._view, constants.TARGET_NAME_FOR_DATABASE(db), this.migrationStateModel._targetDatabaseNames[index]));
|
||||
flexContainer.addItem(createInformationRow(this._view, constants.TARGET_NETWORK_SHARE_LOCATION(db), this.migrationStateModel._databaseBackup.networkShareLocations[index]));
|
||||
});
|
||||
break;
|
||||
case NetworkContainerType.FILE_SHARE:
|
||||
|
||||
@@ -1,111 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as azdata from 'azdata';
|
||||
import { MigrationWizardPage } from '../models/migrationWizardPage';
|
||||
import { MigrationStateModel, StateChangeEvent } from '../models/stateMachine';
|
||||
import * as constants from '../constants/strings';
|
||||
import { WIZARD_INPUT_COMPONENT_WIDTH } from './wizardController';
|
||||
|
||||
export class TempTargetSelectionPage extends MigrationWizardPage {
|
||||
|
||||
private _managedInstanceSubscriptionDropdown!: azdata.DropDownComponent;
|
||||
private _managedInstanceDropdown!: azdata.DropDownComponent;
|
||||
|
||||
constructor(wizard: azdata.window.Wizard, migrationStateModel: MigrationStateModel) {
|
||||
super(wizard, azdata.window.createWizardPage(constants.TARGET_SELECTION_PAGE_TITLE), migrationStateModel);
|
||||
}
|
||||
|
||||
protected async registerContent(view: azdata.ModelView): Promise<void> {
|
||||
|
||||
const managedInstanceSubscriptionDropdownLabel = view.modelBuilder.text()
|
||||
.withProps({
|
||||
value: constants.SUBSCRIPTION
|
||||
}).component();
|
||||
|
||||
this._managedInstanceSubscriptionDropdown = view.modelBuilder.dropDown()
|
||||
.withProps({
|
||||
width: WIZARD_INPUT_COMPONENT_WIDTH
|
||||
}).component();
|
||||
this._managedInstanceSubscriptionDropdown.onValueChanged((e) => {
|
||||
if (e.selected) {
|
||||
this.migrationStateModel._targetSubscription = this.migrationStateModel.getSubscription(e.index);
|
||||
this.migrationStateModel._targetServerInstance = undefined!;
|
||||
this.migrationStateModel._sqlMigrationService = undefined!;
|
||||
this.populateManagedInstanceDropdown();
|
||||
}
|
||||
});
|
||||
|
||||
const managedInstanceDropdownLabel = view.modelBuilder.text().withProps({
|
||||
value: constants.MANAGED_INSTANCE
|
||||
}).component();
|
||||
|
||||
this._managedInstanceDropdown = view.modelBuilder.dropDown()
|
||||
.withProps({
|
||||
width: WIZARD_INPUT_COMPONENT_WIDTH
|
||||
}).component();
|
||||
this._managedInstanceDropdown.onValueChanged((e) => {
|
||||
if (e.selected) {
|
||||
this.migrationStateModel._sqlMigrationServices = undefined!;
|
||||
this.migrationStateModel._targetServerInstance = this.migrationStateModel.getManagedInstance(e.index);
|
||||
}
|
||||
});
|
||||
|
||||
const targetContainer = view.modelBuilder.flexContainer().withItems(
|
||||
[
|
||||
managedInstanceSubscriptionDropdownLabel,
|
||||
this._managedInstanceSubscriptionDropdown,
|
||||
managedInstanceDropdownLabel,
|
||||
this._managedInstanceDropdown
|
||||
]
|
||||
).withLayout({
|
||||
flexFlow: 'column'
|
||||
}).component();
|
||||
|
||||
const form = view.modelBuilder.formContainer()
|
||||
.withFormItems(
|
||||
[
|
||||
{
|
||||
component: targetContainer
|
||||
}
|
||||
]
|
||||
);
|
||||
await view.initializeModel(form.component());
|
||||
}
|
||||
public async onPageEnter(): Promise<void> {
|
||||
this.populateSubscriptionDropdown();
|
||||
}
|
||||
public async onPageLeave(): Promise<void> {
|
||||
}
|
||||
protected async handleStateChange(e: StateChangeEvent): Promise<void> {
|
||||
}
|
||||
|
||||
private async populateSubscriptionDropdown(): Promise<void> {
|
||||
if (!this.migrationStateModel._targetSubscription) {
|
||||
this._managedInstanceSubscriptionDropdown.loading = true;
|
||||
this._managedInstanceDropdown.loading = true;
|
||||
try {
|
||||
this._managedInstanceSubscriptionDropdown.values = await this.migrationStateModel.getSubscriptionsDropdownValues();
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
} finally {
|
||||
this._managedInstanceSubscriptionDropdown.loading = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async populateManagedInstanceDropdown(): Promise<void> {
|
||||
if (!this.migrationStateModel._targetServerInstance) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user