/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { MiaaResourceInfo } from 'arc'; import * as azdata from 'azdata'; import * as azExt from 'az-ext'; import * as vscode from 'vscode'; import { UserCancelledError } from '../common/api'; import { Deferred } from '../common/promise'; import { getTimeStamp, parseIpAndPort } from '../common/utils'; import * as loc from '../localizedConstants'; import { ConnectToMiaaSqlDialog } from '../ui/dialogs/connectMiaaDialog'; import { AzureArcTreeDataProvider } from '../ui/tree/azureArcTreeDataProvider'; import { ControllerModel, Registration } from './controllerModel'; import { ResourceModel } from './resourceModel'; export type DatabaseModel = { name: string, status: string, earliestBackup: string, lastBackup: string }; export type RPModel = { recoveryPointObjective: string, retentionDays: string }; export type PITRModel = { instanceName: string, resourceGroupName: string, location: string, subscriptionId: string, dbName: string, restorePoint: string, earliestPitr: string, latestPitr: string, destDbName: string }; export type UpgradeModel = {}; export const systemDbs = ['master', 'msdb', 'tempdb', 'model']; export class MiaaModel extends ResourceModel { private _config: azExt.SqlMiShowResult | undefined; private _databases: DatabaseModel[] = []; private readonly _onConfigUpdated = new vscode.EventEmitter(); private readonly _onDatabasesUpdated = new vscode.EventEmitter(); private readonly _azApi: azExt.IExtension; public onConfigUpdated = this._onConfigUpdated.event; public onDatabasesUpdated = this._onDatabasesUpdated.event; public configLastUpdated: Date | undefined; public databasesLastUpdated: Date | undefined; public rpSettings: RPModel = { recoveryPointObjective: '', retentionDays: '' }; private _databaseTimeWindow: Map; private _refreshPromise: Deferred | undefined = undefined; private _pitrArgs = { destName: '', managedInstance: '', time: '', noWait: true, dryRun: false }; constructor(_controllerModel: ControllerModel, private _miaaInfo: MiaaResourceInfo, registration: Registration, private _treeDataProvider: AzureArcTreeDataProvider) { super(_controllerModel, _miaaInfo, registration); this._azApi = vscode.extensions.getExtension(azExt.extension.name)?.exports; this._databaseTimeWindow = new Map(); } /** * The username used to connect to this instance */ public get username(): string | undefined { return this._connectionProfile?.userName; } /** * The status of this instance */ public get config(): azExt.SqlMiShowResult | undefined { return this._config; } /** * The cluster endpoint of this instance */ public get clusterEndpoint(): string { return ''; // TODO chgagnon // return this._config?.cluster_endpoint || ''; } public get databases(): DatabaseModel[] { return this._databases; } /** Refreshes the model */ public async refresh(): Promise { // Only allow one refresh to be happening at a time if (this._refreshPromise) { return this._refreshPromise.promise; } this._refreshPromise = new Deferred(); try { try { let result; result = await this._azApi.az.sql.miarc.show( this.info.name, { resourceGroup: undefined, namespace: this.controllerModel.info.namespace }, this.controllerModel.azAdditionalEnvVars ); this._config = result.stdout; this.configLastUpdated = new Date(); this.rpSettings.retentionDays = this._config?.spec?.backup?.retentionPeriodInDays?.toString() ?? ''; this._onConfigUpdated.fire(this._config); this._onDatabasesUpdated.fire(this._databases); } catch (err) { // If an error occurs show a message so the user knows something failed but still // fire the event so callers can know to update (e.g. so dashboards don't show the // loading icon forever) vscode.window.showErrorMessage(loc.fetchConfigFailed(this.info.name, err)); this.configLastUpdated = new Date(); this._onConfigUpdated.fire(undefined); throw err; } // If we have an external endpoint configured then fetch the databases now if (this._config.status.endpoints.primary) { this.getDatabases(false).catch(_err => { // If an error occurs still fire the event so callers can know to // update (e.g. so dashboards don't show the loading icon forever) this.databasesLastUpdated = undefined; this._onDatabasesUpdated.fire(this._databases); }); } else { // Otherwise just fire the event so dashboards can update appropriately this.databasesLastUpdated = undefined; this._onDatabasesUpdated.fire(this._databases); } this._refreshPromise.resolve(); } catch (err) { this._refreshPromise.reject(err); throw err; } finally { this._refreshPromise = undefined; } } public async callGetDatabases(): Promise { try { await this.getDatabases(); } catch (error) { if (error instanceof UserCancelledError) { vscode.window.showWarningMessage(loc.miaaConnectionRequired); } else { vscode.window.showErrorMessage(loc.fetchDatabasesFailed(this.info.name, error)); } throw error; } } /** * Gets the list of databases and adds backup earliest and latest point in time * information, this could be used as an upper and lower time limit for restoring * backup. */ public async getDatabases(promptForConnection: boolean = true): Promise { if (!this._connectionProfile) { await this.getConnectionProfile(promptForConnection); } // We haven't connected yet so do so now and then store the ID for the active connection if (!this._activeConnectionId) { const result = await azdata.connection.connect(this._connectionProfile!, false, false); if (!result.connected) { throw new Error(result.errorMessage); } this._activeConnectionId = result.connectionId!; } const provider = azdata.dataprotocol.getProvider(this._connectionProfile!.providerName, azdata.DataProviderType.MetadataProvider); const ownerUri = await azdata.connection.getUriForConnection(this._activeConnectionId); const databases = await provider.getDatabases(ownerUri); if (!databases) { throw new Error('Could not fetch databases'); } else { if (databases.length > 0 && typeof (databases[0]) === 'object') { for (let i in databases) { const di: azdata.DatabaseInfo = databases[i]; const name = di.options['name']; await this.executeDryRun(di.options['name']); const dm: DatabaseModel = { name: name, status: di.options['state'], earliestBackup: this._databaseTimeWindow.get(name)?.[0] ?? '', lastBackup: this._databaseTimeWindow.get(name)?.[1] ?? '' }; this._databases[i] = dm; } } else { this._databases = (databases).map(db => { return { name: db, status: '-', earliestBackup: '', lastBackup: '' }; }); } } this.databasesLastUpdated = new Date(); this._onDatabasesUpdated.fire(this._databases); } protected createConnectionProfile(): azdata.IConnectionProfile { const ipAndPort = parseIpAndPort(this.config?.status.endpoints.primary || ''); return { serverName: `${ipAndPort.ip},${ipAndPort.port}`, databaseName: '', authenticationType: azdata.connection.AuthenticationType.SqlLogin, providerName: loc.miaaProviderName, connectionName: '', userName: this._miaaInfo.userName || '', password: '', savePassword: true, groupFullName: undefined, saveProfile: true, id: '', groupId: undefined, options: { encrypt: this._miaaInfo.encrypt || true, trustServerCertificate: this._miaaInfo.trustServerCertificate || false } }; } protected async promptForConnection(connectionProfile: azdata.IConnectionProfile): Promise { const connectToSqlDialog = new ConnectToMiaaSqlDialog(this.controllerModel, this); connectToSqlDialog.showDialog(loc.connectToMSSql(this.info.name), connectionProfile); let profileFromDialog = await connectToSqlDialog.waitForClose(); if (profileFromDialog) { this.updateConnectionProfile(profileFromDialog); } else { throw new UserCancelledError(); } } protected async updateConnectionProfile(connectionProfile: azdata.IConnectionProfile): Promise { this._connectionProfile = connectionProfile; this._activeConnectionId = connectionProfile.id; this.info.connectionId = connectionProfile.id; this._miaaInfo.userName = connectionProfile.userName; this._miaaInfo.encrypt = connectionProfile.options.encrypt; this._miaaInfo.trustServerCertificate = connectionProfile.options.trustServerCertificate; await this._treeDataProvider.saveControllers(); } protected async executeDryRun(dbName: string): Promise { // Allow next dry Run to be executed only after 5(300000 ms ) minutes from current time as the log backups are // generated only at 5 minutes interval if ((systemDbs.indexOf(dbName) === -1) && (Date.now() - getTimeStamp(this._databaseTimeWindow.get(dbName)?.[1]) >= 300000)) { try { //Execute dryRun for earliestTime and save latest time as well so there is one call to az cli this._pitrArgs.destName = dbName + '-' + Date.now().toString(); this._pitrArgs.managedInstance = this.info.name; this._pitrArgs.time = new Date().toISOString(); this._pitrArgs.noWait = false; this._pitrArgs.dryRun = true; const result = await this._azApi.az.sql.midbarc.restore( dbName, this._pitrArgs, this.controllerModel.info.namespace, this.controllerModel.azAdditionalEnvVars); const restoreResult = result.stdout; if (restoreResult) { const earliestTime = restoreResult['earliestRestoreTime']; const latestTime = restoreResult['latestRestoreTime']; console.log(loc.earliestPitrRestorePoint + '-' + dbName + ':' + earliestTime); console.log(loc.latestpitrRestorePoint + '-' + dbName + ':' + latestTime); this._databaseTimeWindow.set(dbName, [earliestTime, latestTime]); } } catch (err) { console.log(loc.pitr + ' ' + loc.failed + ':' + err); this._databaseTimeWindow.set(dbName, ['', '']); } } } }