mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-01-19 01:25:36 -05:00
* Implement storage account connectivity check for SQL VM targets * Add missing break statement * Address PR comments
1839 lines
74 KiB
TypeScript
1839 lines
74 KiB
TypeScript
/*---------------------------------------------------------------------------------------------
|
|
* 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 { EOL } from 'os';
|
|
import { getStorageAccountAccessKeys, SqlVMServer } from '../api/azure';
|
|
import { MigrationWizardPage } from '../models/migrationWizardPage';
|
|
import { MigrationMode, MigrationSourceAuthenticationType, MigrationStateModel, NetworkContainerType, NetworkShare, StateChangeEvent, ValidateIrState, ValidationResult } from '../models/stateMachine';
|
|
import * as constants from '../constants/strings';
|
|
import { IconPathHelper } from '../constants/iconPathHelper';
|
|
import { WIZARD_INPUT_COMPONENT_WIDTH } from './wizardController';
|
|
import * as utils from '../api/utils';
|
|
import { MigrationTargetType } from '../api/utils';
|
|
import { logError, TelemetryViews } from '../telemetry';
|
|
import * as styles from '../constants/styles';
|
|
import { TableMigrationSelectionDialog } from '../dialog/tableMigrationSelection/tableMigrationSelectionDialog';
|
|
import { ValidateIrDialog } from '../dialog/validationResults/validateIrDialog';
|
|
import { canTargetConnectToStorageAccount, getSourceConnectionCredentials, getSourceConnectionProfile, getSourceConnectionQueryProvider, getSourceConnectionUri } from '../api/sqlUtils';
|
|
|
|
const WIZARD_TABLE_COLUMN_WIDTH = '200px';
|
|
const WIZARD_TABLE_COLUMN_WIDTH_SMALL = '170px';
|
|
const VALIDATE_IR_CUSTOM_BUTTON_INDEX = 0;
|
|
|
|
const blobResourceGroupErrorStrings = [constants.RESOURCE_GROUP_NOT_FOUND];
|
|
const blobStorageAccountErrorStrings = [constants.NO_STORAGE_ACCOUNT_FOUND, constants.SELECT_RESOURCE_GROUP_PROMPT];
|
|
const blobContainerErrorStrings = [constants.NO_BLOBCONTAINERS_FOUND, constants.SELECT_STORAGE_ACCOUNT];
|
|
const blobFileErrorStrings = [constants.NO_BLOBFILES_FOUND, constants.SELECT_BLOB_CONTAINER];
|
|
const blobFolderErrorStrings = [constants.NO_BLOBFOLDERS_FOUND, constants.SELECT_BLOB_CONTAINER];
|
|
|
|
export class DatabaseBackupPage extends MigrationWizardPage {
|
|
private _view!: azdata.ModelView;
|
|
|
|
private _sourceConnectionContainer!: azdata.FlexContainer;
|
|
private _networkShareContainer!: azdata.FlexContainer;
|
|
private _windowsUserAccountText!: azdata.InputBoxComponent;
|
|
private _passwordText!: azdata.InputBoxComponent;
|
|
private _sourceHelpText!: azdata.TextComponent;
|
|
private _sqlSourceUsernameInput!: azdata.InputBoxComponent;
|
|
private _sqlSourcePassword!: azdata.InputBoxComponent;
|
|
|
|
private _blobContainer!: azdata.FlexContainer;
|
|
private _blobContainerSubscription!: azdata.TextComponent;
|
|
private _blobContainerLocation!: azdata.TextComponent;
|
|
private _blobContainerResourceGroupDropdowns!: azdata.DropDownComponent[];
|
|
private _blobContainerStorageAccountDropdowns!: azdata.DropDownComponent[];
|
|
private _blobContainerDropdowns!: azdata.DropDownComponent[];
|
|
private _blobContainerLastBackupFileDropdowns!: azdata.DropDownComponent[];
|
|
private _blobContainerFolderDropdowns!: azdata.DropDownComponent[];
|
|
private _blobContainerFolderStructureInfoBox!: azdata.TextComponent;
|
|
private _blobContainerVmDatabaseAlreadyExistsInfoBox!: azdata.TextComponent;
|
|
|
|
private _networkShareStorageAccountDetails!: azdata.FlexContainer;
|
|
private _networkShareContainerSubscription!: azdata.TextComponent;
|
|
private _networkShareContainerLocation!: azdata.TextComponent;
|
|
private _networkShareStorageAccountResourceGroupDropdown!: azdata.DropDownComponent;
|
|
private _networkShareContainerStorageAccountDropdown!: azdata.DropDownComponent;
|
|
private _networkShareContainerStorageAccountRefreshButton!: azdata.ButtonComponent;
|
|
private _networkShareVmDatabaseAlreadyExistsInfoBox!: azdata.TextComponent;
|
|
|
|
private _targetDatabaseContainer!: azdata.FlexContainer;
|
|
private _networkShareTargetDatabaseNamesTable!: azdata.DeclarativeTableComponent;
|
|
private _blobContainerTargetDatabaseNamesTable!: azdata.DeclarativeTableComponent;
|
|
private _networkTableContainer!: azdata.FlexContainer;
|
|
private _blobTableContainer!: azdata.FlexContainer;
|
|
private _networkShareTargetDatabaseNames: azdata.InputBoxComponent[] = [];
|
|
private _blobContainerTargetDatabaseNames: azdata.InputBoxComponent[] = [];
|
|
private _networkShareLocations: azdata.InputBoxComponent[] = [];
|
|
private _networkDetailsContainer!: azdata.FlexContainer;
|
|
|
|
private _existingDatabases: string[] = [];
|
|
private _nonPageBlobErrors: string[] = [];
|
|
private _inaccessibleStorageAccounts: string[] = [];
|
|
private _disposables: vscode.Disposable[] = [];
|
|
|
|
// SQL DB table selection
|
|
private _refreshLoading!: azdata.LoadingComponent;
|
|
private _refreshButton!: azdata.ButtonComponent;
|
|
private _databaseTable!: azdata.TableComponent;
|
|
private _migrationTableSection!: azdata.FlexContainer;
|
|
|
|
constructor(wizard: azdata.window.Wizard, migrationStateModel: MigrationStateModel) {
|
|
super(wizard, azdata.window.createWizardPage(constants.DATA_SOURCE_CONFIGURATION_PAGE_TITLE), migrationStateModel);
|
|
}
|
|
|
|
protected async registerContent(view: azdata.ModelView): Promise<void> {
|
|
this._view = view;
|
|
|
|
this._sourceConnectionContainer = this.createSourceCredentialsContainer();
|
|
this._networkDetailsContainer = this.createNetworkDetailsContainer();
|
|
this._targetDatabaseContainer = this.createTargetDatabaseContainer();
|
|
this._networkShareStorageAccountDetails = this.createNetworkShareStorageAccountDetailsContainer();
|
|
this._migrationTableSection = this._migrationTableSelectionContainer();
|
|
|
|
this._disposables.push(
|
|
this.wizard.customButtons[VALIDATE_IR_CUSTOM_BUTTON_INDEX].onClick(
|
|
async e => await this._validateIr()));
|
|
|
|
const form = this._view.modelBuilder.formContainer()
|
|
.withFormItems([
|
|
{ title: '', component: this._sourceConnectionContainer },
|
|
{ title: '', component: this._networkDetailsContainer },
|
|
{ title: '', component: this._networkShareStorageAccountDetails },
|
|
{ title: '', component: this._targetDatabaseContainer },
|
|
{ title: '', component: this._migrationTableSection },
|
|
])
|
|
.withProps({ CSSStyles: { 'padding-top': '0' } })
|
|
.component();
|
|
|
|
this._disposables.push(
|
|
this._view.onClosed(e => {
|
|
this._disposables.forEach(
|
|
d => { try { d.dispose(); } catch { } });
|
|
}));
|
|
|
|
await view.initializeModel(form);
|
|
}
|
|
|
|
private createNetworkDetailsContainer(): azdata.FlexContainer {
|
|
this._networkShareContainer = this.createNetworkShareContainer();
|
|
this._blobContainer = this.createBlobContainer();
|
|
|
|
return this._view.modelBuilder.flexContainer()
|
|
.withLayout({ flexFlow: 'column' })
|
|
.withItems([
|
|
this._networkShareContainer,
|
|
this._blobContainer])
|
|
.component();
|
|
}
|
|
|
|
private createSourceCredentialsContainer(): azdata.FlexContainer {
|
|
const sqlSourceHeader = this._view.modelBuilder.text()
|
|
.withProps({
|
|
value: constants.SOURCE_CREDENTIALS,
|
|
width: WIZARD_INPUT_COMPONENT_WIDTH,
|
|
CSSStyles: { ...styles.SECTION_HEADER_CSS, 'margin-top': '4px' }
|
|
}).component();
|
|
|
|
this._sourceHelpText = this._view.modelBuilder.text()
|
|
.withProps({
|
|
width: WIZARD_INPUT_COMPONENT_WIDTH,
|
|
CSSStyles: { ...styles.BODY_CSS }
|
|
}).component();
|
|
|
|
const usernameLabel = this._view.modelBuilder.text()
|
|
.withProps({
|
|
value: constants.USERNAME,
|
|
width: WIZARD_INPUT_COMPONENT_WIDTH,
|
|
requiredIndicator: true,
|
|
CSSStyles: { ...styles.LABEL_CSS }
|
|
}).component();
|
|
this._sqlSourceUsernameInput = this._view.modelBuilder.inputBox()
|
|
.withProps({
|
|
required: true,
|
|
enabled: false,
|
|
width: WIZARD_INPUT_COMPONENT_WIDTH,
|
|
CSSStyles: { 'margin-top': '-1em' },
|
|
}).component();
|
|
this._disposables.push(
|
|
this._sqlSourceUsernameInput.onTextChanged(
|
|
value => {
|
|
this.migrationStateModel._sqlServerUsername = value;
|
|
this._resetValidationUI();
|
|
}));
|
|
|
|
const sqlPasswordLabel = this._view.modelBuilder.text()
|
|
.withProps({
|
|
value: constants.DATABASE_BACKUP_NETWORK_SHARE_PASSWORD_LABEL,
|
|
width: WIZARD_INPUT_COMPONENT_WIDTH,
|
|
requiredIndicator: true,
|
|
CSSStyles: { ...styles.LABEL_CSS }
|
|
}).component();
|
|
this._sqlSourcePassword = this._view.modelBuilder.inputBox()
|
|
.withProps({
|
|
required: true,
|
|
inputType: 'password',
|
|
width: WIZARD_INPUT_COMPONENT_WIDTH,
|
|
CSSStyles: { 'margin-top': '-1em' },
|
|
}).component();
|
|
this._disposables.push(
|
|
this._sqlSourcePassword.onTextChanged(
|
|
value => {
|
|
this.migrationStateModel._sqlServerPassword = value;
|
|
this._resetValidationUI();
|
|
}));
|
|
|
|
return this._view.modelBuilder.flexContainer()
|
|
.withItems([
|
|
sqlSourceHeader,
|
|
this._sourceHelpText,
|
|
usernameLabel,
|
|
this._sqlSourceUsernameInput,
|
|
sqlPasswordLabel,
|
|
this._sqlSourcePassword])
|
|
.withLayout({ flexFlow: 'column' })
|
|
.withProps({ display: 'none' })
|
|
.component();
|
|
}
|
|
|
|
private createNetworkShareContainer(): azdata.FlexContainer {
|
|
const networkShareHeading = this._view.modelBuilder.text()
|
|
.withProps({
|
|
value: constants.DATABASE_BACKUP_NETWORK_SHARE_HEADER_TEXT,
|
|
width: WIZARD_INPUT_COMPONENT_WIDTH,
|
|
CSSStyles: { ...styles.SECTION_HEADER_CSS, 'margin-top': '24px' }
|
|
}).component();
|
|
|
|
const networkShareHelpText = this._view.modelBuilder.text()
|
|
.withProps({
|
|
value: constants.DATABASE_BACKUP_NC_NETWORK_SHARE_HELP_TEXT,
|
|
width: WIZARD_INPUT_COMPONENT_WIDTH,
|
|
CSSStyles: { ...styles.BODY_CSS }
|
|
}).component();
|
|
|
|
const networkShareInfoBox = this._view.modelBuilder.infoBox()
|
|
.withProps({
|
|
text: constants.DATABASE_SERVICE_ACCOUNT_INFO_TEXT,
|
|
style: 'information',
|
|
width: WIZARD_INPUT_COMPONENT_WIDTH,
|
|
CSSStyles: { ...styles.BODY_CSS }
|
|
}).component();
|
|
|
|
const windowsUserAccountLabel = this._view.modelBuilder.text()
|
|
.withProps({
|
|
value: constants.DATABASE_BACKUP_NETWORK_SHARE_WINDOWS_USER_LABEL,
|
|
description: constants.DATABASE_BACKUP_NETWORK_SHARE_WINDOWS_USER_INFO,
|
|
width: WIZARD_INPUT_COMPONENT_WIDTH,
|
|
requiredIndicator: true,
|
|
CSSStyles: { ...styles.LABEL_CSS },
|
|
}).component();
|
|
this._windowsUserAccountText = this._view.modelBuilder.inputBox()
|
|
.withProps({
|
|
placeHolder: constants.WINDOWS_USER_ACCOUNT,
|
|
required: true,
|
|
validationErrorMessage: constants.INVALID_USER_ACCOUNT,
|
|
width: WIZARD_INPUT_COMPONENT_WIDTH,
|
|
CSSStyles: { ...styles.BODY_CSS, 'margin-top': '-1em' }
|
|
})
|
|
.withValidation((component) => {
|
|
if (this.migrationStateModel.isBackupContainerNetworkShare) {
|
|
if (component.value) {
|
|
if (!/^[A-Za-z0-9\\\._-]{7,}$/.test(component.value)) {
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
return true;
|
|
}).component();
|
|
this._disposables.push(
|
|
this._windowsUserAccountText.onTextChanged((value) => {
|
|
for (let i = 0; i < this.migrationStateModel._databaseBackup.networkShares.length; i++) {
|
|
this.migrationStateModel._databaseBackup.networkShares[i].windowsUser = value;
|
|
}
|
|
this._resetValidationUI();
|
|
}));
|
|
|
|
const passwordLabel = this._view.modelBuilder.text()
|
|
.withProps({
|
|
value: constants.DATABASE_BACKUP_NETWORK_SHARE_PASSWORD_LABEL,
|
|
width: WIZARD_INPUT_COMPONENT_WIDTH,
|
|
requiredIndicator: true,
|
|
CSSStyles: { ...styles.LABEL_CSS, }
|
|
}).component();
|
|
this._passwordText = this._view.modelBuilder.inputBox()
|
|
.withProps({
|
|
placeHolder: constants.DATABASE_BACKUP_NETWORK_SHARE_PASSWORD_PLACEHOLDER,
|
|
inputType: 'password',
|
|
required: true,
|
|
width: WIZARD_INPUT_COMPONENT_WIDTH,
|
|
CSSStyles: { ...styles.BODY_CSS, 'margin-top': '-1em' }
|
|
}).component();
|
|
this._disposables.push(
|
|
this._passwordText.onTextChanged((value) => {
|
|
for (let i = 0; i < this.migrationStateModel._databaseBackup.networkShares.length; i++) {
|
|
this.migrationStateModel._databaseBackup.networkShares[i].password = value;
|
|
}
|
|
this._resetValidationUI();
|
|
}));
|
|
|
|
return this._view.modelBuilder.flexContainer()
|
|
.withItems([
|
|
networkShareHeading,
|
|
networkShareHelpText,
|
|
networkShareInfoBox,
|
|
windowsUserAccountLabel,
|
|
this._windowsUserAccountText,
|
|
passwordLabel,
|
|
this._passwordText])
|
|
.withLayout({ flexFlow: 'column' })
|
|
.withProps({ display: 'none' })
|
|
.component();
|
|
}
|
|
|
|
private _resetValidationUI(): void {
|
|
if (this.wizard.message.level === azdata.window.MessageLevel.Information) {
|
|
this.wizard.message = { text: '' };
|
|
}
|
|
this.migrationStateModel.resetIrValidationResults();
|
|
}
|
|
|
|
private createBlobContainer(): azdata.FlexContainer {
|
|
const blobHeading = this._view.modelBuilder.text()
|
|
.withProps({
|
|
value: constants.DATABASE_BACKUP_BLOB_STORAGE_HEADER_TEXT,
|
|
width: WIZARD_INPUT_COMPONENT_WIDTH,
|
|
CSSStyles: { ...styles.SECTION_HEADER_CSS }
|
|
}).component();
|
|
|
|
const blobHelpText = this._view.modelBuilder.text()
|
|
.withProps({
|
|
value: constants.DATABASE_BACKUP_BLOB_STORAGE_HELP_TEXT,
|
|
width: WIZARD_INPUT_COMPONENT_WIDTH,
|
|
CSSStyles: { ...styles.BODY_CSS, 'margin-bottom': '12px' }
|
|
}).component();
|
|
|
|
const subscriptionLabel = this._view.modelBuilder.text()
|
|
.withProps({
|
|
value: constants.DATABASE_BACKUP_BLOB_STORAGE_SUBSCRIPTION_LABEL,
|
|
CSSStyles: { ...styles.LABEL_CSS }
|
|
}).component();
|
|
this._blobContainerSubscription = this._view.modelBuilder.text()
|
|
.withProps({
|
|
enabled: false,
|
|
CSSStyles: { ...styles.BODY_CSS, 'margin': '0 0 12px 0' }
|
|
}).component();
|
|
|
|
const locationLabel = this._view.modelBuilder.text()
|
|
.withProps({
|
|
value: constants.LOCATION,
|
|
CSSStyles: { ...styles.LABEL_CSS }
|
|
}).component();
|
|
this._blobContainerLocation = this._view.modelBuilder.text()
|
|
.withProps({
|
|
enabled: false,
|
|
CSSStyles: { ...styles.BODY_CSS, 'margin': '0px' }
|
|
}).component();
|
|
|
|
const flexContainer = this._view.modelBuilder.flexContainer()
|
|
.withItems([
|
|
blobHeading,
|
|
blobHelpText,
|
|
subscriptionLabel,
|
|
this._blobContainerSubscription,
|
|
locationLabel,
|
|
this._blobContainerLocation])
|
|
.withLayout({ flexFlow: 'column' })
|
|
.withProps({ display: 'none' })
|
|
.component();
|
|
|
|
return flexContainer;
|
|
}
|
|
|
|
private createTargetDatabaseContainer(): azdata.FlexContainer {
|
|
const headerCssStyles: azdata.CssStyles = {
|
|
...styles.LABEL_CSS,
|
|
'border': 'none',
|
|
'text-align': 'left',
|
|
'box-shadow': 'inset 0px -1px 0px #F3F2F1',
|
|
};
|
|
const rowCssStyle: azdata.CssStyles = {
|
|
...styles.BODY_CSS,
|
|
'border': 'none',
|
|
'font-size': '13px',
|
|
'box-shadow': 'inset 0px -1px 0px #F3F2F1',
|
|
};
|
|
|
|
const networkShareTableText = this._view.modelBuilder.text()
|
|
.withProps({
|
|
value: constants.DATABASE_BACKUP_NETWORK_SHARE_TABLE_HELP_TEXT,
|
|
CSSStyles: { ...styles.SECTION_HEADER_CSS, 'margin-top': '8px' }
|
|
}).component();
|
|
|
|
const blobTableText = this._view.modelBuilder.text()
|
|
.withProps({
|
|
value: constants.DATABASE_BACKUP_BLOB_STORAGE_TABLE_HELP_TEXT,
|
|
CSSStyles: { ...styles.SECTION_HEADER_CSS }
|
|
}).component();
|
|
|
|
this._networkShareTargetDatabaseNamesTable = this._view.modelBuilder.declarativeTable()
|
|
.withProps({
|
|
columns: [
|
|
{
|
|
displayName: constants.SOURCE_DATABASE,
|
|
valueType: azdata.DeclarativeDataType.string,
|
|
rowCssStyles: rowCssStyle,
|
|
headerCssStyles: headerCssStyles,
|
|
isReadOnly: true,
|
|
width: '250px'
|
|
},
|
|
{
|
|
displayName: constants.TARGET_DATABASE_NAME,
|
|
valueType: azdata.DeclarativeDataType.component,
|
|
rowCssStyles: rowCssStyle,
|
|
headerCssStyles: headerCssStyles,
|
|
isReadOnly: true,
|
|
width: WIZARD_TABLE_COLUMN_WIDTH_SMALL
|
|
},
|
|
{
|
|
displayName: constants.NETWORK_SHARE_PATH,
|
|
valueType: azdata.DeclarativeDataType.component,
|
|
rowCssStyles: rowCssStyle,
|
|
headerCssStyles: headerCssStyles,
|
|
isReadOnly: true,
|
|
width: '300px'
|
|
}
|
|
]
|
|
}).component();
|
|
|
|
this._blobContainerTargetDatabaseNamesTable = this._view.modelBuilder.declarativeTable()
|
|
.withProps({
|
|
columns: [
|
|
{
|
|
displayName: constants.SOURCE_DATABASE,
|
|
valueType: azdata.DeclarativeDataType.string,
|
|
rowCssStyles: rowCssStyle,
|
|
headerCssStyles: headerCssStyles,
|
|
isReadOnly: true,
|
|
width: WIZARD_TABLE_COLUMN_WIDTH_SMALL,
|
|
},
|
|
{
|
|
displayName: constants.TARGET_DATABASE_NAME,
|
|
valueType: azdata.DeclarativeDataType.component,
|
|
rowCssStyles: rowCssStyle,
|
|
headerCssStyles: headerCssStyles,
|
|
isReadOnly: true,
|
|
width: WIZARD_TABLE_COLUMN_WIDTH_SMALL
|
|
},
|
|
{
|
|
displayName: constants.RESOURCE_GROUP,
|
|
valueType: azdata.DeclarativeDataType.component,
|
|
rowCssStyles: rowCssStyle,
|
|
headerCssStyles: headerCssStyles,
|
|
isReadOnly: true,
|
|
width: WIZARD_TABLE_COLUMN_WIDTH_SMALL
|
|
},
|
|
{
|
|
displayName: constants.STORAGE_ACCOUNT,
|
|
valueType: azdata.DeclarativeDataType.component,
|
|
rowCssStyles: rowCssStyle,
|
|
headerCssStyles: headerCssStyles,
|
|
isReadOnly: true,
|
|
width: WIZARD_TABLE_COLUMN_WIDTH_SMALL
|
|
},
|
|
{
|
|
displayName: constants.BLOB_CONTAINER,
|
|
valueType: azdata.DeclarativeDataType.component,
|
|
rowCssStyles: rowCssStyle,
|
|
headerCssStyles: headerCssStyles,
|
|
isReadOnly: true,
|
|
width: WIZARD_TABLE_COLUMN_WIDTH_SMALL
|
|
},
|
|
{
|
|
displayName: constants.BLOB_CONTAINER_LAST_BACKUP_FILE,
|
|
valueType: azdata.DeclarativeDataType.component,
|
|
rowCssStyles: rowCssStyle,
|
|
headerCssStyles: headerCssStyles,
|
|
isReadOnly: true,
|
|
width: WIZARD_TABLE_COLUMN_WIDTH_SMALL,
|
|
hidden: true
|
|
},
|
|
{
|
|
displayName: constants.BLOB_CONTAINER_FOLDER,
|
|
valueType: azdata.DeclarativeDataType.component,
|
|
rowCssStyles: rowCssStyle,
|
|
headerCssStyles: headerCssStyles,
|
|
isReadOnly: true,
|
|
width: WIZARD_TABLE_COLUMN_WIDTH_SMALL,
|
|
hidden: true
|
|
},
|
|
]
|
|
}).component();
|
|
|
|
this._networkShareVmDatabaseAlreadyExistsInfoBox = this._view.modelBuilder.infoBox()
|
|
.withProps({
|
|
text: constants.DATABASE_ALREADY_EXISTS_VM_INFO,
|
|
style: 'information',
|
|
width: WIZARD_INPUT_COMPONENT_WIDTH,
|
|
CSSStyles: { ...styles.BODY_CSS, 'display': 'none' }
|
|
}).component();
|
|
|
|
this._networkTableContainer = this._view.modelBuilder.flexContainer()
|
|
.withItems([
|
|
networkShareTableText,
|
|
this._networkShareVmDatabaseAlreadyExistsInfoBox,
|
|
this._networkShareTargetDatabaseNamesTable])
|
|
.withProps({ CSSStyles: { 'display': 'none', } })
|
|
.component();
|
|
|
|
const allFieldsRequiredLabel = this._view.modelBuilder.text()
|
|
.withProps({
|
|
value: constants.ALL_FIELDS_REQUIRED,
|
|
CSSStyles: { ...styles.BODY_CSS }
|
|
}).component();
|
|
|
|
this._blobContainerFolderStructureInfoBox = this._view.modelBuilder.infoBox()
|
|
.withProps({
|
|
text: constants.DATABASE_BACKUP_BLOB_FOLDER_STRUCTURE_INFO,
|
|
style: 'information',
|
|
width: WIZARD_INPUT_COMPONENT_WIDTH,
|
|
CSSStyles: { ...styles.BODY_CSS }
|
|
}).component();
|
|
|
|
this._blobContainerVmDatabaseAlreadyExistsInfoBox = this._view.modelBuilder.infoBox()
|
|
.withProps({
|
|
text: constants.DATABASE_ALREADY_EXISTS_VM_INFO,
|
|
style: 'information',
|
|
width: WIZARD_INPUT_COMPONENT_WIDTH,
|
|
CSSStyles: { ...styles.BODY_CSS, 'display': 'none' }
|
|
}).component();
|
|
|
|
this._blobTableContainer = this._view.modelBuilder.flexContainer()
|
|
.withItems([
|
|
blobTableText,
|
|
allFieldsRequiredLabel,
|
|
this._blobContainerFolderStructureInfoBox,
|
|
this._blobContainerVmDatabaseAlreadyExistsInfoBox,
|
|
this._blobContainerTargetDatabaseNamesTable])
|
|
.withProps({ CSSStyles: { 'display': 'none', } })
|
|
.component();
|
|
|
|
const container = this._view.modelBuilder.flexContainer()
|
|
.withLayout({ flexFlow: 'column' })
|
|
.withItems([
|
|
this._networkTableContainer,
|
|
this._blobTableContainer])
|
|
.withProps({ CSSStyles: { 'display': 'none' } })
|
|
.component();
|
|
return container;
|
|
}
|
|
|
|
private createNetworkShareStorageAccountDetailsContainer(): azdata.FlexContainer {
|
|
const azureAccountHeader = this._view.modelBuilder.text()
|
|
.withProps({
|
|
value: constants.DATABASE_BACKUP_NETWORK_SHARE_AZURE_ACCOUNT_HEADER,
|
|
width: WIZARD_INPUT_COMPONENT_WIDTH,
|
|
CSSStyles: { ...styles.SECTION_HEADER_CSS, 'margin-top': '12px' }
|
|
}).component();
|
|
|
|
const azureAccountHelpText = this._view.modelBuilder.text()
|
|
.withProps({
|
|
value: constants.DATABASE_BACKUP_NETWORK_SHARE_AZURE_ACCOUNT_HELP,
|
|
width: WIZARD_INPUT_COMPONENT_WIDTH,
|
|
CSSStyles: { ...styles.BODY_CSS, 'margin-bottom': '12px' }
|
|
}).component();
|
|
|
|
const subscriptionLabel = this._view.modelBuilder.text()
|
|
.withProps({
|
|
value: constants.SUBSCRIPTION,
|
|
width: WIZARD_INPUT_COMPONENT_WIDTH,
|
|
CSSStyles: { ...styles.LABEL_CSS, 'margin': '0' }
|
|
}).component();
|
|
this._networkShareContainerSubscription = this._view.modelBuilder.text()
|
|
.withProps({
|
|
enabled: false,
|
|
width: WIZARD_INPUT_COMPONENT_WIDTH,
|
|
CSSStyles: { 'margin': '0' }
|
|
}).component();
|
|
|
|
const locationLabel = this._view.modelBuilder.text()
|
|
.withProps({
|
|
value: constants.LOCATION,
|
|
width: WIZARD_INPUT_COMPONENT_WIDTH,
|
|
CSSStyles: { ...styles.LABEL_CSS, 'margin': '12px 0 0' }
|
|
}).component();
|
|
this._networkShareContainerLocation = this._view.modelBuilder.text()
|
|
.withProps({
|
|
enabled: false,
|
|
width: WIZARD_INPUT_COMPONENT_WIDTH,
|
|
CSSStyles: { 'margin': '0' }
|
|
}).component();
|
|
|
|
const resourceGroupLabel = this._view.modelBuilder.text()
|
|
.withProps({
|
|
value: constants.RESOURCE_GROUP,
|
|
width: WIZARD_INPUT_COMPONENT_WIDTH,
|
|
requiredIndicator: true,
|
|
CSSStyles: { ...styles.LABEL_CSS }
|
|
}).component();
|
|
this._networkShareStorageAccountResourceGroupDropdown = this._view.modelBuilder.dropDown()
|
|
.withProps({
|
|
required: true,
|
|
ariaLabel: constants.RESOURCE_GROUP,
|
|
width: WIZARD_INPUT_COMPONENT_WIDTH,
|
|
editable: true,
|
|
fireOnTextChange: true,
|
|
CSSStyles: { 'margin-top': '-1em' },
|
|
}).component();
|
|
this._disposables.push(
|
|
this._networkShareStorageAccountResourceGroupDropdown.onValueChanged(async (value) => {
|
|
if (value && value !== 'undefined') {
|
|
const selectedResourceGroup = this.migrationStateModel._resourceGroups.find(rg => rg.name === value);
|
|
if (selectedResourceGroup) {
|
|
for (let i = 0; i < this.migrationStateModel._databaseBackup.networkShares.length; i++) {
|
|
this.migrationStateModel._databaseBackup.networkShares[i].resourceGroup = selectedResourceGroup;
|
|
}
|
|
this.loadNetworkShareStorageDropdown();
|
|
}
|
|
}
|
|
}));
|
|
|
|
const storageAccountLabel = this._view.modelBuilder.text()
|
|
.withProps({
|
|
value: constants.STORAGE_ACCOUNT,
|
|
width: WIZARD_INPUT_COMPONENT_WIDTH,
|
|
requiredIndicator: true,
|
|
CSSStyles: { ...styles.LABEL_CSS }
|
|
}).component();
|
|
this._networkShareContainerStorageAccountDropdown = this._view.modelBuilder.dropDown()
|
|
.withProps({
|
|
ariaLabel: constants.STORAGE_ACCOUNT,
|
|
required: true,
|
|
width: WIZARD_INPUT_COMPONENT_WIDTH,
|
|
editable: true,
|
|
fireOnTextChange: true,
|
|
}).component();
|
|
this._disposables.push(
|
|
this._networkShareContainerStorageAccountDropdown.onValueChanged(async (value) => {
|
|
if (value && value !== 'undefined') {
|
|
const selectedStorageAccount = this.migrationStateModel._storageAccounts.find(sa => sa.name === value);
|
|
if (selectedStorageAccount) {
|
|
for (let i = 0; i < this.migrationStateModel._databaseBackup.networkShares.length; i++) {
|
|
this.migrationStateModel._databaseBackup.networkShares[i].storageAccount = selectedStorageAccount;
|
|
}
|
|
this.migrationStateModel.resetIrValidationResults();
|
|
|
|
// check for storage account connectivity
|
|
if ((this.migrationStateModel.isSqlMiTarget || this.migrationStateModel.isSqlVmTarget)) {
|
|
if (!(await canTargetConnectToStorageAccount(
|
|
this.migrationStateModel._targetType,
|
|
this.migrationStateModel._targetServerInstance,
|
|
selectedStorageAccount,
|
|
this.migrationStateModel._azureAccount,
|
|
this.migrationStateModel._targetSubscription))) {
|
|
|
|
this._inaccessibleStorageAccounts = [selectedStorageAccount.name];
|
|
} else {
|
|
this._inaccessibleStorageAccounts = [];
|
|
}
|
|
|
|
this.wizard.message = {
|
|
text: this._inaccessibleStorageAccounts.length > 0
|
|
? constants.STORAGE_ACCOUNT_CONNECTIVITY_WARNING(this.migrationStateModel._targetServerInstance.name, this._inaccessibleStorageAccounts, this.migrationStateModel.isSqlMiTarget)
|
|
: '',
|
|
level: azdata.window.MessageLevel.Warning
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}));
|
|
|
|
this._networkShareContainerStorageAccountRefreshButton = this._view.modelBuilder.button()
|
|
.withProps({
|
|
iconPath: IconPathHelper.refresh,
|
|
iconHeight: 18,
|
|
iconWidth: 18,
|
|
height: 25,
|
|
ariaLabel: constants.REFRESH,
|
|
}).component();
|
|
|
|
this._disposables.push(
|
|
this._networkShareContainerStorageAccountRefreshButton.onDidClick(
|
|
async () => {
|
|
this.loadNetworkShareStorageDropdown();
|
|
|
|
// check for storage account connectivity
|
|
const selectedStorageAccount = this.migrationStateModel._storageAccounts.find(sa => sa.name === (this._networkShareContainerStorageAccountDropdown.value as azdata.CategoryValue).displayName);
|
|
if ((this.migrationStateModel.isSqlMiTarget || this.migrationStateModel.isSqlVmTarget) && selectedStorageAccount) {
|
|
if (!(await canTargetConnectToStorageAccount(
|
|
this.migrationStateModel._targetType,
|
|
this.migrationStateModel._targetServerInstance,
|
|
selectedStorageAccount, this.migrationStateModel._azureAccount,
|
|
this.migrationStateModel._targetSubscription))) {
|
|
this._inaccessibleStorageAccounts = [selectedStorageAccount.name];
|
|
} else {
|
|
this._inaccessibleStorageAccounts = [];
|
|
}
|
|
|
|
this.wizard.message = {
|
|
text: this._inaccessibleStorageAccounts.length > 0
|
|
? constants.STORAGE_ACCOUNT_CONNECTIVITY_WARNING(this.migrationStateModel._targetServerInstance.name, this._inaccessibleStorageAccounts, this.migrationStateModel.isSqlMiTarget)
|
|
: '',
|
|
level: azdata.window.MessageLevel.Warning
|
|
}
|
|
}
|
|
}));
|
|
|
|
const storageAccountContainer = this._view.modelBuilder.flexContainer()
|
|
.withProps({ CSSStyles: { 'margin-top': '-1em' } })
|
|
.component();
|
|
|
|
storageAccountContainer.addItem(
|
|
this._networkShareContainerStorageAccountDropdown,
|
|
{ flex: '0 0 auto' });
|
|
|
|
storageAccountContainer.addItem(
|
|
this._networkShareContainerStorageAccountRefreshButton,
|
|
{ flex: '0 0 auto', CSSStyles: { 'margin-left': '5px' } });
|
|
|
|
return this._view.modelBuilder.flexContainer()
|
|
.withLayout({ flexFlow: 'column' })
|
|
.withItems([
|
|
azureAccountHeader,
|
|
azureAccountHelpText,
|
|
subscriptionLabel,
|
|
this._networkShareContainerSubscription,
|
|
locationLabel,
|
|
this._networkShareContainerLocation,
|
|
resourceGroupLabel,
|
|
this._networkShareStorageAccountResourceGroupDropdown,
|
|
storageAccountLabel,
|
|
storageAccountContainer])
|
|
.withProps({ display: 'none' })
|
|
.component();
|
|
}
|
|
|
|
private async _updatePageControlsVisibility(): Promise<void> {
|
|
const isSqlDbTarget = this.migrationStateModel.isSqlDbTarget;
|
|
const isSqlVmTarget = this.migrationStateModel.isSqlVmTarget;
|
|
const isNetworkShare = this.migrationStateModel.isBackupContainerNetworkShare;
|
|
const isBlobContainer = this.migrationStateModel.isBackupContainerBlobContainer;
|
|
|
|
await utils.updateControlDisplay(this._sourceConnectionContainer, isSqlDbTarget || isNetworkShare);
|
|
await utils.updateControlDisplay(this._migrationTableSection, isSqlDbTarget);
|
|
await utils.updateControlDisplay(this._networkDetailsContainer, !isSqlDbTarget);
|
|
await utils.updateControlDisplay(this._targetDatabaseContainer, !isSqlDbTarget);
|
|
|
|
await utils.updateControlDisplay(this._networkShareContainer, isNetworkShare && !isSqlDbTarget);
|
|
await utils.updateControlDisplay(this._networkShareStorageAccountDetails, isNetworkShare && !isSqlDbTarget);
|
|
await utils.updateControlDisplay(this._networkShareVmDatabaseAlreadyExistsInfoBox, isSqlVmTarget);
|
|
await utils.updateControlDisplay(this._networkTableContainer, isNetworkShare && !isSqlDbTarget);
|
|
await utils.updateControlDisplay(this._blobContainer, isBlobContainer && !isSqlDbTarget);
|
|
await utils.updateControlDisplay(this._blobContainerFolderStructureInfoBox, isBlobContainer && !isSqlDbTarget);
|
|
await utils.updateControlDisplay(this._blobContainerVmDatabaseAlreadyExistsInfoBox, isSqlVmTarget);
|
|
await utils.updateControlDisplay(this._blobTableContainer, isBlobContainer && !isSqlDbTarget);
|
|
|
|
await this._windowsUserAccountText.updateProperties({ required: isNetworkShare && !isSqlDbTarget });
|
|
await this._passwordText.updateProperties({ required: isNetworkShare && !isSqlDbTarget });
|
|
await this._sqlSourceUsernameInput.updateProperties({ required: isNetworkShare || isSqlDbTarget });
|
|
await this._sqlSourcePassword.updateProperties({ required: isNetworkShare || isSqlDbTarget });
|
|
}
|
|
|
|
public async onPageEnter(pageChangeInfo: azdata.window.WizardPageChangeInfo): Promise<void> {
|
|
this.wizard.registerNavigationValidator((pageChangeInfo) => {
|
|
if (pageChangeInfo.newPage < pageChangeInfo.lastPage) {
|
|
return true;
|
|
}
|
|
|
|
this.wizard.message = { text: '' };
|
|
const errors: string[] = [];
|
|
|
|
const isSqlDbTarget = this.migrationStateModel.isSqlDbTarget;
|
|
if (isSqlDbTarget) {
|
|
if (!this._validateTableSelection()) {
|
|
errors.push(constants.DATABASE_TABLE_VALIDATE_SELECTION_MESSAGE);
|
|
}
|
|
} else {
|
|
switch (this.migrationStateModel._databaseBackup.networkContainerType) {
|
|
case NetworkContainerType.NETWORK_SHARE:
|
|
if ((<azdata.CategoryValue>this._networkShareStorageAccountResourceGroupDropdown.value)?.displayName === constants.RESOURCE_GROUP_NOT_FOUND) {
|
|
errors.push(constants.INVALID_RESOURCE_GROUP_ERROR);
|
|
}
|
|
if ((<azdata.CategoryValue>this._networkShareContainerStorageAccountDropdown.value)?.displayName === constants.NO_STORAGE_ACCOUNT_FOUND) {
|
|
errors.push(constants.INVALID_STORAGE_ACCOUNT_ERROR);
|
|
}
|
|
break;
|
|
case NetworkContainerType.BLOB_CONTAINER:
|
|
this._blobContainerResourceGroupDropdowns.forEach((v, index) => {
|
|
if (this.shouldDisplayBlobDropdownError(v, blobResourceGroupErrorStrings)) {
|
|
errors.push(constants.INVALID_BLOB_RESOURCE_GROUP_ERROR(this.migrationStateModel._databasesForMigration[index]));
|
|
}
|
|
});
|
|
this._blobContainerStorageAccountDropdowns.forEach((v, index) => {
|
|
if (this.shouldDisplayBlobDropdownError(v, blobStorageAccountErrorStrings)) {
|
|
errors.push(constants.INVALID_BLOB_STORAGE_ACCOUNT_ERROR(this.migrationStateModel._databasesForMigration[index]));
|
|
}
|
|
});
|
|
this._blobContainerDropdowns.forEach((v, index) => {
|
|
if (this.shouldDisplayBlobDropdownError(v, blobContainerErrorStrings)) {
|
|
errors.push(constants.INVALID_BLOB_CONTAINER_ERROR(this.migrationStateModel._databasesForMigration[index]));
|
|
}
|
|
});
|
|
|
|
if (this.migrationStateModel._databaseBackup.migrationMode === MigrationMode.OFFLINE) {
|
|
this._blobContainerLastBackupFileDropdowns.forEach((v, index) => {
|
|
if (this.shouldDisplayBlobDropdownError(v, blobFileErrorStrings)) {
|
|
errors.push(constants.INVALID_BLOB_LAST_BACKUP_FILE_ERROR(this.migrationStateModel._databasesForMigration[index]));
|
|
}
|
|
});
|
|
} else {
|
|
this._blobContainerFolderDropdowns.forEach((v, index) => {
|
|
if (this.shouldDisplayBlobDropdownError(v, blobFolderErrorStrings)) {
|
|
errors.push(constants.INVALID_BLOB_LAST_BACKUP_FOLDER_ERROR(this.migrationStateModel._databasesForMigration[index]));
|
|
}
|
|
});
|
|
}
|
|
|
|
if (this.migrationStateModel.isSqlVmTarget && utils.isTargetSqlVm2014OrBelow(this.migrationStateModel._targetServerInstance as SqlVMServer)) {
|
|
errors.push(...this._nonPageBlobErrors);
|
|
}
|
|
|
|
if (errors.length > 0) {
|
|
const duplicates: Map<string, number[]> = new Map();
|
|
for (let i = 0; i < this.migrationStateModel._targetDatabaseNames.length; i++) {
|
|
const blobContainerId = this.migrationStateModel._databaseBackup.blobs[i].blobContainer?.id;
|
|
if (duplicates.has(blobContainerId)) {
|
|
duplicates.get(blobContainerId)?.push(i);
|
|
} else {
|
|
duplicates.set(blobContainerId, [i]);
|
|
}
|
|
}
|
|
duplicates.forEach((d) => {
|
|
if (d.length > 1) {
|
|
const dupString = `${d.map(index => this.migrationStateModel._databasesForMigration[index]).join(', ')}`;
|
|
errors.push(constants.PROVIDE_UNIQUE_CONTAINERS + dupString);
|
|
}
|
|
});
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (this.migrationStateModel.isSqlMiTarget) {
|
|
this.migrationStateModel._targetDatabaseNames.forEach(t => {
|
|
// Making sure if database with same name is not present on the target Azure SQL
|
|
if (this._existingDatabases.includes(t)) {
|
|
errors.push(constants.DATABASE_ALREADY_EXISTS_MI(t, this.migrationStateModel._targetServerInstance.name));
|
|
}
|
|
});
|
|
}
|
|
|
|
this.wizard.message = {
|
|
text: errors.join(EOL),
|
|
level: azdata.window.MessageLevel.Error
|
|
};
|
|
if (errors.length > 0) {
|
|
return false;
|
|
}
|
|
|
|
if (this.migrationStateModel.isIrMigration) {
|
|
this.wizard.nextButton.enabled = this.migrationStateModel.isIrTargetValidated;
|
|
this.updateValidationResultUI();
|
|
return this.migrationStateModel.isIrTargetValidated;
|
|
} else {
|
|
return true;
|
|
}
|
|
});
|
|
|
|
this.wizard.customButtons[VALIDATE_IR_CUSTOM_BUTTON_INDEX].hidden = !this.migrationStateModel.isIrMigration;
|
|
await this._updatePageControlsVisibility();
|
|
|
|
if (this.migrationStateModel.refreshDatabaseBackupPage) {
|
|
const isSqlDbTarget = this.migrationStateModel.isSqlDbTarget;
|
|
if (isSqlDbTarget) {
|
|
this.wizardPage.title = constants.DATABASE_TABLE_SELECTION_LABEL;
|
|
this.wizardPage.description = constants.DATABASE_TABLE_SELECTION_LABEL;
|
|
await this._loadTableData();
|
|
}
|
|
try {
|
|
const isOfflineMigration = this.migrationStateModel._databaseBackup?.migrationMode === MigrationMode.OFFLINE;
|
|
|
|
// for offline migrations, show last backup file column
|
|
const lastBackupFileColumnIndex = 5;
|
|
const lastBackupFileColumnOldHidden = this._blobContainerTargetDatabaseNamesTable.columns[lastBackupFileColumnIndex].hidden;
|
|
const lastBackupFileColumnNewHidden = !isOfflineMigration;
|
|
if (lastBackupFileColumnOldHidden !== lastBackupFileColumnNewHidden) {
|
|
// clear values prior to hiding columns if changing column visibility
|
|
// to prevent null DeclarativeTableComponent - exception / _view null
|
|
await this._blobContainerTargetDatabaseNamesTable.setDataValues([]);
|
|
}
|
|
this._blobContainerTargetDatabaseNamesTable.columns[lastBackupFileColumnIndex].hidden = lastBackupFileColumnNewHidden;
|
|
|
|
// for online migrations, show folder column
|
|
const folderColumnIndex = 6;
|
|
const folderColumnOldHidden = this._blobContainerTargetDatabaseNamesTable.columns[folderColumnIndex].hidden;
|
|
const folderColumnNewHidden = isOfflineMigration;
|
|
if (folderColumnOldHidden !== folderColumnNewHidden) {
|
|
// clear values prior to hiding columns if changing column visibility
|
|
// to prevent null DeclarativeTableComponent - exception / _view null
|
|
await this._blobContainerTargetDatabaseNamesTable.setDataValues([]);
|
|
}
|
|
this._blobContainerTargetDatabaseNamesTable.columns[folderColumnIndex].hidden = folderColumnNewHidden;
|
|
|
|
const connectionProfile = await getSourceConnectionProfile();
|
|
const queryProvider = await getSourceConnectionQueryProvider();
|
|
let username = '';
|
|
try {
|
|
const query = 'select SUSER_NAME()';
|
|
const ownerUri = await getSourceConnectionUri();
|
|
const results = await queryProvider.runQueryAndReturn(ownerUri, query);
|
|
username = results.rows[0][0]?.displayValue;
|
|
} catch (e) {
|
|
username = connectionProfile.userName;
|
|
}
|
|
|
|
this.migrationStateModel._authenticationType =
|
|
connectionProfile.authenticationType === azdata.connection.AuthenticationType.SqlLogin
|
|
? MigrationSourceAuthenticationType.Sql
|
|
: connectionProfile.authenticationType === azdata.connection.AuthenticationType.Integrated
|
|
? MigrationSourceAuthenticationType.Integrated
|
|
: undefined!;
|
|
this._sourceHelpText.value = constants.SQL_SOURCE_DETAILS(
|
|
this.migrationStateModel._authenticationType,
|
|
connectionProfile.serverName,
|
|
isSqlDbTarget);
|
|
|
|
this._sqlSourceUsernameInput.value = username;
|
|
this._sqlSourcePassword.value = (await getSourceConnectionCredentials()).password;
|
|
|
|
const networkShares = this.migrationStateModel._databaseBackup?.networkShares?.length > 0
|
|
? this.migrationStateModel._databaseBackup?.networkShares
|
|
: this.migrationStateModel.savedInfo?.networkShares ?? [];
|
|
|
|
const networkShare = networkShares?.length > 0
|
|
? networkShares[0]
|
|
: undefined;
|
|
|
|
this._windowsUserAccountText.value = networkShare?.windowsUser ?? '';
|
|
this._passwordText.value = networkShare?.password ?? '';
|
|
|
|
this._networkShareTargetDatabaseNames = [];
|
|
this._networkShareLocations = [];
|
|
this._blobContainerTargetDatabaseNames = [];
|
|
this._blobContainerResourceGroupDropdowns = [];
|
|
this._blobContainerStorageAccountDropdowns = [];
|
|
this._blobContainerDropdowns = [];
|
|
this._blobContainerLastBackupFileDropdowns = [];
|
|
this._blobContainerFolderDropdowns = [];
|
|
this._inaccessibleStorageAccounts = [];
|
|
|
|
if (this.migrationStateModel.isSqlMiTarget) {
|
|
this._existingDatabases = await this.migrationStateModel.getManagedDatabases();
|
|
}
|
|
|
|
const originalTargetDatabaseNames = this.migrationStateModel._targetDatabaseNames;
|
|
const originalNetworkShares = this.migrationStateModel._databaseBackup.networkShares || [];
|
|
const originalBlobs = this.migrationStateModel._databaseBackup.blobs;
|
|
if (this.migrationStateModel._didUpdateDatabasesForMigration ||
|
|
this.migrationStateModel._didDatabaseMappingChange) {
|
|
|
|
this.migrationStateModel._targetDatabaseNames = [];
|
|
this.migrationStateModel._databaseBackup.networkShares = [];
|
|
this.migrationStateModel._databaseBackup.blobs = [];
|
|
}
|
|
|
|
this.migrationStateModel._databasesForMigration.forEach((sourceDatabaseName, index) => {
|
|
let targetDatabaseName = isSqlDbTarget
|
|
? this.migrationStateModel._sourceTargetMapping.get(sourceDatabaseName)?.databaseName ?? sourceDatabaseName
|
|
: sourceDatabaseName;
|
|
let networkShare = <NetworkShare>{};
|
|
let blob = <utils.Blob>{};
|
|
|
|
if (this.migrationStateModel._didUpdateDatabasesForMigration ||
|
|
this.migrationStateModel._didDatabaseMappingChange) {
|
|
|
|
const dbIndex = this.migrationStateModel._sourceDatabaseNames?.indexOf(sourceDatabaseName);
|
|
if (dbIndex > -1) {
|
|
targetDatabaseName = isSqlDbTarget
|
|
? targetDatabaseName
|
|
: originalTargetDatabaseNames[dbIndex] ?? targetDatabaseName;
|
|
networkShare = originalNetworkShares[dbIndex] ?? networkShare;
|
|
blob = originalBlobs[dbIndex] ?? blob;
|
|
} else {
|
|
// network share values are uniform for all dbs in the same migration, except for networkShareLocation
|
|
const previouslySelectedNetworkShare = originalNetworkShares.length > 0
|
|
? originalNetworkShares[0]
|
|
: '';
|
|
|
|
if (previouslySelectedNetworkShare) {
|
|
networkShare = {
|
|
...previouslySelectedNetworkShare,
|
|
networkShareLocation: '',
|
|
};
|
|
}
|
|
}
|
|
}
|
|
this.migrationStateModel._targetDatabaseNames[index] = targetDatabaseName;
|
|
this.migrationStateModel._databaseBackup.networkShares[index] = networkShare;
|
|
this.migrationStateModel._databaseBackup.blobs[index] = blob;
|
|
|
|
const targetDatabaseInput = this._view.modelBuilder.inputBox()
|
|
.withProps({
|
|
required: true,
|
|
value: targetDatabaseName,
|
|
width: WIZARD_TABLE_COLUMN_WIDTH
|
|
}).withValidation(c => {
|
|
//Making sure no databases have duplicate values.
|
|
if (this._networkShareTargetDatabaseNames.filter(t => t.value === c.value).length > 1) {
|
|
c.validationErrorMessage = constants.DUPLICATE_NAME_ERROR;
|
|
return false;
|
|
}
|
|
// Making sure if database with same name is not present on the target Azure SQL
|
|
if (this.migrationStateModel.isSqlMiTarget && this._existingDatabases.includes(c.value!)) {
|
|
c.validationErrorMessage = constants.DATABASE_ALREADY_EXISTS_MI(c.value!, this.migrationStateModel._targetServerInstance.name);
|
|
return false;
|
|
}
|
|
if (c.value!.length < 1 || c.value!.length > 128 || !/[^<>*%&:\\\/?]/.test(c.value!)) {
|
|
c.validationErrorMessage = constants.INVALID_TARGET_NAME_ERROR;
|
|
return false;
|
|
}
|
|
return true;
|
|
}).component();
|
|
this._disposables.push(
|
|
targetDatabaseInput.onTextChanged(async (value) => {
|
|
this.migrationStateModel._targetDatabaseNames[index] = value.trim();
|
|
this._resetValidationUI();
|
|
await this.validateFields(targetDatabaseInput);
|
|
}));
|
|
targetDatabaseInput.value = this.migrationStateModel._targetDatabaseNames[index];
|
|
this._networkShareTargetDatabaseNames.push(targetDatabaseInput);
|
|
|
|
const networkShareLocationInput = this._view.modelBuilder.inputBox()
|
|
.withProps({
|
|
required: true,
|
|
placeHolder: constants.NETWORK_SHARE_PATH_FORMAT,
|
|
validationErrorMessage: constants.INVALID_NETWORK_SHARE_LOCATION,
|
|
width: '300px'
|
|
}).withValidation(c => {
|
|
if (this.migrationStateModel.isBackupContainerNetworkShare) {
|
|
if (c.value) {
|
|
if (!/^[\\\/]{2,}[^\\\/]+[\\\/]+[^\\\/]+/.test(c.value)) {
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
return true;
|
|
}).component();
|
|
this._disposables.push(
|
|
networkShareLocationInput.onTextChanged(async (value) => {
|
|
this.migrationStateModel._databaseBackup.networkShares[index].networkShareLocation = value.trim();
|
|
this._resetValidationUI();
|
|
await this.validateFields(networkShareLocationInput);
|
|
}));
|
|
networkShareLocationInput.value = this.migrationStateModel._databaseBackup.networkShares[index]?.networkShareLocation;
|
|
this._networkShareLocations.push(networkShareLocationInput);
|
|
|
|
const blobTargetDatabaseInput = this._view.modelBuilder.inputBox()
|
|
.withProps({
|
|
required: true,
|
|
value: targetDatabaseName,
|
|
}).withValidation(c => {
|
|
//Making sure no databases have duplicate values.
|
|
if (this._blobContainerTargetDatabaseNames.filter(t => t.value === c.value).length > 1) {
|
|
c.validationErrorMessage = constants.DUPLICATE_NAME_ERROR;
|
|
return false;
|
|
}
|
|
// Making sure if database with same name is not present on the target Azure SQL
|
|
if (this.migrationStateModel.isSqlMiTarget && this._existingDatabases.includes(c.value!)) {
|
|
c.validationErrorMessage = constants.DATABASE_ALREADY_EXISTS_MI(c.value!, this.migrationStateModel._targetServerInstance.name);
|
|
return false;
|
|
}
|
|
if (c.value!.length < 1 || c.value!.length > 128 || !/[^<>*%&:\\\/?]/.test(c.value!)) {
|
|
c.validationErrorMessage = constants.INVALID_TARGET_NAME_ERROR;
|
|
return false;
|
|
}
|
|
return true;
|
|
}).component();
|
|
this._disposables.push(
|
|
blobTargetDatabaseInput.onTextChanged(
|
|
(value) => {
|
|
this.migrationStateModel._targetDatabaseNames[index] = value.trim();
|
|
this._resetValidationUI();
|
|
}));
|
|
|
|
targetDatabaseInput.value = this.migrationStateModel._targetDatabaseNames[index];
|
|
this._blobContainerTargetDatabaseNames.push(blobTargetDatabaseInput);
|
|
|
|
const blobContainerResourceDropdown = this._view.modelBuilder.dropDown()
|
|
.withProps({
|
|
ariaLabel: constants.BLOB_CONTAINER_RESOURCE_GROUP,
|
|
editable: true,
|
|
fireOnTextChange: true,
|
|
required: true,
|
|
}).component();
|
|
|
|
const blobContainerStorageAccountDropdown = this._view.modelBuilder.dropDown()
|
|
.withProps({
|
|
ariaLabel: constants.BLOB_CONTAINER_STORAGE_ACCOUNT,
|
|
editable: true,
|
|
fireOnTextChange: true,
|
|
required: true,
|
|
enabled: false,
|
|
}).component();
|
|
|
|
const blobContainerDropdown = this._view.modelBuilder.dropDown()
|
|
.withProps({
|
|
ariaLabel: constants.BLOB_CONTAINER,
|
|
editable: true,
|
|
fireOnTextChange: true,
|
|
required: true,
|
|
enabled: false,
|
|
}).component();
|
|
|
|
const blobContainerLastBackupFileDropdown = this._view.modelBuilder.dropDown()
|
|
.withProps({
|
|
ariaLabel: constants.BLOB_CONTAINER_LAST_BACKUP_FILE,
|
|
editable: true,
|
|
fireOnTextChange: true,
|
|
required: true,
|
|
enabled: false,
|
|
}).component();
|
|
const blobContainerFolderDropdown = this._view.modelBuilder.dropDown()
|
|
.withProps({
|
|
ariaLabel: constants.BLOB_CONTAINER_FOLDER,
|
|
editable: true,
|
|
fireOnTextChange: true,
|
|
required: true,
|
|
enabled: false,
|
|
}).component();
|
|
|
|
|
|
this._disposables.push(
|
|
blobContainerResourceDropdown.onValueChanged(async (value) => {
|
|
if (value && value !== 'undefined' && this.migrationStateModel._resourceGroups) {
|
|
const selectedResourceGroup = this.migrationStateModel._resourceGroups.find(rg => rg.name === value);
|
|
if (selectedResourceGroup && !blobResourceGroupErrorStrings.includes(value)) {
|
|
this.migrationStateModel._databaseBackup.blobs[index].resourceGroup = selectedResourceGroup;
|
|
this.loadBlobStorageDropdown(index);
|
|
await blobContainerStorageAccountDropdown.updateProperties({ enabled: true });
|
|
} else {
|
|
await this.disableBlobTableDropdowns(index, constants.RESOURCE_GROUP);
|
|
}
|
|
}
|
|
}));
|
|
this._blobContainerResourceGroupDropdowns.push(blobContainerResourceDropdown);
|
|
|
|
this._disposables.push(
|
|
blobContainerStorageAccountDropdown.onValueChanged(async (value) => {
|
|
if (value && value !== 'undefined') {
|
|
const selectedStorageAccount = this.migrationStateModel._storageAccounts.find(sa => sa.name === value);
|
|
if (selectedStorageAccount && !blobStorageAccountErrorStrings.includes(value)) {
|
|
const oldSelectedStorageAccount = this.migrationStateModel._databaseBackup.blobs[index].storageAccount ? this.migrationStateModel._databaseBackup.blobs[index].storageAccount.name : '';
|
|
this.migrationStateModel._databaseBackup.blobs[index].storageAccount = selectedStorageAccount;
|
|
|
|
// check for storage account connectivity
|
|
if ((this.migrationStateModel.isSqlMiTarget || this.migrationStateModel.isSqlVmTarget)) {
|
|
if (this.migrationStateModel._databaseBackup.blobs.filter((e, i) => i !== index).every(blob => blob.storageAccount && blob.storageAccount.name.toLowerCase() !== oldSelectedStorageAccount.toLowerCase())) {
|
|
this._inaccessibleStorageAccounts = this._inaccessibleStorageAccounts.filter(storageAccountName => storageAccountName.toLowerCase() !== oldSelectedStorageAccount.toLowerCase());
|
|
}
|
|
|
|
if (!(await canTargetConnectToStorageAccount(
|
|
this.migrationStateModel._targetType,
|
|
this.migrationStateModel._targetServerInstance,
|
|
selectedStorageAccount,
|
|
this.migrationStateModel._azureAccount,
|
|
this.migrationStateModel._targetSubscription))) {
|
|
|
|
this._inaccessibleStorageAccounts = this._inaccessibleStorageAccounts.filter(storageAccountName => storageAccountName.toLowerCase() !== selectedStorageAccount.name.toLowerCase());
|
|
this._inaccessibleStorageAccounts.push(selectedStorageAccount.name);
|
|
}
|
|
|
|
this.wizard.message = {
|
|
text: this._inaccessibleStorageAccounts.length > 0
|
|
? constants.STORAGE_ACCOUNT_CONNECTIVITY_WARNING(this.migrationStateModel._targetServerInstance.name, this._inaccessibleStorageAccounts, this.migrationStateModel.isSqlMiTarget)
|
|
: '',
|
|
level: azdata.window.MessageLevel.Warning
|
|
}
|
|
}
|
|
|
|
await this.loadBlobContainerDropdown(index);
|
|
await blobContainerDropdown.updateProperties({ enabled: true });
|
|
} else {
|
|
await this.disableBlobTableDropdowns(index, constants.STORAGE_ACCOUNT);
|
|
}
|
|
}
|
|
}));
|
|
this._blobContainerStorageAccountDropdowns.push(blobContainerStorageAccountDropdown);
|
|
|
|
this._disposables.push(
|
|
blobContainerDropdown.onValueChanged(async (value) => {
|
|
if (value && value !== 'undefined' && this.migrationStateModel._blobContainers) {
|
|
const selectedBlobContainer = this.migrationStateModel._blobContainers.find(blob => blob.name === value);
|
|
if (selectedBlobContainer && !blobContainerErrorStrings.includes(value)) {
|
|
this.migrationStateModel._databaseBackup.blobs[index].blobContainer = selectedBlobContainer;
|
|
|
|
// check for block blobs for SQL VM <= 2014
|
|
if (this.migrationStateModel.isSqlVmTarget && utils.isTargetSqlVm2014OrBelow(this.migrationStateModel._targetServerInstance as SqlVMServer)) {
|
|
const backups = await utils.getBlobLastBackupFileNames(
|
|
this.migrationStateModel._azureAccount,
|
|
this.migrationStateModel._databaseBackup.subscription,
|
|
this.migrationStateModel._databaseBackup.blobs[index]?.storageAccount,
|
|
this.migrationStateModel._databaseBackup.blobs[index]?.blobContainer);
|
|
|
|
const errorMessage = constants.INVALID_NON_PAGE_BLOB_BACKUP_FILE_ERROR(this.migrationStateModel._databasesForMigration[index]);
|
|
this._nonPageBlobErrors = this._nonPageBlobErrors.filter(err => err !== errorMessage);
|
|
|
|
const allBackupsPageBlob = backups.every(backup => backup.properties.blobType === 'PageBlob')
|
|
if (!allBackupsPageBlob) {
|
|
this._nonPageBlobErrors.push(errorMessage);
|
|
}
|
|
}
|
|
|
|
if (this.migrationStateModel._databaseBackup.migrationMode === MigrationMode.OFFLINE) {
|
|
await this.loadBlobLastBackupFileDropdown(index);
|
|
await blobContainerLastBackupFileDropdown.updateProperties({ enabled: true });
|
|
} else {
|
|
await this.loadBlobFolderDropdown(index);
|
|
await blobContainerFolderDropdown.updateProperties({ enabled: true });
|
|
}
|
|
} else {
|
|
await this.disableBlobTableDropdowns(index, constants.BLOB_CONTAINER);
|
|
}
|
|
}
|
|
}));
|
|
this._blobContainerDropdowns.push(blobContainerDropdown);
|
|
|
|
if (this.migrationStateModel._databaseBackup.migrationMode === MigrationMode.OFFLINE) {
|
|
this._disposables.push(
|
|
blobContainerLastBackupFileDropdown.onValueChanged(value => {
|
|
if (value && value !== 'undefined') {
|
|
if (this.migrationStateModel._lastFileNames) {
|
|
const selectedLastBackupFile = this.migrationStateModel._lastFileNames.find(fileName => fileName.name === value);
|
|
if (selectedLastBackupFile && !blobFileErrorStrings.includes(value)) {
|
|
this.migrationStateModel._databaseBackup.blobs[index].lastBackupFile = selectedLastBackupFile.name;
|
|
}
|
|
|
|
// check for duplicate storage account/blob container/last backup file combination if migrating multiple databases,
|
|
// as they should all be unique - backups for multiple databases in the same location are not supported
|
|
let backupLocations: string[] = [];
|
|
backupLocations = this.migrationStateModel._databaseBackup.blobs.map(blob => {
|
|
return blob && blob.storageAccount
|
|
? (blob.storageAccount.id + '/' + utils.getBlobContainerNameWithFolder(blob, this.migrationStateModel._databaseBackup.migrationMode === MigrationMode.OFFLINE)).toLowerCase()
|
|
: '';
|
|
}).filter(backupLocation => backupLocation !== '');
|
|
|
|
let uniqueBackupLocations = [...new Set(backupLocations)];
|
|
|
|
if (!isSqlDbTarget && uniqueBackupLocations.length !== backupLocations.length) {
|
|
this.wizard.message = {
|
|
level: azdata.window.MessageLevel.Warning,
|
|
text: constants.DATABASE_BACKUP_BLOB_FOLDER_STRUCTURE_WARNING,
|
|
};
|
|
} else {
|
|
this.wizard.message = {
|
|
text: ''
|
|
};
|
|
}
|
|
}
|
|
}
|
|
}));
|
|
this._blobContainerLastBackupFileDropdowns.push(blobContainerLastBackupFileDropdown);
|
|
} else {
|
|
this._disposables.push(
|
|
blobContainerFolderDropdown.onValueChanged(value => {
|
|
if (value && value !== 'undefined') {
|
|
if (this.migrationStateModel._blobContainerFolders && this.migrationStateModel._blobContainerFolders.includes(value) && !blobFolderErrorStrings.includes(value)) {
|
|
const selectedFolder = value;
|
|
this.migrationStateModel._databaseBackup.blobs[index].folderName = selectedFolder;
|
|
|
|
// check for duplicate storage account/blob container/folder combination if migrating multiple databases,
|
|
// as they should all be unique - backups for multiple databases in the same location are not supported
|
|
let backupLocations: string[] = [];
|
|
backupLocations = this.migrationStateModel._databaseBackup.blobs.map(blob => {
|
|
return blob && blob.storageAccount
|
|
? (blob.storageAccount.id + '/' + utils.getBlobContainerNameWithFolder(blob, this.migrationStateModel._databaseBackup.migrationMode === MigrationMode.OFFLINE)).toLowerCase()
|
|
: '';
|
|
}).filter(backupLocation => backupLocation !== '');
|
|
|
|
let uniqueBackupLocations = [...new Set(backupLocations)];
|
|
|
|
if (!isSqlDbTarget && uniqueBackupLocations.length !== backupLocations.length) {
|
|
this.wizard.message = {
|
|
level: azdata.window.MessageLevel.Warning,
|
|
text: constants.DATABASE_BACKUP_BLOB_FOLDER_STRUCTURE_WARNING,
|
|
};
|
|
} else {
|
|
this.wizard.message = {
|
|
text: ''
|
|
};
|
|
}
|
|
}
|
|
}
|
|
}));
|
|
this._blobContainerFolderDropdowns.push(blobContainerFolderDropdown);
|
|
}
|
|
});
|
|
this.migrationStateModel._sourceDatabaseNames = this.migrationStateModel._databasesForMigration;
|
|
|
|
const networkShareTargetData = this.migrationStateModel._databasesForMigration
|
|
.map((db, index) => [
|
|
{ value: db },
|
|
{ value: this._networkShareTargetDatabaseNames[index] },
|
|
{ value: this._networkShareLocations[index] }]);
|
|
await this._networkShareTargetDatabaseNamesTable.setDataValues(networkShareTargetData);
|
|
|
|
const blobContainerTargetData = this.migrationStateModel._databasesForMigration
|
|
.map((db, index) => [
|
|
{ value: db },
|
|
{ value: this._blobContainerTargetDatabaseNames[index] },
|
|
{ value: this._blobContainerResourceGroupDropdowns[index] },
|
|
{ value: this._blobContainerStorageAccountDropdowns[index] },
|
|
{ value: this._blobContainerDropdowns[index] },
|
|
{ value: this._blobContainerLastBackupFileDropdowns[index] },
|
|
{ value: this._blobContainerFolderDropdowns[index] }]);
|
|
await this._blobContainerTargetDatabaseNamesTable.setDataValues([]);
|
|
await this._blobContainerTargetDatabaseNamesTable.setDataValues(blobContainerTargetData);
|
|
await this.getSubscriptionValues();
|
|
// clear change tracking flags
|
|
this.migrationStateModel.refreshDatabaseBackupPage = false;
|
|
this.migrationStateModel._didUpdateDatabasesForMigration = false;
|
|
this.migrationStateModel._didDatabaseMappingChange = false;
|
|
|
|
this.migrationStateModel._validateIrSqlDb = [];
|
|
this.migrationStateModel._validateIrSqlMi = [];
|
|
this.migrationStateModel._validateIrSqlVm = [];
|
|
} catch (error) {
|
|
console.log(error);
|
|
let errorText = error?.message;
|
|
if (errorText === constants.INVALID_OWNER_URI) {
|
|
errorText = constants.DATABASE_BACKUP_PAGE_LOAD_ERROR;
|
|
}
|
|
this.wizard.message = {
|
|
text: errorText,
|
|
description: error?.stack,
|
|
level: azdata.window.MessageLevel.Error
|
|
};
|
|
}
|
|
|
|
await this.validateFields();
|
|
this.updateValidationResultUI(true);
|
|
}
|
|
}
|
|
|
|
private async _validateIr(): Promise<void> {
|
|
this.wizard.message = { text: '' };
|
|
const dialog = new ValidateIrDialog(
|
|
this.migrationStateModel,
|
|
() => this.updateValidationResultUI());
|
|
|
|
let results: ValidationResult[] = [];
|
|
switch (this.migrationStateModel._targetType) {
|
|
case MigrationTargetType.SQLDB:
|
|
results = this.migrationStateModel._validateIrSqlDb;
|
|
break;
|
|
case MigrationTargetType.SQLMI:
|
|
results = this.migrationStateModel._validateIrSqlMi;
|
|
break;
|
|
case MigrationTargetType.SQLVM:
|
|
results = this.migrationStateModel._validateIrSqlVm;
|
|
break;
|
|
}
|
|
|
|
await dialog.openDialog(constants.VALIDATION_DIALOG_TITLE, results);
|
|
}
|
|
|
|
public updateValidationResultUI(initializing?: boolean): void {
|
|
if (this.migrationStateModel.isIrMigration) {
|
|
const succeeded = this.migrationStateModel.isIrTargetValidated;
|
|
if (succeeded) {
|
|
this.wizard.message = {
|
|
level: azdata.window.MessageLevel.Information,
|
|
text: constants.VALIDATION_MESSAGE_SUCCESS,
|
|
};
|
|
} else {
|
|
const results = this.migrationStateModel.validationTargetResults;
|
|
const hasResults = results.length > 0;
|
|
if (initializing && !hasResults) {
|
|
return;
|
|
}
|
|
|
|
const canceled = results.some(result => result.state === ValidateIrState.Canceled);
|
|
const errors: string[] = results.flatMap(result => result.errors) ?? [];
|
|
const errorsMessage: string = errors.join(EOL);
|
|
const hasErrors = errors.length > 0;
|
|
const msg = hasResults
|
|
? hasErrors
|
|
? canceled
|
|
? constants.VALIDATION_MESSAGE_CANCELED_ERRORS(errorsMessage)
|
|
: constants.VALIDATE_IR_VALIDATION_COMPLETED_ERRORS(errorsMessage)
|
|
: constants.VALIDATION_MESSAGE_CANCELED
|
|
: constants.VALIDATION_MESSAGE_NOT_RUN;
|
|
|
|
this.wizard.message = {
|
|
level: azdata.window.MessageLevel.Error,
|
|
text: msg,
|
|
};
|
|
}
|
|
}
|
|
}
|
|
|
|
public async onPageLeave(pageChangeInfo: azdata.window.WizardPageChangeInfo): Promise<void> {
|
|
this.wizard.registerNavigationValidator(pageChangeInfo => true);
|
|
this.wizard.message = { text: '' };
|
|
this.wizard.customButtons[VALIDATE_IR_CUSTOM_BUTTON_INDEX].hidden = true;
|
|
|
|
if (pageChangeInfo.newPage > pageChangeInfo.lastPage) {
|
|
if (!this.migrationStateModel.isSqlDbTarget) {
|
|
switch (this.migrationStateModel._databaseBackup.networkContainerType) {
|
|
case NetworkContainerType.BLOB_CONTAINER:
|
|
for (let i = 0; i < this.migrationStateModel._databaseBackup.blobs.length; i++) {
|
|
const storageAccount = this.migrationStateModel._databaseBackup.blobs[i].storageAccount;
|
|
this.migrationStateModel._databaseBackup.blobs[i].storageKey = (await getStorageAccountAccessKeys(
|
|
this.migrationStateModel._azureAccount,
|
|
this.migrationStateModel._databaseBackup.subscription,
|
|
storageAccount)).keyName1;
|
|
}
|
|
break;
|
|
case NetworkContainerType.NETWORK_SHARE:
|
|
// All network share migrations use the same storage account
|
|
if (this.migrationStateModel._databaseBackup.networkShares?.length > 0) {
|
|
const storageAccount = this.migrationStateModel._databaseBackup.networkShares[0]?.storageAccount;
|
|
const storageKey = (await getStorageAccountAccessKeys(
|
|
this.migrationStateModel._azureAccount,
|
|
this.migrationStateModel._databaseBackup.subscription,
|
|
storageAccount)).keyName1;
|
|
for (let i = 0; i < this.migrationStateModel._databaseBackup.networkShares.length; i++) {
|
|
this.migrationStateModel._databaseBackup.networkShares[i].storageKey = storageKey;
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
protected async handleStateChange(e: StateChangeEvent): Promise<void> {
|
|
}
|
|
|
|
private _validateTableSelection(): boolean {
|
|
for (const targetDatabaseInfo of this.migrationStateModel._sourceTargetMapping) {
|
|
const databaseInfo = targetDatabaseInfo[1];
|
|
if (databaseInfo) {
|
|
for (const sourceTable of databaseInfo.sourceTables) {
|
|
const tableInfo = sourceTable[1];
|
|
if (tableInfo.selectedForMigration === true) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
private async validateFields(component?: azdata.Component): Promise<void> {
|
|
await this._sqlSourceUsernameInput?.validate();
|
|
await this._sqlSourcePassword?.validate();
|
|
await this._windowsUserAccountText?.validate();
|
|
await this._passwordText?.validate();
|
|
await this._networkShareContainerSubscription?.validate();
|
|
await this._networkShareStorageAccountResourceGroupDropdown?.validate();
|
|
await this._networkShareContainerStorageAccountDropdown?.validate();
|
|
await this._blobContainerSubscription?.validate();
|
|
for (let i = 0; i < this._networkShareTargetDatabaseNames.length; i++) {
|
|
await this._networkShareTargetDatabaseNames[i]?.validate();
|
|
await this._networkShareLocations[i]?.validate();
|
|
await this._blobContainerTargetDatabaseNames[i]?.validate();
|
|
await this._blobContainerResourceGroupDropdowns[i]?.validate();
|
|
await this._blobContainerStorageAccountDropdowns[i]?.validate();
|
|
await this._blobContainerDropdowns[i]?.validate();
|
|
|
|
if (this.migrationStateModel._databaseBackup.migrationMode === MigrationMode.OFFLINE) {
|
|
await this._blobContainerLastBackupFileDropdowns[i]?.validate();
|
|
} else {
|
|
await this._blobContainerFolderDropdowns[i]?.validate();
|
|
}
|
|
}
|
|
if (this.migrationStateModel.isIrMigration) {
|
|
if (this.migrationStateModel.isSqlDbTarget) {
|
|
await this._databaseTable?.validate();
|
|
}
|
|
}
|
|
if (this.migrationStateModel.isBackupContainerNetworkShare) {
|
|
await this._networkShareTargetDatabaseNamesTable.validate();
|
|
}
|
|
await component?.validate();
|
|
}
|
|
|
|
private async getSubscriptionValues(): Promise<void> {
|
|
this._networkShareContainerSubscription.value = this.migrationStateModel._targetSubscription.name;
|
|
this._networkShareContainerLocation.value = await this.migrationStateModel._location.displayName;
|
|
|
|
this._blobContainerSubscription.value = this.migrationStateModel._targetSubscription.name;
|
|
this._blobContainerLocation.value = this.migrationStateModel._location.displayName;
|
|
|
|
this.migrationStateModel._databaseBackup.subscription = this.migrationStateModel._targetSubscription;
|
|
|
|
await this.loadNetworkStorageResourceGroup();
|
|
await this.loadBlobResourceGroup();
|
|
}
|
|
|
|
private async loadNetworkStorageResourceGroup(): Promise<void> {
|
|
try {
|
|
this._networkShareStorageAccountResourceGroupDropdown.loading = true;
|
|
this.migrationStateModel._storageAccounts = await utils.getStorageAccounts(
|
|
this.migrationStateModel._azureAccount,
|
|
this.migrationStateModel._databaseBackup.subscription);
|
|
this.migrationStateModel._resourceGroups = utils.getServiceResourceGroupsByLocation(
|
|
this.migrationStateModel._storageAccounts,
|
|
this.migrationStateModel._location);
|
|
this._networkShareStorageAccountResourceGroupDropdown.values = utils.getResourceDropdownValues(
|
|
this.migrationStateModel._resourceGroups,
|
|
constants.RESOURCE_GROUP_NOT_FOUND);
|
|
|
|
utils.selectDefaultDropdownValue(
|
|
this._networkShareStorageAccountResourceGroupDropdown,
|
|
this.migrationStateModel._databaseBackup?.networkShares[0]?.resourceGroup?.id,
|
|
false);
|
|
} catch (error) {
|
|
logError(TelemetryViews.DatabaseBackupPage, 'ErrorLoadingNetworkStorageResourceGroup', error);
|
|
} finally {
|
|
this._networkShareStorageAccountResourceGroupDropdown.loading = false;
|
|
this.loadNetworkShareStorageDropdown();
|
|
}
|
|
}
|
|
|
|
private loadNetworkShareStorageDropdown(): void {
|
|
try {
|
|
this._networkShareContainerStorageAccountDropdown.loading = true;
|
|
this._networkShareStorageAccountResourceGroupDropdown.loading = true;
|
|
|
|
this._networkShareContainerStorageAccountDropdown.values = utils.getAzureResourceDropdownValues(
|
|
this.migrationStateModel._storageAccounts,
|
|
this.migrationStateModel._location,
|
|
this.migrationStateModel._databaseBackup?.networkShares[0]?.resourceGroup?.name,
|
|
constants.NO_STORAGE_ACCOUNT_FOUND);
|
|
|
|
utils.selectDefaultDropdownValue(this._networkShareContainerStorageAccountDropdown, this.migrationStateModel?._databaseBackup?.networkShares[0]?.storageAccount?.id, false);
|
|
} catch (error) {
|
|
logError(TelemetryViews.DatabaseBackupPage, 'ErrorLoadingNetworkShareStorageDropdown', error);
|
|
} finally {
|
|
this._networkShareContainerStorageAccountDropdown.loading = false;
|
|
this._networkShareStorageAccountResourceGroupDropdown.loading = false;
|
|
}
|
|
}
|
|
|
|
private async loadBlobResourceGroup(): Promise<void> {
|
|
try {
|
|
this._blobContainerResourceGroupDropdowns.forEach(v => v.loading = true);
|
|
this.migrationStateModel._storageAccounts = await utils.getStorageAccounts(
|
|
this.migrationStateModel._azureAccount,
|
|
this.migrationStateModel._databaseBackup.subscription);
|
|
this.migrationStateModel._resourceGroups = utils.getServiceResourceGroupsByLocation(
|
|
this.migrationStateModel._storageAccounts,
|
|
this.migrationStateModel._location);
|
|
const resourceGroupValues = utils.getResourceDropdownValues(
|
|
this.migrationStateModel._resourceGroups,
|
|
constants.RESOURCE_GROUP_NOT_FOUND);
|
|
|
|
this._blobContainerResourceGroupDropdowns.forEach((dropDown, index) => {
|
|
dropDown.values = resourceGroupValues;
|
|
utils.selectDefaultDropdownValue(dropDown, this.migrationStateModel._databaseBackup?.blobs[index]?.resourceGroup?.id, false);
|
|
});
|
|
} catch (error) {
|
|
logError(TelemetryViews.DatabaseBackupPage, 'ErrorLoadingBlobResourceGroup', error);
|
|
} finally {
|
|
this._blobContainerResourceGroupDropdowns.forEach(v => v.loading = false);
|
|
}
|
|
}
|
|
|
|
private loadBlobStorageDropdown(index: number): void {
|
|
const dropDown = this._blobContainerStorageAccountDropdowns[index];
|
|
if (dropDown) {
|
|
try {
|
|
dropDown.loading = true;
|
|
dropDown.values = utils.getAzureResourceDropdownValues(
|
|
this.migrationStateModel._storageAccounts,
|
|
this.migrationStateModel._location,
|
|
this.migrationStateModel._databaseBackup.blobs[index]?.resourceGroup?.name,
|
|
constants.NO_STORAGE_ACCOUNT_FOUND);
|
|
|
|
utils.selectDefaultDropdownValue(
|
|
dropDown,
|
|
this.migrationStateModel._databaseBackup?.blobs[index]?.storageAccount?.id,
|
|
false);
|
|
} catch (error) {
|
|
logError(TelemetryViews.DatabaseBackupPage, 'ErrorLoadingBlobStorageDropdown', error);
|
|
} finally {
|
|
dropDown.loading = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
private async loadBlobContainerDropdown(index: number): Promise<void> {
|
|
const dropDown = this._blobContainerDropdowns[index];
|
|
if (dropDown) {
|
|
try {
|
|
dropDown.loading = true;
|
|
this.migrationStateModel._blobContainers = await utils.getBlobContainer(
|
|
this.migrationStateModel._azureAccount,
|
|
this.migrationStateModel._databaseBackup.subscription,
|
|
this.migrationStateModel._databaseBackup.blobs[index]?.storageAccount);
|
|
|
|
dropDown.values = utils.getResourceDropdownValues(
|
|
this.migrationStateModel._blobContainers,
|
|
constants.NO_BLOBCONTAINERS_FOUND);
|
|
|
|
utils.selectDefaultDropdownValue(
|
|
dropDown,
|
|
this.migrationStateModel._databaseBackup?.blobs[index]?.blobContainer?.id,
|
|
false);
|
|
} catch (error) {
|
|
logError(TelemetryViews.DatabaseBackupPage, 'ErrorLoadingBlobContainers', error);
|
|
} finally {
|
|
dropDown.loading = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
private async loadBlobLastBackupFileDropdown(index: number): Promise<void> {
|
|
const dropDown = this._blobContainerLastBackupFileDropdowns[index];
|
|
if (dropDown) {
|
|
try {
|
|
dropDown.loading = true;
|
|
this.migrationStateModel._lastFileNames = await utils.getBlobLastBackupFileNames(
|
|
this.migrationStateModel._azureAccount,
|
|
this.migrationStateModel._databaseBackup.subscription,
|
|
this.migrationStateModel._databaseBackup.blobs[index]?.storageAccount,
|
|
this.migrationStateModel._databaseBackup.blobs[index]?.blobContainer);
|
|
dropDown.values = utils.getBlobLastBackupFileNamesValues(
|
|
this.migrationStateModel._lastFileNames);
|
|
utils.selectDefaultDropdownValue(
|
|
dropDown,
|
|
this.migrationStateModel._databaseBackup?.blobs[index]?.lastBackupFile,
|
|
false);
|
|
} catch (error) {
|
|
logError(TelemetryViews.DatabaseBackupPage, 'ErrorLoadingBlobLastBackupFiles', error);
|
|
} finally {
|
|
dropDown.loading = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
private async loadBlobFolderDropdown(index: number): Promise<void> {
|
|
const dropDown = this._blobContainerFolderDropdowns[index];
|
|
if (dropDown) {
|
|
try {
|
|
dropDown.loading = true;
|
|
this.migrationStateModel._blobContainerFolders = await utils.getBlobFolders(this.migrationStateModel._azureAccount,
|
|
this.migrationStateModel._databaseBackup.subscription,
|
|
this.migrationStateModel._databaseBackup.blobs[index]?.storageAccount,
|
|
this.migrationStateModel._databaseBackup.blobs[index]?.blobContainer);
|
|
dropDown.values = utils.getBlobFolderValues(this.migrationStateModel._blobContainerFolders);
|
|
utils.selectDefaultDropdownValue(
|
|
dropDown,
|
|
this.migrationStateModel._blobContainerFolders[0],
|
|
false);
|
|
} catch (error) {
|
|
logError(TelemetryViews.DatabaseBackupPage, 'ErrorLoadingBlobFolders', error);
|
|
} finally {
|
|
dropDown.loading = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
private shouldDisplayBlobDropdownError(v: azdata.DropDownComponent, errorStrings: string[]) {
|
|
return v.value === undefined || errorStrings.includes((<azdata.CategoryValue>v.value)?.displayName);
|
|
}
|
|
|
|
private async disableBlobTableDropdowns(rowIndex: number, columnName: string): Promise<void> {
|
|
const dropdownProps = { enabled: false, loading: false };
|
|
const createDropdownValuesWithPrereq = (displayName: string, name: string = '') => [{ displayName, name }];
|
|
|
|
if (this.migrationStateModel._databaseBackup?.migrationMode === MigrationMode.OFFLINE) {
|
|
this._blobContainerLastBackupFileDropdowns[rowIndex].values = createDropdownValuesWithPrereq(constants.SELECT_BLOB_CONTAINER);
|
|
utils.selectDropDownIndex(this._blobContainerLastBackupFileDropdowns[rowIndex], 0);
|
|
await this._blobContainerLastBackupFileDropdowns[rowIndex]?.updateProperties(dropdownProps);
|
|
} else {
|
|
this._blobContainerFolderDropdowns[rowIndex].values = createDropdownValuesWithPrereq(constants.SELECT_BLOB_CONTAINER);
|
|
utils.selectDropDownIndex(this._blobContainerFolderDropdowns[rowIndex], 0);
|
|
await this._blobContainerFolderDropdowns[rowIndex]?.updateProperties(dropdownProps);
|
|
}
|
|
if (columnName === constants.BLOB_CONTAINER) { return; }
|
|
|
|
this._blobContainerDropdowns[rowIndex].values = createDropdownValuesWithPrereq(constants.SELECT_STORAGE_ACCOUNT);
|
|
utils.selectDropDownIndex(this._blobContainerDropdowns[rowIndex], 0);
|
|
await this._blobContainerDropdowns[rowIndex].updateProperties(dropdownProps);
|
|
if (columnName === constants.STORAGE_ACCOUNT) { return; }
|
|
|
|
this._blobContainerStorageAccountDropdowns[rowIndex].values = createDropdownValuesWithPrereq(constants.SELECT_RESOURCE_GROUP_PROMPT);
|
|
utils.selectDropDownIndex(this._blobContainerStorageAccountDropdowns[rowIndex], 0);
|
|
await this._blobContainerStorageAccountDropdowns[rowIndex].updateProperties(dropdownProps);
|
|
}
|
|
|
|
private _migrationTableSelectionContainer(): azdata.FlexContainer {
|
|
const tableMappingHeader = this._view.modelBuilder.text()
|
|
.withProps({
|
|
value: constants.DATABASE_TABLE_SELECTION_LABEL,
|
|
width: WIZARD_INPUT_COMPONENT_WIDTH,
|
|
CSSStyles: { ...styles.SECTION_HEADER_CSS }
|
|
}).component();
|
|
|
|
const tableMappingText = this._view.modelBuilder.text()
|
|
.withProps({
|
|
value: constants.DATABASE_TABLE_SELECTION_DESCRIPTION,
|
|
width: WIZARD_INPUT_COMPONENT_WIDTH,
|
|
CSSStyles: { ...styles.BODY_CSS }
|
|
}).component();
|
|
|
|
const tableMappingInfoBox = this._view.modelBuilder.infoBox()
|
|
.withProps({
|
|
text: constants.DATABASE_SCHEMA_MIGRATION_HELP,
|
|
style: 'information',
|
|
width: WIZARD_INPUT_COMPONENT_WIDTH,
|
|
CSSStyles: { ...styles.BODY_CSS, 'margin': '5px 0 0 0' },
|
|
links: [
|
|
{ text: constants.DATABASE_SCHEMA_MIGRATION_DACPAC_EXTENSION, url: 'https://learn.microsoft.com/sql/azure-data-studio/extensions/sql-server-dacpac-extension' },
|
|
{ text: constants.DATABASE_SCHEMA_MIGRATION_PROJECTS_EXTENSION, url: 'https://learn.microsoft.com/sql/azure-data-studio/extensions/sql-database-project-extension' },
|
|
]
|
|
}).component();
|
|
|
|
this._refreshButton = this._view.modelBuilder.button()
|
|
.withProps({
|
|
buttonType: azdata.ButtonType.Normal,
|
|
iconHeight: 16,
|
|
iconWidth: 16,
|
|
iconPath: IconPathHelper.refresh,
|
|
label: constants.DATABASE_TABLE_REFRESH_LABEL,
|
|
width: 70,
|
|
CSSStyles: { 'margin': '15px 0 0 0' },
|
|
})
|
|
.component();
|
|
this._disposables.push(
|
|
this._refreshButton.onDidClick(
|
|
async e => await this._loadTableData()));
|
|
|
|
this._refreshLoading = this._view.modelBuilder.loadingComponent()
|
|
.withItem(this._refreshButton)
|
|
.withProps({ loading: false })
|
|
.component();
|
|
|
|
const cssClass = undefined;
|
|
this._databaseTable = this._view.modelBuilder.table()
|
|
.withProps({
|
|
forceFitColumns: azdata.ColumnSizingMode.AutoFit,
|
|
height: '600px',
|
|
CSSStyles: { 'margin': '15px 0 0 0' },
|
|
data: [],
|
|
columns: [
|
|
{
|
|
name: constants.DATABASE_TABLE_SOURCE_DATABASE_COLUMN_LABEL,
|
|
value: 'sourceDatabase',
|
|
width: 200,
|
|
type: azdata.ColumnType.text,
|
|
cssClass: cssClass,
|
|
headerCssClass: cssClass,
|
|
},
|
|
{
|
|
name: constants.DATABASE_TABLE_TARGET_DATABASE_COLUMN_LABEL,
|
|
value: 'targetDataase',
|
|
width: 200,
|
|
type: azdata.ColumnType.text,
|
|
cssClass: cssClass,
|
|
headerCssClass: cssClass,
|
|
},
|
|
<azdata.HyperlinkColumn>{
|
|
name: constants.DATABASE_TABLE_SELECTED_TABLES_COLUMN_LABEL,
|
|
value: 'selectedTables',
|
|
width: 160,
|
|
type: azdata.ColumnType.hyperlink,
|
|
cssClass: cssClass,
|
|
headerCssClass: cssClass,
|
|
icon: IconPathHelper.edit,
|
|
},
|
|
],
|
|
})
|
|
.withValidation(table => {
|
|
if (this.migrationStateModel.isSqlDbTarget) {
|
|
return this._validateTableSelection();
|
|
}
|
|
return true;
|
|
})
|
|
.component();
|
|
|
|
this._disposables.push(
|
|
this._databaseTable.onCellAction!(
|
|
async (rowState: azdata.ICellActionEventArgs) => {
|
|
const buttonState = <azdata.ICellActionEventArgs>rowState;
|
|
if (buttonState?.column === 2) {
|
|
// open table selection dialog
|
|
const sourceDatabaseName = this._databaseTable.data[rowState.row][0];
|
|
const targetDatabaseInfo = this.migrationStateModel._sourceTargetMapping.get(sourceDatabaseName);
|
|
const targetDatabaseName = targetDatabaseInfo?.databaseName;
|
|
if (sourceDatabaseName && targetDatabaseName) {
|
|
await this._openTableSelectionDialog(
|
|
sourceDatabaseName,
|
|
targetDatabaseName,
|
|
() => this._loadTableData());
|
|
}
|
|
}
|
|
}));
|
|
|
|
return this._view.modelBuilder.flexContainer()
|
|
.withItems([
|
|
tableMappingHeader,
|
|
tableMappingText,
|
|
tableMappingInfoBox,
|
|
this._refreshLoading,
|
|
this._databaseTable])
|
|
.withLayout({ flexFlow: 'column' })
|
|
.component();
|
|
}
|
|
|
|
private async _openTableSelectionDialog(
|
|
sourceDatabaseName: string,
|
|
targetDatabaseName: string,
|
|
onSaveCallback: () => Promise<void>): Promise<void> {
|
|
const dialog = new TableMigrationSelectionDialog(
|
|
this.migrationStateModel,
|
|
sourceDatabaseName,
|
|
onSaveCallback);
|
|
await dialog.openDialog(
|
|
constants.SELECT_DATABASE_TABLES_TITLE(targetDatabaseName));
|
|
}
|
|
|
|
private async _loadTableData(): Promise<void> {
|
|
this._refreshLoading.loading = true;
|
|
this.wizard.message = { text: '' };
|
|
const data: any[][] = [];
|
|
|
|
this.migrationStateModel._sourceTargetMapping.forEach((targetDatabaseInfo, sourceDatabaseName) => {
|
|
if (sourceDatabaseName) {
|
|
const tableCount = targetDatabaseInfo?.sourceTables.size ?? 0;
|
|
const hasTables = tableCount > 0;
|
|
|
|
let selectedCount = 0;
|
|
targetDatabaseInfo?.sourceTables.forEach(
|
|
tableInfo => selectedCount += tableInfo.selectedForMigration ? 1 : 0);
|
|
|
|
const hasSelectedTables = hasTables && selectedCount > 0;
|
|
data.push([
|
|
sourceDatabaseName,
|
|
targetDatabaseInfo?.databaseName,
|
|
<azdata.HyperlinkColumnCellValue>{ // table selection
|
|
icon: hasSelectedTables
|
|
? IconPathHelper.completedMigration
|
|
: IconPathHelper.edit,
|
|
title: hasTables
|
|
? constants.TABLE_SELECTION_COUNT(selectedCount, tableCount)
|
|
: constants.TABLE_SELECTION_EDIT,
|
|
}]);
|
|
}
|
|
});
|
|
|
|
await this._databaseTable.updateProperty('data', data);
|
|
this._refreshLoading.loading = false;
|
|
}
|
|
}
|