From a74df9b26db0d6c2e2df2398f2c37c5957bf8302 Mon Sep 17 00:00:00 2001 From: Ricardo Marques Date: Thu, 4 Jul 2019 16:56:24 +0100 Subject: [PATCH] mgr/dashboard: Evict a CephFS client Fixes: https://tracker.ceph.com/issues/24892 Signed-off-by: Ricardo Marques --- qa/tasks/mgr/dashboard/test_cephfs.py | 5 ++ .../mgr/dashboard/controllers/cephfs.py | 25 ++++++++ .../cephfs-clients.component.html | 9 ++- .../cephfs-clients.component.spec.ts | 53 +++++++++++++++- .../cephfs-clients.component.ts | 63 ++++++++++++++++++- .../src/app/shared/api/cephfs.service.ts | 4 ++ .../src/app/shared/constants/app.constants.ts | 3 + 7 files changed, 158 insertions(+), 4 deletions(-) diff --git a/qa/tasks/mgr/dashboard/test_cephfs.py b/qa/tasks/mgr/dashboard/test_cephfs.py index e4a2ed4cc44..6deab97d47c 100644 --- a/qa/tasks/mgr/dashboard/test_cephfs.py +++ b/qa/tasks/mgr/dashboard/test_cephfs.py @@ -27,6 +27,11 @@ class CephfsTest(DashboardTestCase): self.assertIn('status', data) self.assertIn('data', data) + def test_cephfs_evict_client_does_not_exist(self): + fs_id = self.fs.get_namespace_id() + data = self._delete("/api/cephfs/{}/client/1234".format(fs_id)) + self.assertStatus(404) + def test_cephfs_get(self): fs_id = self.fs.get_namespace_id() data = self._get("/api/cephfs/{}/".format(fs_id)) diff --git a/src/pybind/mgr/dashboard/controllers/cephfs.py b/src/pybind/mgr/dashboard/controllers/cephfs.py index e16f04c61cc..1e5d806aa47 100644 --- a/src/pybind/mgr/dashboard/controllers/cephfs.py +++ b/src/pybind/mgr/dashboard/controllers/cephfs.py @@ -37,6 +37,13 @@ class CephFS(RESTController): return self._clients(fs_id) + @RESTController.Resource('DELETE', path='/client/{client_id}') + def evict(self, fs_id, client_id): + fs_id = self.fs_id_to_int(fs_id) + client_id = self.client_id_to_int(client_id) + + return self._evict(fs_id, client_id) + @RESTController.Resource('GET') def mds_counters(self, fs_id): """ @@ -86,6 +93,15 @@ class CephFS(RESTController): msg="Invalid cephfs ID {}".format(fs_id), component='cephfs') + @staticmethod + def client_id_to_int(client_id): + try: + return int(client_id) + except ValueError: + raise DashboardException(code='invalid_cephfs_client_id', + msg="Invalid cephfs client ID {}".format(client_id), + component='cephfs') + def _get_mds_names(self, filesystem_id=None): names = [] @@ -282,6 +298,15 @@ class CephFS(RESTController): 'data': clients } + def _evict(self, fs_id, client_id): + clients = self._clients(fs_id) + if not [c for c in clients['data'] if c['id'] == client_id]: + raise cherrypy.HTTPError(404, + "Client {0} does not exist in cephfs {1}".format(client_id, + fs_id)) + CephService.send_command('mds', 'client evict', + srv_spec='{0}:0'.format(fs_id), id=client_id) + class CephFSClients(object): def __init__(self, module_inst, fscid): diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-clients/cephfs-clients.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-clients/cephfs-clients.component.html index 8b84e618f5b..295f1b50d8c 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-clients/cephfs-clients.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-clients/cephfs-clients.component.html @@ -2,5 +2,12 @@ + (fetchData)="refresh()" + selectionType="single" + (updateSelection)="updateSelection($event)"> + + diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-clients/cephfs-clients.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-clients/cephfs-clients.component.spec.ts index 4b5ab9a16bc..66c525d61ad 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-clients/cephfs-clients.component.spec.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-clients/cephfs-clients.component.spec.ts @@ -4,7 +4,13 @@ import { RouterTestingModule } from '@angular/router/testing'; import { BsDropdownModule } from 'ngx-bootstrap/dropdown'; -import { configureTestBed, i18nProviders } from '../../../../testing/unit-test-helper'; +import { ToastrModule } from 'ngx-toastr'; +import { + configureTestBed, + i18nProviders, + PermissionHelper +} from '../../../../testing/unit-test-helper'; +import { TableActionsComponent } from '../../../shared/datatable/table-actions/table-actions.component'; import { SharedModule } from '../../../shared/shared.module'; import { CephfsClientsComponent } from './cephfs-clients.component'; @@ -15,6 +21,7 @@ describe('CephfsClientsComponent', () => { configureTestBed({ imports: [ RouterTestingModule, + ToastrModule.forRoot(), BsDropdownModule.forRoot(), SharedModule, HttpClientTestingModule @@ -26,10 +33,52 @@ describe('CephfsClientsComponent', () => { beforeEach(() => { fixture = TestBed.createComponent(CephfsClientsComponent); component = fixture.componentInstance; - fixture.detectChanges(); }); it('should create', () => { + fixture.detectChanges(); expect(component).toBeTruthy(); }); + + it('should test all TableActions combinations', () => { + const permissionHelper: PermissionHelper = new PermissionHelper(component.permission); + const tableActions: TableActionsComponent = permissionHelper.setPermissionsAndGetActions( + component.tableActions + ); + + expect(tableActions).toEqual({ + 'create,update,delete': { + actions: ['Evict'], + primary: { multiple: 'Evict', executing: 'Evict', single: 'Evict', no: 'Evict' } + }, + 'create,update': { + actions: ['Evict'], + primary: { multiple: 'Evict', executing: 'Evict', single: 'Evict', no: 'Evict' } + }, + 'create,delete': { + actions: [], + primary: { multiple: '', executing: '', single: '', no: '' } + }, + create: { + actions: [], + primary: { multiple: '', executing: '', single: '', no: '' } + }, + 'update,delete': { + actions: ['Evict'], + primary: { multiple: 'Evict', executing: 'Evict', single: 'Evict', no: 'Evict' } + }, + update: { + actions: ['Evict'], + primary: { multiple: 'Evict', executing: 'Evict', single: 'Evict', no: 'Evict' } + }, + delete: { + actions: [], + primary: { multiple: '', executing: '', single: '', no: '' } + }, + 'no-permissions': { + actions: [], + primary: { multiple: '', executing: '', single: '', no: '' } + } + }); + }); }); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-clients/cephfs-clients.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-clients/cephfs-clients.component.ts index af8181ac385..31891ae2f9b 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-clients/cephfs-clients.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-clients/cephfs-clients.component.ts @@ -2,8 +2,18 @@ import { Component, Input, OnInit } from '@angular/core'; import { I18n } from '@ngx-translate/i18n-polyfill'; +import { BsModalRef, BsModalService } from 'ngx-bootstrap/modal'; import { CephfsService } from '../../../shared/api/cephfs.service'; +import { CriticalConfirmationModalComponent } from '../../../shared/components/critical-confirmation-modal/critical-confirmation-modal.component'; +import { ActionLabelsI18n } from '../../../shared/constants/app.constants'; +import { Icons } from '../../../shared/enum/icons.enum'; +import { NotificationType } from '../../../shared/enum/notification-type.enum'; import { ViewCacheStatus } from '../../../shared/enum/view-cache-status.enum'; +import { CdTableAction } from '../../../shared/models/cd-table-action'; +import { CdTableSelection } from '../../../shared/models/cd-table-selection'; +import { Permission } from '../../../shared/models/permissions'; +import { AuthStorageService } from '../../../shared/services/auth-storage.service'; +import { NotificationService } from '../../../shared/services/notification.service'; @Component({ selector: 'cd-cephfs-clients', @@ -14,10 +24,30 @@ export class CephfsClientsComponent implements OnInit { @Input() id: number; + permission: Permission; + tableActions: CdTableAction[]; + modalRef: BsModalRef; clients: any; viewCacheStatus: ViewCacheStatus; + selection = new CdTableSelection(); - constructor(private cephfsService: CephfsService, private i18n: I18n) {} + constructor( + private cephfsService: CephfsService, + private modalService: BsModalService, + private notificationService: NotificationService, + private authStorageService: AuthStorageService, + private i18n: I18n, + private actionLabels: ActionLabelsI18n + ) { + this.permission = this.authStorageService.getPermissions().cephfs; + const evictAction: CdTableAction = { + permission: 'update', + icon: Icons.signOut, + click: () => this.evictClientModal(), + name: this.actionLabels.EVICT + }; + this.tableActions = [evictAction]; + } ngOnInit() { this.clients = { @@ -42,4 +72,35 @@ export class CephfsClientsComponent implements OnInit { this.clients.data = data.data; }); } + + updateSelection(selection: CdTableSelection) { + this.selection = selection; + } + + evictClient(clientId: number) { + this.cephfsService.evictClient(this.id, clientId).subscribe( + () => { + this.refresh(); + this.modalRef.hide(); + this.notificationService.show( + NotificationType.success, + this.i18n('Evicted client "{{clientId}}"', { clientId: clientId }) + ); + }, + () => { + this.modalRef.content.stopLoadingSpinner(); + } + ); + } + + evictClientModal() { + const clientId = this.selection.first().id; + this.modalRef = this.modalService.show(CriticalConfirmationModalComponent, { + initialState: { + itemDescription: 'client', + actionDescription: 'evict', + submitAction: () => this.evictClient(clientId) + } + }); + } } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/cephfs.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/cephfs.service.ts index 745f2589243..a0bbe5c7852 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/cephfs.service.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/cephfs.service.ts @@ -23,6 +23,10 @@ export class CephfsService { return this.http.get(`${this.baseURL}/${id}/clients`); } + evictClient(fsId, clientId) { + return this.http.delete(`${this.baseURL}/${fsId}/client/${clientId}`); + } + getMdsCounters(id) { return this.http.get(`${this.baseURL}/${id}/mds_counters`); } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/constants/app.constants.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/constants/app.constants.ts index 438f9fc5cb3..53f2c124108 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/constants/app.constants.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/constants/app.constants.ts @@ -59,6 +59,7 @@ export enum ActionLabels { COPY = 'Copy', CLONE = 'Clone', UPDATE = 'Update', + EVICT = 'Evict', /* Read-only */ SHOW = 'Show', @@ -85,6 +86,7 @@ export class ActionLabelsI18n { CLONE: string; DEEP_SCRUB: string; DESTROY: string; + EVICT: string; FLATTEN: string; MARK_DOWN: string; MARK_IN: string; @@ -126,6 +128,7 @@ export class ActionLabelsI18n { this.COPY = this.i18n('Copy'); this.DEEP_SCRUB = this.i18n('Deep Scrub'); this.DESTROY = this.i18n('Destroy'); + this.EVICT = this.i18n('Evict'); this.FLATTEN = this.i18n('Flatten'); this.MARK_DOWN = this.i18n('Mark Down'); this.MARK_IN = this.i18n('Mark In'); -- 2.39.5