Adding database backup and accounts page to migration wizard (#13764)

* Added localized strings
Created a db backup page
added radio buttons

* created components for database backup page

* Added account selection page

* Added accounts page

* Some more work done

- Added page validations
- Almost done with db backup except for a few api calls.

* Some more progress
added graph api for storage account

* Finished hooking up all the endpoints on db page.

* Some code fixed and refactoring

* Fixed a ton of validation bugs

* Added common localized strings to the constants file

* some code cleanup

* changed method name to makeHttpGetRequest

* change http result class name

* Added return types and return values to the functions

* removed void returns

* Added more return types and values

* Storing accounts in the map with ids as key
Fixed a bug in case of no subscriptions found

* cleaning up the code

* Fixed localized strings

* Added comments to get request api
Added validation logic to database backup page
removed unnecessary page validations.

* Added some get resource functions in azure core

* Changed thenable to promise

* Added arm calls for file shares and blob storage

* Added field specific validation error message

* Added examples in validation error message.

* Fixed some typings and localized string

* Added live validations to dropdowns

* Fixed method name to getSQLVMservers

* Using older storage package

* Update typings/namings (#13767)

* Update typings

* more typings fixes

* switched fileshares and blobcontainers api to http requests

* removed the extra line

* Adding resource graph documentation link as a comment

* remove makeHttpRequest api from azurecore

Co-authored-by: Charles Gagnon <chgagnon@microsoft.com>
This commit is contained in:
Aasim Khan
2020-12-11 11:54:19 -08:00
committed by GitHub
parent e82f49d390
commit d2a525bbbe
13 changed files with 1160 additions and 21 deletions

View File

@@ -8,6 +8,10 @@ declare module 'azureResource' {
import { DataProvider, Account, TreeItem } from 'azdata';
export namespace azureResource {
/**
* AzureCore core extension supports following resource types of Azure Resource Graph.
* To add more resources, please refer this guide: https://docs.microsoft.com/en-us/azure/governance/resource-graph/reference/supported-tables-resources
*/
export const enum AzureResourceType {
resourceGroup = 'microsoft.resources/subscriptions/resourcegroups',
sqlServer = 'microsoft.sql/servers',
@@ -18,7 +22,8 @@ declare module 'azureResource' {
kustoClusters = 'microsoft.kusto/clusters',
azureArcPostgresServer = 'microsoft.azuredata/postgresinstances',
postgresServer = 'microsoft.dbforpostgresql/servers',
azureArcService = 'microsoft.azuredata/datacontrollers'
azureArcService = 'microsoft.azuredata/datacontrollers',
storageAccount = 'microsoft.storage/storageaccounts',
}
export interface IAzureResourceProvider extends DataProvider {
@@ -75,7 +80,8 @@ declare module 'azureResource' {
fullName: string;
defaultDatabaseName: string;
}
export interface BlobContainer extends AzureResource { }
export interface FileShare extends AzureResource { }
}
}

View File

@@ -5,11 +5,14 @@
import { ResourceGraphClient } from '@azure/arm-resourcegraph';
import { TokenCredentials } from '@azure/ms-rest-js';
import axios, { AxiosRequestConfig } from 'axios';
import * as azdata from 'azdata';
import { GetResourceGroupsResult, GetSubscriptionsResult, ResourceQueryResult } from 'azurecore';
import { HttpGetRequestResult, GetResourceGroupsResult, GetSubscriptionsResult, ResourceQueryResult, GetBlobContainersResult, GetFileSharesResult } from 'azurecore';
import { azureResource } from 'azureResource';
import { EOL } from 'os';
import * as nls from 'vscode-nls';
import { AppContext } from '../appContext';
import { invalidAzureAccount, invalidTenant, unableToFetchTokenError } from '../localizedConstants';
import { AzureResourceServiceNames } from './constants';
import { IAzureResourceSubscriptionFilterService, IAzureResourceSubscriptionService } from './interfaces';
import { AzureResourceGroupService } from './providers/resourceGroup/resourceGroupService';
@@ -106,7 +109,7 @@ export function equals(one: any, other: any): boolean {
export async function getResourceGroups(appContext: AppContext, account?: azdata.Account, subscription?: azureResource.AzureResourceSubscription, ignoreErrors: boolean = false): Promise<GetResourceGroupsResult> {
const result: GetResourceGroupsResult = { resourceGroups: [], errors: [] };
if (!account?.properties?.tenants || !Array.isArray(account.properties.tenants) || !subscription) {
const error = new Error(localize('azure.accounts.getResourceGroups.invalidParamsError', "Invalid account or subscription"));
const error = new Error(invalidAzureAccount);
if (!ignoreErrors) {
throw error;
}
@@ -146,7 +149,7 @@ export async function runResourceQuery<T extends azureResource.AzureGraphResourc
query: string): Promise<ResourceQueryResult<T>> {
const result: ResourceQueryResult<T> = { resources: [], errors: [] };
if (!account?.properties?.tenants || !Array.isArray(account.properties.tenants)) {
const error = new Error(localize('azure.accounts.runResourceQuery.errors.invalidAccount', "Invalid account"));
const error = new Error(invalidAzureAccount);
if (!ignoreErrors) {
throw error;
}
@@ -157,7 +160,7 @@ export async function runResourceQuery<T extends azureResource.AzureGraphResourc
// Check our subscriptions to ensure we have valid ones
subscriptions.forEach(subscription => {
if (!subscription.tenant) {
const error = new Error(localize('azure.accounts.runResourceQuery.errors.noTenantSpecifiedForSubscription', "Invalid tenant for subscription"));
const error = new Error(invalidTenant);
if (!ignoreErrors) {
throw error;
}
@@ -188,7 +191,7 @@ export async function runResourceQuery<T extends azureResource.AzureGraphResourc
resourceClient = new ResourceGraphClient(credential, { baseUri: account.properties.providerSettings.settings.armResource.endpoint });
} catch (err) {
console.error(err);
const error = new Error(localize('azure.accounts.runResourceQuery.errors.unableToFetchToken', "Unable to get token for tenant {0}", tenant.id));
const error = new Error(unableToFetchTokenError(tenant.id));
result.errors.push(error);
continue;
}
@@ -227,7 +230,7 @@ export async function runResourceQuery<T extends azureResource.AzureGraphResourc
export async function getSubscriptions(appContext: AppContext, account?: azdata.Account, ignoreErrors: boolean = false): Promise<GetSubscriptionsResult> {
const result: GetSubscriptionsResult = { subscriptions: [], errors: [] };
if (!account?.properties?.tenants || !Array.isArray(account.properties.tenants)) {
const error = new Error(localize('azure.accounts.getSubscriptions.invalidParamsError', "Invalid account"));
const error = new Error(invalidAzureAccount);
if (!ignoreErrors) {
throw error;
}
@@ -261,7 +264,7 @@ export async function getSubscriptions(appContext: AppContext, account?: azdata.
export async function getSelectedSubscriptions(appContext: AppContext, account?: azdata.Account, ignoreErrors: boolean = false): Promise<GetSubscriptionsResult> {
const result: GetSubscriptionsResult = { subscriptions: [], errors: [] };
if (!account?.properties?.tenants || !Array.isArray(account.properties.tenants)) {
const error = new Error(localize('azure.accounts.getSelectedSubscriptions.invalidParamsError', "Invalid account"));
const error = new Error(invalidAzureAccount);
if (!ignoreErrors) {
throw error;
}
@@ -284,3 +287,102 @@ export async function getSelectedSubscriptions(appContext: AppContext, account?:
}
return result;
}
/**
* makes a GET request to Azure REST apis. Currently, it only supports GET ARM queries.
*/
export async function makeHttpGetRequest(account: azdata.Account, subscription: azureResource.AzureResourceSubscription, ignoreErrors: boolean = false, url: string): Promise<HttpGetRequestResult> {
const result: HttpGetRequestResult = { response: {}, errors: [] };
if (!account?.properties?.tenants || !Array.isArray(account.properties.tenants)) {
const error = new Error(invalidAzureAccount);
if (!ignoreErrors) {
throw error;
}
result.errors.push(error);
}
if (!subscription.tenant) {
const error = new Error(invalidTenant);
if (!ignoreErrors) {
throw error;
}
result.errors.push(error);
}
if (result.errors.length > 0) {
return result;
}
let securityToken: { token: string, tokenType?: string };
try {
securityToken = await azdata.accounts.getAccountSecurityToken(
account,
subscription.tenant!,
azdata.AzureResource.ResourceManagement
);
} catch (err) {
console.error(err);
const error = new Error(unableToFetchTokenError(subscription.tenant));
if (!ignoreErrors) {
throw error;
}
result.errors.push(error);
}
if (result.errors.length > 0) {
return result;
}
const config: AxiosRequestConfig = {
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${securityToken.token}`
},
validateStatus: () => true // Never throw
};
const response = await axios.get(url, config);
if (response.status !== 200) {
let errorMessage: string[] = [];
errorMessage.push(response.status.toString());
errorMessage.push(response.statusText);
if (response.data && response.data.error) {
errorMessage.push(`${response.data.error.code} : ${response.data.error.message}`);
}
const error = new Error(errorMessage.join(EOL));
if (!ignoreErrors) {
throw error;
}
result.errors.push(error);
}
result.response = response;
return result;
}
export async function getBlobContainers(account: azdata.Account, subscription: azureResource.AzureResourceSubscription, storageAccount: azureResource.AzureGraphResource, ignoreErrors: boolean): Promise<GetBlobContainersResult> {
const apiEndpoint = `https://management.azure.com` +
`/subscriptions/${subscription.id}` +
`/resourceGroups/${storageAccount.resourceGroup}` +
`/providers/Microsoft.Storage/storageAccounts/${storageAccount.name}` +
`/blobServices/default/containers?api-version=2019-06-01`;
const response = await makeHttpGetRequest(account, subscription, ignoreErrors, apiEndpoint);
return {
blobContainers: response.response.data.value,
errors: response.errors ? response.errors : []
};
}
export async function getFileShares(account: azdata.Account, subscription: azureResource.AzureResourceSubscription, storageAccount: azureResource.AzureGraphResource, ignoreErrors: boolean): Promise<GetFileSharesResult> {
const apiEndpoint = `https://management.azure.com` +
`/subscriptions/${subscription.id}` +
`/resourceGroups/${storageAccount.resourceGroup}` +
`/providers/Microsoft.Storage/storageAccounts/${storageAccount.name}` +
`/fileServices/default/shares?api-version=2019-06-01`;
const response = await makeHttpGetRequest(account, subscription, ignoreErrors, apiEndpoint);
return {
fileShares: response.response.data.value,
errors: response.errors ? response.errors : []
};
}

View File

@@ -6,6 +6,7 @@
declare module 'azurecore' {
import * as azdata from 'azdata';
import { azureResource } from 'azureResource';
/**
* Covers defining what the azurecore extension exports to other extensions
*
@@ -66,8 +67,14 @@ declare module 'azurecore' {
}
export interface IExtension {
getSubscriptions(account?: azdata.Account, ignoreErrors?: boolean, selectedOnly?: boolean): Thenable<GetSubscriptionsResult>;
getResourceGroups(account?: azdata.Account, subscription?: azureResource.AzureResourceSubscription, ignoreErrors?: boolean): Thenable<GetResourceGroupsResult>;
getSubscriptions(account?: azdata.Account, ignoreErrors?: boolean, selectedOnly?: boolean): Promise<GetSubscriptionsResult>;
getResourceGroups(account?: azdata.Account, subscription?: azureResource.AzureResourceSubscription, ignoreErrors?: boolean): Promise<GetResourceGroupsResult>;
getSqlManagedInstances(account: azdata.Account, subscriptions: azureResource.AzureResourceSubscription[], ignoreErrors?: boolean): Promise<GetSqlManagedInstancesResult>;
getSqlServers(account: azdata.Account, subscriptions: azureResource.AzureResourceSubscription[], ignoreErrors?: boolean): Promise<GetSqlServersResult>;
getSqlVMServers(account: azdata.Account, subscriptions: azureResource.AzureResourceSubscription[], ignoreErrors?: boolean): Promise<GetSqlVMServersResult>;
getStorageAccounts(account: azdata.Account, subscriptions: azureResource.AzureResourceSubscription[], ignoreErrors?: boolean): Promise<GetStorageAccountResult>;
getBlobContainers(account: azdata.Account, subscription: azureResource.AzureResourceSubscription, storageAccount: azureResource.AzureGraphResource, ignoreErrors?: boolean): Promise<GetBlobContainersResult>;
getFileShares(account: azdata.Account, subscription: azureResource.AzureResourceSubscription, storageAccount: azureResource.AzureGraphResource, ignoreErrors?: boolean): Promise<GetFileSharesResult>;
/**
* Converts a region value (@see AzureRegion) into the localized Display Name
* @param region The region value
@@ -80,6 +87,13 @@ declare module 'azurecore' {
export type GetSubscriptionsResult = { subscriptions: azureResource.AzureResourceSubscription[], errors: Error[] };
export type GetResourceGroupsResult = { resourceGroups: azureResource.AzureResourceResourceGroup[], errors: Error[] };
export type GetSqlManagedInstancesResult = { resources: azureResource.AzureGraphResource[], errors: Error[] };
export type GetSqlServersResult = {resources: azureResource.AzureGraphResource[], errors: Error[]};
export type GetSqlVMServersResult = {resources: azureResource.AzureGraphResource[], errors: Error[]};
export type GetStorageAccountResult = {resources: azureResource.AzureGraphResource[], errors: Error[]};
export type GetBlobContainersResult = {blobContainers: azureResource.BlobContainer[] | undefined, errors: Error[]};
export type GetFileSharesResult = {fileShares: azureResource.FileShare[] | undefined, errors: Error[]};
export type ResourceQueryResult<T extends azureResource.AzureGraphResource> = { resources: T[], errors: Error[] };
export type HttpGetRequestResult = { response: any, errors: Error[] };
}

View File

@@ -141,12 +141,12 @@ export async function activate(context: vscode.ExtensionContext): Promise<azurec
});
return {
getSubscriptions(account?: azdata.Account, ignoreErrors?: boolean, selectedOnly: boolean = false): Thenable<azurecore.GetSubscriptionsResult> {
getSubscriptions(account?: azdata.Account, ignoreErrors?: boolean, selectedOnly: boolean = false): Promise<azurecore.GetSubscriptionsResult> {
return selectedOnly
? azureResourceUtils.getSelectedSubscriptions(appContext, account, ignoreErrors)
: azureResourceUtils.getSubscriptions(appContext, account, ignoreErrors);
},
getResourceGroups(account?: azdata.Account, subscription?: azureResource.AzureResourceSubscription, ignoreErrors?: boolean): Thenable<azurecore.GetResourceGroupsResult> { return azureResourceUtils.getResourceGroups(appContext, account, subscription, ignoreErrors); },
getResourceGroups(account?: azdata.Account, subscription?: azureResource.AzureResourceSubscription, ignoreErrors?: boolean): Promise<azurecore.GetResourceGroupsResult> { return azureResourceUtils.getResourceGroups(appContext, account, subscription, ignoreErrors); },
provideResources(): azureResource.IAzureResourceProvider[] {
const arcFeaturedEnabled = vscode.workspace.getConfiguration(constants.extensionConfigSectionName).get('enableArcFeatures');
const providers: azureResource.IAzureResourceProvider[] = [
@@ -164,6 +164,38 @@ export async function activate(context: vscode.ExtensionContext): Promise<azurec
}
return providers;
},
getSqlManagedInstances(account: azdata.Account,
subscriptions: azureResource.AzureResourceSubscription[],
ignoreErrors: boolean): Promise<azurecore.GetSqlManagedInstancesResult> {
return azureResourceUtils.runResourceQuery(account, subscriptions, ignoreErrors, `where type == "${azureResource.AzureResourceType.sqlManagedInstance}"`);
},
getSqlServers(account: azdata.Account,
subscriptions: azureResource.AzureResourceSubscription[],
ignoreErrors: boolean): Promise<azurecore.GetSqlServersResult> {
return azureResourceUtils.runResourceQuery(account, subscriptions, ignoreErrors, `where type == "${azureResource.AzureResourceType.sqlServer}"`);
},
getSqlVMServers(account: azdata.Account,
subscriptions: azureResource.AzureResourceSubscription[],
ignoreErrors: boolean): Promise<azurecore.GetSqlVMServersResult> {
return azureResourceUtils.runResourceQuery(account, subscriptions, ignoreErrors, `where type == "${azureResource.AzureResourceType.virtualMachines}" and properties.storageProfile.imageReference.publisher == "microsoftsqlserver"`);
},
getStorageAccounts(account: azdata.Account,
subscriptions: azureResource.AzureResourceSubscription[],
ignoreErrors: boolean): Promise<azurecore.GetStorageAccountResult> {
return azureResourceUtils.runResourceQuery(account, subscriptions, ignoreErrors, `where type == "${azureResource.AzureResourceType.storageAccount}"`);
},
getBlobContainers(account: azdata.Account,
subscription: azureResource.AzureResourceSubscription,
storageAccount: azureResource.AzureGraphResource,
ignoreErrors: boolean): Promise<azurecore.GetBlobContainersResult> {
return azureResourceUtils.getBlobContainers(account, subscription, storageAccount, ignoreErrors);
},
getFileShares(account: azdata.Account,
subscription: azureResource.AzureResourceSubscription,
storageAccount: azureResource.AzureGraphResource,
ignoreErrors: boolean): Promise<azurecore.GetFileSharesResult> {
return azureResourceUtils.getFileShares(account, subscription, storageAccount, ignoreErrors);
},
getRegionDisplayName: utils.getRegionDisplayName,
runGraphQuery<T extends azureResource.AzureGraphResource>(account: azdata.Account,
subscriptions: azureResource.AzureResourceSubscription[],

View File

@@ -74,3 +74,10 @@ export const azureArcPostgresServer = localize('azurecore.azureArcPostgres', "Az
export const unableToOpenAzureLink = localize('azure.unableToOpenAzureLink', "Unable to open link, missing required values");
export const azureResourcesGridTitle = localize('azure.azureResourcesGridTitle', "Azure Resources (Preview)");
// Azure Request Errors
export const invalidAzureAccount = localize('azurecore.invalidAzureAccount', "Invalid account");
export const invalidTenant = localize('azurecore.invalidTenant', "Invalid tenant for subscription");
export function unableToFetchTokenError(tenant: string): string {
return localize('azurecore.unableToFetchToken', "Unable to get token for tenant {0}", tenant);
}

View File

@@ -8,13 +8,31 @@ import * as azurecore from 'azurecore';
import { azureResource } from 'azureResource';
export class AzurecoreApiStub implements azurecore.IExtension {
getFileShares(_account: azdata.Account, _subscription: azureResource.AzureResourceSubscription, _storageAccount: azureResource.AzureGraphResource, _ignoreErrors?: boolean): Promise<azurecore.GetFileSharesResult> {
throw new Error('Method not implemented.');
}
getBlobContainers(_account: azdata.Account, _subscription: azureResource.AzureResourceSubscription, _storageAccount: azureResource.AzureGraphResource, _ignoreErrors?: boolean): Promise<azurecore.GetBlobContainersResult> {
throw new Error('Method not implemented.');
}
getSqlManagedInstances(_account: azdata.Account, _subscriptions: azureResource.AzureResourceSubscription[], _ignoreErrors?: boolean): Promise<azurecore.GetSqlManagedInstancesResult> {
throw new Error('Method not implemented.');
}
getSqlServers(_account: azdata.Account, _subscriptions: azureResource.AzureResourceSubscription[], _ignoreErrors?: boolean): Promise<azurecore.GetSqlServersResult> {
throw new Error('Method not implemented.');
}
getSqlVMServers(_account: azdata.Account, _subscriptions: azureResource.AzureResourceSubscription[], _ignoreErrors?: boolean): Promise<azurecore.GetSqlVMServersResult> {
throw new Error('Method not implemented.');
}
getStorageAccounts(_account: azdata.Account, _subscriptions: azureResource.AzureResourceSubscription[], _ignoreErrors?: boolean): Promise<azurecore.GetStorageAccountResult> {
throw new Error('Method not implemented.');
}
runGraphQuery<T extends azureResource.AzureGraphResource>(_account: azdata.Account, _subscriptions: azureResource.AzureResourceSubscription[], _ignoreErrors: boolean, _query: string): Promise<azurecore.ResourceQueryResult<T>> {
throw new Error('Method not implemented.');
}
getSubscriptions(_account?: azdata.Account | undefined, _ignoreErrors?: boolean | undefined, _selectedOnly?: boolean | undefined): Thenable<azurecore.GetSubscriptionsResult> {
getSubscriptions(_account?: azdata.Account | undefined, _ignoreErrors?: boolean | undefined, _selectedOnly?: boolean | undefined): Promise<azurecore.GetSubscriptionsResult> {
throw new Error('Method not implemented.');
}
getResourceGroups(_account?: azdata.Account | undefined, _subscription?: azureResource.AzureResourceSubscription | undefined, _ignoreErrors?: boolean | undefined): Thenable<azurecore.GetResourceGroupsResult> {
getResourceGroups(_account?: azdata.Account | undefined, _subscription?: azureResource.AzureResourceSubscription | undefined, _ignoreErrors?: boolean | undefined): Promise<azurecore.GetResourceGroupsResult> {
throw new Error('Method not implemented.');
}
getRegionDisplayName(_region?: string | undefined): string {

View File

@@ -39,7 +39,7 @@ export type SqlManagedInstance = AzureProduct;
export async function getAvailableManagedInstanceProducts(account: azdata.Account, subscription: Subscription): Promise<SqlManagedInstance[]> {
const api = await getAzureCoreAPI();
const result = await api.runGraphQuery<SqlManagedInstance>(account, [subscription], false, `where type == "${azureResource.AzureResourceType.sqlManagedInstance}"`);
const result = await api.getSqlManagedInstances(account, [subscription], false);
return result.resources;
}
@@ -47,7 +47,7 @@ export type SqlServer = AzureProduct;
export async function getAvailableSqlServers(account: azdata.Account, subscription: Subscription): Promise<SqlServer[]> {
const api = await getAzureCoreAPI();
const result = await api.runGraphQuery<SqlServer>(account, [subscription], false, `where type == "${azureResource.AzureResourceType.sqlServer}"`);
const result = await api.getSqlServers(account, [subscription], false);
return result.resources;
}
@@ -55,6 +55,45 @@ export type SqlVMServer = AzureProduct;
export async function getAvailableSqlVMs(account: azdata.Account, subscription: Subscription): Promise<SqlVMServer[]> {
const api = await getAzureCoreAPI();
const result = await api.runGraphQuery<SqlVMServer>(account, [subscription], false, `where type == "${azureResource.AzureResourceType.virtualMachines}" and properties.storageProfile.imageReference.publisher == "microsoftsqlserver"`);
const result = await api.getSqlVMServers(account, [subscription], false);
return result.resources;
}
export type StorageAccount = AzureProduct;
export async function getAvailableStorageAccounts(account: azdata.Account, subscription: Subscription): Promise<StorageAccount[]> {
const api = await getAzureCoreAPI();
const result = await api.getStorageAccounts(account, [subscription], false);
sortResourceArrayByName(result.resources);
return result.resources;
}
export async function getFileShares(account: azdata.Account, subscription: Subscription, storageAccount: StorageAccount): Promise<azureResource.FileShare[]> {
const api = await getAzureCoreAPI();
let result = await api.getFileShares(account, subscription, storageAccount, true);
let fileShares = result.fileShares;
sortResourceArrayByName(fileShares!);
return fileShares!;
}
export async function getBlobContainers(account: azdata.Account, subscription: Subscription, storageAccount: StorageAccount): Promise<azureResource.BlobContainer[]> {
const api = await getAzureCoreAPI();
let result = await api.getBlobContainers(account, subscription, storageAccount, true);
let blobContainers = result.blobContainers;
sortResourceArrayByName(blobContainers!);
return blobContainers!;
}
function sortResourceArrayByName(resourceArray: AzureProduct[] | azureResource.FileShare[] | azureResource.BlobContainer[] | undefined): void {
if (!resourceArray) {
return;
}
resourceArray.sort((a: AzureProduct | azureResource.BlobContainer | azureResource.FileShare, b: AzureProduct | azureResource.BlobContainer | azureResource.FileShare) => {
if (a.name! < b.name!) {
return -1;
}
if (a.name! > b.name!) {
return 1;
}
return 0;
});
}

View File

@@ -7,7 +7,7 @@ import * as azdata from 'azdata';
import { MigrationStateModel, StateChangeEvent } from './stateMachine';
export abstract class MigrationWizardPage {
constructor(
private readonly wizard: azdata.window.Wizard,
protected readonly wizard: azdata.window.Wizard,
protected readonly wizardPage: azdata.window.WizardPage,
protected readonly migrationStateModel: MigrationStateModel
) { }

View File

@@ -26,11 +26,51 @@ export enum State {
EXIT,
}
export enum MigrationCutover {
MANUAL,
AUTOMATIC
}
export enum NetworkContainerType {
FILE_SHARE,
BLOB_CONTAINER,
NETWORK_SHARE
}
export interface NetworkShare {
networkShareLocation: string;
windowsUser: string;
password: string;
storageSubscriptionId: string;
storageAccountId: string;
}
export interface BlobContainer {
subscriptionId: string;
storageAccountId: string;
containerId: string;
}
export interface FileShare {
subscriptionId: string;
storageAccountId: string;
fileShareId: string;
resourceGroupId: string;
}
export interface DatabaseBackupModel {
emailNotification: boolean;
migrationCutover: MigrationCutover;
networkContainerType: NetworkContainerType;
networkContainer: NetworkShare | BlobContainer | FileShare;
azureSecurityToken: string;
}
export interface Model {
readonly sourceConnection: azdata.connection.Connection;
readonly currentState: State;
gatheringInformationError: string | undefined;
skuRecommendations: SKURecommendations | undefined;
azureAccount: azdata.Account | undefined;
databaseBackup: DatabaseBackupModel | undefined;
}
export interface StateChangeEvent {
@@ -44,6 +84,8 @@ export class MigrationStateModel implements Model, vscode.Disposable {
private _gatheringInformationError: string | undefined;
private _skuRecommendations: SKURecommendations | undefined;
private _assessmentResults: mssql.SqlMigrationAssessmentResultItem[] | undefined;
private _azureAccount!: azdata.Account;
private _databaseBackup!: DatabaseBackupModel;
constructor(
private readonly _extensionContext: vscode.ExtensionContext,
@@ -51,6 +93,23 @@ export class MigrationStateModel implements Model, vscode.Disposable {
public readonly migrationService: mssql.ISqlMigrationService
) {
this._currentState = State.INIT;
this.databaseBackup = {} as DatabaseBackupModel;
}
public get azureAccount(): azdata.Account {
return this._azureAccount;
}
public set azureAccount(account: azdata.Account) {
this._azureAccount = account;
}
public get databaseBackup(): DatabaseBackupModel {
return this._databaseBackup;
}
public set databaseBackup(dbBackup: DatabaseBackupModel) {
this._databaseBackup = dbBackup;
}
public get sourceConnection(): azdata.connection.Connection {

View File

@@ -35,3 +35,54 @@ export const SUBSCRIPTION_SELECTION_AZURE_SUBSCRIPTION_TITLE = localize('sql.mig
export const SUBSCRIPTION_SELECTION_AZURE_PRODUCT_TITLE = localize('sql.migration.wizard.subscription.azure.product.title', "Azure Product");
export const CONGRATULATIONS = localize('sql.migration.generic.congratulations', "Congratulations");
// Accounts page
export const ACCOUNTS_SELECTION_PAGE_TITLE = localize('sql.migration.wizard.account.title', "Select your Azure account");
export const ACCOUNT_SELECTION_PAGE_NO_LINKED_ACCOUNTS_ERROR = localize('sql.migration.wizard.account.noaccount.error', "There is no linked account. Please add an account.");
export const ACCOUNT_ADD_BUTTON_LABEL = localize('sql.migration.wizard.account.add.button.label', "Add account");
export function accountLinkedMessage(count: number): string {
return count === 1 ? localize('sql.migration.wizard.account.count.single.message', '{0} account linked', count) : localize('sql.migration.wizard.account.count.multiple.message', '{0} accounts linked', count);
}
// database backup page
export const DATABASE_BACKUP_PAGE_TITLE = localize('sql.migration.database.page.title', "Database Backup");
export const DATABASE_BACKUP_PAGE_DESCRIPTION = localize('sql.migration.database.page.description', "Select the location where we can find your database backups (Full, Differential and Log) to use for migration.");
export const DATABASE_BACKUP_NC_NETWORK_SHARE_RADIO_LABEL = localize('sql.migration.nc.network.share.radio.label', "My database backups are on a network share");
export const DATABASE_BACKUP_NC_NETWORK_SHARE_HELP_TEXT = localize('sql.migration.network.share.help.text', "Enter network share information");
export const DATABASE_BACKUP_NC_BLOB_STORAGE_RADIO_LABEL = localize('sql.migration.nc.blob.storage.radio.label', "My database backups are in an Azure Storage Blob Container");
export const DATABASE_BACKUP_NC_FILE_SHARE_RADIO_LABEL = localize('sql.migration.nc.file.share.radio.label', "My database backups are in an Azure Storage File Share");
export const DATABASE_BACKUP_NETWORK_SHARE_LOCATION_LABEL = localize('sql.migration.network.share.location.label', "Network share location to read backups from.");
export const DATABASE_BACKUP_NETWORK_SHARE_WINDOWS_USER_LABEL = localize('sql.migration.network.share.windows.user.label', "Windows user account with read access to the network share location.");
export const DATABASE_BACKUP_NETWORK_SHARE_PASSWORD_LABEL = localize('sql.migration.network.share.password.label', "Password");
export const DATABASE_BACKUP_NETWORK_SHARE_PASSWORD_PLACEHOLDER = localize('sql.migration.network.share.password.placeholder', "Enter password");
export const DATABASE_BACKUP_NETWORK_SHARE_AZURE_ACCOUNT_HELP = localize('sql.migration.network.share.azure.help', "Enter Azure storage account information where the backup will be copied");
export const DATABASE_BACKUP_NETWORK_SHARE_SUBSCRIPTION_LABEL = localize('sql.migration.network.share.subscription.label', "Select the subscription that contains the storage account.");
export const DATABASE_BACKUP_SUBSCRIPTION_PLACEHOLDER = localize('sql.migration.network.share.subscription.placeholder', "Select subscription");
export const DATABASE_BACKUP_NETWORK_SHARE_NETWORK_STORAGE_ACCOUNT_LABEL = localize('sql.migration.network.share.storage.account.label', "Select the storage account where backup files will be copied.");
export const DATABASE_BACKUP_STORAGE_ACCOUNT_PLACEHOLDER = localize('sql.migration.network.share.storage.account.placeholder', "Select account");
export const DATABASE_BACKUP_BLOB_STORAGE_SUBSCRIPTION_LABEL = localize('sql.migration.blob.storage.subscription.label', "Select the subscription that contains the storage account.");
export const DATABASE_BACKUP_BLOB_STORAGE_ACCOUNT_LABEL = localize('sql.migration.blob.storage.account.label', "Select the storage account that contains the backup files.");
export const DATABASE_BACKUP_BLOB_STORAGE_ACCOUNT_CONTAINER_LABEL = localize('sql.migration.blob.storage.container.label', "Select the container that contains the backup files.");
export const DATABASE_BACKUP_BLOB_STORAGE_ACCOUNT_CONTAINER_PLACEHOLDER = localize('sql.migration.blob.storage.container.placeholder', "Select container");
export const DATABASE_BACKUP_FILE_SHARE_SUBSCRIPTION_LABEL = localize('sql.migration.file.share.subscription.label', "Select the subscription that contains the file share.");
export const DATABASE_BACKUP_FILE_SHARE_STORAGE_ACCOUNT_LABEL = localize('sql.migration.file.share.storage.account.label', "Select the storage account that contains the file share.");
export const DATABASE_BACKUP_FILE_SHARE_LABEL = localize('sql.migration.file.share.label', "Select the file share that contains the backup files.");
export const DATABASE_BACKUP_FILE_SHARE_PLACEHOLDER = localize('sql.migration.file.share.placeholder', "Select share");
export const DATABASE_BACKUP_MIGRATION_CUTOVER_LABEL = localize('sql.migration.database.migration.cutover.label', "Migration Cutover");
export const DATABASE_BACKUP_MIGRATION_CUTOVER_DESCRIPTION = localize('sql.migration.database.migration.cutover.description', "Select how you want to cutover when the migration is complete.");
export const DATABASE_BACKUP_MIGRATION_CUTOVER_AUTOMATIC_LABEL = localize('sql.migration.database.migration.cutover.automatic.label', "Automatically cutover when migration is complete");
export const DATABASE_BACKUP_MIGRATION_CUTOVER_MANUAL_LABEL = localize('sql.migration.database.migration.cutover.manual.label', "Manually cutover when migration is complete");
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_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_STORAGE_ACCOUNT_ERROR = localize('sql.migration.invalid.storageAccout.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.");
export const INVALID_NETWORK_SHARE_LOCATION = localize('sql.migration.invalid.network.share.location', "Invalid network share location format. Example: {0}", '\\\\Servername.domainname.com\\Backupfolder');
export const INVALID_USER_ACCOUNT = localize('sql.migration.invalid.user.account', "Invalid user account format. Example: {0}", 'Domain\\username');

View File

@@ -0,0 +1,106 @@
/*---------------------------------------------------------------------------------------------
* 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 * as vscode from 'vscode';
import { MigrationWizardPage } from '../models/migrationWizardPage';
import { MigrationStateModel, StateChangeEvent } from '../models/stateMachine';
import * as constants from '../models/strings';
export class AccountsSelectionPage extends MigrationWizardPage {
private _azureAccountsDropdown!: azdata.DropDownComponent;
private _accountsMap: Map<string, azdata.Account> = new Map();
constructor(wizard: azdata.window.Wizard, migrationStateModel: MigrationStateModel) {
super(wizard, azdata.window.createWizardPage(constants.ACCOUNTS_SELECTION_PAGE_TITLE), migrationStateModel);
}
protected async registerContent(view: azdata.ModelView): Promise<void> {
const form = view.modelBuilder.formContainer()
.withFormItems(
[
await this.createAzureAccountsDropdown(view)
]
);
await view.initializeModel(form.component());
await this.populateAzureAccountsDropdown();
}
private createAzureAccountsDropdown(view: azdata.ModelView): azdata.FormComponent {
this._azureAccountsDropdown = view.modelBuilder.dropDown().withValidation((c) => {
if ((<azdata.CategoryValue>c.value).displayName === constants.ACCOUNT_SELECTION_PAGE_NO_LINKED_ACCOUNTS_ERROR) {
this.wizard.message = {
text: constants.ACCOUNT_SELECTION_PAGE_NO_LINKED_ACCOUNTS_ERROR,
level: azdata.window.MessageLevel.Error
};
return false;
}
return true;
}).component();
this._azureAccountsDropdown.onValueChanged(async (value) => {
this.migrationStateModel.azureAccount = this._accountsMap.get((this._azureAccountsDropdown.value as azdata.CategoryValue).name)!;
});
const addAccountButton = view.modelBuilder.button()
.withProperties<azdata.ButtonProperties>({
label: constants.ACCOUNT_ADD_BUTTON_LABEL,
width: '100px'
})
.component();
addAccountButton.onDidClick(async (event) => {
await vscode.commands.executeCommand('workbench.actions.modal.linkedAccount');
await this.populateAzureAccountsDropdown();
});
const flexContainer = view.modelBuilder.flexContainer()
.withLayout({
flexFlow: 'column'
})
.withItems([this._azureAccountsDropdown, addAccountButton], { CSSStyles: { 'margin': '10px', } })
.component();
return {
title: '',
component: flexContainer
};
}
private async populateAzureAccountsDropdown(): Promise<void> {
this._azureAccountsDropdown.loading = true;
let accounts = await azdata.accounts.getAllAccounts();
if (accounts.length === 0) {
this._azureAccountsDropdown.value = {
displayName: constants.ACCOUNT_SELECTION_PAGE_NO_LINKED_ACCOUNTS_ERROR,
name: ''
};
return;
}
this._azureAccountsDropdown.values = accounts.map((account): azdata.CategoryValue => {
let accountCategoryValue = {
displayName: account.displayInfo.displayName,
name: account.displayInfo.userId
};
this._accountsMap.set(accountCategoryValue.name, account);
return accountCategoryValue;
});
this.migrationStateModel.azureAccount = accounts[0];
this._azureAccountsDropdown.loading = false;
}
public async onPageEnter(): Promise<void> {
}
public async onPageLeave(): Promise<void> {
}
protected async handleStateChange(e: StateChangeEvent): Promise<void> {
}
}

View File

@@ -0,0 +1,702 @@
/*---------------------------------------------------------------------------------------------
* 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 { azureResource } from 'azureResource';
import { EOL } from 'os';
import { getAvailableStorageAccounts, getBlobContainers, getFileShares, getSubscriptions, StorageAccount, Subscription } from '../api/azure';
import { MigrationWizardPage } from '../models/migrationWizardPage';
import { BlobContainer, FileShare, MigrationCutover, MigrationStateModel, NetworkContainerType, NetworkShare, StateChangeEvent } from '../models/stateMachine';
import * as constants from '../models/strings';
export class DatabaseBackupPage extends MigrationWizardPage {
private _networkShareContainer!: azdata.FlexContainer;
private _networkShareContainerSubscriptionDropdown!: azdata.DropDownComponent;
private _networkShareContainerStorageAccountDropdown!: azdata.DropDownComponent;
private _networkShareLocationText!: azdata.InputBoxComponent;
private _windowsUserAccountText!: azdata.InputBoxComponent;
private _passwordText!: azdata.InputBoxComponent;
private _blobContainer!: azdata.FlexContainer;
private _blobContainerSubscriptionDropdown!: azdata.DropDownComponent;
private _blobContainerStorageAccountDropdown!: azdata.DropDownComponent;
private _blobContainerBlobDropdown!: azdata.DropDownComponent;
private _fileShareContainer!: azdata.FlexContainer;
private _fileShareSubscriptionDropdown!: azdata.DropDownComponent;
private _fileShareStorageAccountDropdown!: azdata.DropDownComponent;
private _fileShareFileShareDropdown!: azdata.DropDownComponent;
private _networkShare = {} as NetworkShare;
private _fileShare = {} as FileShare;
private _blob = {} as BlobContainer;
private _subscriptionDropdownValues: azdata.CategoryValue[] = [];
private _subscriptionMap: Map<string, Subscription> = new Map();
private _storageAccountMap: Map<string, StorageAccount> = new Map();
private _errors: string[] = [];
constructor(wizard: azdata.window.Wizard, migrationStateModel: MigrationStateModel) {
super(wizard, azdata.window.createWizardPage(constants.DATABASE_BACKUP_PAGE_TITLE), migrationStateModel);
this.wizardPage.description = constants.DATABASE_BACKUP_PAGE_DESCRIPTION;
}
protected async registerContent(view: azdata.ModelView): Promise<void> {
this._networkShareContainer = this.createNetworkShareContainer(view);
this._blobContainer = this.createBlobContainer(view);
this._fileShareContainer = this.createFileShareContainer(view);
const networkContainer = view.modelBuilder.flexContainer().withLayout({
flexFlow: 'column'
}).withItems([
this._networkShareContainer,
this._blobContainer,
this._fileShareContainer
]).component();
const form = view.modelBuilder.formContainer()
.withFormItems(
[
this.createBackupLocationComponent(view),
{
title: '',
component: networkContainer
},
this.migrationCutoverContainer(view),
this.emailNotificationContainer(view),
]
);
await view.initializeModel(form.component());
this.toggleNetworkContainerFields(NetworkContainerType.NETWORK_SHARE, this._networkShare);
}
private createBackupLocationComponent(view: azdata.ModelView): azdata.FormComponent {
const buttonGroup = 'networkContainer';
const networkShareButton = view.modelBuilder.radioButton()
.withProps({
name: buttonGroup,
label: constants.DATABASE_BACKUP_NC_NETWORK_SHARE_RADIO_LABEL,
checked: true
}).component();
networkShareButton.onDidClick((e) => this.toggleNetworkContainerFields(NetworkContainerType.NETWORK_SHARE, this._networkShare));
const blobContainerButton = view.modelBuilder.radioButton()
.withProps({
name: buttonGroup,
label: constants.DATABASE_BACKUP_NC_BLOB_STORAGE_RADIO_LABEL,
}).component();
blobContainerButton.onDidClick((e) => this.toggleNetworkContainerFields(NetworkContainerType.BLOB_CONTAINER, this._blob));
const fileShareButton = view.modelBuilder.radioButton()
.withProps({
name: buttonGroup,
label: constants.DATABASE_BACKUP_NC_FILE_SHARE_RADIO_LABEL,
}).component();
fileShareButton.onDidClick((e) => this.toggleNetworkContainerFields(NetworkContainerType.FILE_SHARE, this._fileShare));
const flexContainer = view.modelBuilder.flexContainer().withItems(
[
networkShareButton,
blobContainerButton,
fileShareButton
]
).withLayout({
flexFlow: 'column'
}).component();
return {
title: '',
component: flexContainer
};
}
private createFileShareContainer(view: azdata.ModelView): azdata.FlexContainer {
const subscriptionLabel = view.modelBuilder.text().withProps({
value: constants.DATABASE_BACKUP_FILE_SHARE_SUBSCRIPTION_LABEL,
requiredIndicator: true,
}).component();
this._fileShareSubscriptionDropdown = view.modelBuilder.dropDown().withProps({
required: true,
}).withValidation((c) => {
if (this.migrationStateModel.databaseBackup.networkContainerType === NetworkContainerType.FILE_SHARE) {
if ((<azdata.CategoryValue>c.value).displayName === constants.NO_SUBSCRIPTIONS_FOUND) {
this.addErrorMessage(constants.INVALID_SUBSCRIPTION_ERROR);
return false;
} else {
this.removeErrorMessage(constants.INVALID_SUBSCRIPTION_ERROR);
}
}
return true;
}).component();
this._fileShareSubscriptionDropdown.onValueChanged(async (value) => {
this._fileShare.subscriptionId = (this._fileShareSubscriptionDropdown.value as azdata.CategoryValue).name;
await this.loadFileShareStorageDropdown();
});
const storageAccountLabel = view.modelBuilder.text()
.withProps({
value: constants.DATABASE_BACKUP_FILE_SHARE_STORAGE_ACCOUNT_LABEL,
requiredIndicator: true,
}).component();
this._fileShareStorageAccountDropdown = view.modelBuilder.dropDown()
.withProps({
required: true
}).withValidation((c) => {
if (this.migrationStateModel.databaseBackup.networkContainerType === NetworkContainerType.FILE_SHARE) {
if ((<azdata.CategoryValue>c.value).displayName === constants.NO_STORAGE_ACCOUNT_FOUND) {
this.addErrorMessage(constants.INVALID_STORAGE_ACCOUNT_ERROR);
return false;
} else {
this.removeErrorMessage(constants.INVALID_STORAGE_ACCOUNT_ERROR);
}
}
return true;
}).component();
this._fileShareStorageAccountDropdown.onValueChanged(async (value) => {
this._fileShare.storageAccountId = (this._fileShareStorageAccountDropdown.value as azdata.CategoryValue).name;
await this.loadFileShareDropdown();
});
const fileShareLabel = view.modelBuilder.text()
.withProps({
value: constants.DATABASE_BACKUP_FILE_SHARE_LABEL,
requiredIndicator: true,
}).component();
this._fileShareFileShareDropdown = view.modelBuilder.dropDown()
.withProps({
required: true
}).withValidation((c) => {
if (this.migrationStateModel.databaseBackup.networkContainerType === NetworkContainerType.FILE_SHARE) {
if ((<azdata.CategoryValue>c.value).displayName === constants.NO_FILESHARES_FOUND) {
this.addErrorMessage(constants.INVALID_FILESHARE_ERROR);
return false;
} else {
this.removeErrorMessage(constants.INVALID_FILESHARE_ERROR);
}
}
return true;
}).component();
this._fileShareFileShareDropdown.onValueChanged((value) => {
this._fileShare.fileShareId = (this._fileShareFileShareDropdown.value as azdata.CategoryValue).name;
});
const flexContainer = view.modelBuilder.flexContainer()
.withItems(
[
subscriptionLabel,
this._fileShareSubscriptionDropdown,
storageAccountLabel,
this._fileShareStorageAccountDropdown,
fileShareLabel,
this._fileShareFileShareDropdown
]
).withLayout({
flexFlow: 'column'
}).component();
return flexContainer;
}
private createBlobContainer(view: azdata.ModelView): azdata.FlexContainer {
const subscriptionLabel = view.modelBuilder.text()
.withProps({
value: constants.DATABASE_BACKUP_BLOB_STORAGE_SUBSCRIPTION_LABEL,
requiredIndicator: true,
}).component();
this._blobContainerSubscriptionDropdown = view.modelBuilder.dropDown()
.withProps({
required: true
}).withValidation((c) => {
if (this.migrationStateModel.databaseBackup.networkContainerType === NetworkContainerType.BLOB_CONTAINER) {
if (
(<azdata.CategoryValue>c.value).displayName === constants.NO_SUBSCRIPTIONS_FOUND) {
this.addErrorMessage(constants.INVALID_SUBSCRIPTION_ERROR);
return false;
} else {
this.removeErrorMessage(constants.INVALID_SUBSCRIPTION_ERROR);
}
}
return true;
}).component();
this._blobContainerSubscriptionDropdown.onValueChanged(async (value) => {
this._blob.subscriptionId = (this._blobContainerSubscriptionDropdown.value as azdata.CategoryValue).name;
await this.loadblobStorageDropdown();
});
const storageAccountLabel = view.modelBuilder.text()
.withProps({
value: constants.DATABASE_BACKUP_BLOB_STORAGE_ACCOUNT_LABEL,
requiredIndicator: true,
}).component();
this._blobContainerStorageAccountDropdown = view.modelBuilder.dropDown()
.withProps({
required: true
}).withValidation((c) => {
if (this.migrationStateModel.databaseBackup.networkContainerType === NetworkContainerType.BLOB_CONTAINER) {
if ((<azdata.CategoryValue>c.value).displayName === constants.NO_STORAGE_ACCOUNT_FOUND) {
this.addErrorMessage(constants.INVALID_STORAGE_ACCOUNT_ERROR);
return false;
} else {
this.removeErrorMessage(constants.INVALID_STORAGE_ACCOUNT_ERROR);
}
}
return true;
}).component();
this._blobContainerStorageAccountDropdown.onValueChanged(async (value) => {
this._blob.storageAccountId = (this._blobContainerStorageAccountDropdown.value as azdata.CategoryValue).name;
await this.loadBlobContainerDropdown();
});
const containerLabel = view.modelBuilder.text().withProps({
value: constants.DATABASE_BACKUP_BLOB_STORAGE_ACCOUNT_CONTAINER_LABEL,
requiredIndicator: true,
}).component();
this._blobContainerBlobDropdown = view.modelBuilder.dropDown()
.withProps({
required: true
}).withValidation((c) => {
if (this.migrationStateModel.databaseBackup.networkContainerType === NetworkContainerType.BLOB_CONTAINER) {
if ((<azdata.CategoryValue>c.value).displayName === constants.NO_BLOBCONTAINERS_FOUND) {
this.addErrorMessage(constants.INVALID_BLOBCONTAINER_ERROR);
return false;
} else {
this.removeErrorMessage(constants.INVALID_BLOBCONTAINER_ERROR);
}
}
return true;
}).component();
this._blobContainerBlobDropdown.onValueChanged((value) => {
this._blob.containerId = (this._blobContainerBlobDropdown.value as azdata.CategoryValue).name;
});
const flexContainer = view.modelBuilder.flexContainer()
.withItems(
[
subscriptionLabel,
this._blobContainerSubscriptionDropdown,
storageAccountLabel,
this._blobContainerStorageAccountDropdown,
containerLabel,
this._blobContainerBlobDropdown
]
).withLayout({
flexFlow: 'column'
}).component();
return flexContainer;
}
private createNetworkShareContainer(view: azdata.ModelView): azdata.FlexContainer {
const networkShareHelpText = view.modelBuilder.text()
.withProps({
value: constants.DATABASE_BACKUP_NC_NETWORK_SHARE_HELP_TEXT,
}).component();
const networkShareLocationLabel = view.modelBuilder.text()
.withProps({
value: constants.DATABASE_BACKUP_NETWORK_SHARE_LOCATION_LABEL,
requiredIndicator: true,
}).component();
this._networkShareLocationText = view.modelBuilder.inputBox()
.withProps({
placeHolder: '\\\\Servername.domainname.com\\Backupfolder',
required: true,
validationErrorMessage: constants.INVALID_NETWORK_SHARE_LOCATION
})
.withValidation((component) => {
if (component.value) {
if (!/^(\\)(\\[\w\.-_]+){2,}(\\?)$/.test(component.value)) {
return false;
}
}
return true;
}).component();
this._networkShareLocationText.onTextChanged((value) => {
this._networkShare.networkShareLocation = value;
});
const windowsUserAccountLabel = view.modelBuilder.text()
.withProps({
value: constants.DATABASE_BACKUP_NETWORK_SHARE_WINDOWS_USER_LABEL,
requiredIndicator: true,
}).component();
this._windowsUserAccountText = view.modelBuilder.inputBox()
.withProps({
placeHolder: 'Domain\\username',
required: true,
validationErrorMessage: constants.INVALID_USER_ACCOUNT
})
.withValidation((component) => {
if (component.value) {
if (!/^[a-zA-Z][a-zA-Z0-9\-\.]{0,61}[a-zA-Z]\\\w[\w\.\- ]*$/.test(component.value)) {
return false;
}
}
return true;
}).component();
this._windowsUserAccountText.onTextChanged((value) => {
this._networkShare.windowsUser = value;
});
const passwordLabel = view.modelBuilder.text()
.withProps({
value: constants.DATABASE_BACKUP_NETWORK_SHARE_PASSWORD_LABEL,
requiredIndicator: true,
}).component();
this._passwordText = view.modelBuilder.inputBox()
.withProps({
placeHolder: constants.DATABASE_BACKUP_NETWORK_SHARE_PASSWORD_PLACEHOLDER,
inputType: 'password',
required: true
}).component();
this._passwordText.onTextChanged((value) => {
this._networkShare.password = value;
});
const azureAccountHelpText = view.modelBuilder.text()
.withProps({
value: constants.DATABASE_BACKUP_NETWORK_SHARE_AZURE_ACCOUNT_HELP,
}).component();
const subscriptionLabel = view.modelBuilder.text()
.withProps({
value: constants.DATABASE_BACKUP_NETWORK_SHARE_SUBSCRIPTION_LABEL,
requiredIndicator: true,
}).component();
this._networkShareContainerSubscriptionDropdown = view.modelBuilder.dropDown()
.withProps({
required: true
}).withValidation((c) => {
if (this.migrationStateModel.databaseBackup.networkContainerType === NetworkContainerType.NETWORK_SHARE) {
if ((<azdata.CategoryValue>c.value).displayName === constants.NO_SUBSCRIPTIONS_FOUND) {
this.addErrorMessage(constants.INVALID_SUBSCRIPTION_ERROR);
return false;
} else {
this.removeErrorMessage(constants.INVALID_SUBSCRIPTION_ERROR);
}
}
return true;
}).component();
this._networkShareContainerSubscriptionDropdown.onValueChanged(async (value) => {
this._networkShare.storageSubscriptionId = (this._networkShareContainerSubscriptionDropdown.value as azdata.CategoryValue).name;
await this.loadNetworkShareStorageDropdown();
});
const storageAccountLabel = view.modelBuilder.text()
.withProps({
value: constants.DATABASE_BACKUP_NETWORK_SHARE_NETWORK_STORAGE_ACCOUNT_LABEL,
requiredIndicator: true,
}).component();
this._networkShareContainerStorageAccountDropdown = view.modelBuilder.dropDown()
.withProps({
required: true
}).withValidation((c) => {
if (this.migrationStateModel.databaseBackup.networkContainerType === NetworkContainerType.NETWORK_SHARE) {
if ((<azdata.CategoryValue>c.value).displayName === constants.NO_STORAGE_ACCOUNT_FOUND) {
this.addErrorMessage(constants.INVALID_STORAGE_ACCOUNT_ERROR);
return false;
} else {
this.removeErrorMessage(constants.INVALID_STORAGE_ACCOUNT_ERROR);
}
}
return true;
}).component();
this._networkShareContainerStorageAccountDropdown.onValueChanged((value) => {
this._networkShare.storageAccountId = (this._networkShareContainerStorageAccountDropdown.value as azdata.CategoryValue).name;
});
const flexContainer = view.modelBuilder.flexContainer().withItems(
[
networkShareHelpText,
networkShareLocationLabel,
this._networkShareLocationText,
windowsUserAccountLabel,
this._windowsUserAccountText,
passwordLabel,
this._passwordText,
azureAccountHelpText,
subscriptionLabel,
this._networkShareContainerSubscriptionDropdown,
storageAccountLabel,
this._networkShareContainerStorageAccountDropdown
]
).withLayout({
flexFlow: 'column'
}).component();
return flexContainer;
}
private emailNotificationContainer(view: azdata.ModelView): azdata.FormComponent {
const emailCheckbox = view.modelBuilder.checkBox().withProps({
label: constants.DATABASE_BACKUP_EMAIL_NOTIFICATION_CHECKBOX_LABEL
}).component();
emailCheckbox.onChanged((value) => this.migrationStateModel.databaseBackup.emailNotification = value);
return {
title: constants.DATABASE_BACKUP_EMAIL_NOTIFICATION_LABEL,
component: emailCheckbox
};
}
private migrationCutoverContainer(view: azdata.ModelView): azdata.FormComponent {
const description = view.modelBuilder.text().withProps({
value: constants.DATABASE_BACKUP_MIGRATION_CUTOVER_DESCRIPTION
}).component();
const buttonGroup = 'cutoverContainer';
const automaticButton = view.modelBuilder.radioButton().withProps({
label: constants.DATABASE_BACKUP_MIGRATION_CUTOVER_AUTOMATIC_LABEL,
name: buttonGroup,
checked: true
}).component();
this.migrationStateModel.databaseBackup.migrationCutover = MigrationCutover.AUTOMATIC;
automaticButton.onDidClick((e) => this.migrationStateModel.databaseBackup.migrationCutover = MigrationCutover.AUTOMATIC);
const manualButton = view.modelBuilder.radioButton().withProps({
label: constants.DATABASE_BACKUP_MIGRATION_CUTOVER_MANUAL_LABEL,
name: buttonGroup
}).component();
manualButton.onDidClick((e) => this.migrationStateModel.databaseBackup.migrationCutover = MigrationCutover.MANUAL);
const flexContainer = view.modelBuilder.flexContainer().withItems(
[
description,
automaticButton,
manualButton
]
).withLayout({
flexFlow: 'column'
}).component();
return {
title: constants.DATABASE_BACKUP_MIGRATION_CUTOVER_LABEL,
component: flexContainer
};
}
public async onPageEnter(): Promise<void> {
await this.getSubscriptionValues();
}
public async onPageLeave(): Promise<void> {
}
protected async handleStateChange(e: StateChangeEvent): Promise<void> {
}
private toggleNetworkContainerFields(containerType: NetworkContainerType, networkContainer: NetworkShare | BlobContainer | FileShare): void {
this.migrationStateModel.databaseBackup.networkContainer = networkContainer;
this.migrationStateModel.databaseBackup.networkContainerType = containerType;
this._fileShareContainer.updateCssStyles({ 'display': (containerType === NetworkContainerType.FILE_SHARE) ? 'inline' : 'none' });
this._blobContainer.updateCssStyles({ 'display': (containerType === NetworkContainerType.BLOB_CONTAINER) ? 'inline' : 'none' });
this._networkShareContainer.updateCssStyles({ 'display': (containerType === NetworkContainerType.NETWORK_SHARE) ? 'inline' : 'none' });
this._networkShareLocationText.updateProperties({
required: containerType === NetworkContainerType.NETWORK_SHARE
});
this._windowsUserAccountText.updateProperties({
required: containerType === NetworkContainerType.NETWORK_SHARE
});
this._passwordText.updateProperties({
required: containerType === NetworkContainerType.NETWORK_SHARE
});
}
private async getSubscriptionValues(): Promise<void> {
this._networkShareContainerSubscriptionDropdown.loading = true;
this._fileShareSubscriptionDropdown.loading = true;
this._blobContainerSubscriptionDropdown.loading = true;
let subscriptions: azureResource.AzureResourceSubscription[] = [];
try {
subscriptions = await getSubscriptions(this.migrationStateModel.azureAccount);
subscriptions.forEach((subscription) => {
this._subscriptionMap.set(subscription.id, subscription);
this._subscriptionDropdownValues.push({
name: subscription.id,
displayName: subscription.name + ' - ' + subscription.id,
});
});
if (!this._subscriptionDropdownValues) {
this._subscriptionDropdownValues = [
{
displayName: constants.NO_SUBSCRIPTIONS_FOUND,
name: ''
}
];
}
this._fileShareSubscriptionDropdown.values = this._subscriptionDropdownValues;
this._networkShareContainerSubscriptionDropdown.values = this._subscriptionDropdownValues;
this._blobContainerSubscriptionDropdown.values = this._subscriptionDropdownValues;
this._networkShare.storageSubscriptionId = this._subscriptionDropdownValues[0].name;
this._fileShare.subscriptionId = this._subscriptionDropdownValues[0].name;
this._blob.subscriptionId = this._subscriptionDropdownValues[0].name;
} catch (error) {
console.log(error);
this.setEmptyDropdownPlaceHolder(this._fileShareSubscriptionDropdown, constants.NO_SUBSCRIPTIONS_FOUND);
this.setEmptyDropdownPlaceHolder(this._networkShareContainerSubscriptionDropdown, constants.NO_SUBSCRIPTIONS_FOUND);
this.setEmptyDropdownPlaceHolder(this._blobContainerSubscriptionDropdown, constants.NO_SUBSCRIPTIONS_FOUND);
}
this._networkShareContainerSubscriptionDropdown.loading = false;
this._fileShareSubscriptionDropdown.loading = false;
this._blobContainerSubscriptionDropdown.loading = false;
await this.loadNetworkShareStorageDropdown();
await this.loadFileShareStorageDropdown();
await this.loadblobStorageDropdown();
this._networkShareContainerSubscriptionDropdown.validate();
this._networkShareContainerStorageAccountDropdown.validate();
}
private async loadNetworkShareStorageDropdown(): Promise<void> {
this._networkShareContainerStorageAccountDropdown.loading = true;
const subscriptionId = (<azdata.CategoryValue>this._networkShareContainerSubscriptionDropdown.value).name;
if (!subscriptionId.length) {
this.setEmptyDropdownPlaceHolder(this._networkShareContainerStorageAccountDropdown, constants.NO_STORAGE_ACCOUNT_FOUND);
} else {
const storageAccounts = await this.loadStorageAccounts(this._networkShare.storageSubscriptionId);
if (storageAccounts && storageAccounts.length) {
this._networkShareContainerStorageAccountDropdown.values = storageAccounts.map(s => <azdata.CategoryValue>{ name: s.id, displayName: s.name });
this._networkShare.storageAccountId = storageAccounts[0].id;
}
else {
this.setEmptyDropdownPlaceHolder(this._networkShareContainerStorageAccountDropdown, constants.NO_STORAGE_ACCOUNT_FOUND);
}
}
this._networkShareContainerStorageAccountDropdown.loading = false;
}
private async loadFileShareStorageDropdown(): Promise<void> {
this._fileShareStorageAccountDropdown.loading = true;
this._fileShareFileShareDropdown.loading = true;
const subscriptionId = (<azdata.CategoryValue>this._fileShareSubscriptionDropdown.value).name;
if (!subscriptionId.length) {
this.setEmptyDropdownPlaceHolder(this._fileShareStorageAccountDropdown, constants.NO_STORAGE_ACCOUNT_FOUND);
} else {
const storageAccounts = await this.loadStorageAccounts(this._fileShare.subscriptionId);
if (storageAccounts && storageAccounts.length) {
this._fileShareStorageAccountDropdown.values = storageAccounts.map(s => <azdata.CategoryValue>{ name: s.id, displayName: s.name });
this._fileShare.storageAccountId = storageAccounts[0].id;
}
else {
this.setEmptyDropdownPlaceHolder(this._fileShareStorageAccountDropdown, constants.NO_STORAGE_ACCOUNT_FOUND);
this._fileShareStorageAccountDropdown.loading = false;
}
}
this._fileShareStorageAccountDropdown.loading = false;
await this.loadFileShareDropdown();
}
private async loadblobStorageDropdown(): Promise<void> {
this._blobContainerStorageAccountDropdown.loading = true;
this._blobContainerBlobDropdown.loading = true;
const subscriptionId = (<azdata.CategoryValue>this._blobContainerSubscriptionDropdown.value).name;
if (!subscriptionId.length) {
this.setEmptyDropdownPlaceHolder(this._blobContainerStorageAccountDropdown, constants.NO_STORAGE_ACCOUNT_FOUND);
} else {
const storageAccounts = await this.loadStorageAccounts(this._blob.subscriptionId);
if (storageAccounts.length) {
this._blobContainerStorageAccountDropdown.values = storageAccounts.map(s => <azdata.CategoryValue>{ name: s.id, displayName: s.name });
this._blob.storageAccountId = storageAccounts[0].id;
} else {
this.setEmptyDropdownPlaceHolder(this._blobContainerStorageAccountDropdown, constants.NO_STORAGE_ACCOUNT_FOUND);
}
}
this._blobContainerStorageAccountDropdown.loading = false;
await this.loadBlobContainerDropdown();
}
private async loadStorageAccounts(subscriptionId: string): Promise<StorageAccount[]> {
const storageAccounts = await getAvailableStorageAccounts(this.migrationStateModel.azureAccount, this._subscriptionMap.get(subscriptionId)!);
storageAccounts.forEach(s => {
this._storageAccountMap.set(s.id, s);
});
return storageAccounts;
}
private async loadFileShareDropdown(): Promise<void> {
this._fileShareFileShareDropdown.loading = true;
const storageAccountId = (<azdata.CategoryValue>this._fileShareStorageAccountDropdown.value).name;
if (!storageAccountId.length) {
this.setEmptyDropdownPlaceHolder(this._fileShareFileShareDropdown, constants.NO_FILESHARES_FOUND);
} else {
const fileShares = await getFileShares(this.migrationStateModel.azureAccount, this._subscriptionMap.get(this._fileShare.subscriptionId)!, this._storageAccountMap.get(storageAccountId)!);
if (fileShares && fileShares.length) {
this._fileShareFileShareDropdown.values = fileShares.map(f => <azdata.CategoryValue>{ name: f.id, displayName: f.name });
this._fileShare.fileShareId = fileShares[0].id!;
} else {
this.setEmptyDropdownPlaceHolder(this._fileShareFileShareDropdown, constants.NO_FILESHARES_FOUND);
}
}
this._fileShareFileShareDropdown.loading = false;
}
private async loadBlobContainerDropdown(): Promise<void> {
this._blobContainerBlobDropdown.loading = true;
const storageAccountId = (<azdata.CategoryValue>this._blobContainerStorageAccountDropdown.value).name;
if (!storageAccountId.length) {
this.setEmptyDropdownPlaceHolder(this._blobContainerBlobDropdown, constants.NO_BLOBCONTAINERS_FOUND);
} else {
const blobContainers = await getBlobContainers(this.migrationStateModel.azureAccount, this._subscriptionMap.get(this._blob.subscriptionId)!, this._storageAccountMap.get(storageAccountId)!);
if (blobContainers && blobContainers.length) {
this._blobContainerBlobDropdown.values = blobContainers.map(f => <azdata.CategoryValue>{ name: f.id, displayName: f.name });
this._blob.containerId = blobContainers[0].id!;
} else {
this.setEmptyDropdownPlaceHolder(this._blobContainerBlobDropdown, constants.NO_BLOBCONTAINERS_FOUND);
}
}
this._blobContainerBlobDropdown.loading = false;
}
private setEmptyDropdownPlaceHolder(dropDown: azdata.DropDownComponent, placeholder: string): void {
dropDown.values = [{
displayName: placeholder,
name: ''
}];
}
private addErrorMessage(message: string) {
if (!this._errors.includes(message)) {
this._errors.push(message);
}
this.wizard.message = {
text: this._errors.join(EOL),
level: azdata.window.MessageLevel.Error
};
}
private removeErrorMessage(message: string) {
this._errors = this._errors.filter(e => e !== message);
this.wizard.message = {
text: this._errors.join(EOL),
level: azdata.window.MessageLevel.Error
};
}
}

View File

@@ -11,6 +11,8 @@ import { WIZARD_TITLE } from '../models/strings';
import { MigrationWizardPage } from '../models/migrationWizardPage';
import { SKURecommendationPage } from './skuRecommendationPage';
import { SubscriptionSelectionPage } from './subscriptionSelectionPage';
import { DatabaseBackupPage } from './databaseBackupPage';
import { AccountsSelectionPage } from './accountsSelectionPage';
export class WizardController {
constructor(private readonly extensionContext: vscode.ExtensionContext) {
@@ -34,8 +36,9 @@ export class WizardController {
const sourceConfigurationPage = new SourceConfigurationPage(wizard, stateModel);
const skuRecommendationPage = new SKURecommendationPage(wizard, stateModel);
const subscriptionSelectionPage = new SubscriptionSelectionPage(wizard, stateModel);
const pages: MigrationWizardPage[] = [sourceConfigurationPage, skuRecommendationPage, subscriptionSelectionPage];
const azureAccountsPage = new AccountsSelectionPage(wizard, stateModel);
const databaseBackupPage = new DatabaseBackupPage(wizard, stateModel);
const pages: MigrationWizardPage[] = [sourceConfigurationPage, skuRecommendationPage, subscriptionSelectionPage, azureAccountsPage, databaseBackupPage];
wizard.pages = pages.map(p => p.getwizardPage());