Added validations to configure dialog (#24418)

* Added validations to configure dialog

* Improved validations messages text

* Addressed PR feedback

* Addressed PR feedback

* Fixed build issue

* Version bump

* Changed to single quotes
This commit is contained in:
Steven Marturano
2023-09-15 18:19:04 -04:00
committed by GitHub
parent 9c5ec7dcbc
commit a73929e5e7
7 changed files with 330 additions and 7 deletions

View File

@@ -1516,8 +1516,18 @@ export const TDE_WIZARD_MSG_MANUAL = localize('sql.migration.tde.msg.manual', "Y
export const TDE_WIZARD_MSG_TDE = localize('sql.migration.tde.msg.tde', "You have given Azure Data Studio access to migrate the encryption certificates and database.");
export const TDE_WIZARD_MSG_EMPTY = localize('sql.migration.tde.msg.empty', "No encrypted database selected.");
export const TDE_VALIDATION_TITLE = localize('sql.migration.tde.validation.title', "Validation");
export const TDE_VALIDATION_REQUIREMENTS_MESSAGE = localize('sql.migration.tde.validation.requirements.message', "In order for certificate migration to succeed, you must meet all of the requirements listed below.\n\nClick \"Run validation\" to check that requirements are met.");
export const TDE_VALIDATION_STATUS_PENDING = localize('sql.migration.tde.validation.status.pending', "Pending");
export const TDE_VALIDATION_STATUS_RUNNING = localize('sql.migration.tde.validation.running', "Running");
export const TDE_VALIDATION_STATUS_SUCCEEDED = localize('sql.migration.tde.validation.status.succeeded', "Succeeded");
export const TDE_VALIDATION_STATUS_RUN_VALIDATION = localize('sql.migration.tde.validation.run.validation', "Run validation");
export const TDE_VALIDATION_DESCRIPTION = localize('sql.migration.tde.validation.description', "Description");
export const TDE_VALIDATION_ERROR = localize('sql.migration.tde.validation.error', "Error");
export const TDE_VALIDATION_TROUBLESHOOTING_TIPS = localize('sql.migration.tde.validation.troubleshooting.tips', "Troubleshooting tips");
export function TDE_MIGRATION_ERROR(message: string): string {
return localize('sql.migration.starting.migration.error', "An error occurred while starting the certificate migration: '{0}'", message);
return localize('sql.migration.starting.migration.error', "The following error has occurred while starting the certificate migration: '{0}'", message);
}
export function TDE_MIGRATION_ERROR_DB(name: string, message: string): string {

View File

@@ -9,7 +9,9 @@ import { MigrationStateModel } from '../../models/stateMachine';
import * as constants from '../../constants/strings';
import * as styles from '../../constants/styles';
import * as utils from '../../api/utils';
import { EOL } from 'os';
import { ConfigDialogSetting } from '../../models/tdeModels'
import { IconPathHelper } from '../../constants/iconPathHelper';
export class TdeConfigurationDialog {
@@ -22,8 +24,12 @@ export class TdeConfigurationDialog {
private _adsConfirmationCheckBox!: azdata.CheckBoxComponent;
private _manualMethodWarningContainer!: azdata.FlexContainer;
private _networkPathText!: azdata.InputBoxComponent;
private _validationTable!: azdata.TableComponent;
private _validationMessagesText!: azdata.InputBoxComponent;
private _onClosed: () => void;
private _validationSuccessDescriptionErrorAndTips!: string[][];
constructor(public migrationStateModel: MigrationStateModel, onClosed: () => void) {
this._onClosed = onClosed;
}
@@ -130,6 +136,24 @@ export class TdeConfigurationDialog {
adsMethodButton.onDidChangeCheckedState(async checked => {
if (checked) {
this.migrationStateModel.tdeMigrationConfig.setPendingTdeMigrationMethod(ConfigDialogSetting.ExportCertificates);
let validationTitleData = await this.migrationStateModel.getTdeValidationTitles();
let networkPathValidated =
(this.migrationStateModel.tdeMigrationConfig.getPendingNetworkPath() !== '') &&
(this.migrationStateModel.tdeMigrationConfig.getPendingNetworkPath() === this.migrationStateModel.tdeMigrationConfig.getLastValidatedNetworkPath())
let result = validationTitleData.result.map(validationTitle => {
return [
validationTitle,
{
'icon': networkPathValidated ? IconPathHelper.completedMigration : IconPathHelper.notFound,
'title': networkPathValidated ? constants.TDE_VALIDATION_STATUS_SUCCEEDED : constants.TDE_VALIDATION_STATUS_PENDING
},
networkPathValidated ? constants.TDE_VALIDATION_STATUS_SUCCEEDED : constants.TDE_VALIDATION_STATUS_PENDING
]
});
await this._validationTable.updateProperty('data', result)
await this.updateUI();
}
}));
@@ -228,6 +252,7 @@ export class TdeConfigurationDialog {
this._disposables.push(
this._networkPathText.onTextChanged(async networkPath => {
this.migrationStateModel.tdeMigrationConfig.setPendingNetworkPath(networkPath);
await this.updateUI();
}));
this._adsConfirmationCheckBox = _view.modelBuilder.checkBox()
@@ -245,15 +270,172 @@ export class TdeConfigurationDialog {
await this.updateUI();
}));
const preValidationSeparator = _view.modelBuilder.separator().component();
const validationRequiredLabel = _view.modelBuilder.text()
.withProps({
value: constants.TDE_VALIDATION_REQUIREMENTS_MESSAGE,
CSSStyles: {
...styles.BODY_CSS,
'margin': '4px 2px 4px 2px'
}
}).component();
const runValidationButton = _view.modelBuilder.button()
.withProps(
{
label: constants.TDE_VALIDATION_STATUS_RUN_VALIDATION,
enabled: true
}).component();
this._disposables.push(
runValidationButton.onDidClick(async (e) => {
let data = this._validationTable.data.map((e) => {
return [
e[0],
{
'icon': IconPathHelper.inProgressMigration,
'title': constants.TDE_VALIDATION_STATUS_RUNNING
},
constants.TDE_VALIDATION_STATUS_RUNNING
]
});
await this._validationTable.updateProperty('data', data);
let validationData = await this.migrationStateModel.runTdeValidation(
this.migrationStateModel.tdeMigrationConfig.getPendingNetworkPath());
let allValidationsSucceeded = true;
this._validationSuccessDescriptionErrorAndTips = validationData.result.map(e => {
return [
e.validationStatus.toString(),
e.validationDescription,
e.validationErrorMessage,
e.validationTroubleshootingTips
]
});
let res = validationData.result.map(e => {
if (e.validationStatus < 0) {
allValidationsSucceeded = false;
}
return [
e.validationTitle,
{
'icon': e.validationStatus > 0 ? IconPathHelper.completedMigration : IconPathHelper.error,
'title': e.validationStatusString
},
e.validationStatusString
]
});
await this._validationTable.updateProperty('data', res);
if (allValidationsSucceeded) {
this.migrationStateModel.tdeMigrationConfig.setLastValidatedNetworkPath(
this.migrationStateModel.tdeMigrationConfig.getPendingNetworkPath());
await this.updateUI();
}
}));
this._validationTable = this._createValidationTable(_view);
this._disposables.push(
this._validationTable.onRowSelected(
async (e) => {
const selectedRows: number[] = this._validationTable.selectedRows ?? [];
let message: string = '';
selectedRows.forEach((rowIndex) => {
let successful = this._validationSuccessDescriptionErrorAndTips[rowIndex][0] === "1" // Value will be "1" if successful
let description = this._validationSuccessDescriptionErrorAndTips[rowIndex][1];
let errorMessage = this._validationSuccessDescriptionErrorAndTips[rowIndex][2];
let tips = this._validationSuccessDescriptionErrorAndTips[rowIndex][3];
message = `${constants.TDE_VALIDATION_DESCRIPTION}:${EOL}${description}`;
if (!successful) {
message += `${EOL}${EOL}`;
if (errorMessage?.length > 0) {
message += `${constants.TDE_VALIDATION_ERROR}:${EOL}${errorMessage}${EOL}${EOL}`;
}
message += `${constants.TDE_VALIDATION_TROUBLESHOOTING_TIPS}:${EOL}${tips}`;
}
});
this._validationMessagesText.value = message;
}));
this._validationMessagesText = _view.modelBuilder.inputBox()
.withProps({
inputType: 'text',
height: 142,
multiline: true,
CSSStyles: { 'overflow': 'none auto' }
})
.component();
const postValidationSeparator = _view.modelBuilder.separator().component();
container.addItems([
adsMethodInfoMessage,
networkPathLabel,
this._networkPathText,
this._adsConfirmationCheckBox]);
this._adsConfirmationCheckBox,
preValidationSeparator,
validationRequiredLabel,
runValidationButton,
this._validationTable,
this._validationMessagesText,
postValidationSeparator
]);
return container;
}
private _createValidationTable(view: azdata.ModelView): azdata.TableComponent {
return view.modelBuilder.table()
.withProps({
columns: [
{
value: 'title',
name: constants.TDE_VALIDATION_TITLE,
type: azdata.ColumnType.text,
width: 320,
headerCssClass: 'no-borders',
cssClass: 'no-borders align-with-header',
},
{
value: 'image',
name: '',
type: azdata.ColumnType.icon,
width: 30,
headerCssClass: 'no-borders display-none',
cssClass: 'no-borders align-with-header',
},
{
value: 'message',
name: constants.TDE_MIGRATE_COLUMN_STATUS,
type: azdata.ColumnType.text,
width: 100,
headerCssClass: 'no-borders',
cssClass: 'no-borders align-with-header',
},
],
data: [],
width: 450,
height: 80,
CSSStyles: {
'margin-top': '10px',
'margin-bottom': '10px',
},
})
.component();
}
private createManualWarningContainer(_view: azdata.ModelView): azdata.FlexContainer {
const container = _view.modelBuilder.flexContainer().withProps({
CSSStyles: {
@@ -293,7 +475,7 @@ export class TdeConfigurationDialog {
await utils.updateControlDisplay(this._adsMethodConfirmationContainer, exportCertsUsingAds);
await utils.updateControlDisplay(this._manualMethodWarningContainer, this.migrationStateModel.tdeMigrationConfig.getPendingConfigDialogSetting() === ConfigDialogSetting.DoNotExport);
this.dialog!.okButton.enabled = this.migrationStateModel.tdeMigrationConfig.isAnyChangeReadyToBeApplied()
this.dialog!.okButton.enabled = this.migrationStateModel.tdeMigrationConfig.isAnyChangeReadyToBeApplied();
}
public async openDialog(dialogName?: string,) {

View File

@@ -17,7 +17,7 @@ import { hashString, deepClone, getBlobContainerNameWithFolder, Blob, getLastBac
import { SKURecommendationPage } from '../wizard/skuRecommendationPage';
import { excludeDatabases, getEncryptConnectionValue, getSourceConnectionId, getSourceConnectionProfile, getSourceConnectionServerInfo, getSourceConnectionString, getSourceConnectionUri, getTrustServerCertificateValue, SourceDatabaseInfo, TargetDatabaseInfo } from '../api/sqlUtils';
import { LoginMigrationModel } from './loginMigrationModel';
import { TdeMigrationDbResult, TdeMigrationModel } from './tdeModels';
import { TdeMigrationDbResult, TdeMigrationModel, TdeValidationResult } from './tdeModels';
import { NetworkInterfaceModel } from '../api/dataModels/azure/networkInterfaceModel';
const localize = nls.loadMessageBundle();
@@ -1000,6 +1000,55 @@ export class MigrationStateModel implements Model, vscode.Disposable {
return opResult;
}
public async getTdeValidationTitles(): Promise<OperationResult<string[]>> {
const opResult: OperationResult<string[]> = {
success: false,
result: [],
errors: []
};
try {
opResult.result = await this.migrationService.getTdeValidationTitles() ?? [];
} catch (e) {
console.error(e);
}
return opResult;
}
public async runTdeValidation(networkSharePath: string): Promise<OperationResult<TdeValidationResult[]>> {
const opResult: OperationResult<TdeValidationResult[]> = {
success: false,
result: [],
errors: []
};
const connectionString = await getSourceConnectionString();
try {
let tdeValidationResult = await this.migrationService.runTdeValidation(
connectionString,
networkSharePath);
if (tdeValidationResult !== undefined) {
opResult.result = tdeValidationResult?.map((e) => {
return {
validationTitle: e.validationTitle,
validationDescription: e.validationDescription,
validationTroubleshootingTips: e.validationTroubleshootingTips,
validationErrorMessage: e.validationErrorMessage,
validationStatus: e.validationStatus,
validationStatusString: e.validationStatusString
};
});
}
} catch (e) {
console.error(e);
}
return opResult;
}
public async startMigration() {
const currentConnection = await getSourceConnectionProfile();
const isOfflineMigration = this._databaseBackup.migrationMode === MigrationMode.OFFLINE;

View File

@@ -41,6 +41,15 @@ export interface TdeMigrationDbResult {
message: string;
}
export interface TdeValidationResult {
validationTitle: string;
validationDescription: string;
validationTroubleshootingTips: string;
validationErrorMessage: string;
validationStatus: number;
validationStatusString: string;
}
export class TdeMigrationModel {
// Settings for which the user has clicked the apply button
@@ -53,6 +62,9 @@ export class TdeMigrationModel {
private _pendingExportCertUserConsent: boolean;
private _pendingNetworkPath: string;
// Last network path for which all validations succeeded
private _lastValidatedNetworkPath: string;
private _configurationCompleted: boolean;
private _shownBefore: boolean;
private _encryptedDbs: string[];
@@ -75,6 +87,7 @@ export class TdeMigrationModel {
this._appliedExportCertUserConsent = false;
this._pendingExportCertUserConsent = false;
this._tdeMigrationCompleted = false;
this._lastValidatedNetworkPath = '';
this._tdeMigrationCompleted = this._tdeMigrationCompleted;
}
@@ -176,7 +189,12 @@ export class TdeMigrationModel {
}
if (this._pendingConfigDialogSetting === ConfigDialogSetting.ExportCertificates) {
return this._pendingExportCertUserConsent;
if (this._pendingNetworkPath !== this._lastValidatedNetworkPath) {
return false;
}
return this._pendingNetworkPath.length > 0 &&
this._pendingExportCertUserConsent;
}
return true;
@@ -213,4 +231,12 @@ export class TdeMigrationModel {
public setPendingExportCertUserConsent(pendingExportCertUserConsent: boolean) {
this._pendingExportCertUserConsent = pendingExportCertUserConsent;
}
public setLastValidatedNetworkPath(validatedNetworkPath: string) {
this._lastValidatedNetworkPath = validatedNetworkPath;
}
public getLastValidatedNetworkPath() {
return this._lastValidatedNetworkPath;
}
}

View File

@@ -388,6 +388,11 @@ export const enum VirtualMachineFamily {
standardNVSv4Family
}
export const enum TdeValidationStatus {
Failed = -1,
Succeeded = 1
}
export namespace GetSqlMigrationSkuRecommendationsRequest {
export const type = new RequestType<SqlMigrationSkuRecommendationsParams, SkuRecommendationResult, void, void>('migration/getskurecommendations');
}
@@ -549,3 +554,25 @@ export interface TdeMigrateProgressParams {
message: string;
statusCode: string;
}
export interface TdeValidationResult {
validationTitle: string;
validationDescription: string;
validationTroubleshootingTips: string;
validationErrorMessage: string;
validationStatus: TdeValidationStatus;
validationStatusString: string;
}
export interface TdeValidationParams {
sourceSqlConnectionString: string;
networkSharePath: string;
}
export namespace TdeValidationRequest {
export const type = new RequestType<TdeValidationParams, TdeValidationResult[], void, void>('migration/tdevalidation');
}
export namespace TdeValidationTitlesRequest {
export const type = new RequestType<{}, string[], void, void>('migration/tdevalidationtitles');
}

View File

@@ -316,5 +316,34 @@ export class SqlMigrationService extends MigrationExtensionService implements co
return undefined;
}
}
async runTdeValidation(
sourceSqlConnectionString: string,
networkSharePath: string,
) {
let params: contracts.TdeValidationParams = {
sourceSqlConnectionString: sourceSqlConnectionString,
networkSharePath: networkSharePath,
};
try {
return await this._client.sendRequest(contracts.TdeValidationRequest.type, params);
}
catch (e) {
this._client.logFailedRequest(contracts.TdeValidationRequest.type, e);
}
return undefined;
}
async getTdeValidationTitles() {
try {
return await this._client.sendRequest(contracts.TdeValidationTitlesRequest.type, {});
}
catch (e) {
this._client.logFailedRequest(contracts.TdeValidationRequest.type, e);
}
return undefined;
}
}