From bab595032748847c7af1b35b5f72b241295ae0f1 Mon Sep 17 00:00:00 2001 From: Ivo Almeida Date: Sat, 25 Nov 2023 19:10:35 +0000 Subject: [PATCH] mgr/dashboard: added snap-schedule api and ui list Fixes: https://tracker.ceph.com/issues/63767 Signed-off-by: Ivo Almeida (cherry picked from commit acaad3555414aa1f1b4fa732af70612d50c5e883) --- .../mgr/dashboard/controllers/cephfs.py | 88 ++++++++++++++++- ...ephfs-snapshotschedule-list.component.html | 58 +++++++++++ ...ephfs-snapshotschedule-list.component.scss | 0 ...fs-snapshotschedule-list.component.spec.ts | 30 ++++++ .../cephfs-snapshotschedule-list.component.ts | 99 +++++++++++++++++++ .../cephfs-tabs/cephfs-tabs.component.html | 83 ++++++++++------ .../src/app/ceph/cephfs/cephfs.module.ts | 8 +- .../cephfs-snapshot-schedule.service.spec.ts | 22 +++++ .../api/cephfs-snapshot-schedule.service.ts | 39 ++++++++ .../app/shared/models/snapshot-schedule.ts | 17 ++++ src/pybind/mgr/dashboard/openapi.yaml | 39 ++++++++ 11 files changed, 448 insertions(+), 35 deletions(-) create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-snapshotschedule-list/cephfs-snapshotschedule-list.component.html create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-snapshotschedule-list/cephfs-snapshotschedule-list.component.scss create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-snapshotschedule-list/cephfs-snapshotschedule-list.component.spec.ts create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-snapshotschedule-list/cephfs-snapshotschedule-list.component.ts create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/shared/api/cephfs-snapshot-schedule.service.spec.ts create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/shared/api/cephfs-snapshot-schedule.service.ts create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/shared/models/snapshot-schedule.ts diff --git a/src/pybind/mgr/dashboard/controllers/cephfs.py b/src/pybind/mgr/dashboard/controllers/cephfs.py index 7dc8b4e9b570a..3014ef704fd02 100644 --- a/src/pybind/mgr/dashboard/controllers/cephfs.py +++ b/src/pybind/mgr/dashboard/controllers/cephfs.py @@ -23,6 +23,11 @@ GET_QUOTAS_SCHEMA = { 'max_bytes': (int, ''), 'max_files': (int, '') } +GET_STATFS_SCHEMA = { + 'bytes': (int, ''), + 'files': (int, ''), + 'subdirs': (int, '') +} logger = logging.getLogger("controllers.rgw") @@ -331,13 +336,16 @@ class CephFS(RESTController): standby_table = self.get_standby_table(fsmap['standbys'], mds_versions) + flags = mdsmap['flags_state'] + return { "cephfs": { "id": fs_id, "name": mdsmap['fs_name'], "client_count": client_count, "ranks": rank_table, - "pools": pools_table + "pools": pools_table, + "flags": flags, }, "standbys": standby_table, "versions": mds_versions @@ -360,7 +368,7 @@ class CephFS(RESTController): "No cephfs with id {0}".format(fs_id)) # Decorate the metadata with some fields that will be - # indepdendent of whether it's a kernel or userspace + # independent of whether it's a kernel or userspace # client, so that the javascript doesn't have to grok that. for client in clients: if "ceph_version" in client['client_metadata']: # pragma: no cover - no complexity @@ -519,6 +527,47 @@ class CephFS(RESTController): cfs = self._cephfs_instance(fs_id) return cfs.get_quotas(path) + @RESTController.Resource('POST', path='/write_to_file') + @allow_empty_body + def write_to_file(self, fs_id, path, buf) -> None: + """ + Write some data to the specified path. + :param fs_id: The filesystem identifier. + :param path: The path of the file to write. + :param buf: The str to write to the buf. + """ + cfs = self._cephfs_instance(fs_id) + cfs.write_to_file(path, buf) + + @RESTController.Resource('DELETE', path='/unlink') + def unlink(self, fs_id, path) -> None: + """ + Removes a file, link, or symbolic link. + :param fs_id: The filesystem identifier. + :param path: The path of the file or link to unlink. + """ + cfs = self._cephfs_instance(fs_id) + cfs.unlink(path) + + @RESTController.Resource('GET', path='/statfs') + @EndpointDoc("Get Cephfs statfs of the specified path", + parameters={ + 'fs_id': (str, 'File System Identifier'), + 'path': (str, 'File System Path'), + }, + responses={200: GET_STATFS_SCHEMA}) + def statfs(self, fs_id, path) -> dict: + """ + Get the statfs of the specified path. + :param fs_id: The filesystem identifier. + :param path: The path of the directory/file. + :return: Returns a dictionary containing 'bytes', + 'files' and 'subdirs'. + :rtype: dict + """ + cfs = self._cephfs_instance(fs_id) + return cfs.statfs(path) + @RESTController.Resource('POST', path='/snapshot') @allow_empty_body def snapshot(self, fs_id, path, name=None): @@ -561,7 +610,11 @@ class CephFSClients(object): @ViewCache() def get(self): - return CephService.send_command('mds', 'session ls', srv_spec='{0}:0'.format(self.fscid)) + try: + ret = CephService.send_command('mds', 'session ls', srv_spec='{0}:0'.format(self.fscid)) + except RuntimeError: + ret = [] + return ret @UIRouter('/cephfs', Scope.CEPHFS) @@ -886,3 +939,32 @@ class CephFsSnapshotClone(RESTController): f'Failed to create clone {clone_name}: {err}' ) return f'Clone {clone_name} created successfully' + +@APIRouter('/cephfs/snaphost/schedule', Scope.CEPHFS) +@APIDoc("Cephfs Snapshot Scheduling API", "CephFSSnapshotSchedule") +class CephFSSnapshotSchedule(RESTController): + + def list(self, fs: str, path: str = '/', recursive: bool = True): + error_code, out, err = mgr.remote('snap_schedule', 'snap_schedule_list', + path, recursive, fs, 'plain') + + if len(out) == 0: + return [] + + snapshot_schedule_list = out.split('\n') + output = [] + + for snap in snapshot_schedule_list: + current_path = snap.strip().split(' ')[0] + error_code, status_out, err = mgr.remote('snap_schedule', 'snap_schedule_get', + current_path, fs, 'plain') + output.append(json.loads(status_out)) + + output_json = json.dumps(output) + + if error_code != 0: + raise DashboardException( + f'Failed to get list of snapshot schedules for path {path}: {err}' + ) + + return json.loads(output_json) diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-snapshotschedule-list/cephfs-snapshotschedule-list.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-snapshotschedule-list/cephfs-snapshotschedule-list.component.html new file mode 100644 index 0000000000000..2e270057d579f --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-snapshotschedule-list/cephfs-snapshotschedule-list.component.html @@ -0,0 +1,58 @@ + + + Loading snapshot schedules... + + + + + {{row.path | path}} + + + + + + + + + + + {{ row.path }} + + + + + + + + +
+ + +
+
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-snapshotschedule-list/cephfs-snapshotschedule-list.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-snapshotschedule-list/cephfs-snapshotschedule-list.component.scss new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-snapshotschedule-list/cephfs-snapshotschedule-list.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-snapshotschedule-list/cephfs-snapshotschedule-list.component.spec.ts new file mode 100644 index 0000000000000..a20972f1cf85c --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-snapshotschedule-list/cephfs-snapshotschedule-list.component.spec.ts @@ -0,0 +1,30 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { CephfsSnapshotscheduleListComponent } from './cephfs-snapshotschedule-list.component'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { SharedModule } from '~/app/shared/shared.module'; +import { ToastrModule } from 'ngx-toastr'; +import { RouterTestingModule } from '@angular/router/testing'; +import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; +import { configureTestBed } from '~/testing/unit-test-helper'; + +describe('CephfsSnapshotscheduleListComponent', () => { + let component: CephfsSnapshotscheduleListComponent; + let fixture: ComponentFixture; + + configureTestBed({ + declarations: [CephfsSnapshotscheduleListComponent], + imports: [HttpClientTestingModule, SharedModule, ToastrModule.forRoot(), RouterTestingModule], + providers: [NgbActiveModal] + }); + + beforeEach(() => { + fixture = TestBed.createComponent(CephfsSnapshotscheduleListComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-snapshotschedule-list/cephfs-snapshotschedule-list.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-snapshotschedule-list/cephfs-snapshotschedule-list.component.ts new file mode 100644 index 0000000000000..d5f24e8972849 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-snapshotschedule-list/cephfs-snapshotschedule-list.component.ts @@ -0,0 +1,99 @@ +import { Component, Input, OnChanges, OnInit, SimpleChanges, ViewChild } from '@angular/core'; +import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; +import { BehaviorSubject, Observable } from 'rxjs'; +import { finalize, shareReplay, switchMap } from 'rxjs/operators'; +import { CephfsSnapshotScheduleService } from '~/app/shared/api/cephfs-snapshot-schedule.service'; +import { CdForm } from '~/app/shared/forms/cd-form'; +import { CdTableAction } from '~/app/shared/models/cd-table-action'; +import { CdTableColumn } from '~/app/shared/models/cd-table-column'; +import { CdTableFetchDataContext } from '~/app/shared/models/cd-table-fetch-data-context'; +import { CdTableSelection } from '~/app/shared/models/cd-table-selection'; +import { Permissions } from '~/app/shared/models/permissions'; +import { SnapshotSchedule } from '~/app/shared/models/snapshot-schedule'; +import { AuthStorageService } from '~/app/shared/services/auth-storage.service'; +import { ModalService } from '~/app/shared/services/modal.service'; +import { Icons } from '~/app/shared/enum/icons.enum'; +import { CellTemplate } from '~/app/shared/enum/cell-template.enum'; + +@Component({ + selector: 'cd-cephfs-snapshotschedule-list', + templateUrl: './cephfs-snapshotschedule-list.component.html', + styleUrls: ['./cephfs-snapshotschedule-list.component.scss'] +}) +export class CephfsSnapshotscheduleListComponent extends CdForm implements OnInit, OnChanges { + @Input() fsName!: string; + + @ViewChild('pathTpl', { static: true }) + pathTpl: any; + + snapshotSchedules$!: Observable; + subject$ = new BehaviorSubject([]); + isLoading$ = new BehaviorSubject(true); + columns: CdTableColumn[] = []; + tableActions: CdTableAction[] = []; + context!: CdTableFetchDataContext; + selection = new CdTableSelection(); + permissions!: Permissions; + modalRef!: NgbModalRef; + errorMessage: string = ''; + selectedName: string = ''; + icons = Icons; + + constructor( + private snapshotScheduleService: CephfsSnapshotScheduleService, + private authStorageService: AuthStorageService, + private modalService: ModalService + ) { + super(); + this.permissions = this.authStorageService.getPermissions(); + } + + ngOnChanges(changes: SimpleChanges): void { + if (changes.fsName) { + this.subject$.next([]); + } + } + + ngOnInit(): void { + this.snapshotSchedules$ = this.subject$.pipe( + switchMap(() => + this.snapshotScheduleService + .getSnapshotScheduleList('/', this.fsName) + .pipe(finalize(() => this.isLoading$.next(false))) + ), + shareReplay(1) + ); + + this.columns = [ + { prop: 'path', name: $localize`Path`, flexGrow: 3, cellTemplate: this.pathTpl }, + { prop: 'subvol', name: $localize`Subvolume` }, + { prop: 'schedule', name: $localize`Repeat interval` }, + { prop: 'retention', name: $localize`Retention policy` }, + { prop: 'created_count', name: $localize`Created Count` }, + { prop: 'pruned_count', name: $localize`Deleted Count` }, + { prop: 'start', name: $localize`Start time`, cellTransformation: CellTemplate.timeAgo }, + { prop: 'created', name: $localize`Created`, cellTransformation: CellTemplate.timeAgo } + ]; + + this.tableActions = []; + } + + fetchData() { + this.subject$.next([]); + } + + updateSelection(selection: CdTableSelection) { + this.selection = selection; + } + + openModal(edit = false) { + this.modalService.show( + {}, + { + fsName: 'fs1', + isEdit: edit + }, + { size: 'lg' } + ); + } +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-tabs/cephfs-tabs.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-tabs/cephfs-tabs.component.html index e032634346326..a840692ed7673 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-tabs/cephfs-tabs.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-tabs/cephfs-tabs.component.html @@ -1,15 +1,17 @@ -