mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-02-16 18:46:40 -05:00
[SQL Migration] Implement state validation for SQL VM targets (#21350)
* WIP * Implement POC * Add strings * Disable IR scenario and add info box for source < 2014 * Refactor * Case insensitive string compare * Remove unused strings
This commit is contained in:
@@ -213,6 +213,27 @@ export type SqlVMServer = {
|
|||||||
subscriptionId: string
|
subscriptionId: string
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type VirtualMachineInstanceView = {
|
||||||
|
computerName: string,
|
||||||
|
osName: string,
|
||||||
|
osVersion: string,
|
||||||
|
vmAgent: { [propertyName: string]: string; },
|
||||||
|
disks: { [propertyName: string]: string; }[],
|
||||||
|
bootDiagnostics: { [propertyName: string]: string; },
|
||||||
|
extensions: { [propertyName: string]: string; }[],
|
||||||
|
hyperVGeneration: string,
|
||||||
|
patchStatus: { [propertyName: string]: string; },
|
||||||
|
statuses: InstanceViewStatus[],
|
||||||
|
}
|
||||||
|
|
||||||
|
export type InstanceViewStatus = {
|
||||||
|
code: string,
|
||||||
|
displayStatus: string,
|
||||||
|
level: string,
|
||||||
|
message: string,
|
||||||
|
time: string,
|
||||||
|
}
|
||||||
|
|
||||||
export async function getAvailableSqlDatabaseServers(account: azdata.Account, subscription: Subscription): Promise<AzureSqlDatabaseServer[]> {
|
export async function getAvailableSqlDatabaseServers(account: azdata.Account, subscription: Subscription): Promise<AzureSqlDatabaseServer[]> {
|
||||||
const api = await getAzureCoreAPI();
|
const api = await getAzureCoreAPI();
|
||||||
const path = encodeURI(`/subscriptions/${subscription.id}/providers/Microsoft.Sql/servers?api-version=${SQL_SQLDB_API_VERSION}`);
|
const path = encodeURI(`/subscriptions/${subscription.id}/providers/Microsoft.Sql/servers?api-version=${SQL_SQLDB_API_VERSION}`);
|
||||||
@@ -259,6 +280,19 @@ export async function getAvailableSqlVMs(account: azdata.Account, subscription:
|
|||||||
return response.response.data.value;
|
return response.response.data.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getVMInstanceView(sqlVm: SqlVMServer, account: azdata.Account, subscription: Subscription): Promise<VirtualMachineInstanceView> {
|
||||||
|
const api = await getAzureCoreAPI();
|
||||||
|
const path = encodeURI(`/subscriptions/${subscription.id}/resourceGroups/${getResourceGroupFromId(sqlVm.id)}/providers/Microsoft.Compute/virtualMachines/${sqlVm.name}/instanceView?api-version=2022-08-01`);
|
||||||
|
const host = api.getProviderMetadataForAccount(account).settings.armResource?.endpoint;
|
||||||
|
const response = await api.makeAzureRestRequest(account, subscription, path, azurecore.HttpRequestMethod.GET, undefined, true, host);
|
||||||
|
|
||||||
|
if (response.errors.length > 0) {
|
||||||
|
throw new Error(response.errors.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.response.data;
|
||||||
|
}
|
||||||
|
|
||||||
export type StorageAccount = AzureProduct;
|
export type StorageAccount = AzureProduct;
|
||||||
export async function getAvailableStorageAccounts(account: azdata.Account, subscription: Subscription): Promise<StorageAccount[]> {
|
export async function getAvailableStorageAccounts(account: azdata.Account, subscription: Subscription): Promise<StorageAccount[]> {
|
||||||
const api = await getAzureCoreAPI();
|
const api = await getAzureCoreAPI();
|
||||||
|
|||||||
@@ -548,7 +548,7 @@ export async function getManagedInstancesDropdownValues(managedInstances: azureR
|
|||||||
managedInstances.forEach((managedInstance) => {
|
managedInstances.forEach((managedInstance) => {
|
||||||
if (managedInstance.location.toLowerCase() === location.name.toLowerCase() && managedInstance.resourceGroup?.toLowerCase() === resourceGroup.name.toLowerCase()) {
|
if (managedInstance.location.toLowerCase() === location.name.toLowerCase() && managedInstance.resourceGroup?.toLowerCase() === resourceGroup.name.toLowerCase()) {
|
||||||
let managedInstanceValue: CategoryValue;
|
let managedInstanceValue: CategoryValue;
|
||||||
if (managedInstance.properties.state === 'Ready') {
|
if (managedInstance.properties.state.toLowerCase() === 'Ready'.toLowerCase()) {
|
||||||
managedInstanceValue = {
|
managedInstanceValue = {
|
||||||
name: managedInstance.id,
|
name: managedInstance.id,
|
||||||
displayName: managedInstance.name
|
displayName: managedInstance.name
|
||||||
@@ -618,6 +618,53 @@ export async function getVirtualMachines(account?: Account, subscription?: azure
|
|||||||
return virtualMachines;
|
return virtualMachines;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getVirtualMachinesDropdownValues(virtualMachines: azure.SqlVMServer[], location: azureResource.AzureLocation, resourceGroup: azureResource.AzureResourceResourceGroup, account: Account, subscription: azureResource.AzureResourceSubscription): Promise<CategoryValue[]> {
|
||||||
|
let virtualMachinesValues: CategoryValue[] = [];
|
||||||
|
if (location && resourceGroup) {
|
||||||
|
for (const virtualMachine of virtualMachines) {
|
||||||
|
if (virtualMachine.location.toLowerCase() === location.name.toLowerCase() && azure.getResourceGroupFromId(virtualMachine.id).toLowerCase() === resourceGroup.name.toLowerCase()) {
|
||||||
|
let virtualMachineValue: CategoryValue;
|
||||||
|
|
||||||
|
// 1) check if VM is on by querying underlying compute resource's instance view
|
||||||
|
let vmInstanceView = await azure.getVMInstanceView(virtualMachine, account, subscription);
|
||||||
|
if (!vmInstanceView.statuses.some(status => status.code.toLowerCase() === 'PowerState/running'.toLowerCase())) {
|
||||||
|
virtualMachineValue = {
|
||||||
|
name: virtualMachine.id,
|
||||||
|
displayName: constants.UNAVAILABLE_TARGET_PREFIX(virtualMachine.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2) check for IaaS extension in Full mode
|
||||||
|
else if (virtualMachine.properties.sqlManagement.toLowerCase() !== 'Full'.toLowerCase()) {
|
||||||
|
virtualMachineValue = {
|
||||||
|
name: virtualMachine.id,
|
||||||
|
displayName: constants.UNAVAILABLE_TARGET_PREFIX(virtualMachine.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
else {
|
||||||
|
virtualMachineValue = {
|
||||||
|
name: virtualMachine.id,
|
||||||
|
displayName: virtualMachine.name
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
virtualMachinesValues.push(virtualMachineValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (virtualMachinesValues.length === 0) {
|
||||||
|
virtualMachinesValues = [
|
||||||
|
{
|
||||||
|
displayName: constants.NO_VIRTUAL_MACHINE_FOUND,
|
||||||
|
name: ''
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
return virtualMachinesValues;
|
||||||
|
}
|
||||||
|
|
||||||
export async function getStorageAccounts(account?: Account, subscription?: azureResource.AzureResourceSubscription): Promise<azure.StorageAccount[]> {
|
export async function getStorageAccounts(account?: Account, subscription?: azureResource.AzureResourceSubscription): Promise<azure.StorageAccount[]> {
|
||||||
let storageAccounts: azure.StorageAccount[] = [];
|
let storageAccounts: azure.StorageAccount[] = [];
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -488,7 +488,12 @@ export function ACCOUNT_ACCESS_ERROR(account: AzureAccount, error: Error) {
|
|||||||
export function MI_NOT_READY_ERROR(miName: string, state: string): string {
|
export function MI_NOT_READY_ERROR(miName: string, state: string): string {
|
||||||
return localize('sql.migration.mi.not.ready', "The managed instance '{0}' is unavailable for migration because it is currently in the '{1}' state. To continue, select an available managed instance.", miName, state);
|
return localize('sql.migration.mi.not.ready', "The managed instance '{0}' is unavailable for migration because it is currently in the '{1}' state. To continue, select an available managed instance.", miName, state);
|
||||||
}
|
}
|
||||||
|
export function VM_NOT_READY_IAAS_EXTENSION_ERROR(vmName: string, extensionState: string): string {
|
||||||
|
return localize('sql.migration.vm.not.ready.iaas.extension', "The virtual machine '{0}' is unavailable for migration because the SQL Server IaaS Agent extension is currently in '{1}' mode instead of Full mode. Learn more: https://aka.ms/sql-iaas-extension", vmName, extensionState);
|
||||||
|
}
|
||||||
|
export function VM_NOT_READY_POWER_STATE_ERROR(vmName: string): string {
|
||||||
|
return localize('sql.migration.vm.not.ready.power.state', "The virtual machine '{0}' is unavailable for migration because the underlying virtual machine is not running. Please make sure it is powered on before retrying.", vmName);
|
||||||
|
}
|
||||||
export function SQLDB_NOT_READY_ERROR(sqldbName: string, state: string): string {
|
export function SQLDB_NOT_READY_ERROR(sqldbName: string, state: string): string {
|
||||||
return localize('sql.migration.sqldb.not.ready', "The SQL database server '{0}' is unavailable for migration because it is currently in the '{1}' state. To continue, select an available SQL database server.", sqldbName, state);
|
return localize('sql.migration.sqldb.not.ready', "The SQL database server '{0}' is unavailable for migration because it is currently in the '{1}' state. To continue, select an available SQL database server.", sqldbName, state);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import * as azdata from 'azdata';
|
|||||||
import * as azurecore from 'azurecore';
|
import * as azurecore from 'azurecore';
|
||||||
import * as vscode from 'vscode';
|
import * as vscode from 'vscode';
|
||||||
import * as mssql from 'mssql';
|
import * as mssql from 'mssql';
|
||||||
import { SqlMigrationService, SqlManagedInstance, startDatabaseMigration, StartDatabaseMigrationRequest, StorageAccount, SqlVMServer, getLocationDisplayName, getSqlManagedInstanceDatabases, AzureSqlDatabaseServer, isSqlManagedInstance, isAzureSqlDatabaseServer } from '../api/azure';
|
import { SqlMigrationService, SqlManagedInstance, startDatabaseMigration, StartDatabaseMigrationRequest, StorageAccount, SqlVMServer, getLocationDisplayName, getSqlManagedInstanceDatabases, AzureSqlDatabaseServer, isSqlManagedInstance, isAzureSqlDatabaseServer, VirtualMachineInstanceView } from '../api/azure';
|
||||||
import * as constants from '../constants/strings';
|
import * as constants from '../constants/strings';
|
||||||
import * as nls from 'vscode-nls';
|
import * as nls from 'vscode-nls';
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
@@ -192,6 +192,7 @@ export class MigrationStateModel implements Model, vscode.Disposable {
|
|||||||
public _targetSqlVirtualMachines!: SqlVMServer[];
|
public _targetSqlVirtualMachines!: SqlVMServer[];
|
||||||
public _targetSqlDatabaseServers!: AzureSqlDatabaseServer[];
|
public _targetSqlDatabaseServers!: AzureSqlDatabaseServer[];
|
||||||
public _targetServerInstance!: SqlManagedInstance | SqlVMServer | AzureSqlDatabaseServer;
|
public _targetServerInstance!: SqlManagedInstance | SqlVMServer | AzureSqlDatabaseServer;
|
||||||
|
public _vmInstanceView!: VirtualMachineInstanceView;
|
||||||
public _databaseBackup!: DatabaseBackupModel;
|
public _databaseBackup!: DatabaseBackupModel;
|
||||||
public _storageAccounts!: StorageAccount[];
|
public _storageAccounts!: StorageAccount[];
|
||||||
public _fileShares!: azurecore.azureResource.FileShare[];
|
public _fileShares!: azurecore.azureResource.FileShare[];
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import * as styles from '../constants/styles';
|
|||||||
import { WIZARD_INPUT_COMPONENT_WIDTH } from './wizardController';
|
import { WIZARD_INPUT_COMPONENT_WIDTH } from './wizardController';
|
||||||
import * as utils from '../api/utils';
|
import * as utils from '../api/utils';
|
||||||
import { azureResource } from 'azurecore';
|
import { azureResource } from 'azurecore';
|
||||||
import { AzureSqlDatabaseServer, SqlVMServer } from '../api/azure';
|
import { AzureSqlDatabaseServer, getVMInstanceView, SqlVMServer } from '../api/azure';
|
||||||
import { collectTargetDatabaseInfo, TargetDatabaseInfo } from '../api/sqlUtils';
|
import { collectTargetDatabaseInfo, TargetDatabaseInfo } from '../api/sqlUtils';
|
||||||
import { MigrationLocalStorage, MigrationServiceContext } from '../models/migrationLocalStorage';
|
import { MigrationLocalStorage, MigrationServiceContext } from '../models/migrationLocalStorage';
|
||||||
|
|
||||||
@@ -111,7 +111,7 @@ export class TargetSelectionPage extends MigrationWizardPage {
|
|||||||
if (!targetMi || resourceDropdownValue === constants.NO_MANAGED_INSTANCE_FOUND) {
|
if (!targetMi || resourceDropdownValue === constants.NO_MANAGED_INSTANCE_FOUND) {
|
||||||
errors.push(constants.INVALID_MANAGED_INSTANCE_ERROR);
|
errors.push(constants.INVALID_MANAGED_INSTANCE_ERROR);
|
||||||
}
|
}
|
||||||
if (targetMi && targetMi.properties?.state !== 'Ready') {
|
if (targetMi && targetMi.properties?.state.toLowerCase() !== 'Ready'.toLowerCase()) {
|
||||||
errors.push(constants.MI_NOT_READY_ERROR(targetMi.name, targetMi.properties?.state));
|
errors.push(constants.MI_NOT_READY_ERROR(targetMi.name, targetMi.properties?.state));
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@@ -120,6 +120,17 @@ export class TargetSelectionPage extends MigrationWizardPage {
|
|||||||
if (!targetVm || resourceDropdownValue === constants.NO_VIRTUAL_MACHINE_FOUND) {
|
if (!targetVm || resourceDropdownValue === constants.NO_VIRTUAL_MACHINE_FOUND) {
|
||||||
errors.push(constants.INVALID_VIRTUAL_MACHINE_ERROR);
|
errors.push(constants.INVALID_VIRTUAL_MACHINE_ERROR);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// validate power state from VM instance view
|
||||||
|
const vmInstanceView = this.migrationStateModel._vmInstanceView;
|
||||||
|
if (!vmInstanceView.statuses.some(status => status.code.toLowerCase() === 'PowerState/running'.toLowerCase())) {
|
||||||
|
errors.push(constants.VM_NOT_READY_POWER_STATE_ERROR(targetVm.name));
|
||||||
|
}
|
||||||
|
|
||||||
|
// validate IaaS extension mode
|
||||||
|
if (targetVm.properties.sqlManagement.toLowerCase() !== 'Full'.toLowerCase()) {
|
||||||
|
errors.push(constants.VM_NOT_READY_IAAS_EXTENSION_ERROR(targetVm.name, targetVm.properties.sqlManagement));
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case MigrationTargetType.SQLDB:
|
case MigrationTargetType.SQLDB:
|
||||||
const targetSqlDB = this.migrationStateModel._targetServerInstance as AzureSqlDatabaseServer;
|
const targetSqlDB = this.migrationStateModel._targetServerInstance as AzureSqlDatabaseServer;
|
||||||
@@ -127,7 +138,7 @@ export class TargetSelectionPage extends MigrationWizardPage {
|
|||||||
errors.push(constants.INVALID_SQL_DATABASE_ERROR);
|
errors.push(constants.INVALID_SQL_DATABASE_ERROR);
|
||||||
}
|
}
|
||||||
// TODO: verify what state check is needed/possible?
|
// TODO: verify what state check is needed/possible?
|
||||||
if (targetSqlDB && targetSqlDB.properties?.state !== 'Ready') {
|
if (targetSqlDB && targetSqlDB.properties?.state.toLowerCase() !== 'Ready'.toLowerCase()) {
|
||||||
errors.push(constants.SQLDB_NOT_READY_ERROR(targetSqlDB.name, targetSqlDB.properties.state));
|
errors.push(constants.SQLDB_NOT_READY_ERROR(targetSqlDB.name, targetSqlDB.properties.state));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -642,9 +653,29 @@ export class TargetSelectionPage extends MigrationWizardPage {
|
|||||||
|
|
||||||
switch (this.migrationStateModel._targetType) {
|
switch (this.migrationStateModel._targetType) {
|
||||||
case MigrationTargetType.SQLVM:
|
case MigrationTargetType.SQLVM:
|
||||||
const selectedVm = this.migrationStateModel._targetSqlVirtualMachines?.find(vm => vm.name === value);
|
const selectedVm = this.migrationStateModel._targetSqlVirtualMachines?.find(vm => vm.name === value
|
||||||
|
|| constants.UNAVAILABLE_TARGET_PREFIX(vm.name) === value);
|
||||||
|
|
||||||
if (selectedVm) {
|
if (selectedVm) {
|
||||||
this.migrationStateModel._targetServerInstance = utils.deepClone(selectedVm)! as SqlVMServer;
|
this.migrationStateModel._targetServerInstance = utils.deepClone(selectedVm)! as SqlVMServer;
|
||||||
|
this.migrationStateModel._vmInstanceView = await getVMInstanceView(this.migrationStateModel._targetServerInstance, this.migrationStateModel._azureAccount, this.migrationStateModel._targetSubscription);
|
||||||
|
this.wizard.message = { text: '' };
|
||||||
|
|
||||||
|
// validate power state from VM instance view
|
||||||
|
if (!this.migrationStateModel._vmInstanceView.statuses.some(status => status.code.toLowerCase() === 'PowerState/running'.toLowerCase())) {
|
||||||
|
this.wizard.message = {
|
||||||
|
text: constants.VM_NOT_READY_POWER_STATE_ERROR(this.migrationStateModel._targetServerInstance.name),
|
||||||
|
level: azdata.window.MessageLevel.Error
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// validate IaaS extension mode
|
||||||
|
if (this.migrationStateModel._targetServerInstance.properties.sqlManagement.toLowerCase() !== 'Full'.toLowerCase()) {
|
||||||
|
this.wizard.message = {
|
||||||
|
text: constants.VM_NOT_READY_IAAS_EXTENSION_ERROR(this.migrationStateModel._targetServerInstance.name, this.migrationStateModel._targetServerInstance.properties.sqlManagement),
|
||||||
|
level: azdata.window.MessageLevel.Error
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case MigrationTargetType.SQLMI:
|
case MigrationTargetType.SQLMI:
|
||||||
@@ -654,9 +685,9 @@ export class TargetSelectionPage extends MigrationWizardPage {
|
|||||||
|
|
||||||
if (selectedMi) {
|
if (selectedMi) {
|
||||||
this.migrationStateModel._targetServerInstance = utils.deepClone(selectedMi)! as azureResource.AzureSqlManagedInstance;
|
this.migrationStateModel._targetServerInstance = utils.deepClone(selectedMi)! as azureResource.AzureSqlManagedInstance;
|
||||||
|
|
||||||
this.wizard.message = { text: '' };
|
this.wizard.message = { text: '' };
|
||||||
if (this.migrationStateModel._targetServerInstance.properties.state !== 'Ready') {
|
|
||||||
|
if (this.migrationStateModel._targetServerInstance.properties.state.toLowerCase() !== 'Ready'.toLowerCase()) {
|
||||||
this.wizard.message = {
|
this.wizard.message = {
|
||||||
text: constants.MI_NOT_READY_ERROR(
|
text: constants.MI_NOT_READY_ERROR(
|
||||||
this.migrationStateModel._targetServerInstance.name,
|
this.migrationStateModel._targetServerInstance.name,
|
||||||
@@ -673,7 +704,7 @@ export class TargetSelectionPage extends MigrationWizardPage {
|
|||||||
if (sqlDatabaseServer) {
|
if (sqlDatabaseServer) {
|
||||||
this.migrationStateModel._targetServerInstance = utils.deepClone(sqlDatabaseServer)! as AzureSqlDatabaseServer;
|
this.migrationStateModel._targetServerInstance = utils.deepClone(sqlDatabaseServer)! as AzureSqlDatabaseServer;
|
||||||
this.wizard.message = { text: '' };
|
this.wizard.message = { text: '' };
|
||||||
if (this.migrationStateModel._targetServerInstance.properties.state === 'Ready') {
|
if (this.migrationStateModel._targetServerInstance.properties.state.toLowerCase() === 'Ready'.toLowerCase()) {
|
||||||
this._targetUserNameInputBox.value = this.migrationStateModel._targetServerInstance.properties.administratorLogin;
|
this._targetUserNameInputBox.value = this.migrationStateModel._targetServerInstance.properties.administratorLogin;
|
||||||
} else {
|
} else {
|
||||||
this.wizard.message = {
|
this.wizard.message = {
|
||||||
@@ -941,11 +972,12 @@ export class TargetSelectionPage extends MigrationWizardPage {
|
|||||||
this.migrationStateModel._resourceGroup);
|
this.migrationStateModel._resourceGroup);
|
||||||
break;
|
break;
|
||||||
case MigrationTargetType.SQLVM:
|
case MigrationTargetType.SQLVM:
|
||||||
this._azureResourceDropdown.values = utils.getAzureResourceDropdownValues(
|
this._azureResourceDropdown.values = await utils.getVirtualMachinesDropdownValues(
|
||||||
this.migrationStateModel._targetSqlVirtualMachines,
|
this.migrationStateModel._targetSqlVirtualMachines,
|
||||||
this.migrationStateModel._location,
|
this.migrationStateModel._location,
|
||||||
this.migrationStateModel._resourceGroup?.name,
|
this.migrationStateModel._resourceGroup,
|
||||||
constants.NO_VIRTUAL_MACHINE_FOUND);
|
this.migrationStateModel._azureAccount,
|
||||||
|
this.migrationStateModel._targetSubscription);
|
||||||
break;
|
break;
|
||||||
case MigrationTargetType.SQLDB:
|
case MigrationTargetType.SQLDB:
|
||||||
this._azureResourceDropdown.values = utils.getAzureResourceDropdownValues(
|
this._azureResourceDropdown.values = utils.getAzureResourceDropdownValues(
|
||||||
|
|||||||
Reference in New Issue
Block a user