|
|
|
|
@@ -0,0 +1,664 @@
|
|
|
|
|
/*---------------------------------------------------------------------------------------------
|
|
|
|
|
* 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 * as constants from '../../constants/strings';
|
|
|
|
|
import { validateIrDatabaseMigrationSettings, validateIrSqlDatabaseMigrationSettings } from '../../api/azure';
|
|
|
|
|
import { MigrationStateModel, MigrationTargetType, NetworkShare, ValidateIrState, ValidationResult } from '../../models/stateMachine';
|
|
|
|
|
import { EOL } from 'os';
|
|
|
|
|
import { IconPathHelper } from '../../constants/iconPathHelper';
|
|
|
|
|
|
|
|
|
|
const DialogName = 'ValidateIrDialog';
|
|
|
|
|
|
|
|
|
|
enum ValidationResultIndex {
|
|
|
|
|
message = 0,
|
|
|
|
|
icon = 1,
|
|
|
|
|
status = 2,
|
|
|
|
|
errors = 3,
|
|
|
|
|
state = 4,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export const ValidationStatusLookup: constants.LookupTable<string | undefined> = {
|
|
|
|
|
[ValidateIrState.Canceled]: constants.VALIDATION_STATE_CANCELED,
|
|
|
|
|
[ValidateIrState.Failed]: constants.VALIDATION_STATE_FAILED,
|
|
|
|
|
[ValidateIrState.Pending]: constants.VALIDATION_STATE_PENDING,
|
|
|
|
|
[ValidateIrState.Running]: constants.VALIDATION_STATE_RUNNING,
|
|
|
|
|
[ValidateIrState.Succeeded]: constants.VALIDATION_STATE_SUCCEEDED,
|
|
|
|
|
default: undefined
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export class ValidateIrDialog {
|
|
|
|
|
private _canceled: boolean = true;
|
|
|
|
|
private _dialog: azdata.window.Dialog | undefined;
|
|
|
|
|
private _disposables: vscode.Disposable[] = [];
|
|
|
|
|
private _isOpen: boolean = false;
|
|
|
|
|
private _model!: MigrationStateModel;
|
|
|
|
|
private _resultsTable!: azdata.TableComponent;
|
|
|
|
|
private _startLoader!: azdata.LoadingComponent;
|
|
|
|
|
private _startButton!: azdata.ButtonComponent;
|
|
|
|
|
private _cancelButton!: azdata.ButtonComponent;
|
|
|
|
|
private _copyButton!: azdata.ButtonComponent;
|
|
|
|
|
private _validationResult: any[][] = [];
|
|
|
|
|
private _valdiationErrors: string[] = [];
|
|
|
|
|
private _onClosed: () => void;
|
|
|
|
|
|
|
|
|
|
constructor(
|
|
|
|
|
model: MigrationStateModel,
|
|
|
|
|
onClosed: () => void) {
|
|
|
|
|
this._model = model;
|
|
|
|
|
this._onClosed = onClosed;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public async openDialog(dialogTitle: string, results?: ValidationResult[]): Promise<void> {
|
|
|
|
|
if (!this._isOpen) {
|
|
|
|
|
this._isOpen = true;
|
|
|
|
|
this._dialog = azdata.window.createModelViewDialog(
|
|
|
|
|
dialogTitle,
|
|
|
|
|
DialogName,
|
|
|
|
|
600);
|
|
|
|
|
|
|
|
|
|
const promise = this._initializeDialog(this._dialog);
|
|
|
|
|
azdata.window.openDialog(this._dialog);
|
|
|
|
|
await promise;
|
|
|
|
|
|
|
|
|
|
return this._runValidation(results);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async _initializeDialog(dialog: azdata.window.Dialog): Promise<void> {
|
|
|
|
|
return new Promise<void>((resolve, reject) => {
|
|
|
|
|
dialog.registerContent(async (view) => {
|
|
|
|
|
try {
|
|
|
|
|
dialog.okButton.label = constants.VALIDATE_IR_DONE_BUTTON;
|
|
|
|
|
dialog.okButton.position = 'left';
|
|
|
|
|
dialog.okButton.enabled = false;
|
|
|
|
|
dialog.cancelButton.position = 'left';
|
|
|
|
|
|
|
|
|
|
this._disposables.push(
|
|
|
|
|
dialog.cancelButton.onClick(
|
|
|
|
|
e => {
|
|
|
|
|
this._canceled = true;
|
|
|
|
|
this._saveResults();
|
|
|
|
|
this._onClosed();
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
this._disposables.push(
|
|
|
|
|
dialog.okButton.onClick(
|
|
|
|
|
e => this._onClosed()));
|
|
|
|
|
|
|
|
|
|
const headingText = view.modelBuilder.text()
|
|
|
|
|
.withProps({
|
|
|
|
|
value: constants.VALIDATE_IR_HEADING,
|
|
|
|
|
CSSStyles: {
|
|
|
|
|
'font-size': '13px',
|
|
|
|
|
'font-weight': '400',
|
|
|
|
|
'margin-bottom': '10px',
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
.component();
|
|
|
|
|
this._startLoader = view.modelBuilder.loadingComponent()
|
|
|
|
|
.withProps({
|
|
|
|
|
loading: false,
|
|
|
|
|
CSSStyles: { 'margin': '5px 0 0 10px' }
|
|
|
|
|
})
|
|
|
|
|
.component();
|
|
|
|
|
const headingContainer = view.modelBuilder.flexContainer()
|
|
|
|
|
.withLayout({
|
|
|
|
|
flexFlow: 'row',
|
|
|
|
|
justifyContent: 'flex-start',
|
|
|
|
|
})
|
|
|
|
|
.withItems([headingText, this._startLoader], { flex: '0 0 auto' })
|
|
|
|
|
.component();
|
|
|
|
|
|
|
|
|
|
this._resultsTable = await this._createResultsTable(view);
|
|
|
|
|
|
|
|
|
|
this._startButton = view.modelBuilder.button()
|
|
|
|
|
.withProps({
|
|
|
|
|
iconPath: IconPathHelper.restartDataCollection,
|
|
|
|
|
iconHeight: 18,
|
|
|
|
|
iconWidth: 18,
|
|
|
|
|
width: 100,
|
|
|
|
|
label: constants.VALIDATE_IR_START_VALIDATION,
|
|
|
|
|
}).component();
|
|
|
|
|
|
|
|
|
|
this._cancelButton = view.modelBuilder.button()
|
|
|
|
|
.withProps({
|
|
|
|
|
iconPath: IconPathHelper.stop,
|
|
|
|
|
iconHeight: 18,
|
|
|
|
|
iconWidth: 18,
|
|
|
|
|
width: 100,
|
|
|
|
|
label: constants.VALIDATE_IR_STOP_VALIDATION,
|
|
|
|
|
enabled: false,
|
|
|
|
|
}).component();
|
|
|
|
|
this._copyButton = view.modelBuilder.button()
|
|
|
|
|
.withProps({
|
|
|
|
|
iconPath: IconPathHelper.copy,
|
|
|
|
|
iconHeight: 18,
|
|
|
|
|
iconWidth: 18,
|
|
|
|
|
width: 150,
|
|
|
|
|
label: constants.VALIDATE_IR_COPY_RESULTS,
|
|
|
|
|
enabled: false,
|
|
|
|
|
}).component();
|
|
|
|
|
|
|
|
|
|
this._disposables.push(
|
|
|
|
|
this._startButton.onDidClick(
|
|
|
|
|
async (e) => await this._runValidation()));
|
|
|
|
|
this._disposables.push(
|
|
|
|
|
this._cancelButton.onDidClick(
|
|
|
|
|
e => {
|
|
|
|
|
this._cancelButton.enabled = false;
|
|
|
|
|
this._canceled = true;
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
this._disposables.push(
|
|
|
|
|
this._copyButton.onDidClick(
|
|
|
|
|
async (e) => this._copyValidationResults()));
|
|
|
|
|
|
|
|
|
|
const toolbar = view.modelBuilder.toolbarContainer()
|
|
|
|
|
.withToolbarItems([
|
|
|
|
|
{ component: this._startButton },
|
|
|
|
|
{ component: this._cancelButton },
|
|
|
|
|
{ component: this._copyButton }])
|
|
|
|
|
.component();
|
|
|
|
|
|
|
|
|
|
const resultsHeading = view.modelBuilder.text()
|
|
|
|
|
.withProps({
|
|
|
|
|
value: constants.VALIDATE_IR_RESULTS_HEADING,
|
|
|
|
|
CSSStyles: {
|
|
|
|
|
'font-size': '16px',
|
|
|
|
|
'font-weight': '600',
|
|
|
|
|
'margin-bottom': '10px'
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
.component();
|
|
|
|
|
const resultsText = view.modelBuilder.inputBox()
|
|
|
|
|
.withProps({
|
|
|
|
|
inputType: 'text',
|
|
|
|
|
height: 200,
|
|
|
|
|
multiline: true,
|
|
|
|
|
CSSStyles: { 'overflow': 'none auto' }
|
|
|
|
|
})
|
|
|
|
|
.component();
|
|
|
|
|
|
|
|
|
|
this._disposables.push(
|
|
|
|
|
this._resultsTable.onRowSelected(
|
|
|
|
|
async (e) => await this._updateResultsInfoBox(resultsText)));
|
|
|
|
|
|
|
|
|
|
const flex = view.modelBuilder.flexContainer()
|
|
|
|
|
.withItems([
|
|
|
|
|
headingContainer,
|
|
|
|
|
toolbar,
|
|
|
|
|
this._resultsTable,
|
|
|
|
|
resultsHeading,
|
|
|
|
|
resultsText],
|
|
|
|
|
{ flex: '0 0 auto' })
|
|
|
|
|
.withProps({ CSSStyles: { 'margin': '0 0 0 15px' } })
|
|
|
|
|
.withLayout({
|
|
|
|
|
flexFlow: 'column',
|
|
|
|
|
height: '100%',
|
|
|
|
|
width: 565,
|
|
|
|
|
}).component();
|
|
|
|
|
|
|
|
|
|
this._disposables.push(
|
|
|
|
|
view.onClosed(e =>
|
|
|
|
|
this._disposables.forEach(
|
|
|
|
|
d => { try { d.dispose(); } catch { } })));
|
|
|
|
|
|
|
|
|
|
await view.initializeModel(flex);
|
|
|
|
|
resolve();
|
|
|
|
|
} catch (ex) {
|
|
|
|
|
reject(ex);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private _saveResults(): void {
|
|
|
|
|
const results = this._validationResults();
|
|
|
|
|
switch (this._model._targetType) {
|
|
|
|
|
case MigrationTargetType.SQLDB:
|
|
|
|
|
this._model._validateIrSqlDb = results;
|
|
|
|
|
break;
|
|
|
|
|
case MigrationTargetType.SQLMI:
|
|
|
|
|
this._model._validateIrSqlMi = results;
|
|
|
|
|
break;
|
|
|
|
|
case MigrationTargetType.SQLVM:
|
|
|
|
|
this._model._validateIrSqlVm = results;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private _validationResults(): ValidationResult[] {
|
|
|
|
|
return this._validationResult.map(result => {
|
|
|
|
|
const state = result[ValidationResultIndex.state];
|
|
|
|
|
const finalState = this._canceled
|
|
|
|
|
? state === ValidateIrState.Running || state === ValidateIrState.Pending
|
|
|
|
|
? ValidateIrState.Canceled
|
|
|
|
|
: state
|
|
|
|
|
: state;
|
|
|
|
|
const errors = result[ValidationResultIndex.errors] ?? [];
|
|
|
|
|
return {
|
|
|
|
|
errors: errors,
|
|
|
|
|
state: finalState,
|
|
|
|
|
};
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async _runValidation(results?: ValidationResult[]): Promise<void> {
|
|
|
|
|
try {
|
|
|
|
|
this._startLoader.loading = true;
|
|
|
|
|
this._startButton.enabled = false;
|
|
|
|
|
this._cancelButton.enabled = true;
|
|
|
|
|
this._copyButton.enabled = false;
|
|
|
|
|
this._dialog!.okButton.enabled = false;
|
|
|
|
|
this._dialog!.cancelButton.enabled = true;
|
|
|
|
|
if (this._model.isIrTargetValidated && results) {
|
|
|
|
|
await this._initializeResults(results);
|
|
|
|
|
} else {
|
|
|
|
|
await this._validate();
|
|
|
|
|
}
|
|
|
|
|
} finally {
|
|
|
|
|
this._startLoader.loading = false;
|
|
|
|
|
this._startButton.enabled = true;
|
|
|
|
|
this._cancelButton.enabled = false;
|
|
|
|
|
this._copyButton.enabled = true;
|
|
|
|
|
this._dialog!.okButton.enabled = this._model.isIrTargetValidated;
|
|
|
|
|
this._dialog!.cancelButton.enabled = !this._model.isIrTargetValidated;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async _copyValidationResults(): Promise<void> {
|
|
|
|
|
const errorsText = this._valdiationErrors.join(EOL);
|
|
|
|
|
const msg = errorsText.length === 0
|
|
|
|
|
? constants.VALIDATE_IR_VALIDATION_COMPLETED
|
|
|
|
|
: constants.VALIDATE_IR_VALIDATION_COMPLETED_ERRORS(errorsText);
|
|
|
|
|
return vscode.env.clipboard.writeText(msg);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async _updateResultsInfoBox(text: azdata.InputBoxComponent): Promise<void> {
|
|
|
|
|
const selectedRows: number[] = this._resultsTable.selectedRows ?? [];
|
|
|
|
|
const statusMessages: string[] = [];
|
|
|
|
|
if (selectedRows.length > 0) {
|
|
|
|
|
for (let i = 0; i < selectedRows.length; i++) {
|
|
|
|
|
const row = selectedRows[i];
|
|
|
|
|
const results: any[] = this._validationResult[row];
|
|
|
|
|
const status = results[ValidationResultIndex.status];
|
|
|
|
|
const errors = results[ValidationResultIndex.errors];
|
|
|
|
|
statusMessages.push(
|
|
|
|
|
constants.VALIDATE_IR_VALIDATION_STATUS(ValidationStatusLookup[status], errors));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const msg = statusMessages.length > 0
|
|
|
|
|
? statusMessages.join(EOL)
|
|
|
|
|
: '';
|
|
|
|
|
text.value = msg;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async _createResultsTable(view: azdata.ModelView): Promise<azdata.TableComponent> {
|
|
|
|
|
return view.modelBuilder.table()
|
|
|
|
|
.withProps({
|
|
|
|
|
columns: [
|
|
|
|
|
{
|
|
|
|
|
value: 'test',
|
|
|
|
|
name: constants.VALIDATE_IR_COLUMN_VALIDATION_STEPS,
|
|
|
|
|
type: azdata.ColumnType.text,
|
|
|
|
|
width: 380,
|
|
|
|
|
headerCssClass: 'no-borders',
|
|
|
|
|
cssClass: 'no-borders align-with-header',
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
value: 'image',
|
|
|
|
|
name: '',
|
|
|
|
|
type: azdata.ColumnType.icon,
|
|
|
|
|
width: 20,
|
|
|
|
|
headerCssClass: 'no-borders display-none',
|
|
|
|
|
cssClass: 'no-borders align-with-header',
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
value: 'message',
|
|
|
|
|
name: constants.VALIDATE_IR_COLUMN_STATUS,
|
|
|
|
|
type: azdata.ColumnType.text,
|
|
|
|
|
width: 150,
|
|
|
|
|
headerCssClass: 'no-borders',
|
|
|
|
|
cssClass: 'no-borders align-with-header',
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
data: [],
|
|
|
|
|
width: 580,
|
|
|
|
|
height: 300,
|
|
|
|
|
CSSStyles: {
|
|
|
|
|
'margin-top': '10px',
|
|
|
|
|
'margin-bottom': '10px',
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
.component();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async _initializeResults(results?: ValidationResult[]): Promise<void> {
|
|
|
|
|
this._valdiationErrors = [];
|
|
|
|
|
if (this._model.isSqlDbTarget) {
|
|
|
|
|
// initialize validation results view for sqldb target
|
|
|
|
|
await this._initSqlDbIrResults(results);
|
|
|
|
|
} else {
|
|
|
|
|
// initialize validation results view for sqlmi, sqlvm targets
|
|
|
|
|
await this._initTestIrResults(results);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async _validate(): Promise<void> {
|
|
|
|
|
this._canceled = false;
|
|
|
|
|
await this._initializeResults();
|
|
|
|
|
|
|
|
|
|
if (this._model.isSqlDbTarget) {
|
|
|
|
|
await this._validateSqlDbMigration();
|
|
|
|
|
} else {
|
|
|
|
|
await this._validateDatabaseMigration();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this._saveResults();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async _validateDatabaseMigration(): Promise<void> {
|
|
|
|
|
const sqlConnections = await azdata.connection.getConnections();
|
|
|
|
|
const currentConnection = sqlConnections.find(
|
|
|
|
|
value => value.connectionId === this._model.sourceConnectionId);
|
|
|
|
|
const sourceServerName = currentConnection?.serverName!;
|
|
|
|
|
const trustServerCertificate = currentConnection?.options?.trustServerCertificate === true;
|
|
|
|
|
const databaseCount = this._model._databasesForMigration.length;
|
|
|
|
|
const sourceDatabaseName = this._model._databasesForMigration[0];
|
|
|
|
|
const networkShare = this._model._databaseBackup.networkShares[0];
|
|
|
|
|
let testNumber: number = 0;
|
|
|
|
|
|
|
|
|
|
const validate = async (
|
|
|
|
|
sourceDatabase: string,
|
|
|
|
|
network: NetworkShare,
|
|
|
|
|
testIrOnline: boolean,
|
|
|
|
|
testSourceLocationConnectivity: boolean,
|
|
|
|
|
testSourceConnectivity: boolean,
|
|
|
|
|
testBlobConnectivity: boolean): Promise<boolean> => {
|
|
|
|
|
try {
|
|
|
|
|
await this._updateValidateIrResults(testNumber, ValidateIrState.Running);
|
|
|
|
|
const response = await validateIrDatabaseMigrationSettings(
|
|
|
|
|
this._model,
|
|
|
|
|
sourceServerName,
|
|
|
|
|
trustServerCertificate,
|
|
|
|
|
sourceDatabase,
|
|
|
|
|
network,
|
|
|
|
|
testIrOnline,
|
|
|
|
|
testSourceLocationConnectivity,
|
|
|
|
|
testSourceConnectivity,
|
|
|
|
|
testBlobConnectivity);
|
|
|
|
|
if (response?.errors?.length > 0) {
|
|
|
|
|
const errors = response.errors.map(
|
|
|
|
|
error => constants.VALIDATE_IR_VALIDATION_RESULT_ERROR(
|
|
|
|
|
sourceDatabase,
|
|
|
|
|
network.networkShareLocation,
|
|
|
|
|
error));
|
|
|
|
|
await this._updateValidateIrResults(testNumber, ValidateIrState.Failed, errors);
|
|
|
|
|
} else {
|
|
|
|
|
await this._updateValidateIrResults(testNumber, ValidateIrState.Succeeded);
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
await this._updateValidateIrResults(
|
|
|
|
|
testNumber,
|
|
|
|
|
ValidateIrState.Failed,
|
|
|
|
|
[constants.VALIDATE_IR_VALIDATION_RESULT_API_ERROR(sourceDatabase, error)]);
|
|
|
|
|
}
|
|
|
|
|
return false;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// validate integration runtime (IR) is online
|
|
|
|
|
if (!await validate(sourceDatabaseName, networkShare, true, false, false, false)) {
|
|
|
|
|
this._canceled = true;
|
|
|
|
|
await this._updateValidateIrResults(testNumber + 1, ValidateIrState.Canceled, [constants.VALIDATE_IR_VALIDATION_CANCELED])
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
testNumber++;
|
|
|
|
|
|
|
|
|
|
// validate blob container connectivity
|
|
|
|
|
if (!await validate(sourceDatabaseName, networkShare, false, false, false, true)) {
|
|
|
|
|
await this._updateValidateIrResults(testNumber + 1, ValidateIrState.Canceled, [constants.VALIDATE_IR_VALIDATION_CANCELED])
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (let i = 0; i < databaseCount; i++) {
|
|
|
|
|
const sourceDatabaseName = this._model._databasesForMigration[i];
|
|
|
|
|
const networkShare = this._model._databaseBackup.networkShares[i];
|
|
|
|
|
testNumber++;
|
|
|
|
|
if (this._canceled) {
|
|
|
|
|
await this._updateValidateIrResults(testNumber, ValidateIrState.Canceled, [constants.VALIDATE_IR_VALIDATION_CANCELED])
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
// validate source connectivity
|
|
|
|
|
await validate(sourceDatabaseName, networkShare, false, false, true, false);
|
|
|
|
|
testNumber++;
|
|
|
|
|
if (this._canceled) {
|
|
|
|
|
await this._updateValidateIrResults(testNumber, ValidateIrState.Canceled, [constants.VALIDATE_IR_VALIDATION_CANCELED])
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
// valdiate source location / network share connectivity
|
|
|
|
|
await validate(sourceDatabaseName, networkShare, false, true, false, false);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async _validateSqlDbMigration(): Promise<void> {
|
|
|
|
|
const sqlConnections = await azdata.connection.getConnections();
|
|
|
|
|
const currentConnection = sqlConnections.find(
|
|
|
|
|
value => value.connectionId === this._model.sourceConnectionId);
|
|
|
|
|
const sourceServerName = currentConnection?.serverName!;
|
|
|
|
|
const trustServerCertificate = currentConnection?.options['trustServerCertificate'] === true;
|
|
|
|
|
const databaseCount = this._model._databasesForMigration.length;
|
|
|
|
|
const sourceDatabaseName = this._model._databasesForMigration[0];
|
|
|
|
|
const targetDatabaseName = this._model._sourceTargetMapping.get(sourceDatabaseName)?.databaseName ?? '';
|
|
|
|
|
let testNumber: number = 0;
|
|
|
|
|
|
|
|
|
|
const validate = async (
|
|
|
|
|
sourceDatabase: string,
|
|
|
|
|
targetDatabase: string,
|
|
|
|
|
testIrOnline: boolean,
|
|
|
|
|
testSourceConnectivity: boolean,
|
|
|
|
|
testTargetConnectivity: boolean): Promise<boolean> => {
|
|
|
|
|
|
|
|
|
|
await this._updateValidateIrResults(testNumber, ValidateIrState.Running);
|
|
|
|
|
try {
|
|
|
|
|
const response = await validateIrSqlDatabaseMigrationSettings(
|
|
|
|
|
this._model,
|
|
|
|
|
sourceServerName,
|
|
|
|
|
trustServerCertificate,
|
|
|
|
|
sourceDatabase,
|
|
|
|
|
targetDatabase,
|
|
|
|
|
testIrOnline,
|
|
|
|
|
testSourceConnectivity,
|
|
|
|
|
testTargetConnectivity);
|
|
|
|
|
if (response?.errors?.length > 0) {
|
|
|
|
|
const errors = response.errors.map(
|
|
|
|
|
error => constants.VALIDATE_IR_SQLDB_VALIDATION_RESULT_ERROR(
|
|
|
|
|
sourceDatabase,
|
|
|
|
|
targetDatabase,
|
|
|
|
|
error));
|
|
|
|
|
await this._updateValidateIrResults(testNumber, ValidateIrState.Failed, errors);
|
|
|
|
|
} else {
|
|
|
|
|
await this._updateValidateIrResults(testNumber, ValidateIrState.Succeeded);
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
await this._updateValidateIrResults(
|
|
|
|
|
testNumber,
|
|
|
|
|
ValidateIrState.Failed,
|
|
|
|
|
[constants.VALIDATE_IR_VALIDATION_RESULT_API_ERROR(sourceDatabase, error)]);
|
|
|
|
|
}
|
|
|
|
|
return false;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// validate IR is online
|
|
|
|
|
if (!await validate(sourceDatabaseName, targetDatabaseName, true, false, false)) {
|
|
|
|
|
this._canceled = true;
|
|
|
|
|
await this._updateValidateIrResults(testNumber + 1, ValidateIrState.Canceled, [constants.VALIDATE_IR_VALIDATION_CANCELED]);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (let i = 0; i < databaseCount; i++) {
|
|
|
|
|
const sourceDatabaseName = this._model._databasesForMigration[i];
|
|
|
|
|
const targetDatabaseName = this._model._sourceTargetMapping.get(sourceDatabaseName)?.databaseName ?? '';
|
|
|
|
|
|
|
|
|
|
testNumber++;
|
|
|
|
|
if (this._canceled) {
|
|
|
|
|
await this._updateValidateIrResults(testNumber, ValidateIrState.Canceled, [constants.VALIDATE_IR_VALIDATION_CANCELED]);
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
// validate source connectivity
|
|
|
|
|
await validate(sourceDatabaseName, targetDatabaseName, false, true, false);
|
|
|
|
|
|
|
|
|
|
testNumber++;
|
|
|
|
|
if (this._canceled) {
|
|
|
|
|
await this._updateValidateIrResults(testNumber, ValidateIrState.Canceled, [constants.VALIDATE_IR_VALIDATION_CANCELED]);
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
// validate target connectivity
|
|
|
|
|
await validate(sourceDatabaseName, targetDatabaseName, false, false, true);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async _initTestIrResults(results?: ValidationResult[]): Promise<void> {
|
|
|
|
|
this._validationResult = [];
|
|
|
|
|
|
|
|
|
|
this._addValidationResult(constants.VALIDATE_IR_VALIDATION_RESULT_LABEL_SHIR);
|
|
|
|
|
this._addValidationResult(constants.VALIDATE_IR_VALIDATION_RESULT_LABEL_STORAGE);
|
|
|
|
|
|
|
|
|
|
for (let i = 0; i < this._model._databasesForMigration.length; i++) {
|
|
|
|
|
const sourceDatabaseName = this._model._databasesForMigration[i];
|
|
|
|
|
const networkShare = this._model._databaseBackup.networkShares[i];
|
|
|
|
|
|
|
|
|
|
this._addValidationResult(
|
|
|
|
|
constants.VALIDATE_IR_VALIDATION_RESULT_LABEL_SOURCE_DATABASE(
|
|
|
|
|
sourceDatabaseName));
|
|
|
|
|
this._addValidationResult(
|
|
|
|
|
constants.VALIDATE_IR_VALIDATION_RESULT_LABEL_NETWORK_SHARE(
|
|
|
|
|
networkShare.networkShareLocation));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (results && results.length > 0) {
|
|
|
|
|
for (let row = 0; row < results.length; row++) {
|
|
|
|
|
await this._updateValidateIrResults(
|
|
|
|
|
row,
|
|
|
|
|
results[row].state,
|
|
|
|
|
results[row].errors,
|
|
|
|
|
false);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const data = this._validationResult.map(row => [
|
|
|
|
|
row[ValidationResultIndex.message],
|
|
|
|
|
row[ValidationResultIndex.icon],
|
|
|
|
|
row[ValidationResultIndex.status]]);
|
|
|
|
|
await this._resultsTable.updateProperty('data', data);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async _initSqlDbIrResults(results?: ValidationResult[]): Promise<void> {
|
|
|
|
|
this._validationResult = [];
|
|
|
|
|
this._addValidationResult(constants.VALIDATE_IR_VALIDATION_RESULT_LABEL_SHIR);
|
|
|
|
|
|
|
|
|
|
this._model._databasesForMigration
|
|
|
|
|
.forEach(sourceDatabaseName => {
|
|
|
|
|
this._addValidationResult(
|
|
|
|
|
constants.VALIDATE_IR_VALIDATION_RESULT_LABEL_SOURCE_DATABASE(
|
|
|
|
|
sourceDatabaseName));
|
|
|
|
|
|
|
|
|
|
const targetDatabaseName = this._model._sourceTargetMapping.get(sourceDatabaseName)?.databaseName ?? '';
|
|
|
|
|
this._addValidationResult(
|
|
|
|
|
constants.VALIDATE_IR_VALIDATION_RESULT_LABEL_TARGET_DATABASE(
|
|
|
|
|
targetDatabaseName));
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (results && results.length > 0) {
|
|
|
|
|
for (let row = 0; row < results.length; row++) {
|
|
|
|
|
await this._updateValidateIrResults(
|
|
|
|
|
row,
|
|
|
|
|
results[row].state,
|
|
|
|
|
results[row].errors,
|
|
|
|
|
false);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const data = this._validationResult.map(row => [
|
|
|
|
|
row[ValidationResultIndex.message],
|
|
|
|
|
row[ValidationResultIndex.icon],
|
|
|
|
|
row[ValidationResultIndex.status]]);
|
|
|
|
|
await this._resultsTable.updateProperty('data', data);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private _addValidationResult(message: string): void {
|
|
|
|
|
this._validationResult.push([
|
|
|
|
|
message,
|
|
|
|
|
<azdata.IconColumnCellValue>{
|
|
|
|
|
icon: IconPathHelper.notStartedMigration,
|
|
|
|
|
title: ValidationStatusLookup[ValidateIrState.Pending],
|
|
|
|
|
},
|
|
|
|
|
ValidationStatusLookup[ValidateIrState.Pending],
|
|
|
|
|
[],
|
|
|
|
|
ValidateIrState.Pending]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async _updateValidateIrResults(row: number, state: ValidateIrState, errors: string[] = [], updateTable: boolean = true): Promise<void> {
|
|
|
|
|
if (state === ValidateIrState.Canceled) {
|
|
|
|
|
for (let cancelRow = row; cancelRow < this._validationResult.length; cancelRow++) {
|
|
|
|
|
await this._updateResults(cancelRow, state, errors);
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
await this._updateResults(row, state, errors);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (updateTable) {
|
|
|
|
|
const data = this._validationResult.map(row => [
|
|
|
|
|
row[ValidationResultIndex.message],
|
|
|
|
|
row[ValidationResultIndex.icon],
|
|
|
|
|
row[ValidationResultIndex.status]]);
|
|
|
|
|
await this._resultsTable.updateProperty('data', data);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this._valdiationErrors.push(...errors);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async _updateResults(row: number, state: ValidateIrState, errors: string[] = []): Promise<void> {
|
|
|
|
|
const result = this._validationResult[row];
|
|
|
|
|
const status = ValidationStatusLookup[state];
|
|
|
|
|
const statusMsg = state === ValidateIrState.Failed && errors.length > 0
|
|
|
|
|
? constants.VALIDATE_IR_VALIDATION_STATUS_ERROR_COUNT(status, errors.length)
|
|
|
|
|
: status;
|
|
|
|
|
|
|
|
|
|
const statusMessage = errors.length > 0
|
|
|
|
|
? constants.VALIDATE_IR_VALIDATION_STATUS_ERROR(status, errors)
|
|
|
|
|
: statusMsg;
|
|
|
|
|
|
|
|
|
|
this._validationResult[row] = [
|
|
|
|
|
result[ValidationResultIndex.message],
|
|
|
|
|
<azdata.IconColumnCellValue>{
|
|
|
|
|
icon: this._getValidationStateImage(state),
|
|
|
|
|
title: statusMessage,
|
|
|
|
|
},
|
|
|
|
|
statusMsg,
|
|
|
|
|
errors,
|
|
|
|
|
state];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private _getValidationStateImage(state: ValidateIrState): azdata.IconPath {
|
|
|
|
|
switch (state) {
|
|
|
|
|
case ValidateIrState.Canceled:
|
|
|
|
|
return IconPathHelper.cancel;
|
|
|
|
|
case ValidateIrState.Failed:
|
|
|
|
|
return IconPathHelper.error;
|
|
|
|
|
case ValidateIrState.Running:
|
|
|
|
|
return IconPathHelper.inProgressMigration;
|
|
|
|
|
case ValidateIrState.Succeeded:
|
|
|
|
|
return IconPathHelper.completedMigration;
|
|
|
|
|
case ValidateIrState.Pending:
|
|
|
|
|
default:
|
|
|
|
|
return IconPathHelper.notStartedMigration;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|