Skip to content

Commit

Permalink
mgr/dashboard: Evict a CephFS client
Browse files Browse the repository at this point in the history
Fixes: https://tracker.ceph.com/issues/24892

Signed-off-by: Ricardo Marques <rimarques@suse.com>
  • Loading branch information
ricardoasmarques committed Jul 12, 2019
1 parent 910da20 commit ce3087f
Show file tree
Hide file tree
Showing 7 changed files with 172 additions and 4 deletions.
5 changes: 5 additions & 0 deletions qa/tasks/mgr/dashboard/test_cephfs.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@ def test_cephfs_clients(self):
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))
Expand Down
25 changes: 25 additions & 0 deletions src/pybind/mgr/dashboard/controllers/cephfs.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,13 @@ def clients(self, fs_id):

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):
"""
Expand Down Expand Up @@ -86,6 +93,15 @@ def fs_id_to_int(fs_id):
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 = []

Expand Down Expand Up @@ -282,6 +298,15 @@ def _clients(self, fs_id):
'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):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,15 @@ import { RouterTestingModule } from '@angular/router/testing';

import { BsDropdownModule } from 'ngx-bootstrap/dropdown';

import { configureTestBed, i18nProviders } from '../../../../testing/unit-test-helper';
import { By } from '@angular/platform-browser';
import { ToastrModule } from 'ngx-toastr';
import {
configureTestBed,
i18nProviders,
PermissionHelper
} from '../../../../testing/unit-test-helper';
import { ActionLabels } from '../../../shared/constants/app.constants';
import { TableActionsComponent } from '../../../shared/datatable/table-actions/table-actions.component';
import { SharedModule } from '../../../shared/shared.module';
import { CephfsClientsComponent } from './cephfs-clients.component';

Expand All @@ -15,6 +23,7 @@ describe('CephfsClientsComponent', () => {
configureTestBed({
imports: [
RouterTestingModule,
ToastModule.forRoot(),
BsDropdownModule.forRoot(),
SharedModule,
HttpClientTestingModule
Expand All @@ -26,10 +35,64 @@ describe('CephfsClientsComponent', () => {
beforeEach(() => {
fixture = TestBed.createComponent(CephfsClientsComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});

it('should create', () => {
fixture.detectChanges();
expect(component).toBeTruthy();
});

describe('show action buttons and drop down actions depending on permissions', () => {
let tableActions: TableActionsComponent;
let permissionHelper: PermissionHelper;

const getTableActionComponent = (): TableActionsComponent => {
fixture.detectChanges();
return fixture.debugElement.query(By.directive(TableActionsComponent)).componentInstance;
};

beforeEach(() => {
permissionHelper = new PermissionHelper(component.permission, () =>
getTableActionComponent()
);
});

describe('with update', () => {
beforeEach(() => {
tableActions = permissionHelper.setPermissionsAndGetActions(1, 1, 1);
});

it(`show 'Evict' as main action`, () => {
permissionHelper.testScenarios({
fn: () => tableActions.getCurrentButton().name,
single: ActionLabels.EVICT,
empty: ActionLabels.EVICT
});
});

it(`'Evict' is the only available action`, () => {
expect(tableActions.tableActions.length).toBe(1);
expect(tableActions.tableActions).toEqual(component.tableActions);
});
});

describe('without update', () => {
beforeEach(() => {
tableActions = permissionHelper.setPermissionsAndGetActions(1, 0, 1);
});

it('shows no main action', () => {
permissionHelper.testScenarios({
fn: () => tableActions.getCurrentButton(),
single: undefined,
empty: undefined
});
});

it('shows no actions', () => {
expect(tableActions.tableActions.length).toBe(0);
expect(tableActions.tableActions).toEqual([]);
});
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -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 = {
Expand All @@ -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)
}
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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`);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ export enum ActionLabels {
/* Non-standard actions */
COPY = 'Copy',
CLONE = 'Clone',
EVICT = 'Evict',

/* Read-only */
SHOW = 'Show',
Expand All @@ -84,6 +85,7 @@ export class ActionLabelsI18n {
CLONE: string;
DEEP_SCRUB: string;
DESTROY: string;
EVICT: string;
FLATTEN: string;
MARK_DOWN: string;
MARK_IN: string;
Expand Down Expand Up @@ -124,6 +126,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');
Expand Down

0 comments on commit ce3087f

Please sign in to comment.