From c5e39cb9210a5087f78232051a67f4f4f11c0229 Mon Sep 17 00:00:00 2001 From: Pedro Gonzalez Gomez Date: Wed, 9 Aug 2023 02:05:40 +0200 Subject: [PATCH] mgr/dashboard: create subvolumegroup Fixes: https://tracker.ceph.com/issues/62363 Signed-off-by: Pedro Gonzalez Gomez --- .../mgr/dashboard/controllers/cephfs.py | 12 +- .../cephfs-subvolume-group.component.html | 12 +- .../cephfs-subvolume-group.component.ts | 65 ++++++-- .../cephfs-subvolumegroup-form.component.html | 139 ++++++++++++++++++ .../cephfs-subvolumegroup-form.component.scss | 0 ...phfs-subvolumegroup-form.component.spec.ts | 39 +++++ .../cephfs-subvolumegroup-form.component.ts | 135 +++++++++++++++++ .../cephfs-tabs/cephfs-tabs.component.html | 3 +- .../src/app/ceph/cephfs/cephfs.module.ts | 4 +- .../api/cephfs-subvolume-group.service.ts | 48 +++++- .../shared/services/task-message.service.ts | 7 + src/pybind/mgr/dashboard/openapi.yaml | 40 +++++ 12 files changed, 489 insertions(+), 15 deletions(-) create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolumegroup-form/cephfs-subvolumegroup-form.component.html create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolumegroup-form/cephfs-subvolumegroup-form.component.scss create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolumegroup-form/cephfs-subvolumegroup-form.component.spec.ts create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolumegroup-form/cephfs-subvolumegroup-form.component.ts diff --git a/src/pybind/mgr/dashboard/controllers/cephfs.py b/src/pybind/mgr/dashboard/controllers/cephfs.py index 2f72b02905c17..79a4c5d4645ab 100644 --- a/src/pybind/mgr/dashboard/controllers/cephfs.py +++ b/src/pybind/mgr/dashboard/controllers/cephfs.py @@ -683,12 +683,12 @@ class CephFSSubvolumeGroups(RESTController): def get(self, vol_name): if not vol_name: raise DashboardException( - 'Error listing subvolume groups') + f'Error listing subvolume groups for {vol_name}') error_code, out, err = mgr.remote('volumes', '_cmd_fs_subvolumegroup_ls', None, {'vol_name': vol_name}) if error_code != 0: raise DashboardException( - 'Error listing subvolume groups') + f'Error listing subvolume groups for {vol_name}') subvolume_groups = json.loads(out) for group in subvolume_groups: error_code, out, err = mgr.remote('volumes', '_cmd_fs_subvolumegroup_info', @@ -700,3 +700,11 @@ class CephFSSubvolumeGroups(RESTController): ) group['info'] = json.loads(out) return subvolume_groups + + def create(self, vol_name: str, group_name: str, **kwargs): + error_code, _, err = mgr.remote('volumes', '_cmd_fs_subvolumegroup_create', None, { + 'vol_name': vol_name, 'group_name': group_name, **kwargs}) + if error_code != 0: + raise DashboardException( + f'Failed to create subvolume group {group_name}: {err}' + ) diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolume-group/cephfs-subvolume-group.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolume-group/cephfs-subvolume-group.component.html index 27cdc155424e6..653bd77a0c768 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolume-group/cephfs-subvolume-group.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolume-group/cephfs-subvolume-group.component.html @@ -5,7 +5,17 @@ columnMode="flex" [columns]="columns" selectionType="single" - [hasDetails]="false"> + [hasDetails]="false" + (fetchData)="fetchData()"> + +
+ + +
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolume-group/cephfs-subvolume-group.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolume-group/cephfs-subvolume-group.component.ts index 63a36f27bc0b9..e1fc307afaf99 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolume-group/cephfs-subvolume-group.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolume-group/cephfs-subvolume-group.component.ts @@ -1,13 +1,20 @@ import { Component, Input, OnInit, ViewChild } from '@angular/core'; -import { Observable, of } from 'rxjs'; -import { catchError } from 'rxjs/operators'; +import { Observable, ReplaySubject, of } from 'rxjs'; +import { catchError, shareReplay, switchMap } from 'rxjs/operators'; import { CephfsSubvolumeGroupService } from '~/app/shared/api/cephfs-subvolume-group.service'; import { CellTemplate } from '~/app/shared/enum/cell-template.enum'; +import { Icons } from '~/app/shared/enum/icons.enum'; +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 { CephfsSubvolumeGroup } from '~/app/shared/models/cephfs-subvolume-group.model'; +import { CephfsSubvolumegroupFormComponent } from '../cephfs-subvolumegroup-form/cephfs-subvolumegroup-form.component'; +import { ActionLabelsI18n } from '~/app/shared/constants/app.constants'; +import { AuthStorageService } from '~/app/shared/services/auth-storage.service'; +import { ModalService } from '~/app/shared/services/modal.service'; +import { Permissions } from '~/app/shared/models/permissions'; @Component({ selector: 'cd-cephfs-subvolume-group', @@ -32,14 +39,26 @@ export class CephfsSubvolumeGroupComponent implements OnInit { @Input() fsName: any; + @Input() pools: any[]; columns: CdTableColumn[]; + tableActions: CdTableAction[]; context: CdTableFetchDataContext; selection = new CdTableSelection(); + icons = Icons; + permissions: Permissions; subvolumeGroup$: Observable; + subject = new ReplaySubject(); - constructor(private cephfsSubvolumeGroup: CephfsSubvolumeGroupService) {} + constructor( + private cephfsSubvolumeGroup: CephfsSubvolumeGroupService, + private actionLabels: ActionLabelsI18n, + private modalService: ModalService, + private authStorageService: AuthStorageService + ) { + this.permissions = this.authStorageService.getPermissions(); + } ngOnInit(): void { this.columns = [ @@ -78,15 +97,43 @@ export class CephfsSubvolumeGroupComponent implements OnInit { cellTransformation: CellTemplate.timeAgo } ]; + + this.tableActions = [ + { + name: this.actionLabels.CREATE, + permission: 'create', + icon: Icons.add, + click: () => + this.modalService.show( + CephfsSubvolumegroupFormComponent, + { + fsName: this.fsName, + pools: this.pools + }, + { size: 'lg' } + ) + } + ]; + + this.subvolumeGroup$ = this.subject.pipe( + switchMap(() => + this.cephfsSubvolumeGroup.get(this.fsName).pipe( + catchError(() => { + this.context.error(); + return of(null); + }) + ) + ), + shareReplay(1) + ); + } + + fetchData() { + this.subject.next(); } ngOnChanges() { - this.subvolumeGroup$ = this.cephfsSubvolumeGroup.get(this.fsName).pipe( - catchError(() => { - this.context.error(); - return of(null); - }) - ); + this.subject.next(); } updateSelection(selection: CdTableSelection) { diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolumegroup-form/cephfs-subvolumegroup-form.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolumegroup-form/cephfs-subvolumegroup-form.component.html new file mode 100644 index 0000000000000..b3a26cb15ccd0 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolumegroup-form/cephfs-subvolumegroup-form.component.html @@ -0,0 +1,139 @@ + + {{ action | titlecase }} {{ resource | upperFirst }} + + +
+ + + +
+
+
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolumegroup-form/cephfs-subvolumegroup-form.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolumegroup-form/cephfs-subvolumegroup-form.component.scss new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolumegroup-form/cephfs-subvolumegroup-form.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolumegroup-form/cephfs-subvolumegroup-form.component.spec.ts new file mode 100644 index 0000000000000..ea5b63e2b854c --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolumegroup-form/cephfs-subvolumegroup-form.component.spec.ts @@ -0,0 +1,39 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { CephfsSubvolumegroupFormComponent } from './cephfs-subvolumegroup-form.component'; +import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; +import { ToastrModule } from 'ngx-toastr'; +import { SharedModule } from '~/app/shared/shared.module'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { ReactiveFormsModule } from '@angular/forms'; +import { RouterTestingModule } from '@angular/router/testing'; + +describe('CephfsSubvolumegroupFormComponent', () => { + let component: CephfsSubvolumegroupFormComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [CephfsSubvolumegroupFormComponent], + providers: [NgbActiveModal], + imports: [ + SharedModule, + ToastrModule.forRoot(), + ReactiveFormsModule, + HttpClientTestingModule, + RouterTestingModule + ] + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(CephfsSubvolumegroupFormComponent); + component = fixture.componentInstance; + component.pools = []; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolumegroup-form/cephfs-subvolumegroup-form.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolumegroup-form/cephfs-subvolumegroup-form.component.ts new file mode 100644 index 0000000000000..3168889f1fc56 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolumegroup-form/cephfs-subvolumegroup-form.component.ts @@ -0,0 +1,135 @@ +import { Component, OnInit } from '@angular/core'; +import { FormControl, Validators } from '@angular/forms'; +import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; +import { CephfsSubvolumeGroupService } from '~/app/shared/api/cephfs-subvolume-group.service'; +import { ActionLabelsI18n, URLVerbs } from '~/app/shared/constants/app.constants'; +import { CdFormGroup } from '~/app/shared/forms/cd-form-group'; +import { FinishedTask } from '~/app/shared/models/finished-task'; +import { TaskWrapperService } from '~/app/shared/services/task-wrapper.service'; +import { Pool } from '../../pool/pool'; +import { FormatterService } from '~/app/shared/services/formatter.service'; +import { CdTableColumn } from '~/app/shared/models/cd-table-column'; +import _ from 'lodash'; +import { CdValidators } from '~/app/shared/forms/cd-validators'; + +@Component({ + selector: 'cd-cephfs-subvolumegroup-form', + templateUrl: './cephfs-subvolumegroup-form.component.html', + styleUrls: ['./cephfs-subvolumegroup-form.component.scss'] +}) +export class CephfsSubvolumegroupFormComponent implements OnInit { + fsName: string; + pools: Pool[]; + + subvolumegroupForm: CdFormGroup; + + action: string; + resource: string; + + dataPools: Pool[]; + + columns: CdTableColumn[]; + scopePermissions: Array = []; + scopes: string[] = ['owner', 'group', 'others']; + + constructor( + public activeModal: NgbActiveModal, + private actionLabels: ActionLabelsI18n, + private taskWrapper: TaskWrapperService, + private cephfsSubvolumeGroupService: CephfsSubvolumeGroupService, + private formatter: FormatterService + ) { + this.action = this.actionLabels.CREATE; + this.resource = $localize`subvolume group`; + } + + ngOnInit(): void { + this.columns = [ + { + prop: 'scope', + name: $localize`All`, + flexGrow: 0.5 + }, + { + prop: 'read', + name: $localize`Read`, + flexGrow: 0.5, + cellClass: 'text-center' + }, + { + prop: 'write', + name: $localize`Write`, + flexGrow: 0.5, + cellClass: 'text-center' + }, + { + prop: 'execute', + name: $localize`Execute`, + flexGrow: 0.5, + cellClass: 'text-center' + } + ]; + + this.dataPools = this.pools.filter((pool) => pool.type === 'data'); + this.createForm(); + } + + createForm() { + this.subvolumegroupForm = new CdFormGroup({ + volumeName: new FormControl({ value: this.fsName, disabled: true }), + subvolumegroupName: new FormControl('', { + validators: [Validators.required], + asyncValidators: [ + CdValidators.unique( + this.cephfsSubvolumeGroupService.exists, + this.cephfsSubvolumeGroupService, + null, + null, + this.fsName + ) + ] + }), + pool: new FormControl(this.dataPools[0]?.pool, { + validators: [Validators.required] + }), + size: new FormControl(null, { + updateOn: 'blur' + }), + uid: new FormControl(null), + gid: new FormControl(null), + mode: new FormControl({}) + }); + } + + submit() { + const subvolumegroupName = this.subvolumegroupForm.getValue('subvolumegroupName'); + const pool = this.subvolumegroupForm.getValue('pool'); + const size = this.formatter.toBytes(this.subvolumegroupForm.getValue('size')); + const uid = this.subvolumegroupForm.getValue('uid'); + const gid = this.subvolumegroupForm.getValue('gid'); + const mode = this.formatter.toOctalPermission(this.subvolumegroupForm.getValue('mode')); + this.taskWrapper + .wrapTaskAroundCall({ + task: new FinishedTask('cephfs/subvolume/group/' + URLVerbs.CREATE, { + subvolumegroupName: subvolumegroupName + }), + call: this.cephfsSubvolumeGroupService.create( + this.fsName, + subvolumegroupName, + pool, + size, + uid, + gid, + mode + ) + }) + .subscribe({ + error: () => { + this.subvolumegroupForm.setErrors({ cdSubmitButton: true }); + }, + complete: () => { + this.activeModal.close(); + } + }); + } +} 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 8a41575c36fc5..0ad69ccf50a33 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 @@ -24,7 +24,8 @@ Subvolume groups - + diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs.module.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs.module.ts index 51ca4f96e1a8a..a83e0f16870fc 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs.module.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs.module.ts @@ -18,6 +18,7 @@ import { CephfsTabsComponent } from './cephfs-tabs/cephfs-tabs.component'; import { CephfsSubvolumeListComponent } from './cephfs-subvolume-list/cephfs-subvolume-list.component'; import { CephfsSubvolumeFormComponent } from './cephfs-subvolume-form/cephfs-subvolume-form.component'; import { CephfsSubvolumeGroupComponent } from './cephfs-subvolume-group/cephfs-subvolume-group.component'; +import { CephfsSubvolumegroupFormComponent } from './cephfs-subvolumegroup-form/cephfs-subvolumegroup-form.component'; @NgModule({ imports: [ @@ -43,7 +44,8 @@ import { CephfsSubvolumeGroupComponent } from './cephfs-subvolume-group/cephfs-s CephfsSubvolumeListComponent, CephfsSubvolumeFormComponent, CephfsDirectoriesComponent, - CephfsSubvolumeGroupComponent + CephfsSubvolumeGroupComponent, + CephfsSubvolumegroupFormComponent ] }) export class CephfsModule {} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/cephfs-subvolume-group.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/cephfs-subvolume-group.service.ts index 853041fe8b755..db7a9db91bb98 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/cephfs-subvolume-group.service.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/cephfs-subvolume-group.service.ts @@ -1,7 +1,9 @@ import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; -import { Observable } from 'rxjs'; +import { Observable, of } from 'rxjs'; import { CephfsSubvolumeGroup } from '../models/cephfs-subvolume-group.model'; +import _ from 'lodash'; +import { mapTo, catchError } from 'rxjs/operators'; @Injectable({ providedIn: 'root' @@ -14,4 +16,48 @@ export class CephfsSubvolumeGroupService { get(volName: string): Observable { return this.http.get(`${this.baseURL}/${volName}`); } + + create( + volName: string, + groupName: string, + poolName: string, + size: number, + uid: number, + gid: number, + mode: string + ) { + return this.http.post( + this.baseURL, + { + vol_name: volName, + group_name: groupName, + pool_layout: poolName, + size: size, + uid: uid, + gid: gid, + mode: mode + }, + { observe: 'response' } + ); + } + + info(volName: string, groupName: string) { + return this.http.get(`${this.baseURL}/${volName}/info`, { + params: { + group_name: groupName + } + }); + } + + exists(groupName: string, volName: string) { + return this.info(volName, groupName).pipe( + mapTo(true), + catchError((error: Event) => { + if (_.isFunction(error.preventDefault)) { + error.preventDefault(); + } + return of(false); + }) + ); + } } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/task-message.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/task-message.service.ts index fc5b08be8bd81..f0bc2825144a3 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/task-message.service.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/task-message.service.ts @@ -358,6 +358,9 @@ export class TaskMessageService { ), 'cephfs/subvolume/create': this.newTaskMessage(this.commonOperations.create, (metadata) => this.subvolume(metadata) + ), + 'cephfs/subvolume/group/create': this.newTaskMessage(this.commonOperations.create, (metadata) => + this.subvolumegroup(metadata) ) }; @@ -422,6 +425,10 @@ export class TaskMessageService { return $localize`subvolume '${metadata.subVolumeName}'`; } + subvolumegroup(metadata: any) { + return $localize`subvolume group '${metadata.subvolumegroupName}'`; + } + crudMessageId(id: string) { return $localize`${id}`; } diff --git a/src/pybind/mgr/dashboard/openapi.yaml b/src/pybind/mgr/dashboard/openapi.yaml index 55801683fbd7a..1772563ac0a1a 100644 --- a/src/pybind/mgr/dashboard/openapi.yaml +++ b/src/pybind/mgr/dashboard/openapi.yaml @@ -1721,6 +1721,46 @@ paths: - jwt: [] tags: - CephFSSubvolume + /api/cephfs/subvolume/group: + post: + parameters: [] + requestBody: + content: + application/json: + schema: + properties: + group_name: + type: string + vol_name: + type: string + required: + - vol_name + - group_name + type: object + responses: + '201': + content: + application/vnd.ceph.api.v1.0+json: + type: object + description: Resource created. + '202': + content: + application/vnd.ceph.api.v1.0+json: + type: object + description: Operation is still executing. Please check the task queue. + '400': + description: Operation exception. Please check the response body for details. + '401': + description: Unauthenticated access. Please login first. + '403': + description: Unauthorized access. Please check your permissions. + '500': + description: Unexpected error. Please check the response body for the stack + trace. + security: + - jwt: [] + tags: + - CephfsSubvolumeGroup /api/cephfs/subvolume/group/{vol_name}: get: parameters: -- 2.39.5