mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-01-29 17:23:25 -05:00
[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:
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user