[SQL-Migration] Enable login migrations to SQL VM (#21776)

This PR adds support for migrating logins to SQL VM. Adding support for 2 scenarios supported here: VMs with private IP and public IP.
This commit is contained in:
AkshayMata
2023-01-31 09:47:16 -08:00
committed by GitHub
parent 66bdc54c89
commit fcece32cdd
5 changed files with 255 additions and 31 deletions

View File

@@ -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<azurecore.IExtension> {
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<string, NetworkInterface>,
};
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<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 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<any> {
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<any> {
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<StorageAccount[]> {
const api = await getAzureCoreAPI();

View File

@@ -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<NetworkInterface> {
return getAzureResourceGivenId(account, subscription, nicId, this.NETWORK_API_VERSION);
}
public static async getPublicIpAddress(account: azdata.Account, subscription: Subscription, publicIpAddressId: string): Promise<PublicIpAddress> {
return getAzureResourceGivenId(account, subscription, publicIpAddressId, this.NETWORK_API_VERSION);
}
public static async getVmNetworkInterfaces(account: azdata.Account, subscription: Subscription, sqlVm: SqlVMServer): Promise<Map<string, NetworkInterface>> {
const computeVMs = await getComputeVM(sqlVm, account, subscription);
const networkInterfaces = new Map<string, any>();
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;
}
}

View File

@@ -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<TableInfo[]> {
const ownerUri = await azdata.connection.getUriForConnection(sourceConnectionId);
const connectionProvider = azdata.dataprotocol.getProvider<azdata.ConnectionProvider>(
@@ -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<string[]> {
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) {

View File

@@ -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<void> {
// 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<string> {
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) {

View File

@@ -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(