diff --git a/extensions/big-data-cluster/package.json b/extensions/big-data-cluster/package.json index 86d56c344d..3c6ed62131 100644 --- a/extensions/big-data-cluster/package.json +++ b/extensions/big-data-cluster/package.json @@ -49,6 +49,14 @@ { "command": "bigDataClusters.command.mount", "when": "false" + }, + { + "command": "bigDataClusters.command.refreshmount", + "when": "false" + }, + { + "command": "bigDataClusters.command.deletemount", + "when": "false" } ], "view/title": [ @@ -80,6 +88,16 @@ "command": "bigDataClusters.command.mount", "when": "nodeType=~/^mssqlCluster/ && nodeType!=mssqlCluster:message && nodeSubType=~/^(?!:mount).*$/", "group": "1mssqlCluster@10" + }, + { + "command": "bigDataClusters.command.refreshmount", + "when": "nodeType == mssqlCluster:folder && nodeSubType==:mount:", + "group": "1mssqlCluster@11" + }, + { + "command": "bigDataClusters.command.deletemount", + "when": "nodeType == mssqlCluster:folder && nodeSubType==:mount:", + "group": "1mssqlCluster@12" } ] }, @@ -112,6 +130,14 @@ { "command": "bigDataClusters.command.mount", "title": "%command.mount.title%" + }, + { + "command": "bigDataClusters.command.refreshmount", + "title": "%command.refreshmount.title%" + }, + { + "command": "bigDataClusters.command.deletemount", + "title": "%command.deletemount.title%" } ] }, diff --git a/extensions/big-data-cluster/package.nls.json b/extensions/big-data-cluster/package.nls.json index 80563511d0..b39b874efd 100644 --- a/extensions/big-data-cluster/package.nls.json +++ b/extensions/big-data-cluster/package.nls.json @@ -5,5 +5,7 @@ "command.deleteController.title" : "Delete", "command.refreshController.title" : "Refresh", "command.manageController.title" : "Manage", - "command.mount.title" : "Mount HDFS" + "command.mount.title" : "Mount HDFS", + "command.refreshmount.title" : "Refresh Mount", + "command.deletemount.title" : "Delete Mount" } diff --git a/extensions/big-data-cluster/src/bigDataCluster/controller/clusterControllerApi.ts b/extensions/big-data-cluster/src/bigDataCluster/controller/clusterControllerApi.ts index ac0b147d6d..1467503944 100644 --- a/extensions/big-data-cluster/src/bigDataCluster/controller/clusterControllerApi.ts +++ b/extensions/big-data-cluster/src/bigDataCluster/controller/clusterControllerApi.ts @@ -214,6 +214,37 @@ export class ClusterController { } } + public async refreshMount(mountPath: string): Promise { + let auth = await this.authPromise; + const api = new DefaultApiWrapper(this.username, this.password, this._url, auth); + + try { + const mountStatus = await api.refreshMount('', '', mountPath); + return { + response: mountStatus.response, + status: mountStatus.body + }; + } catch (error) { + // TODO handle 401 by reauthenticating + throw new ControllerError(error, localize('bdc.error.refreshHdfs', "Error refreshing mount")); + } + } + + public async deleteMount(mountPath: string): Promise { + let auth = await this.authPromise; + const api = new DefaultApiWrapper(this.username, this.password, this._url, auth); + + try { + const mountStatus = await api.deleteMount('', '', mountPath); + return { + response: mountStatus.response, + status: mountStatus.body + }; + } catch (error) { + // TODO handle 401 by reauthenticating + throw new ControllerError(error, localize('bdc.error.deleteHdfs', "Error deleting mount")); + } + } } /** * Fixes missing protocol and wrong character for port entered by user diff --git a/extensions/big-data-cluster/src/bigDataCluster/dialog/mountHdfsDialog.ts b/extensions/big-data-cluster/src/bigDataCluster/dialog/mountHdfsDialog.ts index 35bad4dad4..7660981b96 100644 --- a/extensions/big-data-cluster/src/bigDataCluster/dialog/mountHdfsDialog.ts +++ b/extensions/big-data-cluster/src/bigDataCluster/dialog/mountHdfsDialog.ts @@ -6,13 +6,15 @@ import * as vscode from 'vscode'; import * as azdata from 'azdata'; import * as nls from 'vscode-nls'; -import { ClusterController, ControllerError, MountInfo, MountState } from '../controller/clusterControllerApi'; +import { ClusterController, ControllerError, MountInfo, MountState, IEndPointsResponse } from '../controller/clusterControllerApi'; import { AuthType } from '../constants'; const localize = nls.loadMessageBundle(); const basicAuthDisplay = localize('basicAuthName', "Basic"); const integratedAuthDisplay = localize('integratedAuthName', "Windows Authentication"); +const mountConfigutationTitle = localize('mount.main.section', "Mount Configuration"); +const hdfsPathTitle = localize('mount.hdfsPath', "HDFS Path"); function getAuthCategory(name: AuthType): azdata.CategoryValue { if (name === 'basic') { @@ -125,6 +127,31 @@ abstract class HdfsDialogModelBase { return new ClusterController(this.props.url, this.props.auth, this.props.username, this.props.password, true); } + protected async createAndVerifyControllerConnection(): Promise { + // We pre-fetch the endpoints here to verify that the information entered is correct (the user is able to connect) + let controller = this.createController(); + let response: IEndPointsResponse; + try { + response = await controller.getEndPoints(); + if (!response || !response.endPoints) { + throw new Error(localize('mount.hdfs.loginerror1', "Login to controller failed")); + } + } catch (err) { + throw new Error(localize('mount.hdfs.loginerror2', "Login to controller failed: {0}", err.message)); + } + return controller; + } + + protected throwIfMissingUsernamePassword(): void { + if (this.props.auth === 'basic') { + // Verify username and password as we can't make them required in the UI + if (!this.props.username) { + throw new Error(localize('err.controller.username.required', "Username is required")); + } else if (!this.props.password) { + throw new Error(localize('err.controller.password.required', "Password is required")); + } + } + } } export class MountHdfsDialogModel extends HdfsDialogModelBase { @@ -135,37 +162,26 @@ export class MountHdfsDialogModel extends HdfsDialogModelBase { - if (this.props.auth === 'basic') { - // Verify username and password as we can't make them required in the UI - if (!this.props.username) { - throw new Error(localize('err.controller.username.required', "Username is required")); - } else if (!this.props.password) { - throw new Error(localize('err.controller.password.required', "Password is required")); - } - } + this.throwIfMissingUsernamePassword(); // Validate credentials this.credentials = convertCredsToJson(this.props.credentials); // We pre-fetch the endpoints here to verify that the information entered is correct (the user is able to connect) - let controller = this.createController(); - let response = await controller.getEndPoints(); - if (response && response.endPoints) { - if (this._canceled) { - return; - } - // - azdata.tasks.startBackgroundOperation( - { - connection: undefined, - displayName: localize('mount.task.name', "Mounting HDFS folder on path {0}", this.props.hdfsPath), - description: '', - isCancelable: false, - operation: op => { - this.onSubmit(controller, op); - } - } - ); + let controller = await this.createAndVerifyControllerConnection(); + if (this._canceled) { + return; } + azdata.tasks.startBackgroundOperation( + { + connection: undefined, + displayName: localize('mount.task.name', "Mounting HDFS folder on path {0}", this.props.hdfsPath), + description: '', + isCancelable: false, + operation: op => { + this.onSubmit(controller, op); + } + } + ); } private async onSubmit(controller: ClusterController, op: azdata.BackgroundOperation): Promise { @@ -295,9 +311,9 @@ abstract class HdfsDialogBase { this.getMainSection(), connectionSection ]).withLayout({ width: '100%' }).component(); - this.onAuthChanged(); await view.initializeModel(formModel); + this.onAuthChanged(); }); this.dialog.registerCloseValidator(async () => await this.validate()); @@ -329,6 +345,16 @@ abstract class HdfsDialogBase { await this.model.onCancel(); } } + + protected async reportError(error: any): Promise { + this.dialog.message = { + text: (typeof error === 'string') ? error : error.message, + level: azdata.window.MessageLevel.Error + }; + if (this.model && this.model.onError) { + await this.model.onError(error as ControllerError); + } + } } export class MountHdfsDialog extends HdfsDialogBase { private pathInputBox: azdata.InputBoxComponent; @@ -340,9 +366,12 @@ export class MountHdfsDialog extends HdfsDialogBase { } protected getMainSection(): azdata.FormComponentGroup { + const newMountName = '/mymount'; + let pathVal = this.model.props.hdfsPath; + pathVal = (!pathVal || pathVal === '/') ? newMountName : (pathVal + newMountName); this.pathInputBox = this.uiModelBuilder.inputBox() .withProperties({ - value: this.model.props.hdfsPath + value: pathVal }).component(); this.remoteUriInputBox = this.uiModelBuilder.inputBox() .withProperties({ @@ -360,7 +389,7 @@ export class MountHdfsDialog extends HdfsDialogBase { components: [ { component: this.pathInputBox, - title: localize('mount.hdfsPath', "HDFS Path"), + title: hdfsPathTitle, required: true }, { component: this.remoteUriInputBox, @@ -372,7 +401,7 @@ export class MountHdfsDialog extends HdfsDialogBase { required: false } ], - title: localize('mount.main.section', "Mount Configuration") + title: mountConfigutationTitle }; } @@ -389,14 +418,168 @@ export class MountHdfsDialog extends HdfsDialogBase { }); return true; } catch (error) { - this.dialog.message = { - text: (typeof error === 'string') ? error : error.message, - level: azdata.window.MessageLevel.Error - }; - if (this.model && this.model.onError) { - await this.model.onError(error as ControllerError); - } + await this.reportError(error); return false; } } } + +export class RefreshMountDialog extends HdfsDialogBase { + private pathInputBox: azdata.InputBoxComponent; + + constructor(model: RefreshMountModel) { + super(localize('refreshmount.dialog.title', "Refresh Mount"), model); + } + + protected getMainSection(): azdata.FormComponentGroup { + this.pathInputBox = this.uiModelBuilder.inputBox() + .withProperties({ + value: this.model.props.hdfsPath + }).component(); + return { + components: [ + { + component: this.pathInputBox, + title: hdfsPathTitle, + required: true + } + ], + title: mountConfigutationTitle + }; + } + + protected async validate(): Promise { + try { + await this.model.onComplete({ + url: this.urlInputBox && this.urlInputBox.value, + auth: this.authValue, + username: this.usernameInputBox && this.usernameInputBox.value, + password: this.passwordInputBox && this.passwordInputBox.value, + hdfsPath: this.pathInputBox && this.pathInputBox.value + }); + return true; + } catch (error) { + await this.reportError(error); + return false; + } + } +} + +export class RefreshMountModel extends HdfsDialogModelBase { + + constructor(props: MountHdfsProperties) { + super(props); + } + + protected async handleCompleted(): Promise { + this.throwIfMissingUsernamePassword(); + + // We pre-fetch the endpoints here to verify that the information entered is correct (the user is able to connect) + let controller = await this.createAndVerifyControllerConnection(); + if (this._canceled) { + return; + } + azdata.tasks.startBackgroundOperation( + { + connection: undefined, + displayName: localize('refreshmount.task.name', "Refreshing HDFS Mount on path {0}", this.props.hdfsPath), + description: '', + isCancelable: false, + operation: op => { + this.onSubmit(controller, op); + } + } + ); + } + + private async onSubmit(controller: ClusterController, op: azdata.BackgroundOperation): Promise { + try { + await controller.refreshMount(this.props.hdfsPath); + op.updateStatus(azdata.TaskStatus.Succeeded, localize('refreshmount.task.submitted', "Refresh mount request submitted")); + } catch (error) { + const errMsg = (error instanceof Error) ? error.message : error; + vscode.window.showErrorMessage(errMsg); + op.updateStatus(azdata.TaskStatus.Failed, errMsg); + } + } +} + +export class DeleteMountDialog extends HdfsDialogBase { + private pathInputBox: azdata.InputBoxComponent; + + constructor(model: DeleteMountModel) { + super(localize('deleteMount.dialog.title', "Delete Mount"), model); + } + + protected getMainSection(): azdata.FormComponentGroup { + this.pathInputBox = this.uiModelBuilder.inputBox() + .withProperties({ + value: this.model.props.hdfsPath + }).component(); + return { + components: [ + { + component: this.pathInputBox, + title: hdfsPathTitle, + required: true + } + ], + title: mountConfigutationTitle + }; + } + + protected async validate(): Promise { + try { + await this.model.onComplete({ + url: this.urlInputBox && this.urlInputBox.value, + auth: this.authValue, + username: this.usernameInputBox && this.usernameInputBox.value, + password: this.passwordInputBox && this.passwordInputBox.value, + hdfsPath: this.pathInputBox && this.pathInputBox.value + }); + return true; + } catch (error) { + await this.reportError(error); + return false; + } + } +} + +export class DeleteMountModel extends HdfsDialogModelBase { + + constructor(props: MountHdfsProperties) { + super(props); + } + + protected async handleCompleted(): Promise { + this.throwIfMissingUsernamePassword(); + + // We pre-fetch the endpoints here to verify that the information entered is correct (the user is able to connect) + let controller = await this.createAndVerifyControllerConnection(); + if (this._canceled) { + return; + } + azdata.tasks.startBackgroundOperation( + { + connection: undefined, + displayName: localize('deletemount.task.name', "Deleting HDFS Mount on path {0}", this.props.hdfsPath), + description: '', + isCancelable: false, + operation: op => { + this.onSubmit(controller, op); + } + } + ); + } + + private async onSubmit(controller: ClusterController, op: azdata.BackgroundOperation): Promise { + try { + await controller.deleteMount(this.props.hdfsPath); + op.updateStatus(azdata.TaskStatus.Succeeded, localize('deletemount.task.submitted', "Delete mount request submitted")); + } catch (error) { + const errMsg = (error instanceof Error) ? error.message : error; + vscode.window.showErrorMessage(errMsg); + op.updateStatus(azdata.TaskStatus.Failed, errMsg); + } + } +} diff --git a/extensions/big-data-cluster/src/extension.ts b/extensions/big-data-cluster/src/extension.ts index d08d2e8918..1a1f8dd575 100644 --- a/extensions/big-data-cluster/src/extension.ts +++ b/extensions/big-data-cluster/src/extension.ts @@ -15,7 +15,7 @@ import { AddControllerDialogModel, AddControllerDialog } from './bigDataCluster/ import { ControllerNode } from './bigDataCluster/tree/controllerTreeNode'; import { BdcDashboard } from './bigDataCluster/dialog/bdcDashboard'; import { BdcDashboardModel } from './bigDataCluster/dialog/bdcDashboardModel'; -import { MountHdfsDialogModel, MountHdfsProperties, MountHdfsDialog } from './bigDataCluster/dialog/mountHdfsDialog'; +import { MountHdfsDialogModel as MountHdfsModel, MountHdfsProperties, MountHdfsDialog, DeleteMountDialog, DeleteMountModel, RefreshMountDialog, RefreshMountModel } from './bigDataCluster/dialog/mountHdfsDialog'; import { getControllerEndpoint } from './bigDataCluster/utils'; const localize = nls.loadMessageBundle(); @@ -25,6 +25,8 @@ const DeleteControllerCommand = 'bigDataClusters.command.deleteController'; const RefreshControllerCommand = 'bigDataClusters.command.refreshController'; const ManageControllerCommand = 'bigDataClusters.command.manageController'; const MountHdfsCommand = 'bigDataClusters.command.mount'; +const RefreshMountCommand = 'bigDataClusters.command.refreshmount'; +const DeleteMountCommand = 'bigDataClusters.command.deletemount'; const endpointNotFoundError = localize('mount.error.endpointNotFound', "Controller endpoint information was not found"); @@ -69,13 +71,43 @@ function registerCommands(context: vscode.ExtensionContext, treeDataProvider: Co vscode.commands.registerCommand(MountHdfsCommand, e => mountHdfs(e).catch(error => { vscode.window.showErrorMessage(error instanceof Error ? error.message : error); })); + vscode.commands.registerCommand(RefreshMountCommand, e => refreshMount(e).catch(error => { + vscode.window.showErrorMessage(error instanceof Error ? error.message : error); + })); + vscode.commands.registerCommand(DeleteMountCommand, e => deleteMount(e).catch(error => { + vscode.window.showErrorMessage(error instanceof Error ? error.message : error); + })); } async function mountHdfs(explorerContext?: azdata.ObjectExplorerContext): Promise { + let mountProps = await getMountProps(explorerContext); + if (mountProps) { + let dialog = new MountHdfsDialog(new MountHdfsModel(mountProps)); + dialog.showDialog(); + } +} + +async function refreshMount(explorerContext?: azdata.ObjectExplorerContext): Promise { + let mountProps = await getMountProps(explorerContext); + if (mountProps) { + let dialog = new RefreshMountDialog(new RefreshMountModel(mountProps)); + dialog.showDialog(); + } +} + +async function deleteMount(explorerContext?: azdata.ObjectExplorerContext): Promise { + let mountProps = await getMountProps(explorerContext); + if (mountProps) { + let dialog = new DeleteMountDialog(new DeleteMountModel(mountProps)); + dialog.showDialog(); + } +} + +async function getMountProps(explorerContext?: azdata.ObjectExplorerContext): Promise { let endpoint = await lookupController(explorerContext); if (!endpoint) { vscode.window.showErrorMessage(endpointNotFoundError); - return; + return undefined; } let profile = explorerContext.connectionProfile; let mountProps: MountHdfsProperties = { @@ -85,8 +117,7 @@ async function mountHdfs(explorerContext?: azdata.ObjectExplorerContext): Promis password: profile.password, hdfsPath: getHdsfPath(explorerContext.nodeInfo.nodePath) }; - let dialog = new MountHdfsDialog(new MountHdfsDialogModel(mountProps)); - dialog.showDialog(); + return mountProps; } function getHdsfPath(nodePath: string): string {