diff --git a/extensions/sql-migration/src/api/azure.ts b/extensions/sql-migration/src/api/azure.ts index 34a5976ab4..a42e5a8394 100644 --- a/extensions/sql-migration/src/api/azure.ts +++ b/extensions/sql-migration/src/api/azure.ts @@ -10,12 +10,14 @@ import * as constants from '../constants/strings'; import { getSessionIdHeader } from './utils'; import { URL } from 'url'; import { MigrationSourceAuthenticationType, MigrationStateModel, NetworkShare } from '../models/stateMachine'; +import { NetworkInterface } from './dataModels/azure/networkInterfaceModel'; const ARM_MGMT_API_VERSION = '2021-04-01'; const SQL_VM_API_VERSION = '2021-11-01-preview'; const SQL_MI_API_VERSION = '2021-11-01-preview'; const SQL_SQLDB_API_VERSION = '2021-11-01-preview'; const DMSV2_API_VERSION = '2022-03-30-preview'; +const COMPUTE_VM_API_VERSION = '2022-08-01'; async function getAzureCoreAPI(): Promise { const api = (await vscode.extensions.getExtension(azurecore.extension.name)?.activate()) as azurecore.IExtension; @@ -168,11 +170,6 @@ export interface ServerPrivateEndpointConnection { readonly id?: string; readonly properties?: PrivateEndpointConnectionProperties; } - -export function isAzureSqlDatabaseServer(instance: any): instance is AzureSqlDatabaseServer { - return (instance as AzureSqlDatabaseServer) !== undefined; -} - export interface AzureSqlDatabaseServer { id: string, name: string, @@ -197,6 +194,10 @@ export interface AzureSqlDatabaseServer { }, } +export function isAzureSqlDatabaseServer(instance: any): instance is AzureSqlDatabaseServer { + return (instance as AzureSqlDatabaseServer) !== undefined; +} + export type SqlVMServer = { properties: { virtualMachineResourceId: string, @@ -210,9 +211,14 @@ export type SqlVMServer = { name: string, type: string, tenantId: string, - subscriptionId: string + subscriptionId: string, + networkInterfaces: Map, }; +export function isSqlVMServer(instance: any): instance is SqlVMServer { + return (instance as SqlVMServer) !== undefined; +} + export type VirtualMachineInstanceView = { computerName: string, osName: string, @@ -224,6 +230,7 @@ export type VirtualMachineInstanceView = { hyperVGeneration: string, patchStatus: { [propertyName: string]: string; }, statuses: InstanceViewStatus[], + networkProfile: any, } export type InstanceViewStatus = { @@ -282,7 +289,7 @@ export async function getAvailableSqlVMs(account: azdata.Account, subscription: export async function getVMInstanceView(sqlVm: SqlVMServer, account: azdata.Account, subscription: Subscription): Promise { 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 path = encodeURI(`/subscriptions/${subscription.id}/resourceGroups/${getResourceGroupFromId(sqlVm.id)}/providers/Microsoft.Compute/virtualMachines/${sqlVm.name}/instanceView?api-version=${COMPUTE_VM_API_VERSION}`); const host = api.getProviderMetadataForAccount(account).settings.armResource?.endpoint; const response = await api.makeAzureRestRequest(account, subscription, path, azurecore.HttpRequestMethod.GET, undefined, true, host); @@ -293,6 +300,24 @@ export async function getVMInstanceView(sqlVm: SqlVMServer, account: azdata.Acco return response.response.data; } +export async function getAzureResourceGivenId(account: azdata.Account, subscription: Subscription, id: string, apiVersion: string): Promise { + const api = await getAzureCoreAPI(); + const path = encodeURI(`${id}?api-version=${apiVersion}`); + 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 async function getComputeVM(sqlVm: SqlVMServer, account: azdata.Account, subscription: Subscription): Promise { + const path = encodeURI(`/subscriptions/${subscription.id}/resourceGroups/${getResourceGroupFromId(sqlVm.id)}/providers/Microsoft.Compute/virtualMachines/${sqlVm.name}`); + return getAzureResourceGivenId(account, subscription, path, COMPUTE_VM_API_VERSION); +} + export type StorageAccount = AzureProduct; export async function getAvailableStorageAccounts(account: azdata.Account, subscription: Subscription): Promise { const api = await getAzureCoreAPI(); diff --git a/extensions/sql-migration/src/api/dataModels/azure/networkInterfaceModel.ts b/extensions/sql-migration/src/api/dataModels/azure/networkInterfaceModel.ts new file mode 100644 index 0000000000..a46e9c0bf8 --- /dev/null +++ b/extensions/sql-migration/src/api/dataModels/azure/networkInterfaceModel.ts @@ -0,0 +1,148 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as azdata from 'azdata'; +import { getAzureResourceGivenId, getComputeVM, SqlVMServer, Subscription } from '../../azure'; + +export interface NetworkResource { + id: string, + name: string, + type: string, + location: string, + properties: any +} + +export interface NetworkInterface extends NetworkResource { + properties: { + ipConfigurations: NetworkInterfaceIpConfiguration[], + primary: boolean, + provisioningState: string, + resourceGuid: string, + virtualMachine: { + id: string, + }, + }, +} + +export interface NetworkInterfaceIpConfiguration extends NetworkResource { + properties: { + primary: boolean, + privateIPAddress: string, + privateIPAddressVersion: string, + provisioningState: string, + publicIPAddress: NetworkResource + } +} + +export interface PublicIpAddress extends NetworkResource { + properties: { + ipAddress: string + } +} + +export class NetworkInterfaceModel { + public static IPv4VersionType = "IPv4".toLocaleLowerCase(); + private static NETWORK_API_VERSION = '2022-09-01'; + + public static getPrimaryNetworkInterface(networkInterfaces: NetworkInterface[]): NetworkInterface | undefined { + if (networkInterfaces && networkInterfaces.length > 0) { + const primaryNetworkInterface = networkInterfaces.find(nic => nic.properties.primary === true); + if (primaryNetworkInterface) { + return primaryNetworkInterface; + } + } + + return undefined; + } + + public static getPrimaryIpConfiguration(networkInterface: NetworkInterface): NetworkInterfaceIpConfiguration | undefined { + const hasIpConfigurations = networkInterface?.properties?.ipConfigurations && networkInterface.properties.ipConfigurations?.length > 0; + if (!hasIpConfigurations) { + return undefined; + } + + // If the primary property exists, return the primary, otherwise, return the first ip configuration + let primaryIpConfig = networkInterface.properties.ipConfigurations.find((ipConfig: NetworkInterfaceIpConfiguration) => ipConfig.properties.primary); + if (primaryIpConfig) { + return primaryIpConfig; + } + + // Otherwise, find the first configuration with a public ip address. + primaryIpConfig = networkInterface.properties.ipConfigurations.find(ipConfiguration => ipConfiguration.properties.publicIPAddress !== undefined); + if (primaryIpConfig) { + return primaryIpConfig; + } + + // Otherwise, return the first ipv4 configuration + primaryIpConfig = networkInterface.properties.ipConfigurations.find(ipConfiguration => ipConfiguration.properties.privateIPAddressVersion.toLocaleLowerCase() === NetworkInterfaceModel.IPv4VersionType); + return primaryIpConfig; + } + + public static getIpAddress(networkInterfaces: NetworkInterface[]): string { + const primaryNetworkInterface = this.getPrimaryNetworkInterface(networkInterfaces); + + if (!primaryNetworkInterface) { + return ""; + } + + const ipConfig = this.getPrimaryIpConfiguration(primaryNetworkInterface); + if (ipConfig && ipConfig.properties.publicIPAddress) { + return ipConfig.properties.publicIPAddress.properties.ipAddress; + } + + if (ipConfig && ipConfig.properties.privateIPAddress) { + return ipConfig.properties.privateIPAddress; + } + + return ""; + } + + public static getPublicIpAddressId(networkInterfaces: NetworkInterface[]): string | undefined { + const primaryNetworkInterface = this.getPrimaryNetworkInterface(networkInterfaces); + + if (!primaryNetworkInterface) { + return undefined; + } + + const ipConfig = this.getPrimaryIpConfiguration(primaryNetworkInterface); + if (ipConfig && ipConfig.properties.publicIPAddress) { + return ipConfig.properties.publicIPAddress.id; + } + + return undefined; + } + + public static async getNetworkInterfaces(account: azdata.Account, subscription: Subscription, nicId: string): Promise { + return getAzureResourceGivenId(account, subscription, nicId, this.NETWORK_API_VERSION); + } + + public static async getPublicIpAddress(account: azdata.Account, subscription: Subscription, publicIpAddressId: string): Promise { + return getAzureResourceGivenId(account, subscription, publicIpAddressId, this.NETWORK_API_VERSION); + } + + public static async getVmNetworkInterfaces(account: azdata.Account, subscription: Subscription, sqlVm: SqlVMServer): Promise> { + const computeVMs = await getComputeVM(sqlVm, account, subscription); + const networkInterfaces = new Map(); + + if (!computeVMs?.properties?.networkProfile?.networkInterfaces) { + return networkInterfaces; + } + + for (const nic of computeVMs.properties.networkProfile.networkInterfaces) { + const nicId = nic.id; + const nicData = await this.getNetworkInterfaces(account, subscription, nicId); + const publicIpAddressId = NetworkInterfaceModel.getPublicIpAddressId([nicData]); + let primaryIpConfig = NetworkInterfaceModel.getPrimaryIpConfiguration(nicData); + + if (primaryIpConfig && publicIpAddressId) { + primaryIpConfig.properties.publicIPAddress = await this.getPublicIpAddress(account, subscription, publicIpAddressId); + } + + networkInterfaces.set(nicId, nicData); + } + + return networkInterfaces; + } +} diff --git a/extensions/sql-migration/src/api/sqlUtils.ts b/extensions/sql-migration/src/api/sqlUtils.ts index 71c13b497e..739224d7d5 100644 --- a/extensions/sql-migration/src/api/sqlUtils.ts +++ b/extensions/sql-migration/src/api/sqlUtils.ts @@ -5,10 +5,11 @@ import * as azdata from 'azdata'; import { azureResource } from 'azurecore'; -import { AzureSqlDatabase, AzureSqlDatabaseServer } from './azure'; +import { AzureSqlDatabase, AzureSqlDatabaseServer, isAzureSqlDatabaseServer, isSqlManagedInstance, isSqlVMServer, SqlManagedInstance, SqlVMServer } from './azure'; import { generateGuid } from './utils'; import * as utils from '../api/utils'; import { TelemetryAction, TelemetryViews, logError } from '../telemetry'; +import { NetworkInterfaceModel } from './dataModels/azure/networkInterfaceModel'; const query_database_tables_sql = ` SELECT @@ -166,12 +167,14 @@ function getSqlDbConnectionProfile( } export function getConnectionProfile( - serverName: string, + server: string | SqlManagedInstance | SqlVMServer | AzureSqlDatabaseServer, azureResourceId: string, userName: string, - password: string): azdata.IConnectionProfile { + password: string, + trustServerCert: boolean = false): azdata.IConnectionProfile { const connectId = generateGuid(); + const serverName = extractNameFromServer(server); return { serverName: serverName, id: connectId, @@ -194,7 +197,7 @@ export function getConnectionProfile( connectionTimeout: 60, columnEncryptionSetting: 'Enabled', encrypt: true, - trustServerCertificate: false, + trustServerCertificate: trustServerCert, connectRetryCount: '1', connectRetryInterval: '10', applicationName: 'azdata', @@ -202,6 +205,29 @@ export function getConnectionProfile( }; } +function extractNameFromServer( + server: string | SqlManagedInstance | SqlVMServer | AzureSqlDatabaseServer): string { + + // No need to extract name if the server is a string + if (typeof server === 'string') { + return server + } + + if (isSqlVMServer(server)) { + // For sqlvm, we need to use ip address from the network interface to connect to the server + const sqlVm = server as SqlVMServer; + const networkInterfaces = Array.from(sqlVm.networkInterfaces.values()); + return NetworkInterfaceModel.getIpAddress(networkInterfaces); + } + + // check if the target server is a managed instance or a VM + if (isSqlManagedInstance(server) || isAzureSqlDatabaseServer(server)) { + return server.properties.fullyQualifiedDomainName; + } + + return ""; +} + export async function collectSourceDatabaseTableInfo(sourceConnectionId: string, sourceDatabase: string): Promise { const ownerUri = await azdata.connection.getUriForConnection(sourceConnectionId); const connectionProvider = azdata.dataprotocol.getProvider( @@ -387,16 +413,17 @@ export async function collectSourceLogins( } export async function collectTargetLogins( - targetServer: AzureSqlDatabaseServer, + targetServer: SqlManagedInstance | SqlVMServer | AzureSqlDatabaseServer, userName: string, password: string, includeWindowsAuth: boolean = true): Promise { const connectionProfile = getConnectionProfile( - targetServer.properties.fullyQualifiedDomainName, + targetServer, targetServer.id, userName, - password); + password, + true /* trustServerCertificate */); const result = await azdata.connection.connect(connectionProfile, false, false); if (result.connected && result.connectionId) { diff --git a/extensions/sql-migration/src/models/stateMachine.ts b/extensions/sql-migration/src/models/stateMachine.ts index bca4f435c6..709a75516b 100644 --- a/extensions/sql-migration/src/models/stateMachine.ts +++ b/extensions/sql-migration/src/models/stateMachine.ts @@ -7,7 +7,7 @@ import * as azdata from 'azdata'; import * as azurecore from 'azurecore'; import * as vscode from 'vscode'; import * as mssql from 'mssql'; -import { SqlMigrationService, SqlManagedInstance, startDatabaseMigration, StartDatabaseMigrationRequest, StorageAccount, SqlVMServer, getLocationDisplayName, getSqlManagedInstanceDatabases, AzureSqlDatabaseServer, isSqlManagedInstance, isAzureSqlDatabaseServer, VirtualMachineInstanceView } from '../api/azure'; +import { SqlMigrationService, SqlManagedInstance, startDatabaseMigration, StartDatabaseMigrationRequest, StorageAccount, SqlVMServer, getLocationDisplayName, getSqlManagedInstanceDatabases, AzureSqlDatabaseServer, VirtualMachineInstanceView } from '../api/azure'; import * as constants from '../constants/strings'; import * as nls from 'vscode-nls'; import { v4 as uuidv4 } from 'uuid'; @@ -533,24 +533,13 @@ export class MigrationStateModel implements Model, vscode.Disposable { return await azdata.connection.getConnectionString(this._sourceConnectionId, true); } - public async setTargetServerName(): Promise { - // If target server name has already been set, we can skip this part - if (this._targetServerName) { - return; - } - - if (isSqlManagedInstance(this._targetServerInstance) || isAzureSqlDatabaseServer(this._targetServerInstance)) { - this._targetServerName = this._targetServerName ?? this._targetServerInstance.properties.fullyQualifiedDomainName; - } - } - public async getTargetConnectionString(): Promise { - await this.setTargetServerName(); const connectionProfile = getConnectionProfile( - this._targetServerName, + this._targetServerInstance, this._targetServerInstance.id, this._targetUserName, - this._targetPassword); + this._targetPassword, + true /* trustServerCertificate */); const result = await azdata.connection.connect(connectionProfile, false, false); if (result.connected && result.connectionId) { diff --git a/extensions/sql-migration/src/wizard/loginMigrationTargetSelectionPage.ts b/extensions/sql-migration/src/wizard/loginMigrationTargetSelectionPage.ts index c14ffe36d0..807368c620 100644 --- a/extensions/sql-migration/src/wizard/loginMigrationTargetSelectionPage.ts +++ b/extensions/sql-migration/src/wizard/loginMigrationTargetSelectionPage.ts @@ -13,8 +13,9 @@ import * as styles from '../constants/styles'; import { WIZARD_INPUT_COMPONENT_WIDTH } from './wizardController'; import * as utils from '../api/utils'; import { azureResource } from 'azurecore'; -import { AzureSqlDatabaseServer, SqlVMServer } from '../api/azure'; +import { AzureSqlDatabaseServer, getVMInstanceView, SqlVMServer } from '../api/azure'; import { collectSourceLogins, collectTargetLogins, isSysAdmin, LoginTableInfo } from '../api/sqlUtils'; +import { NetworkInterfaceModel } from '../api/dataModels/azure/networkInterfaceModel'; export class LoginMigrationTargetSelectionPage extends MigrationWizardPage { private _view!: azdata.ModelView; @@ -604,7 +605,7 @@ export class LoginMigrationTargetSelectionPage extends MigrationWizardPage { this._testConectionButton.onDidClick(async (value) => { this.wizard.message = { text: '' }; - const targetDatabaseServer = this.migrationStateModel._targetServerInstance as AzureSqlDatabaseServer; + const targetDatabaseServer = this.migrationStateModel._targetServerInstance; const userName = this.migrationStateModel._targetUserName; const password = this.migrationStateModel._targetPassword; const loginsOnTarget: string[] = []; @@ -744,6 +745,31 @@ export class LoginMigrationTargetSelectionPage extends MigrationWizardPage { const selectedVm = this.migrationStateModel._targetSqlVirtualMachines.find(vm => vm.name === value); if (selectedVm) { this.migrationStateModel._targetServerInstance = utils.deepClone(selectedVm)! as SqlVMServer; + this.migrationStateModel._vmInstanceView = await getVMInstanceView(this.migrationStateModel._targetServerInstance, this.migrationStateModel._azureAccount, this.migrationStateModel._targetSubscription); + this.migrationStateModel._targetServerInstance.networkInterfaces = await NetworkInterfaceModel.getVmNetworkInterfaces( + this.migrationStateModel._azureAccount, + this.migrationStateModel._targetSubscription, + this.migrationStateModel._targetServerInstance); + + this.wizard.message = { text: '' }; + + // validate power state from VM instance view + const runningState = 'PowerState/running'.toLowerCase(); + if (!this.migrationStateModel._vmInstanceView.statuses.some(status => status.code.toLowerCase() === runningState)) { + this.wizard.message = { + text: constants.VM_NOT_READY_POWER_STATE_ERROR(this.migrationStateModel._targetServerInstance.name), + level: azdata.window.MessageLevel.Warning + }; + } + + // validate IaaS extension mode + const fullMode = 'Full'.toLowerCase(); + if (this.migrationStateModel._targetServerInstance.properties.sqlManagement.toLowerCase() !== fullMode) { + this.wizard.message = { + text: constants.VM_NOT_READY_IAAS_EXTENSION_ERROR(this.migrationStateModel._targetServerInstance.name, this.migrationStateModel._targetServerInstance.properties.sqlManagement), + level: azdata.window.MessageLevel.Warning + }; + } } break; case MigrationTargetType.SQLMI: @@ -963,6 +989,15 @@ export class LoginMigrationTargetSelectionPage extends MigrationWizardPage { this.migrationStateModel._location, this.migrationStateModel._resourceGroup?.name, constants.NO_VIRTUAL_MACHINE_FOUND); + + let response = await utils.getVirtualMachinesDropdownValues( + this.migrationStateModel._targetSqlVirtualMachines, + this.migrationStateModel._location, + this.migrationStateModel._resourceGroup, + this.migrationStateModel._azureAccount, + this.migrationStateModel._targetSubscription); + console.log(response); + break; case MigrationTargetType.SQLDB: this._azureResourceDropdown.values = utils.getAzureResourceDropdownValues(