mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-02-05 09:35:39 -05:00
[SQL-Migration] Login Migration Improvements (#21694)
This PR adds various login migration improvements: - Enabled windows login by prompting user for AAD domain name if a windows login is selected image - Adds new login details dialog which gives granular status on each step of the login migration for each login image - Checks if windows login migration is supported for selected target type, and only collections source windows logins accordingly - Perf optimization by source and target login in background of step 1 in order to significantly speed up loading of page 2
This commit is contained in:
235
extensions/sql-migration/src/models/loginMigrationModel.ts
Normal file
235
extensions/sql-migration/src/models/loginMigrationModel.ts
Normal file
@@ -0,0 +1,235 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as mssql from 'mssql';
|
||||
import { MultiStepResult, MultiStepState } from '../dialog/generic/multiStepStatusDialog';
|
||||
import * as constants from '../constants/strings';
|
||||
import { LoginTableInfo } from '../api/sqlUtils';
|
||||
|
||||
type ExceptionMap = { [login: string]: any }
|
||||
|
||||
export enum LoginType {
|
||||
Windows_Login = 'windows_login',
|
||||
SQL_Login = 'sql_login',
|
||||
}
|
||||
|
||||
export enum LoginMigrationStep {
|
||||
NotStarted = -1,
|
||||
MigrateLogins = 0,
|
||||
EstablishUserMapping = 1,
|
||||
MigrateServerRolesAndSetPermissions = 2,
|
||||
MigrationCompleted = 3,
|
||||
}
|
||||
|
||||
export function GetLoginMigrationStepString(step: LoginMigrationStep): string {
|
||||
switch (step) {
|
||||
case LoginMigrationStep.NotStarted:
|
||||
return constants.NOT_STARTED;
|
||||
case LoginMigrationStep.MigrateLogins:
|
||||
return constants.MIGRATE_LOGINS;
|
||||
case LoginMigrationStep.EstablishUserMapping:
|
||||
return constants.ESTABLISH_USER_MAPPINGS;
|
||||
case LoginMigrationStep.MigrateServerRolesAndSetPermissions:
|
||||
return constants.MIGRATE_SERVER_ROLES_AND_SET_PERMISSIONS;
|
||||
case LoginMigrationStep.MigrationCompleted:
|
||||
return constants.LOGIN_MIGRATION_COMPLETED;
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
export interface LoginMigrationStepState {
|
||||
loginName: string;
|
||||
stepName: LoginMigrationStep;
|
||||
status: MultiStepState;
|
||||
errors: string[];
|
||||
}
|
||||
|
||||
export interface Login {
|
||||
loginName: string;
|
||||
overallStatus: MultiStepState;
|
||||
statusPerStep: Map<LoginMigrationStep, LoginMigrationStepState>;
|
||||
}
|
||||
|
||||
export class LoginMigrationModel {
|
||||
public resultsPerStep: Map<mssql.LoginMigrationStep, mssql.StartLoginMigrationResult>;
|
||||
public collectedSourceLogins: boolean = false;
|
||||
public collectedTargetLogins: boolean = false;;
|
||||
public loginsOnSource: LoginTableInfo[] = [];
|
||||
public loginsOnTarget: string[] = [];
|
||||
private _currentStepIdx: number = 0;;
|
||||
private _logins: Map<string, Login>;
|
||||
private _loginMigrationSteps: LoginMigrationStep[] = [];
|
||||
|
||||
constructor() {
|
||||
this.resultsPerStep = new Map<mssql.LoginMigrationStep, mssql.StartLoginMigrationResult>();
|
||||
this._logins = new Map<string, Login>();
|
||||
this.SetLoginMigrationSteps();
|
||||
}
|
||||
|
||||
public get currentStep(): LoginMigrationStep {
|
||||
return this._currentStepIdx >= this._loginMigrationSteps.length ? LoginMigrationStep.MigrationCompleted : this._loginMigrationSteps[this._currentStepIdx];
|
||||
}
|
||||
|
||||
public get isMigrationComplete(): boolean {
|
||||
return this._currentStepIdx === this._loginMigrationSteps.length;
|
||||
}
|
||||
|
||||
public AddLoginMigrationResults(step: LoginMigrationStep, newResult: mssql.StartLoginMigrationResult): void {
|
||||
const exceptionMap = this._getExceptionMapWithNormalizedKeys(newResult.exceptionMap);
|
||||
this._currentStepIdx = this._loginMigrationSteps.findIndex(s => s === step) + 1;
|
||||
|
||||
for (const loginName of this._logins.keys()) {
|
||||
const status = loginName in exceptionMap ? MultiStepState.Failed : MultiStepState.Succeeded;
|
||||
const errors = loginName in exceptionMap ? this._extractErrors(exceptionMap, loginName) : [];
|
||||
this._addStepStateForLogin(loginName, step, status, errors);
|
||||
|
||||
if (this.isMigrationComplete) {
|
||||
const loginStatus = this._didAnyStepFail(loginName) ? MultiStepState.Failed : MultiStepState.Succeeded;
|
||||
this._markLoginStatus(loginName, loginStatus);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public ReportException(step: LoginMigrationStep, error: any): void {
|
||||
this._currentStepIdx = this._loginMigrationSteps.findIndex(s => s === step) + 1;
|
||||
|
||||
for (const loginName of this._logins.keys()) {
|
||||
// Mark current step as failed with the error message and mark remaining messages as canceled
|
||||
let errors = [error.message];
|
||||
this._addStepStateForLogin(loginName, step, MultiStepState.Failed, errors);
|
||||
this._markRemainingSteps(loginName, MultiStepState.Canceled);
|
||||
this._markLoginStatus(loginName, MultiStepState.Failed);
|
||||
}
|
||||
|
||||
this._markMigrationComplete();
|
||||
}
|
||||
|
||||
public GetLoginMigrationResults(loginName: string): MultiStepResult[] {
|
||||
let loginResults: MultiStepResult[] = [];
|
||||
let login = this._getLogin(loginName);
|
||||
|
||||
for (const step of this._loginMigrationSteps) {
|
||||
// The default steps and state will be added if no steps have completed
|
||||
let stepResult: MultiStepResult = {
|
||||
stepName: GetLoginMigrationStepString(step),
|
||||
state: MultiStepState.Pending,
|
||||
errors: [],
|
||||
}
|
||||
|
||||
// If the step has completed, then the login will have the stored status
|
||||
if (login?.statusPerStep.has(step)) {
|
||||
let stepStatus = login!.statusPerStep.get(step);
|
||||
stepResult.state = stepStatus!.status;
|
||||
stepResult.errors = stepStatus!.errors;
|
||||
} else if (step === this.currentStep) {
|
||||
stepResult.state = MultiStepState.Running;
|
||||
}
|
||||
|
||||
loginResults.push(stepResult);
|
||||
}
|
||||
|
||||
return loginResults;
|
||||
}
|
||||
|
||||
public AddNewLogins(logins: string[]) {
|
||||
logins.forEach(login => this._addNewLogin(login));
|
||||
}
|
||||
|
||||
public SetLoginMigrationSteps(steps: LoginMigrationStep[] = []) {
|
||||
this._loginMigrationSteps = [];
|
||||
|
||||
if (steps.length === 0) {
|
||||
this._loginMigrationSteps.push(LoginMigrationStep.MigrateLogins);
|
||||
this._loginMigrationSteps.push(LoginMigrationStep.EstablishUserMapping);
|
||||
this._loginMigrationSteps.push(LoginMigrationStep.MigrateServerRolesAndSetPermissions);
|
||||
} else {
|
||||
this._loginMigrationSteps = steps;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private _getLogin(loginName: string) {
|
||||
return this._logins.get(loginName.toLocaleLowerCase());
|
||||
}
|
||||
|
||||
private _addNewLogin(loginName: string, status: MultiStepState = MultiStepState.Pending) {
|
||||
let newLogin: Login = {
|
||||
loginName: loginName,
|
||||
overallStatus: status,
|
||||
statusPerStep: new Map<LoginMigrationStep, LoginMigrationStepState>(),
|
||||
}
|
||||
|
||||
this._logins.set(loginName.toLocaleLowerCase(), newLogin);
|
||||
}
|
||||
|
||||
private _addStepStateForLogin(loginName: string, step: LoginMigrationStep, stepStatus: MultiStepState, errors: string[] = []) {
|
||||
const loginExist = this._logins.has(loginName);
|
||||
|
||||
if (!loginExist) {
|
||||
this._addNewLogin(loginName, MultiStepState.Running);
|
||||
}
|
||||
|
||||
const login = this._getLogin(loginName);
|
||||
|
||||
if (login) {
|
||||
login.overallStatus = MultiStepState.Running;
|
||||
login.statusPerStep.set(
|
||||
step,
|
||||
{
|
||||
loginName: loginName,
|
||||
stepName: step,
|
||||
status: stepStatus,
|
||||
errors: errors
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private _markLoginStatus(loginName: string, status: MultiStepState) {
|
||||
const loginExist = this._logins.has(loginName);
|
||||
|
||||
if (!loginExist) {
|
||||
this._addNewLogin(loginName, MultiStepState.Running);
|
||||
}
|
||||
|
||||
let login = this._getLogin(loginName);
|
||||
|
||||
if (login) {
|
||||
login.overallStatus = status;
|
||||
}
|
||||
}
|
||||
|
||||
private _didAnyStepFail(loginName: string) {
|
||||
const login = this._getLogin(loginName);
|
||||
if (login) {
|
||||
return Object.values(login.statusPerStep).every(status => status === MultiStepState.Failed);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private _getExceptionMapWithNormalizedKeys(exceptionMap: ExceptionMap): ExceptionMap {
|
||||
return Object.keys(exceptionMap).reduce((result: ExceptionMap, key: string) => {
|
||||
result[key.toLocaleLowerCase()] = exceptionMap[key];
|
||||
return result;
|
||||
}, {});
|
||||
}
|
||||
|
||||
private _extractErrors(exceptionMap: ExceptionMap, loginName: string): string[] {
|
||||
return exceptionMap[loginName].map((exception: any) => typeof exception.InnerException !== 'undefined'
|
||||
&& exception.InnerException !== null ? exception.InnerException.Message : exception.Message);
|
||||
}
|
||||
|
||||
private _markMigrationComplete() {
|
||||
this._currentStepIdx = this._loginMigrationSteps.length;
|
||||
}
|
||||
|
||||
private _markRemainingSteps(loginName: string, status: MultiStepState) {
|
||||
for (let i = this._currentStepIdx; i < this._loginMigrationSteps.length; i++) {
|
||||
this._addStepStateForLogin(loginName, this._loginMigrationSteps[i], status, []);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,7 @@ import { sendSqlMigrationActionEvent, TelemetryAction, TelemetryViews, logError
|
||||
import { hashString, deepClone } from '../api/utils';
|
||||
import { SKURecommendationPage } from '../wizard/skuRecommendationPage';
|
||||
import { excludeDatabases, getConnectionProfile, LoginTableInfo, SourceDatabaseInfo, TargetDatabaseInfo } from '../api/sqlUtils';
|
||||
import { LoginMigrationModel, LoginMigrationStep } from './loginMigrationModel';
|
||||
const localize = nls.loadMessageBundle();
|
||||
|
||||
export enum ValidateIrState {
|
||||
@@ -249,6 +250,7 @@ export class MigrationStateModel implements Model, vscode.Disposable {
|
||||
public _aadDomainName!: string;
|
||||
public _loginMigrationsResult!: mssql.StartLoginMigrationResult;
|
||||
public _loginMigrationsError: any;
|
||||
public _loginMigrationModel: LoginMigrationModel;
|
||||
|
||||
public readonly _refreshGetSkuRecommendationIntervalInMinutes = 10;
|
||||
public readonly _performanceDataQueryIntervalInSeconds = 30;
|
||||
@@ -303,6 +305,7 @@ export class MigrationStateModel implements Model, vscode.Disposable {
|
||||
this._skuTargetPercentile = 95;
|
||||
this._skuEnablePreview = false;
|
||||
this._skuEnableElastic = false;
|
||||
this._loginMigrationModel = new LoginMigrationModel();
|
||||
}
|
||||
|
||||
public get validationTargetResults(): ValidationResult[] {
|
||||
@@ -559,23 +562,22 @@ export class MigrationStateModel implements Model, vscode.Disposable {
|
||||
|
||||
public async migrateLogins(): Promise<Boolean> {
|
||||
try {
|
||||
this._loginMigrationModel.AddNewLogins(this._loginsForMigration.map(row => row.loginName));
|
||||
|
||||
const sourceConnectionString = await this.getSourceConnectionString();
|
||||
const targetConnectionString = await this.getTargetConnectionString();
|
||||
console.log('Starting Login Migration at: ', new Date());
|
||||
|
||||
console.time("migrateLogins")
|
||||
var response = (await this.migrationService.migrateLogins(
|
||||
sourceConnectionString,
|
||||
targetConnectionString,
|
||||
this._loginsForMigration.map(row => row.loginName),
|
||||
this._aadDomainName
|
||||
))!;
|
||||
console.timeEnd("migrateLogins")
|
||||
|
||||
this.updateLoginMigrationResults(response)
|
||||
this.updateLoginMigrationResults(response);
|
||||
this._loginMigrationModel.AddLoginMigrationResults(LoginMigrationStep.MigrateLogins, response);
|
||||
} catch (error) {
|
||||
console.log('Failed Login Migration at: ', new Date());
|
||||
logError(TelemetryViews.LoginMigrationWizard, 'StartLoginMigrationFailed', error);
|
||||
this._loginMigrationModel.ReportException(LoginMigrationStep.MigrateLogins, error);
|
||||
this._loginMigrationsError = error;
|
||||
return false;
|
||||
}
|
||||
@@ -589,19 +591,18 @@ export class MigrationStateModel implements Model, vscode.Disposable {
|
||||
const sourceConnectionString = await this.getSourceConnectionString();
|
||||
const targetConnectionString = await this.getTargetConnectionString();
|
||||
|
||||
console.time("establishUserMapping")
|
||||
var response = (await this.migrationService.establishUserMapping(
|
||||
sourceConnectionString,
|
||||
targetConnectionString,
|
||||
this._loginsForMigration.map(row => row.loginName),
|
||||
this._aadDomainName
|
||||
))!;
|
||||
console.timeEnd("establishUserMapping")
|
||||
|
||||
this.updateLoginMigrationResults(response)
|
||||
this.updateLoginMigrationResults(response);
|
||||
this._loginMigrationModel.AddLoginMigrationResults(LoginMigrationStep.EstablishUserMapping, response);
|
||||
} catch (error) {
|
||||
console.log('Failed Login Migration at: ', new Date());
|
||||
logError(TelemetryViews.LoginMigrationWizard, 'StartLoginMigrationFailed', error);
|
||||
this._loginMigrationModel.ReportException(LoginMigrationStep.MigrateLogins, error);
|
||||
this._loginMigrationsError = error;
|
||||
return false;
|
||||
}
|
||||
@@ -615,24 +616,19 @@ export class MigrationStateModel implements Model, vscode.Disposable {
|
||||
const sourceConnectionString = await this.getSourceConnectionString();
|
||||
const targetConnectionString = await this.getTargetConnectionString();
|
||||
|
||||
console.time("migrateServerRolesAndSetPermissions")
|
||||
var response = (await this.migrationService.migrateServerRolesAndSetPermissions(
|
||||
sourceConnectionString,
|
||||
targetConnectionString,
|
||||
this._loginsForMigration.map(row => row.loginName),
|
||||
this._aadDomainName
|
||||
))!;
|
||||
console.timeEnd("migrateServerRolesAndSetPermissions")
|
||||
|
||||
this.updateLoginMigrationResults(response)
|
||||
this.updateLoginMigrationResults(response);
|
||||
this._loginMigrationModel.AddLoginMigrationResults(LoginMigrationStep.MigrateServerRolesAndSetPermissions, response);
|
||||
|
||||
console.log('Ending Login Migration at: ', new Date());
|
||||
console.log('Login migration response: ', response);
|
||||
|
||||
console.log('AKMA DEBUG response: ', response);
|
||||
} catch (error) {
|
||||
console.log('Failed Login Migration at: ', new Date());
|
||||
logError(TelemetryViews.LoginMigrationWizard, 'StartLoginMigrationFailed', error);
|
||||
this._loginMigrationModel.ReportException(LoginMigrationStep.MigrateLogins, error);
|
||||
this._loginMigrationsError = error;
|
||||
return false;
|
||||
}
|
||||
@@ -1408,6 +1404,10 @@ export class MigrationStateModel implements Model, vscode.Disposable {
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
public get isWindowsAuthMigrationSupported(): boolean {
|
||||
return this._targetType === MigrationTargetType.SQLMI;
|
||||
}
|
||||
}
|
||||
|
||||
export interface ServerAssessment {
|
||||
|
||||
Reference in New Issue
Block a user