Files
azuredatastudio/extensions/arc/src/models/miaaModel.ts

280 lines
10 KiB
TypeScript

/*---------------------------------------------------------------------------------------------
* 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<azExt.SqlMiShowResult | undefined>();
private readonly _onDatabasesUpdated = new vscode.EventEmitter<DatabaseModel[]>();
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<string, string[]>;
private _refreshPromise: Deferred<void> | 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 = <azExt.IExtension>vscode.extensions.getExtension(azExt.extension.name)?.exports;
this._databaseTimeWindow = new Map<string, string[]>();
}
/**
* 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<void> {
// 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<void> {
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<void> {
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<azdata.MetadataProvider>(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 = <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 = (<string[]>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<void> {
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<void> {
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<void> {
// 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, ['', '']);
}
}
}
}