Login Migration Pre Validation Changes (#25897)

* Login Migration Pre Validation Changes

* Minor string changes
This commit is contained in:
Bhavikshah123 2024-09-17 14:32:12 +05:30 коммит произвёл GitHub
Родитель 03b386a375
Коммит ebe265ccb1
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: B5690EEEBB952194
6 изменённых файлов: 828 добавлений и 12 удалений

Просмотреть файл

@ -480,8 +480,9 @@ export function LOGIN_MIGRATIONS_GET_LOGINS_ERROR_TITLE(targetType: string): str
export function LOGIN_MIGRATIONS_GET_LOGINS_ERROR(message: string): string {
return localize('sql.migration.wizard.target.login.error', "Error getting login information: {0}", message);
}
export const SELECT_LOGIN_TO_CONTINUE = localize('sql.migration.select.database.to.continue', "Please select 1 or more logins for migration");
export const ENTER_AAD_DOMAIN_NAME = localize('sql.login.migration.enter.AAD.domain.name.to.continue', "Microsoft Entra ID Domain name is required to migrate Windows login. Please enter an AAD Domain Name or deselect windows login(s).");
export const SELECT_LOGIN_TO_CONTINUE = localize('sql.migration.select.login.to.continue', "Select one or more logins to run validation.");
export const SELECT_LOGIN_AND_RUN_VALIDATION_TO_CONTINUE = localize('sql.migration.select.login.run.validation.to.continue', "To perform login migration, please run validation for one or more logins.");
export const ENTER_ENTRA_ID = localize('sql.login.migration.enter.entra.id.to.continue', "Microsoft Entra Domain is required to migrate Windows login. Please enter Microsoft Entra Domain name or deselect windows login(s)");
export const LOGIN_MIGRATE_BUTTON_TEXT = localize('sql.migration.start.login.migration.button', "Migrate");
export function LOGIN_MIGRATIONS_GET_CONNECTION_STRING(dataSource: string, id: string, pass: string): string {
return localize('sql.login.migration.get.connection.string', "data source={0};initial catalog=master;user id={1};password={2};TrustServerCertificate=True;Integrated Security=false;", dataSource, id, pass);
@ -509,8 +510,8 @@ export const LOGINS_NOT_FOUND = localize('sql.login.migration.logins.not.found',
export const LOGIN_MIGRATION_STATUS_SUCCEEDED = localize('sql.login.migration.status.succeeded', "Succeeded");
export const LOGIN_MIGRATION_STATUS_FAILED = localize('sql.login.migration.status.failed', "Failed");
export const LOGIN_MIGRATION_STATUS_IN_PROGRESS = localize('sql.login.migration.status.in.progress', "In progress");
export const LOGIN_MIGRATIONS_AAD_DOMAIN_NAME_INPUT_BOX_LABEL = localize('sql.login.migration.aad.domain.name.input.box.label', "Microsoft Entra ID Domain Name (only required to migrate Windows Authenication Logins)");
export const LOGIN_MIGRATIONS_AAD_DOMAIN_NAME_INPUT_BOX_PLACEHOLDER = localize('sql.login.migration.aad.domain.name.input.box.placeholder', "Enter AAD Domain Name");
export const LOGIN_MIGRATIONS_ENTRA_ID_INPUT_BOX_LABEL = localize('sql.login.migration.entra.id.input.box.label', "Microsoft Entra Domain Name (only required to migrate Windows Authentication Logins)");
export const LOGIN_MIGRATIONS_ENTRA_ID_INPUT_BOX_PLACEHOLDER = localize('sql.login.migration.entra.id.input.box.placeholder', "Enter Microsoft Entra Domain name");
export function LOGIN_MIGRATIONS_LOGIN_STATUS_DETAILS_TITLE(loginName: string): string {
return localize('sql.login.migration.login.status.details.title', "Migration status details for {0}", loginName);
}
@ -823,6 +824,7 @@ export const TABLE_SELECTION_HASROWS_COLUMN = localize('sql.migration.table.sele
export const VALIDATION_DIALOG_TITLE = localize('sql.migration.validation.dialog.title', "Running validation");
export const VALIDATION_MESSAGE_SUCCESS = localize('sql.migration.validation.success', "Validation completed successfully. Please click Next to proceed with the migration.");
export const LOGIN_MIGRATION_VALIDATION_MESSAGE_SUCCESS = localize('login.migration.validation.success', "Validation completed successfully. Please click migrate to proceed with the migration.");
export function VALIDATION_MESSAGE_CANCELED_ERRORS(msg: string): string {
return localize(
'sql.migration.validation.canceled.errors',
@ -1016,6 +1018,12 @@ export const VALIDATE_IR_COLUMN_VALIDATION_STEPS = localize('sql.migration.valid
export const VALIDATE_IR_COLUMN_STATUS = localize('sql.migration.validate.ir.column.status', "Status");
export const VALIDATE_IR_VALIDATION_RESULT_LABEL_SHIR = localize('sql.migration.validate.ir.validation.result.label.shir', "Integration runtime connectivity");
export const VALIDATE_IR_VALIDATION_RESULT_LABEL_STORAGE = localize('sql.migration.validate.ir.validation.result.label.storage', "Azure storage connectivity");
export const VALIDATE_LOGIN_MIGRATION_VALIDATION_RESULT_LABEL_SYSADMIN = localize('sql.migration.validate.login.migration.validation.result.label.sysadmin',
"Validating the sysadmin permission on source and target");
export const VALIDATE_LOGIN_MIGRATION_VALIDATION_RESULT_LABEL_ENTRAID = localize('sql.migration.validate.login.migration.validation.result.label.entraid',
"Validating the microsoft entra id");
export const VALIDATE_LOGIN_MIGRATION_VALIDATION_RESULT_LABEL_USERMAPPING = localize('sql.migration.validate.login.migration.validation.result.label.user.mapping',
"Validating the user mapping");
export function VALIDATE_IR_VALIDATION_RESULT_LABEL_SOURCE_DATABASE(databaseName: string): string {
return localize(
@ -1079,6 +1087,113 @@ export function VALIDATION_IR_BUTTON_MISSING_ERROR_MESSAGE(details: string[]): s
missingDetails);
}
// Validate Login migration validation dialog
export const VALIDATE_LOGIN_MIGRATION_DONE_BUTTON = localize('sql.migration.validate.login.migration.done.button', "Done");
export const VALIDATE_LOGIN_MIGRATION_HEADING = localize('sql.migration.validate.login.migration.heading', "We are validating the following:");
export const VALIDATE_LOGIN_MIGRATION_START_VALIDATION = localize('sql.migration.validate.login.migration.start.validation', "Start validation");
export const VALIDATE_LOGIN_MIGRATION_UNSUCCESSFUL_REVALIDATION = localize('sql.migration.validate.login.migration.unsuccessful.revalidation', "Revalidate unsuccessful steps");
export const VALIDATE_LOGIN_MIGRATION_STOP_VALIDATION = localize('sql.migration.validate.login.migration.stop.validation', "Stop validation");
export const VALIDATE_LOGIN_MIGRATION_COPY_RESULTS = localize('sql.migration.validate.login.migration.copy.results', "Copy validation results");
export const VALIDATE_LOGIN_MIGRATION_RESULTS_HEADING = localize('sql.migration.validate.login.migration.results.heading', "Validation step details");
export const VALIDATE_LOGIN_MIGRATION_VALIDATION_COMPLETED = localize('sql.migration.validate.login.migration.validation.completed', "Validation completed successfully.");
export const VALIDATE_LOGIN_MIGRATION_VALIDATION_CANCELED = localize('sql.migration.validate.login.migration.validation.camceled', "Validation check canceled");
export function VALIDATE_LOGIN_MIGRATION_VALIDATION_COMPLETED_ERRORS(msg: string): string {
return localize(
'sql.migration.validate.login.migration.completed.errors',
"Validation completed with the following error(s):{0}{1}", EOL, msg);
}
export function VALIDATE_LOGIN_MIGRATION_VALIDATION_STATUS(state: string | undefined, errors?: string[]): string {
const status = state ?? '';
if (errors && errors.length > 0) {
return localize(
'sql.migration.validate.login.migration.status.errors',
"Validation status: {0}{1}{2}", status, EOL, errors.join(EOL));
} else {
return localize(
'sql.migration.validate.login.migration.status',
"Validation status: {0}", status);
}
}
export const VALIDATE_LOGIN_MIGRATION_ERROR_GATEWAY_TIMEOUT = localize('sql.migration.validate.error.gatewaytimeout', "A time-out was encountered while validating a resource connection. Learn more: https://aka.ms/dms-migrations-troubleshooting.");
export function VALIDATE_LOGIN_MIGRATION_VALIDATION_STATUS_ERROR_COUNT(state: string | undefined, errorCount: number): string {
const status = state ?? '';
return errorCount > 1
? localize(
'sql.migration.validate.login.migration.status.error.count.many',
"{0} - {1} errors",
status,
errorCount)
: localize(
'sql.migration.validate.login.migration.status.error.count.one',
"{0} - 1 error",
status);
}
export function VALIDATE_LOGIN_MIGRATION_VALIDATION_STATUS_ERROR(state: string | undefined, errors: string[]): string {
const status = state ?? '';
return localize(
'sql.migration.validate.login.migration.status.error',
"{0}{1}{2}",
status,
EOL,
errors.join(EOL));
}
export function VALIDATE_LOGIN_MIGRATION_SYSADMIN_PERMISSION_VALIDATION_RESULT_ERROR(serverName: string, error: any,): string {
return localize(
'sql.migration.validate.ir.sqldb.validation.result.error',
"Sys Admin Permission Pre Validation check error{0}server: {1}{0}Error: {2} - {3}{0}Follow the steps mentioned here https://aka.ms/loginvalidationerror and re-run the validation before performing login/s migration.",
EOL,
serverName,
error.ErrorCodeString,
error.Message);
}
export function VALIDATE_LOGIN_MIGRATION_ENTRA_ID_VALIDATION_RESULT_ERROR(entraID: string, error: any,): string {
return localize(
'sql.migration.validate.ir.sqldb.validation.result.error',
"Entra ID Pre Validation check error{0}Entra ID: {1}{0}Error: {2} - {3}{0}Follow the steps mentioned here https://aka.ms/loginvalidationerror and re-run the validation or uncheck the failed logins before performing login/s migration.",
EOL,
entraID,
error.ErrorCodeString,
error.Message);
}
export function VALIDATE_LOGIN_MIGRATION_USER_MAPPING_VALIDATION_RESULT_ERROR(name: string, error: any,): string {
return localize(
'sql.migration.validate.ir.sqldb.validation.result.error',
"User Mapping Pre Validation check error{0}Name: {1}{0}Error: {2} - {3}{0}Follow the steps mentioned here https://aka.ms/loginvalidationerror and re-run the validation or uncheck the failed logins before performing login/s migration.",
EOL,
name,
error.ErrorCodeString,
error.Message);
}
export function GET_LOGIN_MIGRATION_VALIDATION_ERROR(validationFunctionName: any, name: string, error: any): any {
switch (validationFunctionName) {
case 'validateUserMapping':
return VALIDATE_LOGIN_MIGRATION_USER_MAPPING_VALIDATION_RESULT_ERROR(name, error);
case 'validateAADDomainName':
return VALIDATE_LOGIN_MIGRATION_ENTRA_ID_VALIDATION_RESULT_ERROR(name, error)
case 'validateSysAdminPermission':
return VALIDATE_LOGIN_MIGRATION_SYSADMIN_PERMISSION_VALIDATION_RESULT_ERROR(name, error)
default:
return '';
}
}
export function VALIDATE_LOGIN_MIGRATION_VALIDATION_RESULT_API_ERROR(error: Error): string {
return localize(
'sql.migration.validate.login.migration.validation.result.api.error',
"Validation check error{0}Error: {1} - {2}",
EOL,
error.name,
error.message);
}
// common strings
export const WARNING = localize('sql.migration.warning', "Warning");
export const ERROR = localize('sql.migration.error', "Error");

Просмотреть файл

@ -0,0 +1,610 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. 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 { IconPathHelper } from '../../constants/iconPathHelper';
import { LoginMigrationValidationResult, MigrationStateModel, ValidateLoginMigrationValidationState } from '../../models/stateMachine';
import { getSourceConnectionString, getTargetConnectionString } from '../../api/sqlUtils';
import { logError, TelemetryViews } from '../../telemetry';
import { EOL } from 'os';
const DialogName = 'LoginPreMigrationValidationDialog';
enum HttpStatusCodes {
GatewayTimeout = "504\r\n",
}
enum HttpStatusExceptionCodes {
ConnectionTimeoutError = "ConnectionTimeoutError",
}
enum ValidationResultIndex {
message = 0,
icon = 1,
status = 2,
errors = 3,
state = 4,
}
export const ValidationStatusLookup: constants.LookupTable<string | undefined> = {
[ValidateLoginMigrationValidationState.Canceled]: constants.VALIDATION_STATE_CANCELED,
[ValidateLoginMigrationValidationState.Failed]: constants.VALIDATION_STATE_FAILED,
[ValidateLoginMigrationValidationState.Pending]: constants.VALIDATION_STATE_PENDING,
[ValidateLoginMigrationValidationState.Running]: constants.VALIDATION_STATE_RUNNING,
[ValidateLoginMigrationValidationState.Succeeded]: constants.VALIDATION_STATE_SUCCEEDED,
default: undefined
};
export class LoginPreMigrationValidationDialog {
private _canceled: boolean = true;
private _dialog: azdata.window.Dialog | undefined;
private _isOpen: boolean = false;
private _model!: MigrationStateModel;
private _resultsTable!: azdata.TableComponent;
private _startLoader!: azdata.LoadingComponent;
private _startButton!: azdata.ButtonComponent;
private _revalidationButton!: azdata.ButtonComponent;
private _cancelButton!: azdata.ButtonComponent;
private _copyButton!: azdata.ButtonComponent;
private _validationResult: any[][] = [];
private _disposables: vscode.Disposable[] = [];
private _valdiationErrors: string[] = [];
private _onClosed: () => void;
constructor(
model: MigrationStateModel,
onClosed: () => void) {
this._model = model;
this._onClosed = onClosed;
}
public async openDialog(dialogTitle: string, results?: LoginMigrationValidationResult[]): 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_LOGIN_MIGRATION_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_LOGIN_MIGRATION_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_LOGIN_MIGRATION_START_VALIDATION,
}).component();
this._cancelButton = view.modelBuilder.button()
.withProps({
iconPath: IconPathHelper.stop,
iconHeight: 18,
iconWidth: 18,
width: 100,
label: constants.VALIDATE_LOGIN_MIGRATION_STOP_VALIDATION,
enabled: false,
}).component();
this._revalidationButton = view.modelBuilder.button()
.withProps({
iconPath: IconPathHelper.redo,
iconHeight: 18,
iconWidth: 18,
width: 170,
label: constants.VALIDATE_LOGIN_MIGRATION_UNSUCCESSFUL_REVALIDATION,
enabled: false,
}).component();
this._copyButton = view.modelBuilder.button()
.withProps({
iconPath: IconPathHelper.copy,
iconHeight: 18,
iconWidth: 18,
width: 140,
label: constants.VALIDATE_LOGIN_MIGRATION_COPY_RESULTS,
enabled: false,
}).component();
this._disposables.push(
this._startButton.onDidClick(
async (e) => await this._runValidation()));
this._disposables.push(
this._revalidationButton.onDidClick(
async (e) => await this._runUnsuccessfulRevalidation()));
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._revalidationButton },
{ component: this._copyButton }])
.component();
const resultsHeading = view.modelBuilder.text()
.withProps({
value: constants.VALIDATE_LOGIN_MIGRATION_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 _copyValidationResults(): any {
const errorsText = this._valdiationErrors.join(EOL);
const msg = errorsText.length === 0
? constants.VALIDATE_LOGIN_MIGRATION_VALIDATION_COMPLETED
: constants.VALIDATE_LOGIN_MIGRATION_VALIDATION_COMPLETED_ERRORS(errorsText);
return vscode.env.clipboard.writeText(msg);
}
private async _runUnsuccessfulRevalidation(): Promise<any> {
try {
this._startLoader.loading = true;
this._startButton.enabled = false;
this._revalidationButton.enabled = false;
this._cancelButton.enabled = true;
this._copyButton.enabled = false;
this._dialog!.okButton.enabled = false;
this._dialog!.cancelButton.enabled = true;
if (!this._model.isLoginMigrationTargetValidated) {
await this._revalidate();
}
} finally {
this._startLoader.loading = false;
this._startButton.enabled = true;
this._revalidationButton.enabled = !this._model.isLoginMigrationTargetValidated;
this._cancelButton.enabled = false;
this._copyButton.enabled = true;
this._dialog!.okButton.enabled = this._model.isLoginMigrationTargetValidated;
this._dialog!.cancelButton.enabled = !this._model.isLoginMigrationTargetValidated;
}
}
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_LOGIN_MIGRATION_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: "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: "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 _saveResults(): void {
const results = this._validationResults();
this._model._validateLoginMigration = results;
}
private _validationResults(): LoginMigrationValidationResult[] {
return this._validationResult.map(result => {
const state = result[ValidationResultIndex.state];
const finalState = this._canceled
? (state === ValidateLoginMigrationValidationState.Running || state === ValidateLoginMigrationValidationState.Pending)
? ValidateLoginMigrationValidationState.Canceled
: state
: state;
const errors = result[ValidationResultIndex.errors] ?? [];
return {
errors: errors,
state: finalState,
};
});
}
private async _runValidation(results?: LoginMigrationValidationResult[]): Promise<void> {
try {
this._startLoader.loading = true;
this._startButton.enabled = false;
this._revalidationButton.enabled = false;
this._cancelButton.enabled = true;
this._copyButton.enabled = false;
this._dialog!.okButton.enabled = false;
this._dialog!.cancelButton.enabled = true;
await this._validate();
} finally {
this._startLoader.loading = false;
this._startButton.enabled = true;
this._revalidationButton.enabled = !this._model.isLoginMigrationTargetValidated;
this._cancelButton.enabled = false;
this._copyButton.enabled = true;
this._dialog!.okButton.enabled = this._model.isLoginMigrationTargetValidated;
this._dialog!.cancelButton.enabled = !this._model.isLoginMigrationTargetValidated;
}
}
private async _validate(): Promise<void> {
this._canceled = false;
await this._initializeResults();
await this._validateLoginMigration();
this._saveResults();
}
private async _revalidate(): Promise<void> {
await this._initLoginMigrationResultsForRevalidation();
await this._validateLoginMigration(true);
this._saveResults();
}
private _formatError(error: Error): Error {
if (error?.message?.startsWith(HttpStatusCodes.GatewayTimeout)) {
return {
name: HttpStatusExceptionCodes.ConnectionTimeoutError,
message: constants.VALIDATE_LOGIN_MIGRATION_ERROR_GATEWAY_TIMEOUT,
};
}
return error;
}
private async _validateLoginMigration(skipSuccessfulSteps: boolean = false): Promise<void> {
let testNumber: number = 0;
const validate = async (
validationFunction: any,
loginList: string[],
aadDomainName: string,
): Promise<boolean> => {
try {
await this._updateValidateLoginMigrationResults(testNumber, ValidateLoginMigrationValidationState.Running);
const sourceConnectionString: string = await getSourceConnectionString();
const targetConnectionString: string = await getTargetConnectionString(
this._model.targetServerName,
this._model._targetServerInstance.id,
this._model._targetUserName,
this._model._targetPassword,
this._model._targetPort,
// for login migration, connect to target Azure SQL with true/true
// to-do: take as input from the user, should be true/false for DB/MI but true/true for VM
true /* encryptConnection */,
true /* trustServerCertificate */);
const validationFunctionName = validationFunction.name.replace('bound ', '');
var validationResult = await validationFunction(
sourceConnectionString,
targetConnectionString,
loginList,
aadDomainName);
if (validationResult !== undefined) {
if (Object.keys(validationResult.exceptionMap).length > 0) {
var errors: any[] = []
Object.keys(validationResult.exceptionMap).forEach(name => {
const error = validationResult?.exceptionMap[name][0]
errors.push(constants.GET_LOGIN_MIGRATION_VALIDATION_ERROR(
validationFunctionName,
name,
error))
});
await this._updateValidateLoginMigrationResults(testNumber, ValidateLoginMigrationValidationState.Failed, errors);
} else {
await this._updateValidateLoginMigrationResults(testNumber, ValidateLoginMigrationValidationState.Succeeded);
return true;
}
}
} catch (error) {
const err = this._formatError(error);
logError(TelemetryViews.LoginMigrationPreValdationDialog, err.message, error);
await this._updateValidateLoginMigrationResults(
testNumber,
ValidateLoginMigrationValidationState.Failed,
[constants.VALIDATE_LOGIN_MIGRATION_VALIDATION_RESULT_API_ERROR(err)]);
}
return false;
};
// validate sys admin permissions
if (!skipSuccessfulSteps || this._validationResult[testNumber][ValidationResultIndex.state] !== ValidateLoginMigrationValidationState.Succeeded) {
var loginList: string[] = [];
var aadDomainName = "";
if (!await validate(this._model.migrationService.validateSysAdminPermission.bind(this._model.migrationService), loginList, aadDomainName)) {
this._canceled = true;
await this._updateValidateLoginMigrationResults(testNumber + 1, ValidateLoginMigrationValidationState.Canceled, [constants.VALIDATE_LOGIN_MIGRATION_VALIDATION_CANCELED]);
return;
}
}
testNumber++;
if (this._canceled) {
await this._updateValidateLoginMigrationResults(testNumber, ValidateLoginMigrationValidationState.Canceled, [constants.VALIDATE_LOGIN_MIGRATION_VALIDATION_CANCELED]);
}
// validate Entra ID
else if (!skipSuccessfulSteps || this._validationResult[testNumber][ValidationResultIndex.state] !== ValidateLoginMigrationValidationState.Succeeded) {
var loginList: string[] = this._model._loginMigrationModel.loginsForMigration.map(row => row.loginName);
var aadDomainName = this._model._aadDomainName;
if (!await validate(this._model.migrationService.validateAADDomainName.bind(this._model.migrationService), loginList, aadDomainName)) {
this._canceled = true;
await this._updateValidateLoginMigrationResults(testNumber + 1, ValidateLoginMigrationValidationState.Canceled, [constants.VALIDATE_LOGIN_MIGRATION_VALIDATION_CANCELED]);
return;
}
}
testNumber++;
if (this._canceled) {
await this._updateValidateLoginMigrationResults(testNumber, ValidateLoginMigrationValidationState.Canceled, [constants.VALIDATE_LOGIN_MIGRATION_VALIDATION_CANCELED]);
}
// validate user mapping
else if (!skipSuccessfulSteps || this._validationResult[testNumber][ValidationResultIndex.state] !== ValidateLoginMigrationValidationState.Succeeded) {
var loginList: string[] = this._model._loginMigrationModel.loginsForMigration.map(row => row.loginName);
var aadDomainName = this._model._aadDomainName;
if (!await validate(this._model.migrationService.validateUserMapping.bind(this._model.migrationService), loginList, aadDomainName)) {
this._canceled = true;
await this._updateValidateLoginMigrationResults(testNumber + 1, ValidateLoginMigrationValidationState.Canceled, [constants.VALIDATE_LOGIN_MIGRATION_VALIDATION_CANCELED]);
return;
}
}
}
private async _updateValidateLoginMigrationResults(row: number, state: ValidateLoginMigrationValidationState, errors: string[] = [], updateTable: boolean = true): Promise<void> {
if (state === ValidateLoginMigrationValidationState.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: ValidateLoginMigrationValidationState, errors: string[] = []): Promise<void> {
const result = this._validationResult[row];
const status = ValidationStatusLookup[state];
const statusMsg = state === ValidateLoginMigrationValidationState.Failed && errors.length > 0
? constants.VALIDATE_LOGIN_MIGRATION_VALIDATION_STATUS_ERROR_COUNT(status, errors.length)
: status;
const statusMessage = errors.length > 0
? constants.VALIDATE_LOGIN_MIGRATION_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: ValidateLoginMigrationValidationState): azdata.IconPath {
switch (state) {
case ValidateLoginMigrationValidationState.Canceled:
return IconPathHelper.cancel;
case ValidateLoginMigrationValidationState.Failed:
return IconPathHelper.error;
case ValidateLoginMigrationValidationState.Running:
return IconPathHelper.inProgressMigration;
case ValidateLoginMigrationValidationState.Succeeded:
return IconPathHelper.completedMigration;
case ValidateLoginMigrationValidationState.Pending:
default:
return IconPathHelper.notStartedMigration;
}
}
private async _initializeResults(results?: LoginMigrationValidationResult[]): Promise<void> {
this._valdiationErrors = [];
this._validationResult = [];
this._addValidationResult(constants.VALIDATE_LOGIN_MIGRATION_VALIDATION_RESULT_LABEL_SYSADMIN);
this._addValidationResult(constants.VALIDATE_LOGIN_MIGRATION_VALIDATION_RESULT_LABEL_ENTRAID);
this._addValidationResult(constants.VALIDATE_LOGIN_MIGRATION_VALIDATION_RESULT_LABEL_USERMAPPING);
if (results && results.length > 0) {
for (let row = 0; row < results.length; row++) {
await this._updateValidateLoginMigrationResults(
row,
results[row].state,
results[row].errors,
false);
}
}
const data = this._validationResult.map(row => [
row[ValidationResultIndex.message],
row[ValidationResultIndex.icon],
"Pending"]);
await this._resultsTable.updateProperty('data', data);
}
private async _initLoginMigrationResultsForRevalidation(): Promise<void> {
this._valdiationErrors = [];
this._canceled = false;
let testNumber: number = 0;
this._validationResult.forEach(async element => {
if (element[ValidationResultIndex.state] !== ValidateLoginMigrationValidationState.Succeeded) {
await this._updateValidateLoginMigrationResults(testNumber++, ValidateLoginMigrationValidationState.Pending);
}
else {
testNumber++;
}
});
}
private _addValidationResult(message: string): void {
this._validationResult.push([
message,
<azdata.IconColumnCellValue>{
icon: IconPathHelper.notStartedMigration,
title: ValidationStatusLookup[ValidateLoginMigrationValidationState.Pending],
},
ValidationStatusLookup[ValidateLoginMigrationValidationState.Pending],
[],
ValidateLoginMigrationValidationState.Pending]);
}
}

Просмотреть файл

@ -29,11 +29,24 @@ export enum ValidateIrState {
Canceled = 'Canceled',
}
export enum ValidateLoginMigrationValidationState {
Pending = 'Pending',
Running = 'Running',
Succeeded = 'Succeeded',
Failed = 'Failed',
Canceled = 'Canceled',
}
export interface ValidationResult {
errors: string[];
state: ValidateIrState;
}
export interface LoginMigrationValidationResult {
errors: string[];
state: ValidateLoginMigrationValidationState;
}
export enum State {
INIT,
COLLECTING_SOURCE_INFO,
@ -238,6 +251,7 @@ export class MigrationStateModel implements Model, vscode.Disposable {
public _validateIrSqlDb: ValidationResult[] = [];
public _validateIrSqlMi: ValidationResult[] = [];
public _validateIrSqlVm: ValidationResult[] = [];
public _validateLoginMigration: LoginMigrationValidationResult[] = [];
public _skuRecommendationResults!: SkuRecommendation;
public _skuRecommendationPerformanceDataSource!: PerformanceDataSourceOptions;
@ -358,6 +372,14 @@ export class MigrationStateModel implements Model, vscode.Disposable {
r.state === ValidateIrState.Succeeded)
}
public get isLoginMigrationTargetValidated(): boolean {
const results = this._validateLoginMigration ?? [];
return results.length > 1
&& results.every(r =>
r.errors.length === 0 &&
r.state === ValidateLoginMigrationValidationState.Succeeded)
}
public get migrationTargetServerName(): string {
switch (this._targetType) {
case MigrationTargetType.SQLMI:

Просмотреть файл

@ -43,6 +43,7 @@ export enum TelemetryViews {
LoginMigrationTargetSelectionPage = 'LoginMigrationTargetSelectionPage',
LoginMigrationSelectorPage = 'LoginMigrationSelectorPage',
LoginMigrationStatusPage = 'LoginMigrationStatusPage',
LoginMigrationPreValdationDialog = 'LoginMigrationPreValdationDialog',
TdeConfigurationDialog = 'TdeConfigurationDialog',
TdeMigrationDialog = 'TdeMigrationDialog',
ValidIrDialog = 'validIrDialog',

Просмотреть файл

@ -6,7 +6,7 @@
import * as azdata from 'azdata';
import * as vscode from 'vscode';
import { MigrationWizardPage } from '../models/migrationWizardPage';
import { MigrationStateModel, StateChangeEvent } from '../models/stateMachine';
import { LoginMigrationValidationResult, MigrationStateModel, StateChangeEvent, ValidateLoginMigrationValidationState } from '../models/stateMachine';
import * as constants from '../constants/strings';
import { debounce, getLoginStatusImage, getLoginStatusMessage, getSourceLogins } from '../api/utils';
import * as styles from '../constants/styles';
@ -17,6 +17,10 @@ import { getTelemetryProps, logError, sendSqlMigrationActionEvent, TelemetryActi
import { CollectingSourceLoginsFailed, CollectingTargetLoginsFailed } from '../models/loginMigrationModel';
import { WizardController } from './wizardController';
import { Tab } from 'azdata';
import { LoginPreMigrationValidationDialog } from '../dialog/loginMigration/loginPreMigrationValidationDialog';
import { EOL } from 'os';
const VALIDATE_LOGIN_MIGRATION_CUSTOM_BUTTON_INDEX = 0;
export class LoginSelectorPage extends MigrationWizardPage {
private _view!: azdata.ModelView;
@ -57,6 +61,10 @@ export class LoginSelectorPage extends MigrationWizardPage {
}).component();
flex.addItem(await this.createRootContainer(view), { flex: '1 1 auto' });
this._disposables.push(
this.wizard.customButtons[VALIDATE_LOGIN_MIGRATION_CUSTOM_BUTTON_INDEX].onClick(
async e => await this._validateLoginPreMigration()));
this._disposables.push(this._view.onClosed(e => {
this._disposables.forEach(
d => { try { d.dispose(); } catch { } });
@ -71,8 +79,11 @@ export class LoginSelectorPage extends MigrationWizardPage {
constants.WIZARD_CANCEL_REASON_NEED_TO_REVIEW_LOGIN_SELECTION
]);
this.wizard.customButtons[VALIDATE_LOGIN_MIGRATION_CUSTOM_BUTTON_INDEX].hidden = false;
this._isCurrentPage = true;
this.updateNextButton();
this.wizard.registerNavigationValidator((pageChangeInfo) => {
this.wizard.message = {
text: '',
@ -83,9 +94,9 @@ export class LoginSelectorPage extends MigrationWizardPage {
return true;
}
if (this.selectedLogins().length === 0) {
if (this.selectedLogins().length === 0 || !this.migrationStateModel.isLoginMigrationTargetValidated) {
this.wizard.message = {
text: constants.SELECT_LOGIN_TO_CONTINUE,
text: constants.SELECT_LOGIN_AND_RUN_VALIDATION_TO_CONTINUE,
level: azdata.window.MessageLevel.Error
};
return false;
@ -93,7 +104,7 @@ export class LoginSelectorPage extends MigrationWizardPage {
if (this.migrationStateModel._loginMigrationModel.selectedWindowsLogins && !this.migrationStateModel._aadDomainName) {
this.wizard.message = {
text: constants.ENTER_AAD_DOMAIN_NAME,
text: constants.ENTER_ENTRA_ID,
level: azdata.window.MessageLevel.Error
};
return false;
@ -110,6 +121,8 @@ export class LoginSelectorPage extends MigrationWizardPage {
}
public async onPageLeave(): Promise<void> {
this.wizard.customButtons[VALIDATE_LOGIN_MIGRATION_CUSTOM_BUTTON_INDEX].hidden = true;
this.wizard.registerNavigationValidator((pageChangeInfo) => {
return true;
});
@ -151,7 +164,7 @@ export class LoginSelectorPage extends MigrationWizardPage {
// target user name
const aadDomainNameLabel = this._view.modelBuilder.text()
.withProps({
value: constants.LOGIN_MIGRATIONS_AAD_DOMAIN_NAME_INPUT_BOX_LABEL,
value: constants.LOGIN_MIGRATIONS_ENTRA_ID_INPUT_BOX_LABEL,
requiredIndicator: false,
CSSStyles: { ...styles.LABEL_CSS }
}).component();
@ -160,7 +173,7 @@ export class LoginSelectorPage extends MigrationWizardPage {
.withProps({
width: '300px',
inputType: 'text',
placeHolder: constants.LOGIN_MIGRATIONS_AAD_DOMAIN_NAME_INPUT_BOX_PLACEHOLDER,
placeHolder: constants.LOGIN_MIGRATIONS_ENTRA_ID_INPUT_BOX_PLACEHOLDER,
required: false,
}).component();
@ -680,6 +693,56 @@ export class LoginSelectorPage extends MigrationWizardPage {
await this._loginSelectorTable.updateProperty("height", selectedWindowsLogins ? 600 : 650);
}
public updateValidationResultUI(initializing?: boolean): void {
const succeeded = this.migrationStateModel.isLoginMigrationTargetValidated;
if (succeeded) {
this.wizard.message = {
level: azdata.window.MessageLevel.Information,
text: constants.LOGIN_MIGRATION_VALIDATION_MESSAGE_SUCCESS,
};
} else {
const results = this.migrationStateModel._validateLoginMigration;
const hasResults = results.length > 0;
if (initializing && !hasResults) {
return;
}
const canceled = results.some(result => result.state === ValidateLoginMigrationValidationState.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_LOGIN_MIGRATION_VALIDATION_COMPLETED_ERRORS(errorsMessage)
: constants.VALIDATION_MESSAGE_CANCELED
: constants.VALIDATION_MESSAGE_NOT_RUN;
this.wizard.message = {
level: azdata.window.MessageLevel.Error,
text: msg,
};
}
}
private async _validateLoginPreMigration(): Promise<void> {
if (this.migrationStateModel?._loginMigrationModel.loginsForMigration?.length <= 0) {
this.wizard.message = {
text: constants.SELECT_LOGIN_TO_CONTINUE,
level: azdata.window.MessageLevel.Error
};
return;
}
const dialog = new LoginPreMigrationValidationDialog(
this.migrationStateModel,
() => this.updateValidationResultUI());
let results: LoginMigrationValidationResult[] = [];
results = this.migrationStateModel._validateLoginMigration;
await dialog.openDialog(constants.VALIDATION_DIALOG_TITLE, results);
}
private async updateValuesOnSelection() {
const selectedLogins = this.selectedLogins() || [];
await this._loginCount.updateProperties({
@ -688,7 +751,6 @@ export class LoginSelectorPage extends MigrationWizardPage {
this._loginSelectorTable.data?.length || 0)
});
this.migrationStateModel._loginMigrationModel.loginsForMigration = selectedLogins;
this.migrationStateModel._loginMigrationModel.loginsForMigration = selectedLogins;
await this.refreshAADInputBox();
this.updateNextButton();

Просмотреть файл

@ -218,11 +218,17 @@ export class WizardController {
this._wizardObject.nextButton.secondary = false;
this._wizardObject.cancelButton.hidden = true;
const validateButton = azdata.window.createButton(
loc.RUN_VALIDATION,
'left');
validateButton.secondary = false;
validateButton.hidden = true;
const customCancelButton = azdata.window.createButton(
loc.CANCEL,
'right');
customCancelButton.secondary = true;
this._wizardObject.customButtons = [customCancelButton];
this._wizardObject.customButtons = [validateButton, customCancelButton];
const targetSelectionPage = new LoginMigrationTargetSelectionPage(this._wizardObject, stateModel, this);