]> git.apps.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard: Evict a CephFS client 28898/head
authorRicardo Marques <rimarques@suse.com>
Thu, 4 Jul 2019 15:56:24 +0000 (16:56 +0100)
committerRicardo Marques <rimarques@suse.com>
Fri, 19 Jul 2019 08:00:15 +0000 (09:00 +0100)
Fixes: https://tracker.ceph.com/issues/24892
Signed-off-by: Ricardo Marques <rimarques@suse.com>
qa/tasks/mgr/dashboard/test_cephfs.py
src/pybind/mgr/dashboard/controllers/cephfs.py
src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-clients/cephfs-clients.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-clients/cephfs-clients.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-clients/cephfs-clients.component.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/api/cephfs.service.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/constants/app.constants.ts

index e4a2ed4cc447331b30b284d8ad9c6f3d214fdad1..6deab97d47c8fe1b76213daa4fc9f0a611adcc68 100644 (file)
@@ -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))
index e16f04c61cc4d16aef11e565b82b13963d061b86..1e5d806aa478da306c62c2b026f7e89362a58fc7 100644 (file)
@@ -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):
index 8b84e618f5b25422eb2a22e35e6f56fd083e5f8b..295f1b50d8c4235d4e404d30f2856266bbc87180 100644 (file)
@@ -2,5 +2,12 @@
 
 <cd-table [data]="clients.data"
           [columns]="clients.columns"
-          (fetchData)="refresh()">
+          (fetchData)="refresh()"
+          selectionType="single"
+          (updateSelection)="updateSelection($event)">
+  <cd-table-actions class="table-actions"
+                    [permission]="permission"
+                    [selection]="selection"
+                    [tableActions]="tableActions">
+  </cd-table-actions>
 </cd-table>
index 4b5ab9a16bc9e330ea1441236f68d081410acd34..66c525d61adf6d9feffd7e68d9b228faec0d970a 100644 (file)
@@ -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: '' }
+      }
+    });
+  });
 });
index af8181ac385148645bd2291d14955fab047d2fb9..31891ae2f9b2ef1278f772fa9411868b917ce601 100644 (file)
@@ -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)
+      }
+    });
+  }
 }
index 745f2589243db13c23ce1ce1b96077be18c179d6..a0bbe5c7852e73b41997f3e6589fcb2563e65c0c 100644 (file)
@@ -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`);
   }
index 438f9fc5cb31047b5624287038008e7e46297dac..53f2c124108dcd045d3121c4afe871fed0963838 100644 (file)
@@ -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');