/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import * as vscode from 'vscode'; import * as azdata from 'azdata'; import * as azurecore from 'azurecore'; import { azureResource } from 'azureResource'; import * as constants from '../constants/strings'; import { getSessionIdHeader } from './utils'; import { ProvisioningState } from '../models/migrationLocalStorage'; async function getAzureCoreAPI(): Promise { const api = (await vscode.extensions.getExtension(azurecore.extension.name)?.activate()) as azurecore.IExtension; if (!api) { throw new Error('azure core API undefined for sql-migration'); } return api; } export type Subscription = azureResource.AzureResourceSubscription; export async function getSubscriptions(account: azdata.Account): Promise { const api = await getAzureCoreAPI(); const subscriptions = await api.getSubscriptions(account, false); let listOfSubscriptions = subscriptions.subscriptions; sortResourceArrayByName(listOfSubscriptions); return subscriptions.subscriptions; } export async function getLocations(account: azdata.Account, subscription: Subscription): Promise { const api = await getAzureCoreAPI(); const response = await api.getLocations(account, subscription, true); const dataMigrationResourceProvider = (await api.makeAzureRestRequest(account, subscription, `/subscriptions/${subscription.id}/providers/Microsoft.DataMigration?api-version=2021-04-01`, azurecore.HttpRequestMethod.GET)).response.data; const sqlMigratonResource = dataMigrationResourceProvider.resourceTypes.find((r: any) => r.resourceType === 'SqlMigrationServices'); const sqlMigrationResourceLocations = sqlMigratonResource.locations; if (response.errors.length > 0) { throw new Error(response.errors.toString()); } sortResourceArrayByName(response.locations); const filteredLocations = response.locations.filter(loc => { return sqlMigrationResourceLocations.includes(loc.displayName); }); // Only including the regions that have migration service deployed for public preview. const publicPreviewLocations = [ 'eastus', 'canadaeast', 'canadacentral', 'centralus', 'westus2', 'westus', 'southcentralus', 'westeurope', 'uksouth', 'australiaeast', 'southeastasia', 'japaneast', 'centralindia', 'eastus2', 'eastus2euap', 'francecentral', 'southindia', 'australiasoutheast', 'northcentralus' ]; return filteredLocations.filter(v => publicPreviewLocations.includes(v.name)); } export type AzureProduct = azureResource.AzureGraphResource; export async function getResourceGroups(account: azdata.Account, subscription: Subscription): Promise { const api = await getAzureCoreAPI(); const result = await api.getResourceGroups(account, subscription, false); sortResourceArrayByName(result.resourceGroups); return result.resourceGroups; } export async function createResourceGroup(account: azdata.Account, subscription: Subscription, resourceGroupName: string, location: string): Promise { const api = await getAzureCoreAPI(); const result = await api.createResourceGroup(account, subscription, resourceGroupName, location, false); return result.resourceGroup; } export type SqlManagedInstance = azureResource.AzureSqlManagedInstance; export async function getAvailableManagedInstanceProducts(account: azdata.Account, subscription: Subscription): Promise { const api = await getAzureCoreAPI(); const result = await api.getSqlManagedInstances(account, [subscription], false); sortResourceArrayByName(result.resources); return result.resources; } export async function getSqlManagedInstanceDatabases(account: azdata.Account, subscription: Subscription, managedInstance: SqlManagedInstance): Promise { const api = await getAzureCoreAPI(); const result = await api.getManagedDatabases(account, subscription, managedInstance, false); sortResourceArrayByName(result.databases); return result.databases; } export type SqlServer = AzureProduct; export async function getAvailableSqlServers(account: azdata.Account, subscription: Subscription): Promise { const api = await getAzureCoreAPI(); const result = await api.getSqlServers(account, [subscription], false); return result.resources; } export type SqlVMServer = { properties: { virtualMachineResourceId: string, provisioningState: string, sqlImageOffer: string, sqlManagement: string, sqlImageSku: string }, location: string, id: string, name: string, type: string, tenantId: string, subscriptionId: string }; export async function getAvailableSqlVMs(account: azdata.Account, subscription: Subscription, resourceGroup: azureResource.AzureResourceResourceGroup): Promise { const api = await getAzureCoreAPI(); const path = `/subscriptions/${subscription.id}/resourceGroups/${resourceGroup.name}/providers/Microsoft.SqlVirtualMachine/sqlVirtualMachines?api-version=2017-03-01-preview`; const response = await api.makeAzureRestRequest(account, subscription, path, azurecore.HttpRequestMethod.GET, undefined, true); if (response.errors.length > 0) { throw new Error(response.errors.toString()); } sortResourceArrayByName(response.response.data.value); return response.response.data.value; } export type StorageAccount = AzureProduct; export async function getAvailableStorageAccounts(account: azdata.Account, subscription: Subscription): Promise { const api = await getAzureCoreAPI(); const result = await api.getStorageAccounts(account, [subscription], false); sortResourceArrayByName(result.resources); return result.resources; } export async function getFileShares(account: azdata.Account, subscription: Subscription, storageAccount: StorageAccount): Promise { const api = await getAzureCoreAPI(); let result = await api.getFileShares(account, subscription, storageAccount, true); let fileShares = result.fileShares; sortResourceArrayByName(fileShares); return fileShares!; } export async function getBlobContainers(account: azdata.Account, subscription: Subscription, storageAccount: StorageAccount): Promise { const api = await getAzureCoreAPI(); let result = await api.getBlobContainers(account, subscription, storageAccount, true); let blobContainers = result.blobContainers; sortResourceArrayByName(blobContainers); return blobContainers!; } export async function getBlobs(account: azdata.Account, subscription: Subscription, storageAccount: StorageAccount, containerName: string): Promise { const api = await getAzureCoreAPI(); let result = await api.getBlobs(account, subscription, storageAccount, containerName, true); let blobNames = result.blobs; sortResourceArrayByName(blobNames); return blobNames!; } export async function getSqlMigrationService(account: azdata.Account, subscription: Subscription, resourceGroupName: string, regionName: string, sqlMigrationServiceName: string, sessionId: string): Promise { const api = await getAzureCoreAPI(); const path = `/subscriptions/${subscription.id}/resourceGroups/${resourceGroupName}/providers/Microsoft.DataMigration/sqlMigrationServices/${sqlMigrationServiceName}?api-version=2020-09-01-preview`; const response = await api.makeAzureRestRequest(account, subscription, path, azurecore.HttpRequestMethod.GET, undefined, true, undefined, getSessionIdHeader(sessionId)); if (response.errors.length > 0) { throw new Error(response.errors.toString()); } response.response.data.properties.resourceGroup = getResourceGroupFromId(response.response.data.id); return response.response.data; } export async function getSqlMigrationServices(account: azdata.Account, subscription: Subscription, resouceGroupName: string, sessionId: string): Promise { const api = await getAzureCoreAPI(); const path = `/subscriptions/${subscription.id}/resourceGroups/${resouceGroupName}/providers/Microsoft.DataMigration/sqlMigrationServices?api-version=2020-09-01-preview`; const response = await api.makeAzureRestRequest(account, subscription, path, azurecore.HttpRequestMethod.GET, undefined, true, undefined, getSessionIdHeader(sessionId)); if (response.errors.length > 0) { throw new Error(response.errors.toString()); } sortResourceArrayByName(response.response.data.value); response.response.data.value.forEach((sms: SqlMigrationService) => { sms.properties.resourceGroup = getResourceGroupFromId(sms.id); }); return response.response.data.value; } export async function createSqlMigrationService(account: azdata.Account, subscription: Subscription, resourceGroupName: string, regionName: string, sqlMigrationServiceName: string, sessionId: string): Promise { const api = await getAzureCoreAPI(); const path = `/subscriptions/${subscription.id}/resourceGroups/${resourceGroupName}/providers/Microsoft.DataMigration/sqlMigrationServices/${sqlMigrationServiceName}?api-version=2020-09-01-preview`; const requestBody = { 'location': regionName }; const response = await api.makeAzureRestRequest(account, subscription, path, azurecore.HttpRequestMethod.PUT, requestBody, true, undefined, getSessionIdHeader(sessionId)); if (response.errors.length > 0) { throw new Error(response.errors.toString()); } const asyncUrl = response.response.headers['azure-asyncoperation']; const maxRetry = 24; let i = 0; for (i = 0; i < maxRetry; i++) { const asyncResponse = await api.makeAzureRestRequest(account, subscription, asyncUrl.replace('https://management.azure.com/', ''), azurecore.HttpRequestMethod.GET, undefined, true, undefined, getSessionIdHeader(sessionId)); const creationStatus = asyncResponse.response.data.status; if (creationStatus === ProvisioningState.Succeeded) { break; } else if (creationStatus === ProvisioningState.Failed) { throw new Error(asyncResponse.errors.toString()); } await new Promise(resolve => setTimeout(resolve, 5000)); //adding 5 sec delay before getting creation status } if (i === maxRetry) { throw new Error(constants.DMS_PROVISIONING_FAILED); } return response.response.data; } export async function getSqlMigrationServiceAuthKeys(account: azdata.Account, subscription: Subscription, resourceGroupName: string, regionName: string, sqlMigrationServiceName: string, sessionId: string): Promise { const api = await getAzureCoreAPI(); const path = `/subscriptions/${subscription.id}/resourceGroups/${resourceGroupName}/providers/Microsoft.DataMigration/sqlMigrationServices/${sqlMigrationServiceName}/ListAuthKeys?api-version=2020-09-01-preview`; const response = await api.makeAzureRestRequest(account, subscription, path, azurecore.HttpRequestMethod.POST, undefined, true, undefined, getSessionIdHeader(sessionId)); if (response.errors.length > 0) { throw new Error(response.errors.toString()); } return { authKey1: response?.response?.data?.authKey1 ?? '', authKey2: response?.response?.data?.authKey2 ?? '' }; } export async function regenerateSqlMigrationServiceAuthKey(account: azdata.Account, subscription: Subscription, resourceGroupName: string, regionName: string, sqlMigrationServiceName: string, keyName: string, sessionId: string): Promise { const api = await getAzureCoreAPI(); const path = `/subscriptions/${subscription.id}/resourceGroups/${resourceGroupName}/providers/Microsoft.DataMigration/sqlMigrationServices/${sqlMigrationServiceName}/regenerateAuthKeys?api-version=2020-09-01-preview`; const requestBody = { 'location': regionName, 'keyName': keyName, }; const response = await api.makeAzureRestRequest(account, subscription, path, azurecore.HttpRequestMethod.POST, requestBody, true, undefined, getSessionIdHeader(sessionId)); if (response.errors.length > 0) { throw new Error(response.errors.toString()); } return { authKey1: response?.response?.data?.authKey1 ?? '', authKey2: response?.response?.data?.authKey2 ?? '' }; } export async function getStorageAccountAccessKeys(account: azdata.Account, subscription: Subscription, storageAccount: StorageAccount): Promise { const api = await getAzureCoreAPI(); const response = await api.getStorageAccountAccessKey(account, subscription, storageAccount, true); if (response.errors.length > 0) { throw new Error(response.errors.toString()); } return { keyName1: response?.keyName1, keyName2: response?.keyName2 }; } export async function getSqlMigrationServiceMonitoringData(account: azdata.Account, subscription: Subscription, resourceGroupName: string, regionName: string, sqlMigrationService: string, sessionId: string): Promise { const api = await getAzureCoreAPI(); const path = `/subscriptions/${subscription.id}/resourceGroups/${resourceGroupName}/providers/Microsoft.DataMigration/sqlMigrationServices/${sqlMigrationService}/monitoringData?api-version=2020-09-01-preview`; const response = await api.makeAzureRestRequest(account, subscription, path, azurecore.HttpRequestMethod.GET, undefined, true, undefined, getSessionIdHeader(sessionId)); if (response.errors.length > 0) { throw new Error(response.errors.toString()); } return response.response.data; } export async function startDatabaseMigration(account: azdata.Account, subscription: Subscription, regionName: string, targetServer: SqlManagedInstance | SqlVMServer, targetDatabaseName: string, requestBody: StartDatabaseMigrationRequest, sessionId: string): Promise { const api = await getAzureCoreAPI(); const path = `${targetServer.id}/providers/Microsoft.DataMigration/databaseMigrations/${targetDatabaseName}?api-version=2020-09-01-preview`; const response = await api.makeAzureRestRequest(account, subscription, path, azurecore.HttpRequestMethod.PUT, requestBody, true, undefined, getSessionIdHeader(sessionId)); if (response.errors.length > 0) { throw new Error(response.errors.toString()); } const asyncUrl = response.response.headers['azure-asyncoperation']; return { asyncUrl: asyncUrl, status: response.response.status, databaseMigration: response.response.data }; } export async function getMigrationStatus(account: azdata.Account, subscription: Subscription, migration: DatabaseMigration, sessionId: string, asyncUrl: string): Promise { const api = await getAzureCoreAPI(); const migrationOperationId = getMigrationOperationId(migration, asyncUrl); const path = `${migration.id}?migrationOperationId=${migrationOperationId}&$expand=MigrationStatusDetails&api-version=2020-09-01-preview`; const response = await api.makeAzureRestRequest(account, subscription, path, azurecore.HttpRequestMethod.GET, undefined, true, undefined, getSessionIdHeader(sessionId)); if (response.errors.length > 0) { throw new Error(response.errors.toString()); } return response.response.data; } function getMigrationOperationId(migration: DatabaseMigration, asyncUrl: string): string { // migrationOperationId may be undefined when provisioning has failed // fall back to the operationId from the asyncUrl in the create migration response if (migration.properties.migrationOperationId) { return migration.properties.migrationOperationId; } return asyncUrl ? vscode.Uri.parse(asyncUrl)?.path?.split('/').reverse()[0] : ''; } export async function getMigrationAsyncOperationDetails(account: azdata.Account, subscription: Subscription, url: string, sessionId: string): Promise { const api = await getAzureCoreAPI(); const response = await api.makeAzureRestRequest(account, subscription, url.replace('https://management.azure.com/', ''), azurecore.HttpRequestMethod.GET, undefined, true, undefined, getSessionIdHeader(sessionId)); if (response.errors.length > 0) { throw new Error(response.errors.toString()); } return response.response.data; } export async function listMigrationsBySqlMigrationService(account: azdata.Account, subscription: Subscription, sqlMigrationService: SqlMigrationService, sessionId: string): Promise { const api = await getAzureCoreAPI(); const path = `${sqlMigrationService.id}/listMigrations?$expand=MigrationStatusDetails&api-version=2020-09-01-preview`; const response = await api.makeAzureRestRequest(account, subscription, path, azurecore.HttpRequestMethod.GET, undefined, true, undefined, getSessionIdHeader(sessionId)); if (response.errors.length > 0) { throw new Error(response.errors.toString()); } return response.response.data.value; } export async function startMigrationCutover(account: azdata.Account, subscription: Subscription, migrationStatus: DatabaseMigration, sessionId: string): Promise { const api = await getAzureCoreAPI(); const path = `${migrationStatus.id}/operations/${migrationStatus.properties.migrationOperationId}/cutover?api-version=2020-09-01-preview`; const response = await api.makeAzureRestRequest(account, subscription, path, azurecore.HttpRequestMethod.POST, undefined, true, undefined, getSessionIdHeader(sessionId)); if (response.errors.length > 0) { throw new Error(response.errors.toString()); } return response.response.data.value; } export async function stopMigration(account: azdata.Account, subscription: Subscription, migrationStatus: DatabaseMigration, sessionId: string): Promise { const api = await getAzureCoreAPI(); const path = `${migrationStatus.id}/operations/${migrationStatus.properties.migrationOperationId}/cancel?api-version=2020-09-01-preview`; const response = await api.makeAzureRestRequest(account, subscription, path, azurecore.HttpRequestMethod.POST, undefined, true, undefined, getSessionIdHeader(sessionId)); if (response.errors.length > 0) { throw new Error(response.errors.toString()); } } export async function getLocationDisplayName(location: string): Promise { const api = await getAzureCoreAPI(); return await api.getRegionDisplayName(location); } type SortableAzureResources = AzureProduct | azureResource.FileShare | azureResource.BlobContainer | azureResource.Blob | azureResource.AzureResourceSubscription | SqlMigrationService; function sortResourceArrayByName(resourceArray: SortableAzureResources[]): void { if (!resourceArray) { return; } resourceArray.sort((a: SortableAzureResources, b: SortableAzureResources) => { if (a.name.toLowerCase() < b.name.toLowerCase()) { return -1; } if (a.name.toLowerCase() > b.name.toLowerCase()) { return 1; } return 0; }); } export function getResourceGroupFromId(id: string): string { return id.replace(RegExp('^(.*?)/resourceGroups/'), '').replace(RegExp('/providers/.*'), '').toLowerCase(); } export interface SqlMigrationServiceProperties { name: string; subscriptionId: string; resourceGroup: string; location: string; provisioningState: string; integrationRuntimeState?: string; isProvisioned?: boolean; } export interface SqlMigrationService { properties: SqlMigrationServiceProperties; location: string; id: string; name: string; error: { code: string, message: string } } export interface SqlMigrationServiceAuthenticationKeys { authKey1: string, authKey2: string } export interface GetStorageAccountAccessKeysResult { keyName1: string, keyName2: string } export interface IntegrationRuntimeMonitoringData { name: string, nodes: IntegrationRuntimeNode[]; } export interface IntegrationRuntimeNode { availableMemoryInMB: number, concurrentJobsLimit: number concurrentJobsRunning: number, cpuUtilization: number, nodeName: string receivedBytes: number sentBytes: number } export interface StartDatabaseMigrationRequest { location: string, properties: { sourceDatabaseName: string, migrationService: string, backupConfiguration: { targetLocation?: { storageAccountResourceId: string, accountKey: string, }, sourceLocation?: SourceLocation }, sourceSqlConnection: { authentication: string, dataSource: string, username: string, password: string }, scope: string, autoCutoverConfiguration: AutoCutoverConfiguration, } } export interface StartDatabaseMigrationResponse { status: number, databaseMigration: DatabaseMigration asyncUrl: string } export interface DatabaseMigration { properties: DatabaseMigrationProperties; id: string; name: string; type: string; } export interface DatabaseMigrationProperties { scope: string; provisioningState: 'Succeeded' | 'Failed' | 'Creating'; provisioningError: string; migrationStatus: 'InProgress' | 'Failed' | 'Succeeded' | 'Creating' | 'Completing' | 'Canceling'; migrationStatusDetails?: MigrationStatusDetails; startedOn: string; endedOn: string; sourceSqlConnection: SqlConnectionInfo; sourceDatabaseName: string; targetDatabaseCollation: string; migrationService: string; migrationOperationId: string; backupConfiguration: BackupConfiguration; autoCutoverConfiguration: AutoCutoverConfiguration; migrationFailureError: ErrorInfo; } export interface MigrationStatusDetails { migrationState: string; startedOn: string; endedOn: string; fullBackupSetInfo: BackupSetInfo; lastRestoredBackupSetInfo: BackupSetInfo; activeBackupSets: BackupSetInfo[]; blobContainerName: string; isFullBackupRestored: boolean; restoreBlockingReason: string; fileUploadBlockingErrors: string[]; currentRestoringFileName: string; lastRestoredFilename: string; pendingLogBackupsCount: number; invalidFiles: string[]; } export interface SqlConnectionInfo { dataSource: string; authentication: string; username: string; password: string; encryptConnection: string; trustServerCertificate: string; } export interface BackupConfiguration { sourceLocation?: SourceLocation; targetLocation?: TargetLocation; } export interface AutoCutoverConfiguration { autoCutover: boolean; lastBackupName?: string; } export interface ErrorInfo { code: string; message: string; } export interface BackupSetInfo { backupSetId: string; firstLSN: string; lastLSN: string; backupType: string; listOfBackupFiles: BackupFileInfo[]; backupStartDate: string; backupFinishDate: string; isBackupRestored: boolean; backupSize: number; compressedBackupSize: number; hasBackupChecksums: boolean; familyCount: number; } export interface SourceLocation { fileShare?: DatabaseMigrationFileShare; azureBlob?: DatabaseMigrationAzureBlob; } export interface TargetLocation { storageAccountResourceId: string; accountKey: string; } export interface BackupFileInfo { fileName: string; status: 'Arrived' | 'Uploading' | 'Uploaded' | 'Restoring' | 'Restored' | 'Cancelled' | 'Ignored'; totalSize: number; dataRead: number; dataWritten: number; copyThroughput: number; copyDuration: number; familySequenceNumber: number; } export interface DatabaseMigrationFileShare { path: string; username: string; password: string; } export interface DatabaseMigrationAzureBlob { storageAccountResourceId: string; accountKey: string; blobContainerName: string; } export interface AzureAsyncOperationResource { name: string, status: string, startTime: string, endTime: string, percentComplete: number, error: ErrorInfo }