mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-01-18 01:25:37 -05:00
* Automatically migrate certs on last page of wizard * Updated TDE configuration dialog wording * Added ConfigDialogSetting to track user selection * Added numberOfDbsWithTde telemetry prop * Migrate certs button moved back to target page * Enable next button on cert migration success * Reset TDE migration result if target changes * Addressed PR feedback * Added TDE navigation validator * Fixed typo
551 lines
17 KiB
TypeScript
551 lines
17 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 * as constants from '../../constants/strings';
|
|
import { logError, TelemetryAction, TelemetryViews, sendSqlMigrationActionEvent, getTelemetryProps } from '../../telemetry';
|
|
import { EOL } from 'os';
|
|
import { MigrationStateModel, OperationResult } from '../../models/stateMachine';
|
|
import { IconPathHelper } from '../../constants/iconPathHelper';
|
|
import { TdeMigrationState, TdeMigrationResult, TdeMigrationDbState, TdeDatabaseMigrationState, TdeMigrationDbResult } from '../../models/tdeModels';
|
|
|
|
const DialogName = 'TdeMigrationDialog';
|
|
|
|
export enum TdeValidationResultIndex {
|
|
name = 0,
|
|
icon = 1,
|
|
status = 2,
|
|
errors = 3,
|
|
state = 4,
|
|
updated = 5
|
|
}
|
|
|
|
export const ValidationStatusLookup: constants.LookupTable<string | undefined> = {
|
|
[TdeMigrationState.Canceled]: constants.STATE_CANCELED,
|
|
[TdeMigrationState.Failed]: constants.STATE_FAILED,
|
|
[TdeMigrationState.Pending]: constants.STATE_PENDING,
|
|
[TdeMigrationState.Running]: constants.STATE_RUNNING,
|
|
[TdeMigrationState.Succeeded]: constants.STATE_SUCCEEDED,
|
|
default: undefined
|
|
};
|
|
|
|
|
|
export class TdeMigrationDialog {
|
|
|
|
//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 _startMigrationLoader!: azdata.LoadingComponent;
|
|
private _retryMigrationButton!: azdata.ButtonComponent;
|
|
private _copyButton!: azdata.ButtonComponent;
|
|
private _headingText!: azdata.TextComponent;
|
|
private _progressReportText!: azdata.TextComponent;
|
|
private _validationResult: any[][] = [];
|
|
private _dbRowsMap: Map<string, number> = new Map<string, number>();
|
|
private _tdeMigrationResult: TdeMigrationResult = {
|
|
state: TdeMigrationState.Pending,
|
|
dbList: []
|
|
};
|
|
private _valdiationErrors: string[] = [];
|
|
private _completedDatabasesCount: number = 0;
|
|
private _certMigrationEventEmitter;
|
|
|
|
constructor(
|
|
model: MigrationStateModel,
|
|
certMigrationEventEmitter: vscode.EventEmitter<TdeMigrationResult>) {
|
|
this._model = model;
|
|
this._certMigrationEventEmitter = certMigrationEventEmitter;
|
|
}
|
|
|
|
public async openDialog(): Promise<void> {
|
|
if (!this._isOpen) {
|
|
this._isOpen = true;
|
|
this._dialog = azdata.window.createModelViewDialog(
|
|
constants.TDE_MIGRATEDIALOG_TITLE,
|
|
DialogName,
|
|
600);
|
|
|
|
const promise = this._initializeDialog(this._dialog);
|
|
azdata.window.openDialog(this._dialog);
|
|
await promise;
|
|
|
|
await this._loadMigrationResults();
|
|
|
|
// This will prevent that it tryes to auto run when the last execution was successful, fails don't get persisted on the ui, only reported in the events.
|
|
if (this._tdeMigrationResult.state === TdeMigrationState.Pending) {
|
|
await this._runTdeMigration();
|
|
}
|
|
}
|
|
}
|
|
|
|
private async _initializeDialog(dialog: azdata.window.Dialog): Promise<void> {
|
|
return new Promise<void>((resolve, reject) => {
|
|
dialog.registerContent(async (view) => {
|
|
try {
|
|
dialog.okButton.label = constants.TDE_MIGRATE_DONE_BUTTON;
|
|
dialog.okButton.position = 'left';
|
|
dialog.okButton.enabled = false;
|
|
dialog.cancelButton.position = 'left';
|
|
|
|
this._headingText = view.modelBuilder.text()
|
|
.withProps({
|
|
value: constants.TDE_MIGRATE_HEADING,
|
|
CSSStyles: {
|
|
'font-size': '13px',
|
|
'font-weight': '400',
|
|
'margin-bottom': '10px',
|
|
},
|
|
})
|
|
.component();
|
|
this._startMigrationLoader = view.modelBuilder.loadingComponent()
|
|
.withProps({
|
|
loading: false,
|
|
CSSStyles: { 'margin': '5px 0 0 10px' }
|
|
})
|
|
.component();
|
|
this._progressReportText = view.modelBuilder.text()
|
|
.withProps({
|
|
value: '',
|
|
CSSStyles: {
|
|
'font-size': '13px',
|
|
'font-weight': '400',
|
|
'margin-bottom': '10px',
|
|
'margin-left': '5px'
|
|
},
|
|
})
|
|
.component();
|
|
|
|
const headingContainer = view.modelBuilder.flexContainer()
|
|
.withLayout({
|
|
flexFlow: 'row',
|
|
justifyContent: 'flex-start',
|
|
})
|
|
.withItems([this._headingText, this._progressReportText, this._startMigrationLoader], { flex: '0 0 auto' })
|
|
.component();
|
|
|
|
this._resultsTable = await this._createResultsTable(view);
|
|
|
|
this._retryMigrationButton = view.modelBuilder.button()
|
|
.withProps({
|
|
iconPath: IconPathHelper.restartDataCollection,
|
|
iconHeight: 18,
|
|
iconWidth: 18,
|
|
width: 100,
|
|
label: constants.TDE_MIGRATE_RETRY_VALIDATION,
|
|
}).component();
|
|
this._copyButton = view.modelBuilder.button()
|
|
.withProps({
|
|
iconPath: IconPathHelper.copy,
|
|
iconHeight: 18,
|
|
iconWidth: 18,
|
|
width: 150,
|
|
label: constants.TDE_MIGRATE_COPY_RESULTS,
|
|
enabled: false,
|
|
}).component();
|
|
|
|
this._disposables.push(
|
|
this._retryMigrationButton.onDidClick(
|
|
async (e) => await this._retryTdeMigration()));
|
|
|
|
this._disposables.push(
|
|
this._copyButton.onDidClick(
|
|
async (e) => this._copyValidationResults()));
|
|
|
|
const toolbar = view.modelBuilder.toolbarContainer()
|
|
.withToolbarItems([
|
|
{ component: this._retryMigrationButton }
|
|
//{ component: this._copyButton }
|
|
])
|
|
.component();
|
|
|
|
const resultsHeading = view.modelBuilder.text()
|
|
.withProps({
|
|
value: constants.TDE_MIGRATE_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 async _loadMigrationResults(): Promise<void> {
|
|
const tdeMigrationResult = this._model.tdeMigrationConfig.lastTdeMigrationResult();
|
|
this._progressReportText.value = '';
|
|
|
|
if (tdeMigrationResult.state === TdeMigrationState.Pending) {
|
|
//First time it is called. Should auto start.
|
|
this._headingText.value = constants.TDE_MIGRATE_RESULTS_HEADING;
|
|
|
|
//Initialize results using the tde enabled databases;
|
|
tdeMigrationResult.dbList = this._model.tdeMigrationConfig.getTdeEnabledDatabases().map<TdeMigrationDbState>(
|
|
db => ({
|
|
name: db,
|
|
dbState: TdeDatabaseMigrationState.Running,
|
|
message: ''
|
|
}
|
|
));
|
|
|
|
this._startMigrationLoader.loading = true;
|
|
this._retryMigrationButton.enabled = false;
|
|
this._copyButton.enabled = false;
|
|
this._dialog!.okButton.enabled = false;
|
|
this._dialog!.cancelButton.enabled = true;
|
|
} else {
|
|
//It already ran. Just load the previous status.
|
|
this._headingText.value = constants.TDE_MIGRATE_RESULTS_HEADING_PREVIOUS;
|
|
this._startMigrationLoader.loading = false;
|
|
this._retryMigrationButton.enabled = true;
|
|
this._copyButton.enabled = true;
|
|
this._dialog!.okButton.enabled = true;
|
|
this._dialog!.cancelButton.enabled = true;
|
|
}
|
|
|
|
//Grab copy of data with a different result reference. Done here because it is closer to the assigment on the true path.
|
|
this._tdeMigrationResult = {
|
|
state: tdeMigrationResult.state,
|
|
dbList: tdeMigrationResult.dbList
|
|
};
|
|
|
|
await this._populateTableResults();
|
|
}
|
|
|
|
private async _retryTdeMigration(): Promise<void> {
|
|
this._model.tdeMigrationConfig.resetTdeMigrationResult();
|
|
const tdeMigrationResult = this._model.tdeMigrationConfig.lastTdeMigrationResult();
|
|
tdeMigrationResult.dbList = this._model.tdeMigrationConfig.getTdeEnabledDatabases().map<TdeMigrationDbState>(
|
|
db => ({
|
|
name: db,
|
|
dbState: TdeDatabaseMigrationState.Running,
|
|
message: ''
|
|
}
|
|
));
|
|
|
|
this._tdeMigrationResult = {
|
|
state: tdeMigrationResult.state,
|
|
dbList: tdeMigrationResult.dbList
|
|
};
|
|
|
|
await this._populateTableResults();
|
|
|
|
await this._runTdeMigration();
|
|
}
|
|
|
|
private _updateProgressText(): void {
|
|
this._progressReportText.value = constants.TDE_COMPLETED_STATUS(this._completedDatabasesCount, this._model.tdeMigrationConfig.getTdeEnabledDatabasesCount());
|
|
}
|
|
|
|
private async _runTdeMigration(): Promise<void> {
|
|
//Update the UI buttons
|
|
this._headingText.value = constants.TDE_MIGRATE_RESULTS_HEADING;
|
|
this._startMigrationLoader.loading = true;
|
|
this._retryMigrationButton.enabled = false;
|
|
this._copyButton.enabled = false;
|
|
this._dialog!.okButton.enabled = false;
|
|
this._dialog!.cancelButton.enabled = true;
|
|
|
|
|
|
//Send the external command
|
|
try {
|
|
this._completedDatabasesCount = 0;
|
|
this._updateProgressText();
|
|
|
|
//Get access token
|
|
const accessToken = await azdata.accounts.getAccountSecurityToken(this._model._azureAccount, this._model._azureTenant.id, azdata.AzureResource.ResourceManagement);
|
|
|
|
const operationResult = await this._model.startTdeMigration(accessToken!.token, this._updateTableResultRow.bind(this));
|
|
|
|
await this._updateTableFromOperationResult(operationResult);
|
|
|
|
if (operationResult.success) {
|
|
this._dialog!.okButton.enabled = true;
|
|
|
|
this._tdeMigrationResult = {
|
|
state: TdeMigrationState.Succeeded,
|
|
dbList: operationResult.result.map<TdeMigrationDbState>(
|
|
db => ({
|
|
name: db.name,
|
|
dbState: TdeDatabaseMigrationState.Succeeded,
|
|
message: db.message
|
|
}
|
|
))
|
|
};
|
|
|
|
this._model.tdeMigrationConfig.setTdeMigrationResult(this._tdeMigrationResult); // Set value on success.
|
|
this._certMigrationEventEmitter.fire(this._tdeMigrationResult);
|
|
|
|
sendSqlMigrationActionEvent(
|
|
TelemetryViews.TdeMigrationDialog,
|
|
TelemetryAction.TdeMigrationSuccess,
|
|
{
|
|
...getTelemetryProps(this._model),
|
|
'numberOfDbsWithTde': this._model.tdeMigrationConfig.getTdeEnabledDatabasesCount().toString()
|
|
},
|
|
{}
|
|
);
|
|
}
|
|
else {
|
|
this._dialog!.okButton.enabled = false;
|
|
this._certMigrationEventEmitter.fire({ state: TdeMigrationState.Failed, dbList: [] });
|
|
|
|
sendSqlMigrationActionEvent(
|
|
TelemetryViews.TdeMigrationDialog,
|
|
TelemetryAction.TdeMigrationFailures,
|
|
{
|
|
...getTelemetryProps(this._model),
|
|
'numberOfDbsWithTde': this._model.tdeMigrationConfig.getTdeEnabledDatabasesCount().toString()
|
|
},
|
|
{}
|
|
);
|
|
}
|
|
|
|
this._startMigrationLoader.loading = false;
|
|
this._retryMigrationButton.enabled = true;
|
|
this._copyButton.enabled = true;
|
|
|
|
this._completedDatabasesCount = this._model.tdeMigrationConfig.getTdeEnabledDatabasesCount(); //Force the total to match
|
|
this._updateProgressText();
|
|
|
|
} catch (error) {
|
|
//Catch any exception and failed any pending table.
|
|
this._startMigrationLoader.loading = false;
|
|
this._retryMigrationButton.enabled = true;
|
|
this._copyButton.enabled = false;
|
|
this._dialog!.okButton.enabled = false;
|
|
this._progressReportText.value = '';
|
|
|
|
logError(TelemetryViews.TdeMigrationDialog, TelemetryAction.TdeMigrationClientException, error);
|
|
}
|
|
|
|
this._headingText.value = constants.TDE_MIGRATE_RESULTS_HEADING_COMPLETED;
|
|
}
|
|
|
|
private async _copyValidationResults(): Promise<void> {
|
|
const errorsText = this._valdiationErrors.join(EOL);
|
|
const msg = errorsText.length === 0
|
|
? constants.TDE_MIGRATE_VALIDATION_COMPLETED
|
|
: constants.TDE_MIGRATE_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[TdeValidationResultIndex.status];
|
|
const errors = results[TdeValidationResultIndex.errors];
|
|
statusMessages.push(
|
|
constants.TDE_MIGRATE_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.TDE_MIGRATE_COLUMN_DATABASES,
|
|
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.TDE_MIGRATE_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 _updateTableFromOperationResult(operationResult: OperationResult<TdeMigrationDbResult[]>): Promise<void> {
|
|
let anyRowUpdated = false;
|
|
|
|
operationResult.result.forEach((element) => {
|
|
const rowResultsIndex = this._dbRowsMap.get(element.name)!; //Checked already at the beginning of the method
|
|
const currentRow = this._validationResult[rowResultsIndex];
|
|
|
|
if (!currentRow[TdeValidationResultIndex.updated]) {
|
|
anyRowUpdated = true;
|
|
this._updateValidationResultRow(element.name, element.success, element.message);
|
|
}
|
|
});
|
|
|
|
if (anyRowUpdated) {
|
|
// Update the table
|
|
await this._updateTableData();
|
|
}
|
|
}
|
|
|
|
private async _updateTableResultRow(dbName: string, succeeded: boolean, message: string, statusCode: string): Promise<void> {
|
|
if (!this._dbRowsMap.has(dbName)) {
|
|
return; //Table not found
|
|
}
|
|
|
|
this._updateValidationResultRow(dbName, succeeded, message);
|
|
|
|
if (!succeeded) {
|
|
logError(TelemetryViews.TdeMigrationDialog, statusCode, {});
|
|
}
|
|
|
|
// Update the table
|
|
await this._updateTableData();
|
|
|
|
// When the updates come after the method finished. Thread related, out of our control.
|
|
if (this._completedDatabasesCount < this._model.tdeMigrationConfig.getTdeEnabledDatabasesCount()) {
|
|
this._completedDatabasesCount++; // Increase the completed count
|
|
this._updateProgressText();
|
|
}
|
|
}
|
|
|
|
private _updateValidationResultRow(dbName: string, succeeded: boolean, message: string) {
|
|
const rowResultsIndex = this._dbRowsMap.get(dbName)!; //Checked already at the beginning of the method
|
|
const tmpRow = this._buildRow({
|
|
name: dbName,
|
|
dbState: (succeeded) ? TdeDatabaseMigrationState.Succeeded : TdeDatabaseMigrationState.Failed,
|
|
message: message
|
|
},
|
|
true);
|
|
|
|
// Update the local result
|
|
this._validationResult[rowResultsIndex] = tmpRow;
|
|
}
|
|
|
|
private async _updateTableData() {
|
|
const data = this._validationResult.map(row => [
|
|
row[TdeValidationResultIndex.name],
|
|
row[TdeValidationResultIndex.icon],
|
|
row[TdeValidationResultIndex.status]]);
|
|
|
|
await this._resultsTable.updateProperty('data', data);
|
|
}
|
|
|
|
private async _populateTableResults(): Promise<void> {
|
|
//Create the local result from the model.
|
|
this._validationResult = this._tdeMigrationResult.dbList.map(db => this._buildRow(db));
|
|
this._dbRowsMap = this._validationResult.reduce(function (map: Map<string, number>, row: any[], currentIndex) {
|
|
const dbName = row[TdeValidationResultIndex.name];
|
|
map.set(dbName, currentIndex);
|
|
return map;
|
|
}, new Map<string, number>());
|
|
|
|
//Update the table.
|
|
await this._updateTableData();
|
|
}
|
|
|
|
private _buildRow(db: TdeMigrationDbState, updated: boolean = false): any[] {
|
|
|
|
const statusMsg = ValidationStatusLookup[db.dbState];
|
|
|
|
const statusMessage = (db.dbState === TdeDatabaseMigrationState.Failed || db.dbState === TdeDatabaseMigrationState.Canceled)
|
|
? constants.TDE_MIGRATE_STATUS_ERROR(db.dbState, db.message)
|
|
: statusMsg;
|
|
|
|
const row: any[] = [
|
|
db.name,
|
|
<azdata.IconColumnCellValue>{
|
|
icon: this._getValidationStateImage(db.dbState),
|
|
title: statusMessage,
|
|
},
|
|
ValidationStatusLookup[db.dbState],
|
|
db.message,
|
|
statusMsg,
|
|
updated
|
|
];
|
|
|
|
return row;
|
|
}
|
|
|
|
private _getValidationStateImage(state: TdeDatabaseMigrationState): azdata.IconPath {
|
|
switch (state) {
|
|
case TdeDatabaseMigrationState.Canceled:
|
|
return IconPathHelper.cancel;
|
|
case TdeDatabaseMigrationState.Failed:
|
|
return IconPathHelper.error;
|
|
case TdeDatabaseMigrationState.Running:
|
|
return IconPathHelper.inProgressMigration;
|
|
case TdeDatabaseMigrationState.Succeeded:
|
|
return IconPathHelper.completedMigration;
|
|
default:
|
|
return IconPathHelper.notStartedMigration;
|
|
}
|
|
}
|
|
}
|