[SQL Migration] SKU recommendation improvements + SQL DB integration bug fixes (#20174)

* WIP - show error message for failed SKU recommendation

* WIP - run query to get correct instance name

* WIP - integrate elastic model recommendation

* Remove private endpoint restriction text

* Add feature switch for elastic recommendation

* Clean up

* Clean up

* Misc UI fixes

* Update package.json with updated azdata dependency

* Remove unused lines

* Fix broken next button

* Vbump extension to 1.0.6

* Update SQL DB card to show number of recommendations for correct model
This commit is contained in:
Raymond Truong
2022-08-26 14:06:33 -07:00
committed by GitHub
parent bd0c4cdb51
commit bfcbd60d24
12 changed files with 300 additions and 181 deletions

View File

@@ -70,7 +70,7 @@ export const SKU_RECOMMENDATION_ERROR = (serverName: string): string => {
return localize('sql.migration.wizard.sku.error', "An error occurred while generating SKU recommendations for the server '{0}'.", serverName);
};
export const SKU_RECOMMENDATION_NO_RECOMMENDATION = localize('sql.migration.wizard.sku.error.noRecommendation', 'No recommendation available');
export const SKU_RECOMMENDATION_NO_RECOMMENDATION_REASON = localize('sql.migration.wizard.sku.error.noRecommendation.reason', 'No SKU recommendations were generated, as there were no SKUs which could satisfy the performance characteristics of your source. Try selecting a different target platform, adjusting recommendation parameters, or selecting a different set of databases to assess.');
export const SKU_RECOMMENDATION_NO_RECOMMENDATION_REASON = localize('sql.migration.wizard.sku.error.noRecommendation.reason', 'No SKU recommendations were generated, as there were no SKUs which could satisfy the performance characteristics of your source. Try selecting a different target platform, adjusting recommendation parameters, selecting a different set of databases to assess, or changing the recommendation model.');
export const SKU_RECOMMENDATION_ASSESSMENT_ERROR = (serverName: string): string => {
return localize('sql.migration.wizard.sku.assessment.error', "An error occurred while assessing the server '{0}'.", serverName);
};
@@ -142,7 +142,7 @@ export const STOP_PERFORMANCE_COLLECTION = localize('sql.migration.sku.stop.perf
export const RESTART_PERFORMANCE_COLLECTION = localize('sql.migration.sku.restart.performance.collection', "Restart data collection");
export const AZURE_RECOMMENDATION_CARD_NOT_ENABLED = localize('sql.migration.sku.card.azureRecommendation.notEnabled', "Azure recommendation is not available. Click “Get Azure recommendation” button below");
export const AZURE_RECOMMENDATION_CARD_IN_PROGRESS = localize('sql.migration.sku.card.azureRecommendation.inProgress', "Azure recommendation will be displayed once data collection is complete.");
export const AZURE_RECOMMENDATION_STATUS_NOT_ENABLED = localize('sql.migration.sku.azureRecommendation.status.notEnabled', "Azure recommendation collects and analyzes performance data and then recommends an appropriate sized database in Azure for your workload.");
export const AZURE_RECOMMENDATION_STATUS_NOT_ENABLED = localize('sql.migration.sku.azureRecommendation.status.notEnabled', "Azure recommendation collects and analyzes performance data and then recommends an appropriate sized target in Azure for your workload.");
export const AZURE_RECOMMENDATION_STATUS_IN_PROGRESS = localize('sql.migration.sku.azureRecommendation.status.inProgress', "Data collection in progress. Generating initial recommendations...");
export const AZURE_RECOMMENDATION_STATUS_REFINING = localize('sql.migration.sku.azureRecommendation.status.refining', "Data collection still in progress. Refining existing recommendations...");
export const AZURE_RECOMMENDATION_STATUS_STOPPED = localize('sql.migration.sku.azureRecommendation.status.stopped', "Data collection for Azure recommendations has been stopped.");
@@ -156,7 +156,6 @@ export const AZURE_RECOMMENDATION_TOOLTIP_IN_PROGRESS = localize('sql.migration.
export const AZURE_RECOMMENDATION_START = localize('sql.migration.sku.azureRecommendation.start', "Start");
export const AZURE_RECOMMENDATION_START_POPUP = localize('sql.migration.sku.azureRecommendation.start.popup', "Starting performance data collection...");
export const AZURE_RECOMMENDATION_OPEN_EXISTING_POPUP = localize('sql.migration.sku.azureRecommendation.openExisting.popup', "Generating Azure recommendations using provided performance data...");
export const AZURE_RECOMMENDATION_STOP_POPUP = localize('sql.migration.sku.azureRecommendation.stop.popup', "Stopping performance data collection...");
export const AZURE_RECOMMENDATION_DESCRIPTION = localize('sql.migration.sku.azureRecommendation.description', "Azure recommendation requires performance data of SQL server instance to provide target recommendation. Enable performance data collection to receive the target recommendation for the databases you want to migrate. The longer this will be enabled the better the recommendation. You can disable performance data collection at any time.");
export const AZURE_RECOMMENDATION_DESCRIPTION2 = localize('sql.migration.sku.azureRecommendation.description2', "You can also choose to select this data from an existing folder, if you have already collected it previously.");
@@ -266,6 +265,8 @@ export const SCALE_FACTOR_TOOLTIP = localize('sql.migration.sku.parameters.scale
export const INVALID_SCALE_FACTOR = localize('sql.migration.sku.parameters.scale.factor.invalid', "Invalid scale factor. Enter a positive integer value.");
export const PERCENTAGE_UTILIZATION = localize('sql.migration.sku.parameters.percentage.utilization', "Percentage utilization");
export const PERCENTAGE_UTILIZATION_TOOLTIP = localize('sql.migration.sku.parameters.percentage.utilization.tooltip', "Percentile of data points to be used during aggregation of the performance data.");
export const ELASTIC_RECOMMENDATION_LABEL = localize('sql.migration.sku.parameters.enable.elastic', "Enable elastic recommendation");
export const ELASTIC_RECOMMENDATION_INFO = localize('sql.migration.sku.parameters.enable.elastic.info', "Elastic recommendation uses an alternate recommendation model which utilizes personalized price-performance profiling against existing on-cloud customers.");
export function PERCENTAGE(val: number): string {
return localize('sql.migration.sku.percentage', "{0}%", val);
}
@@ -424,7 +425,6 @@ export const DATABASE_BACKUP_NETWORK_SHARE_PASSWORD_LABEL = localize('sql.migrat
export const DATABASE_BACKUP_NETWORK_SHARE_PASSWORD_PLACEHOLDER = localize('sql.migration.network.share.password.placeholder', "Enter password.");
export const DATABASE_BACKUP_NETWORK_SHARE_AZURE_ACCOUNT_HEADER = localize('sql.migration.network.share.azure.header', "Storage account details");
export const DATABASE_BACKUP_NETWORK_SHARE_AZURE_ACCOUNT_HELP = localize('sql.migration.network.share.azure.help', "Provide the Azure Storage account where the backups will be uploaded to.");
export const DATABASE_BACKUP_PRIVATE_ENDPOINT_INFO_TEXT = localize('sql.migration.database.private.endpoint.info.text', "Ensure that the Azure Storage account does not use a private endpoint.");
export const DUPLICATE_NAME_ERROR = localize('sql.migration.unique.name', "Select a unique name for this target database");
export function DATABASE_ALREADY_EXISTS_MI(dbName: string, targetName: string): string {
return localize('sql.migration.database.already.exists', "Database '{0}' already exists on the target managed instance '{1}'.", dbName, targetName);

View File

@@ -83,17 +83,19 @@ export class AssessmentResultsDialog {
this._disposables.push(
this._saveButton.onClick(async () => {
const folder = await utils.promptUserForFolder();
const destinationFilePath = path.join(folder, AssessmentResultsDialog._assessmentReportName);
if (this.model._assessmentReportFilePath) {
fs.copyFile(this.model._assessmentReportFilePath, destinationFilePath, (err) => {
if (err) {
console.log(err);
} else {
void vscode.window.showInformationMessage(constants.SAVE_ASSESSMENT_REPORT_SUCCESS(destinationFilePath));
}
});
} else {
console.log('assessment report not found');
if (folder) {
const destinationFilePath = path.join(folder, AssessmentResultsDialog._assessmentReportName);
if (this.model._assessmentReportFilePath) {
fs.copyFile(this.model._assessmentReportFilePath, destinationFilePath, (err) => {
if (err) {
console.log(err);
} else {
void vscode.window.showInformationMessage(constants.SAVE_ASSESSMENT_REPORT_SUCCESS(destinationFilePath));
}
});
} else {
console.log('assessment report not found');
}
}
}));
this.dialog.customButtons = [this._saveButton];

View File

@@ -333,8 +333,6 @@ export class GetAzureRecommendationDialog {
const serverName = (await this.migrationStateModel.getSourceConnectionProfile()).serverName;
const errors: string[] = [];
try {
void vscode.window.showInformationMessage(constants.AZURE_RECOMMENDATION_OPEN_EXISTING_POPUP);
await this.skuRecommendationPage.startCardLoading();
await this.migrationStateModel.getSkuRecommendations();

View File

@@ -24,12 +24,11 @@ export class SkuEditParametersDialog {
private _scaleFactorInput!: azdata.InputBoxComponent;
private _targetPercentileDropdown!: azdata.DropDownComponent;
private _enablePreviewValue!: boolean;
private _enableElasticRecommendation!: boolean;
constructor(
public skuRecommendationPage: SKURecommendationPage,
public migrationStateModel: MigrationStateModel) {
this._enablePreviewValue = true;
}
private async initializeDialog(dialog: azdata.window.Dialog): Promise<void> {
@@ -182,6 +181,70 @@ export class SkuEditParametersDialog {
CSSStyles: { ...styles.BODY_CSS, }
}).component();
const enableElasticLabel = _view.modelBuilder.text().withProps({
value: constants.ELASTIC_RECOMMENDATION_LABEL,
width: WIZARD_INPUT_COMPONENT_WIDTH,
requiredIndicator: true,
CSSStyles: {
...styles.LABEL_CSS,
}
}).component();
const elasticButtonGroup = 'enableElasticRecommendations';
const enableElasticRadioButtonContainer = _view.modelBuilder.flexContainer()
.withProps({
CSSStyles: {
'flex-direction': 'row',
'width': 'fit-content',
'margin-top': '-1em',
'margin-bottom': '8px',
}
}).component();
const enableElasticButton = _view.modelBuilder.radioButton()
.withProps({
name: elasticButtonGroup,
label: constants.YES,
checked: this._enableElasticRecommendation,
CSSStyles: {
...styles.BODY_CSS,
'width': 'fit-content',
'margin': '0'
},
}).component();
this._disposables.push(enableElasticButton.onDidChangeCheckedState(async (e) => {
if (e) {
this._enableElasticRecommendation = true;
}
}));
const disableElasticButton = _view.modelBuilder.radioButton()
.withProps({
name: elasticButtonGroup,
label: constants.NO,
checked: !this._enableElasticRecommendation,
CSSStyles: {
...styles.BODY_CSS,
'width': 'fit-content',
'margin': '0 12px',
}
}).component();
this._disposables.push(disableElasticButton.onDidChangeCheckedState(async (e) => {
if (e) {
this._enableElasticRecommendation = false;
}
}));
enableElasticRadioButtonContainer.addItems([
enableElasticButton,
disableElasticButton
]);
const enableElasticInfoBox = _view.modelBuilder.infoBox()
.withProps({
text: constants.ELASTIC_RECOMMENDATION_INFO,
style: 'information',
CSSStyles: {
...styles.BODY_CSS,
}
}).component();
container.addItems([
description,
scaleFactorLabel,
@@ -191,6 +254,9 @@ export class SkuEditParametersDialog {
enablePreviewLabel,
enablePreviewRadioButtonContainer,
enablePreviewInfoBox,
enableElasticLabel,
enableElasticRadioButtonContainer,
enableElasticInfoBox,
]);
return container;
}
@@ -219,6 +285,7 @@ export class SkuEditParametersDialog {
this._scaleFactorInput.value = this.migrationStateModel._skuScalingFactor.toString();
this._enablePreviewValue = this.migrationStateModel._skuEnablePreview;
this._enableElasticRecommendation = this.migrationStateModel._skuEnableElastic;
(<azdata.CategoryValue[]>this._targetPercentileDropdown.values)?.forEach((percentile, index) => {
if ((<azdata.CategoryValue>percentile).name.toLowerCase() === this.migrationStateModel._skuTargetPercentile.toString()) {
selectDropDownIndex(this._targetPercentileDropdown, index);
@@ -232,6 +299,7 @@ export class SkuEditParametersDialog {
this.migrationStateModel._skuScalingFactor = Number(this._scaleFactorInput.value!);
this.migrationStateModel._skuTargetPercentile = Number((<azdata.CategoryValue>this._targetPercentileDropdown.value).name);
this.migrationStateModel._skuEnablePreview = this._enablePreviewValue;
this.migrationStateModel._skuEnableElastic = this._enableElasticRecommendation;
await this.skuRecommendationPage.refreshSkuParameters();
}

View File

@@ -20,6 +20,7 @@ export class SkuRecommendationResultsDialog {
private _isOpen: boolean = false;
private dialog: azdata.window.Dialog | undefined;
private migrationStateModel: MigrationStateModel;
// Dialog Name for Telemetry
public dialogName: string | undefined;
@@ -45,6 +46,7 @@ export class SkuRecommendationResultsDialog {
}
this.title = constants.RECOMMENDATIONS_TITLE(this.targetName);
this.migrationStateModel = model;
}
private async initializeDialog(dialog: azdata.window.Dialog): Promise<void> {
@@ -158,17 +160,24 @@ export class SkuRecommendationResultsDialog {
if (this._targetType === MigrationTargetType.SQLDB) {
const databaseNameLabel = _view.modelBuilder.text()
.withProps({
value: constants.SOURCE_DATABASE,
CSSStyles: { ...styles.LABEL_CSS, 'margin': '0', }
}).component();
const databaseNameValue = _view.modelBuilder.text()
.withProps({
value: recommendation.databaseName!,
CSSStyles: { ...styles.SECTION_HEADER_CSS, }
CSSStyles: { ...styles.BODY_CSS, 'margin': '0', }
}).component();
recommendationContainer.addItem(databaseNameLabel);
recommendationContainer.addItem(databaseNameValue);
}
const targetDeploymentTypeLabel = _view.modelBuilder.text()
.withProps({
value: constants.TARGET_DEPLOYMENT_TYPE,
CSSStyles: { ...styles.LABEL_CSS, 'margin': '0', }
CSSStyles: { ...styles.LABEL_CSS, 'margin': '12px 0 0', }
}).component();
const targetDeploymentTypeValue = _view.modelBuilder.text()
.withProps({
@@ -456,15 +465,25 @@ export class SkuRecommendationResultsDialog {
switch (this._targetType) {
case MigrationTargetType.SQLMI:
this.targetRecommendations = recommendations?.sqlMiRecommendationResults;
if (this.migrationStateModel._skuEnableElastic) {
this.targetRecommendations = recommendations?.elasticSqlMiRecommendationResults;
} else {
this.targetRecommendations = recommendations?.sqlMiRecommendationResults;
}
break;
case MigrationTargetType.SQLVM:
// elastic model currently doesn't support SQL VM, so show the baseline model results regardless of user preference
// this.targetRecommendations = recommendations?.elasticModelResults.sqlDbRecommendationResults;
this.targetRecommendations = recommendations?.sqlVmRecommendationResults;
break;
case MigrationTargetType.SQLDB:
this.targetRecommendations = recommendations?.sqlDbRecommendationResults;
if (this.migrationStateModel._skuEnableElastic) {
this.targetRecommendations = recommendations?.elasticSqlDbRecommendationResults;
} else {
this.targetRecommendations = recommendations?.sqlDbRecommendationResults;
}
break;
}
@@ -484,38 +503,39 @@ export class SkuRecommendationResultsDialog {
this._disposables.push(
this._saveButton.onClick(async () => {
const folder = await utils.promptUserForFolder();
if (folder) {
if (this.model._skuRecommendationReportFilePaths) {
if (this.model._skuRecommendationReportFilePaths) {
let sourceFilePath: string | undefined;
let destinationFilePath: string | undefined;
let sourceFilePath: string | undefined;
let destinationFilePath: string | undefined;
switch (this._targetType) {
case MigrationTargetType.SQLMI:
sourceFilePath = this.model._skuRecommendationReportFilePaths.find(filePath => filePath.includes('SkuRecommendationReport-AzureSqlManagedInstance'));
destinationFilePath = path.join(folder, 'SkuRecommendationReport-AzureSqlManagedInstance.html');
break;
switch (this._targetType) {
case MigrationTargetType.SQLMI:
sourceFilePath = this.model._skuRecommendationReportFilePaths.find(filePath => filePath.includes('SkuRecommendationReport-AzureSqlManagedInstance'));
destinationFilePath = path.join(folder, 'SkuRecommendationReport-AzureSqlManagedInstance.html');
break;
case MigrationTargetType.SQLVM:
sourceFilePath = this.model._skuRecommendationReportFilePaths.find(filePath => filePath.includes('SkuRecommendationReport-AzureSqlVirtualMachine'));
destinationFilePath = path.join(folder, 'SkuRecommendationReport-AzureSqlVirtualMachine.html');
break;
case MigrationTargetType.SQLVM:
sourceFilePath = this.model._skuRecommendationReportFilePaths.find(filePath => filePath.includes('SkuRecommendationReport-AzureSqlVirtualMachine'));
destinationFilePath = path.join(folder, 'SkuRecommendationReport-AzureSqlVirtualMachine.html');
break;
case MigrationTargetType.SQLDB:
sourceFilePath = this.model._skuRecommendationReportFilePaths.find(filePath => filePath.includes('SkuRecommendationReport-AzureSqlDatabase'));
destinationFilePath = path.join(folder, 'SkuRecommendationReport-AzureSqlDatabase.html');
break;
}
fs.copyFile(sourceFilePath!, destinationFilePath, (err) => {
if (err) {
console.log(err);
} else {
void vscode.window.showInformationMessage(constants.SAVE_RECOMMENDATION_REPORT_SUCCESS(destinationFilePath!));
case MigrationTargetType.SQLDB:
sourceFilePath = this.model._skuRecommendationReportFilePaths.find(filePath => filePath.includes('SkuRecommendationReport-AzureSqlDatabase'));
destinationFilePath = path.join(folder, 'SkuRecommendationReport-AzureSqlDatabase.html');
break;
}
});
} else {
console.log('recommendation report not found');
fs.copyFile(sourceFilePath!, destinationFilePath, (err) => {
if (err) {
console.log(err);
} else {
void vscode.window.showInformationMessage(constants.SAVE_RECOMMENDATION_REPORT_SUCCESS(destinationFilePath!));
}
});
} else {
console.log('recommendation report not found');
}
}
}));
this.dialog.customButtons = [this._saveButton];

View File

@@ -225,6 +225,7 @@ export class MigrationStateModel implements Model, vscode.Disposable {
public _perfDataCollectionErrors!: string[];
public _perfDataCollectionIsCollecting!: boolean;
public readonly _refreshGetSkuRecommendationIntervalInMinutes = 10;
public readonly _performanceDataQueryIntervalInSeconds = 30;
public readonly _staticDataQueryIntervalInSeconds = 60;
public readonly _numberOfPerformanceDataQueryIterations = 19;
@@ -233,11 +234,12 @@ export class MigrationStateModel implements Model, vscode.Disposable {
public readonly _recommendationTargetPlatforms = [MigrationTargetType.SQLDB, MigrationTargetType.SQLMI, MigrationTargetType.SQLVM];
public refreshPerfDataCollectionFrequency = this._performanceDataQueryIntervalInSeconds * 1000;
public refreshGetSkuRecommendationFrequency = constants.TIME_IN_MINUTES(10);
public refreshGetSkuRecommendationFrequency = constants.TIME_IN_MINUTES(this._refreshGetSkuRecommendationIntervalInMinutes);
public _skuScalingFactor!: number;
public _skuTargetPercentile!: number;
public _skuEnablePreview!: boolean;
public _skuEnableElastic!: boolean;
public refreshDatabaseBackupPage!: boolean;
public retryMigration!: boolean;
@@ -273,7 +275,8 @@ export class MigrationStateModel implements Model, vscode.Disposable {
this._skuScalingFactor = 100;
this._skuTargetPercentile = 95;
this._skuEnablePreview = true;
this._skuEnablePreview = false;
this._skuEnableElastic = false;
}
public get sourceConnectionId(): string {
@@ -371,15 +374,28 @@ export class MigrationStateModel implements Model, vscode.Disposable {
public async getSkuRecommendations(): Promise<SkuRecommendation> {
try {
const serverInfo = await azdata.connection.getServerInfo(this.sourceConnectionId);
const machineName = (<any>serverInfo)['machineName']; // contains the correct machine name but not necessarily the correct instance name
const instanceName = (await this.getSourceConnectionProfile()).serverName; // contains the correct instance name but not necessarily the correct machine name
let fullInstanceName: string;
if (instanceName.includes('\\')) {
fullInstanceName = machineName + '\\' + instanceName.substring(instanceName.indexOf('\\') + 1);
// execute a query against the source to get the correct instance name
const connectionProfile = await this.getSourceConnectionProfile();
const connectionUri = await azdata.connection.getUriForConnection(this._sourceConnectionId);
const queryProvider = azdata.dataprotocol.getProvider<azdata.QueryProvider>(connectionProfile.providerId, azdata.DataProviderType.QueryProvider);
const queryString = 'select @@servername;';
const queryResult = await queryProvider.runQueryAndReturn(connectionUri, queryString);
if (queryResult.rowCount > 0) {
fullInstanceName = queryResult.rows[0][0].displayValue;
} else {
fullInstanceName = machineName;
// get the instance name from connection info in case querying for the instance name doesn't work for whatever reason
const serverInfo = await azdata.connection.getServerInfo(this.sourceConnectionId);
const machineName = (<any>serverInfo)['machineName']; // contains the correct machine name but not necessarily the correct instance name
const instanceName = (await this.getSourceConnectionProfile()).serverName; // contains the correct instance name but not necessarily the correct machine name
if (instanceName.includes('\\')) {
fullInstanceName = machineName + '\\' + instanceName.substring(instanceName.indexOf('\\') + 1);
} else {
fullInstanceName = machineName;
}
}
const response = (await this.migrationService.getSkuRecommendations(
@@ -398,40 +414,18 @@ export class MigrationStateModel implements Model, vscode.Disposable {
// clone list of databases currently being assessed and store them, so that if the user ever changes the list we can refresh new recommendations
this._skuRecommendationRecommendedDatabaseList = this._databasesForAssessment.slice();
if (response?.sqlDbRecommendationResults || response?.sqlMiRecommendationResults || response?.sqlVmRecommendationResults) {
if (response) {
this._skuRecommendationResults = {
recommendations: {
sqlDbRecommendationResults: response?.sqlDbRecommendationResults ?? [],
sqlMiRecommendationResults: response?.sqlMiRecommendationResults ?? [],
sqlVmRecommendationResults: response?.sqlVmRecommendationResults ?? [],
instanceRequirements: response?.instanceRequirements,
skuRecommendationReportPaths: response?.skuRecommendationReportPaths
},
};
this._skuRecommendationReportFilePaths = response.skuRecommendationReportPaths;
} else {
this._skuRecommendationResults = {
recommendations: {
sqlDbRecommendationResults: [],
sqlMiRecommendationResults: [],
sqlVmRecommendationResults: [],
instanceRequirements: response?.instanceRequirements,
skuRecommendationReportPaths: response?.skuRecommendationReportPaths
},
recommendations: response
};
this._skuRecommendationReportFilePaths = this._skuEnableElastic ? response.elasticSkuRecommendationReportPaths : response.skuRecommendationReportPaths;
}
} catch (error) {
logError(TelemetryViews.SkuRecommendationWizard, 'GetSkuRecommendationFailed', error);
this._skuRecommendationResults = {
recommendations: {
sqlDbRecommendationResults: this._skuRecommendationApiResponse?.sqlDbRecommendationResults ?? [],
sqlMiRecommendationResults: this._skuRecommendationApiResponse?.sqlMiRecommendationResults ?? [],
sqlVmRecommendationResults: this._skuRecommendationApiResponse?.sqlVmRecommendationResults ?? [],
instanceRequirements: this._skuRecommendationApiResponse?.instanceRequirements,
skuRecommendationReportPaths: this._skuRecommendationApiResponse?.skuRecommendationReportPaths
},
recommendations: this._skuRecommendationApiResponse,
recommendationError: error
};
} // Generating all the telemetry asynchronously as we don't need to block the user for it.
@@ -442,42 +436,59 @@ export class MigrationStateModel implements Model, vscode.Disposable {
private async generateSkuRecommendationTelemetry(): Promise<void> {
try {
this._skuRecommendationResults?.recommendations?.sqlDbRecommendationResults
.map((e, i) => [e, this._skuRecommendationResults?.recommendations?.elasticSqlDbRecommendationResults[i]])
.forEach(resultPair => {
// Send telemetry for recommended DB SKUs
sendSqlMigrationActionEvent(
TelemetryViews.SkuRecommendationWizard,
TelemetryAction.GetDBSkuRecommendation,
{
'sessionId': this._sessionId,
'recommendedSku': JSON.stringify(resultPair[0]?.targetSku),
'elasticRecommendedSku': JSON.stringify(resultPair[1]?.targetSku),
'recommendationDurationInMs': JSON.stringify(this._skuRecommendationResults?.recommendations?.sqlDbRecommendationDurationInMs),
'elasticRecommendationDurationInMs': JSON.stringify(this._skuRecommendationResults?.recommendations?.elasticSqlDbRecommendationDurationInMs),
},
{}
);
});
this._skuRecommendationResults?.recommendations?.sqlMiRecommendationResults?.forEach(resultItem => {
// Send telemetry for recommended MI SKU
sendSqlMigrationActionEvent(
TelemetryViews.SkuRecommendationWizard,
TelemetryAction.GetMISkuRecommendation,
{
'sessionId': this._sessionId,
'recommendedSku': JSON.stringify(resultItem?.targetSku)
},
{});
});
this._skuRecommendationResults?.recommendations?.sqlMiRecommendationResults
.map((e, i) => [e, this._skuRecommendationResults?.recommendations?.elasticSqlMiRecommendationResults[i]])
.forEach(resultPair => {
// Send telemetry for recommended MI SKUs
sendSqlMigrationActionEvent(
TelemetryViews.SkuRecommendationWizard,
TelemetryAction.GetMISkuRecommendation,
{
'sessionId': this._sessionId,
'recommendedSku': JSON.stringify(resultPair[0]?.targetSku),
'elasticRecommendedSku': JSON.stringify(resultPair[1]?.targetSku),
'recommendationDurationInMs': JSON.stringify(this._skuRecommendationResults?.recommendations?.sqlMiRecommendationDurationInMs),
'elasticRecommendationDurationInMs': JSON.stringify(this._skuRecommendationResults?.recommendations?.elasticSqlMiRecommendationDurationInMs),
},
{}
);
});
this._skuRecommendationResults?.recommendations?.sqlVmRecommendationResults?.forEach(resultItem => {
// Send telemetry for recommended VM SKU
sendSqlMigrationActionEvent(
TelemetryViews.SkuRecommendationWizard,
TelemetryAction.GetVMSkuRecommendation,
{
'sessionId': this._sessionId,
'recommendedSku': JSON.stringify(resultItem?.targetSku)
},
{});
});
this._skuRecommendationResults?.recommendations?.sqlDbRecommendationResults?.forEach(resultItem => {
// Send telemetry for recommended SQLDB SKU
sendSqlMigrationActionEvent(
TelemetryViews.SkuRecommendationWizard,
TelemetryAction.GetSqlDbSkuRecommendation,
{
'sessionId': this._sessionId,
'recommendedSku': JSON.stringify(resultItem?.targetSku)
},
{});
});
this._skuRecommendationResults?.recommendations?.sqlVmRecommendationResults
.map((e, i) => [e, this._skuRecommendationResults?.recommendations?.elasticSqlVmRecommendationResults[i]])
.forEach(resultPair => {
// Send telemetry for recommended VM SKUs
sendSqlMigrationActionEvent(
TelemetryViews.SkuRecommendationWizard,
TelemetryAction.GetVMSkuRecommendation,
{
'sessionId': this._sessionId,
'recommendedSku': JSON.stringify(resultPair[0]?.targetSku),
'elasticRecommendedSku': JSON.stringify(resultPair[1]?.targetSku),
'recommendationDurationInMs': JSON.stringify(this._skuRecommendationResults?.recommendations?.sqlVmRecommendationDurationInMs),
'elasticRecommendationDurationInMs': JSON.stringify(this._skuRecommendationResults?.recommendations?.elasticSqlVmRecommendationDurationInMs),
},
{}
);
});
// Send Instance requirements used for calculating recommendations
sendSqlMigrationActionEvent(
@@ -517,18 +528,19 @@ export class MigrationStateModel implements Model, vscode.Disposable {
}))
},
{
'cpuRequirementInCores': this._skuRecommendationResults?.recommendations?.instanceRequirements?.cpuRequirementInCores,
'dataStorageRequirementInMB': this._skuRecommendationResults?.recommendations?.instanceRequirements?.dataStorageRequirementInMB,
'logStorageRequirementInMB': this._skuRecommendationResults?.recommendations?.instanceRequirements?.logStorageRequirementInMB,
'memoryRequirementInMB': this._skuRecommendationResults?.recommendations?.instanceRequirements?.memoryRequirementInMB,
'dataIOPSRequirement': this._skuRecommendationResults?.recommendations?.instanceRequirements?.dataIOPSRequirement,
'logIOPSRequirement': this._skuRecommendationResults?.recommendations?.instanceRequirements?.logIOPSRequirement,
'ioLatencyRequirementInMs': this._skuRecommendationResults?.recommendations?.instanceRequirements?.ioLatencyRequirementInMs,
'ioThroughputRequirementInMBps': this._skuRecommendationResults?.recommendations?.instanceRequirements?.ioThroughputRequirementInMBps,
'tempDBSizeInMB': this._skuRecommendationResults?.recommendations?.instanceRequirements?.tempDBSizeInMB,
'aggregationTargetPercentile': this._skuRecommendationResults?.recommendations?.instanceRequirements?.aggregationTargetPercentile,
'numberOfDataPointsAnalyzed': this._skuRecommendationResults?.recommendations?.instanceRequirements?.numberOfDataPointsAnalyzed,
});
'cpuRequirementInCores': this._skuRecommendationResults?.recommendations?.instanceRequirements?.cpuRequirementInCores!,
'dataStorageRequirementInMB': this._skuRecommendationResults?.recommendations?.instanceRequirements?.dataStorageRequirementInMB!,
'logStorageRequirementInMB': this._skuRecommendationResults?.recommendations?.instanceRequirements?.logStorageRequirementInMB!,
'memoryRequirementInMB': this._skuRecommendationResults?.recommendations?.instanceRequirements?.memoryRequirementInMB!,
'dataIOPSRequirement': this._skuRecommendationResults?.recommendations?.instanceRequirements?.dataIOPSRequirement!,
'logIOPSRequirement': this._skuRecommendationResults?.recommendations?.instanceRequirements?.logIOPSRequirement!,
'ioLatencyRequirementInMs': this._skuRecommendationResults?.recommendations?.instanceRequirements?.ioLatencyRequirementInMs!,
'ioThroughputRequirementInMBps': this._skuRecommendationResults?.recommendations?.instanceRequirements?.ioThroughputRequirementInMBps!,
'tempDBSizeInMB': this._skuRecommendationResults?.recommendations?.instanceRequirements?.tempDBSizeInMB!,
'aggregationTargetPercentile': this._skuRecommendationResults?.recommendations?.instanceRequirements?.aggregationTargetPercentile!,
'numberOfDataPointsAnalyzed': this._skuRecommendationResults?.recommendations?.instanceRequirements?.numberOfDataPointsAnalyzed!,
}
);
} catch (e) {
logError(TelemetryViews.SkuRecommendationWizard, 'GetSkuRecommendationTelemetryFailed', e);
@@ -1191,6 +1203,6 @@ export interface ServerAssessment {
}
export interface SkuRecommendation {
recommendations: mssql.SkuRecommendationResult;
recommendations?: mssql.SkuRecommendationResult;
recommendationError?: Error;
}

View File

@@ -57,7 +57,7 @@ export enum TelemetryAction {
OnPageLeave = 'OnPageLeave',
GetMISkuRecommendation = 'GetMISkuRecommendation',
GetVMSkuRecommendation = 'GetVMSkuRecommendation',
GetSqlDbSkuRecommendation = 'GetSqlDbSkuRecommendation',
GetDBSkuRecommendation = 'GetDBSkuRecommendation',
GetInstanceRequirements = 'GetInstanceRequirements',
StartDataCollection = 'StartDataCollection',
StopDataCollection = 'StopDataCollection'

View File

@@ -413,14 +413,6 @@ export class DatabaseBackupPage extends MigrationWizardPage {
CSSStyles: { ...styles.SECTION_HEADER_CSS }
}).component();
const azureStoragePrivateEndpointInfoBox = this._view.modelBuilder.infoBox()
.withProps({
text: constants.DATABASE_BACKUP_PRIVATE_ENDPOINT_INFO_TEXT,
style: 'information',
width: WIZARD_INPUT_COMPONENT_WIDTH,
CSSStyles: { ...styles.BODY_CSS }
}).component();
this._networkShareTargetDatabaseNamesTable = this._view.modelBuilder.declarativeTable()
.withProps({
columns: [
@@ -523,7 +515,6 @@ export class DatabaseBackupPage extends MigrationWizardPage {
.withItems([
blobTableText,
allFieldsRequiredLabel,
azureStoragePrivateEndpointInfoBox,
this._blobContainerTargetDatabaseNamesTable])
.withProps({ CSSStyles: { 'display': 'none', } })
.component();
@@ -553,14 +544,6 @@ export class DatabaseBackupPage extends MigrationWizardPage {
CSSStyles: { ...styles.BODY_CSS, 'margin-bottom': '12px' }
}).component();
const azureStoragePrivateEndpointInfoBox = this._view.modelBuilder.infoBox()
.withProps({
text: constants.DATABASE_BACKUP_PRIVATE_ENDPOINT_INFO_TEXT,
style: 'information',
width: WIZARD_INPUT_COMPONENT_WIDTH,
CSSStyles: { ...styles.BODY_CSS }
}).component();
const subscriptionLabel = this._view.modelBuilder.text()
.withProps({
value: constants.SUBSCRIPTION,
@@ -673,7 +656,6 @@ export class DatabaseBackupPage extends MigrationWizardPage {
.withItems([
azureAccountHeader,
azureAccountHelpText,
azureStoragePrivateEndpointInfoBox,
subscriptionLabel,
this._networkShareContainerSubscription,
locationLabel,

View File

@@ -68,10 +68,12 @@ export class SKURecommendationPage extends MigrationWizardPage {
private _skuScaleFactorText!: azdata.TextComponent;
private _skuTargetPercentileText!: azdata.TextComponent;
private _skuEnablePreviewSkuText!: azdata.TextComponent;
private _skuEnableElasticRecommendationsText!: azdata.TextComponent;
private assessmentGroupContainer!: azdata.FlexContainer;
private _disposables: vscode.Disposable[] = [];
private _serverName: string = '';
private _supportedProducts: Product[] = [
{
type: MigrationTargetType.SQLMI,
@@ -357,11 +359,11 @@ export class SKURecommendationPage extends MigrationWizardPage {
CSSStyles: { 'margin': '12px 0' }
}).component();
const serverName = this.migrationStateModel.serverName || (await this.migrationStateModel.getSourceConnectionProfile()).serverName;
this._serverName = this.migrationStateModel.serverName || (await this.migrationStateModel.getSourceConnectionProfile()).serverName;
const miDialog = new AssessmentResultsDialog('ownerUri', this.migrationStateModel, constants.ASSESSMENT_TILE(serverName), this, MigrationTargetType.SQLMI);
const vmDialog = new AssessmentResultsDialog('ownerUri', this.migrationStateModel, constants.ASSESSMENT_TILE(serverName), this, MigrationTargetType.SQLVM);
const dbDialog = new AssessmentResultsDialog('ownerUri', this.migrationStateModel, constants.ASSESSMENT_TILE(serverName), this, MigrationTargetType.SQLDB);
const miDialog = new AssessmentResultsDialog('ownerUri', this.migrationStateModel, constants.ASSESSMENT_TILE(this._serverName), this, MigrationTargetType.SQLMI);
const vmDialog = new AssessmentResultsDialog('ownerUri', this.migrationStateModel, constants.ASSESSMENT_TILE(this._serverName), this, MigrationTargetType.SQLVM);
const dbDialog = new AssessmentResultsDialog('ownerUri', this.migrationStateModel, constants.ASSESSMENT_TILE(this._serverName), this, MigrationTargetType.SQLDB);
this._disposables.push(button.onDidClick(async (e) => {
switch (this._rbg.selectedCardId) {
@@ -437,8 +439,6 @@ export class SKURecommendationPage extends MigrationWizardPage {
level: azdata.window.MessageLevel.Error
};
const serverName = (await this.migrationStateModel.getSourceConnectionProfile()).serverName;
if (this.migrationStateModel._runAssessments) {
const errors: string[] = [];
await this._setAssessmentState(true, false);
@@ -453,22 +453,22 @@ export class SKURecommendationPage extends MigrationWizardPage {
e => `message: ${e.message}${EOL}errorSummary: ${e.errorSummary}${EOL}possibleCauses: ${e.possibleCauses}${EOL}guidance: ${e.guidance}${EOL}errorId: ${e.errorId}`)!);
}
} catch (e) {
errors.push(constants.SKU_RECOMMENDATION_ASSESSMENT_UNEXPECTED_ERROR(serverName, e));
errors.push(constants.SKU_RECOMMENDATION_ASSESSMENT_UNEXPECTED_ERROR(this._serverName, e));
logError(TelemetryViews.MigrationWizardSkuRecommendationPage, 'SkuRecommendationUnexpectedError', e);
} finally {
this.migrationStateModel._runAssessments = errors.length > 0;
if (errors.length > 0) {
this.wizard.message = {
text: constants.SKU_RECOMMENDATION_ASSESSMENT_ERROR(serverName),
text: constants.SKU_RECOMMENDATION_ASSESSMENT_ERROR(this._serverName),
description: errors.join(EOL),
level: azdata.window.MessageLevel.Error
};
this._assessmentStatusIcon.iconPath = IconPathHelper.error;
this._igComponent.value = constants.ASSESSMENT_FAILED(serverName);
this._detailsComponent.value = constants.SKU_RECOMMENDATION_ASSESSMENT_ERROR(serverName);
this._igComponent.value = constants.ASSESSMENT_FAILED(this._serverName);
this._detailsComponent.value = constants.SKU_RECOMMENDATION_ASSESSMENT_ERROR(this._serverName);
} else {
this._assessmentStatusIcon.iconPath = IconPathHelper.completedMigration;
this._igComponent.value = constants.ASSESSMENT_COMPLETED(serverName);
this._igComponent.value = constants.ASSESSMENT_COMPLETED(this._serverName);
this._detailsComponent.value = constants.SKU_RECOMMENDATION_ALL_SUCCESSFUL(
this.migrationStateModel._assessmentResults?.databaseAssessments?.length);
}
@@ -476,7 +476,7 @@ export class SKURecommendationPage extends MigrationWizardPage {
} else {
// use prior assessment results
this._assessmentStatusIcon.iconPath = IconPathHelper.completedMigration;
this._igComponent.value = constants.ASSESSMENT_COMPLETED(serverName);
this._igComponent.value = constants.ASSESSMENT_COMPLETED(this._serverName);
this._detailsComponent.value = constants.SKU_RECOMMENDATION_ALL_SUCCESSFUL(
this.migrationStateModel._assessmentResults?.databaseAssessments?.length);
}
@@ -529,20 +529,30 @@ export class SKURecommendationPage extends MigrationWizardPage {
}
private async _setAssessmentState(assessing: boolean, failedAssessment: boolean): Promise<void> {
await utils.updateControlDisplay(this._assessmentComponent, assessing);
await utils.updateControlDisplay(this._skipAssessmentCheckbox, !assessing && failedAssessment);
await utils.updateControlDisplay(
this._assessmentComponent,
assessing,
'block');
await utils.updateControlDisplay(
this._skipAssessmentCheckbox,
!assessing && failedAssessment,
'block');
await utils.updateControlDisplay(
this._skipAssessmentSubText,
!assessing && failedAssessment,
'block');
await utils.updateControlDisplay(this._formContainer.component(), !assessing);
await utils.updateControlDisplay(
this._formContainer.component(),
!assessing,
'block');
await utils.updateControlDisplay(
this._chooseTargetComponent,
!failedAssessment || this._skipAssessmentCheckbox.checked === true);
!failedAssessment || this._skipAssessmentCheckbox.checked === true,
'block');
await utils.updateControlDisplay(
this.assessmentGroupContainer,
this._rbg.selectedCardId !== undefined && (!failedAssessment || this._skipAssessmentCheckbox.checked === true));
this._rbg.selectedCardId !== undefined && (!failedAssessment || this._skipAssessmentCheckbox.checked === true),
'inline');
this._assessmentLoader.loading = assessing;
}
@@ -611,8 +621,6 @@ export class SKURecommendationPage extends MigrationWizardPage {
if (!this.migrationStateModel._assessmentResults) {
this._rbg.cards[index].descriptions[CardDescriptionIndex.ASSESSMENT_STATUS].textValue = '';
} else {
// TO-DO: add the assessed db counts
// this._rbg.cards[index].descriptions[5].textValue = constants.ASSESSED_DBS(dbCount);
if (this.hasRecommendations()) {
this._rbg.cards[index].descriptions[CardDescriptionIndex.VIEW_SKU_DETAILS].linkDisplayValue = constants.VIEW_DETAILS;
this._rbg.cards[index].descriptions[CardDescriptionIndex.SKU_RECOMMENDATION].textStyles = {
@@ -641,7 +649,11 @@ export class SKURecommendationPage extends MigrationWizardPage {
constants.CAN_BE_MIGRATED(dbWithoutIssuesCount, dbCount);
if (this.hasRecommendations()) {
recommendation = this.migrationStateModel._skuRecommendationResults.recommendations.sqlMiRecommendationResults[0];
if (this.migrationStateModel._skuEnableElastic) {
recommendation = this.migrationStateModel._skuRecommendationResults.recommendations?.elasticSqlMiRecommendationResults[0];
} else {
recommendation = this.migrationStateModel._skuRecommendationResults.recommendations?.sqlMiRecommendationResults[0];
}
// result returned but no SKU recommended
if (!recommendation?.targetSku) {
@@ -672,7 +684,8 @@ export class SKURecommendationPage extends MigrationWizardPage {
constants.CAN_BE_MIGRATED(dbCount, dbCount);
if (this.hasRecommendations()) {
recommendation = this.migrationStateModel._skuRecommendationResults.recommendations.sqlVmRecommendationResults[0];
// elastic model currently doesn't support SQL VM, so show the baseline model results regardless of user preference
recommendation = this.migrationStateModel._skuRecommendationResults.recommendations?.sqlVmRecommendationResults[0];
// result returned but no SKU recommended
if (!recommendation?.targetSku) {
@@ -708,9 +721,10 @@ export class SKURecommendationPage extends MigrationWizardPage {
constants.CAN_BE_MIGRATED(dbWithoutIssuesCount, dbCount);
if (this.hasRecommendations()) {
const successfulRecommendationsCount =
this.migrationStateModel._skuRecommendationResults.recommendations.sqlDbRecommendationResults
.filter(r => r.targetSku !== null).length;
const recommendations = this.migrationStateModel._skuEnableElastic
? this.migrationStateModel._skuRecommendationResults.recommendations!.elasticSqlDbRecommendationResults
: this.migrationStateModel._skuRecommendationResults.recommendations!.sqlDbRecommendationResults;
const successfulRecommendationsCount = recommendations.filter(r => r.targetSku !== null).length;
this._rbg.cards[index].descriptions[CardDescriptionIndex.SKU_RECOMMENDATION].textValue =
constants.RECOMMENDATIONS_AVAILABLE(successfulRecommendationsCount);
}
@@ -768,7 +782,7 @@ export class SKURecommendationPage extends MigrationWizardPage {
private async createAssessmentInfo(): Promise<azdata.TextComponent> {
this._assessmentInfo = this._view.modelBuilder.text()
.withProps({
value: constants.ASSESSMENT_IN_PROGRESS_CONTENT((await this.migrationStateModel.getSourceConnectionProfile()).serverName),
value: constants.ASSESSMENT_IN_PROGRESS_CONTENT(this._serverName),
CSSStyles: {
...styles.BODY_CSS,
'width': '660px'
@@ -1070,6 +1084,9 @@ export class SKURecommendationPage extends MigrationWizardPage {
this.migrationStateModel._skuEnablePreview ? constants.YES : constants.NO);
this._skuEnablePreviewSkuText = skuEnablePreviewParameterGroup.text;
const skuEnableElasticRecommendationsParameterGroup = createParameterGroup(constants.ELASTIC_RECOMMENDATION_LABEL, this.migrationStateModel._skuEnableElastic ? constants.YES : constants.NO);
this._skuEnableElasticRecommendationsText = skuEnableElasticRecommendationsParameterGroup.text;
const parametersContainer = _view.modelBuilder.flexContainer()
.withProps({
CSSStyles: {
@@ -1082,6 +1099,7 @@ export class SKURecommendationPage extends MigrationWizardPage {
scaleFactorParameterGroup.flexContainer,
skuTargetPercentileParameterGroup.flexContainer,
skuEnablePreviewParameterGroup.flexContainer,
skuEnableElasticRecommendationsParameterGroup.flexContainer
]);
container.addItems([
@@ -1096,6 +1114,7 @@ export class SKURecommendationPage extends MigrationWizardPage {
this._skuScaleFactorText.value = this.migrationStateModel._skuScalingFactor.toString();
this._skuTargetPercentileText.value = constants.PERCENTAGE(this.migrationStateModel._skuTargetPercentile);
this._skuEnablePreviewSkuText.value = this.migrationStateModel._skuEnablePreview ? constants.YES : constants.NO;
this._skuEnableElasticRecommendationsText.value = this.migrationStateModel._skuEnableElastic ? constants.YES : constants.NO;
await this.refreshAzureRecommendation();
}
@@ -1103,6 +1122,16 @@ export class SKURecommendationPage extends MigrationWizardPage {
await this.startCardLoading();
this._skuLastRefreshTimeText.value = constants.LAST_REFRESHED_TIME();
await this.migrationStateModel.getSkuRecommendations();
const skuRecommendationError = this.migrationStateModel._skuRecommendationResults?.recommendationError;
if (skuRecommendationError) {
this.wizard.message = {
text: constants.SKU_RECOMMENDATION_ERROR(this._serverName),
description: skuRecommendationError.message,
level: azdata.window.MessageLevel.Error
};
}
await this.refreshSkuRecommendationComponents();
this._skuLastRefreshTimeText.value = constants.LAST_REFRESHED_TIME(new Date().toLocaleString());
}

View File

@@ -109,7 +109,6 @@ export class TargetSelectionPage extends MigrationWizardPage {
// and came back, forcibly reload the location/resource group/resource values since they will now be different
this._migrationTargetPlatform = this.migrationStateModel._targetType;
await this._azureResourceTable.setDataValues([]);
this._targetPasswordInputBox.value = '';
this.migrationStateModel._sqlMigrationServices = undefined!;
this.migrationStateModel._targetServerInstance = undefined!;
@@ -119,7 +118,6 @@ export class TargetSelectionPage extends MigrationWizardPage {
}
if (this.migrationStateModel._didUpdateDatabasesForMigration) {
await this._azureResourceTable.setDataValues([]);
this._initializeSourceTargetDatabaseMap();
this._updateConnectionButtonState();
}