From 8bf6c5958458b9fcffa1e1d090eaa3e7dec4a250 Mon Sep 17 00:00:00 2001 From: Nizamudeen A Date: Thu, 3 Aug 2023 17:30:40 +0530 Subject: [PATCH] mgr/dashboard: cephfs subvolume creation form Fixes: https://tracker.ceph.com/issues/62345 Signed-off-by: Nizamudeen A --- .../mgr/dashboard/controllers/cephfs.py | 24 ++- .../cephfs-subvolume-form.component.html | 156 ++++++++++++++++++ .../cephfs-subvolume-form.component.scss | 0 .../cephfs-subvolume-form.component.spec.ts | 39 +++++ .../cephfs-subvolume-form.component.ts | 138 ++++++++++++++++ .../cephfs-subvolume-list.component.html | 12 +- .../cephfs-subvolume-list.component.ts | 63 ++++++- .../cephfs-tabs/cephfs-tabs.component.html | 3 +- .../src/app/ceph/cephfs/cephfs.module.ts | 4 +- .../auth/role-form/role-form.component.ts | 1 - .../shared/api/cephfs-subvolume.service.ts | 50 +++++- .../src/app/shared/api/cephfs.service.ts | 4 + .../src/app/shared/forms/cd-validators.ts | 7 +- .../app/shared/services/formatter.service.ts | 18 ++ .../shared/services/task-message.service.ts | 7 + src/pybind/mgr/dashboard/openapi.yaml | 72 ++++++++ 16 files changed, 579 insertions(+), 19 deletions(-) create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolume-form/cephfs-subvolume-form.component.html create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolume-form/cephfs-subvolume-form.component.scss create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolume-form/cephfs-subvolume-form.component.spec.ts create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolume-form/cephfs-subvolume-form.component.ts diff --git a/src/pybind/mgr/dashboard/controllers/cephfs.py b/src/pybind/mgr/dashboard/controllers/cephfs.py index c38cc940fd8b7..69de90f3780ba 100644 --- a/src/pybind/mgr/dashboard/controllers/cephfs.py +++ b/src/pybind/mgr/dashboard/controllers/cephfs.py @@ -641,7 +641,7 @@ class CephFSSubvolume(RESTController): error_code, out, err = mgr.remote( 'volumes', '_cmd_fs_subvolume_ls', None, {'vol_name': vol_name}) if error_code != 0: - raise RuntimeError( + raise DashboardException( f'Failed to list subvolumes for volume {vol_name}: {err}' ) subvolumes = json.loads(out) @@ -649,8 +649,28 @@ class CephFSSubvolume(RESTController): error_code, out, err = mgr.remote('volumes', '_cmd_fs_subvolume_info', None, { 'vol_name': vol_name, 'sub_name': subvolume['name']}) if error_code != 0: - raise RuntimeError( + raise DashboardException( f'Failed to get info for subvolume {subvolume["name"]}: {err}' ) subvolume['info'] = json.loads(out) return subvolumes + + @RESTController.Resource('GET') + def info(self, vol_name: str, subvol_name: str): + error_code, out, err = mgr.remote('volumes', '_cmd_fs_subvolume_info', None, { + 'vol_name': vol_name, 'sub_name': subvol_name}) + if error_code != 0: + raise DashboardException( + f'Failed to get info for subvolume {subvol_name}: {err}' + ) + return json.loads(out) + + def create(self, vol_name: str, subvol_name: str, **kwargs): + error_code, _, err = mgr.remote('volumes', '_cmd_fs_subvolume_create', None, { + 'vol_name': vol_name, 'sub_name': subvol_name, **kwargs}) + if error_code != 0: + raise DashboardException( + f'Failed to create subvolume {subvol_name}: {err}' + ) + + return f'Subvolume {subvol_name} created successfully' diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolume-form/cephfs-subvolume-form.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolume-form/cephfs-subvolume-form.component.html new file mode 100644 index 0000000000000..4e3bb68014829 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolume-form/cephfs-subvolume-form.component.html @@ -0,0 +1,156 @@ + + {{ action | titlecase }} {{ resource | upperFirst }} + + +
+ + + +
+
+
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolume-form/cephfs-subvolume-form.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolume-form/cephfs-subvolume-form.component.scss new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolume-form/cephfs-subvolume-form.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolume-form/cephfs-subvolume-form.component.spec.ts new file mode 100644 index 0000000000000..9407290f31688 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolume-form/cephfs-subvolume-form.component.spec.ts @@ -0,0 +1,39 @@ +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { CephfsSubvolumeFormComponent } from './cephfs-subvolume-form.component'; +import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; +import { ToastrModule } from 'ngx-toastr'; +import { SharedModule } from '~/app/shared/shared.module'; +import { RouterTestingModule } from '@angular/router/testing'; +import { ReactiveFormsModule } from '@angular/forms'; + +describe('CephfsSubvolumeFormComponent', () => { + let component: CephfsSubvolumeFormComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [CephfsSubvolumeFormComponent], + providers: [NgbActiveModal], + imports: [ + SharedModule, + ToastrModule.forRoot(), + ReactiveFormsModule, + HttpClientTestingModule, + RouterTestingModule + ] + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(CephfsSubvolumeFormComponent); + 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-subvolume-form/cephfs-subvolume-form.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolume-form/cephfs-subvolume-form.component.ts new file mode 100644 index 0000000000000..09d52dabe3b74 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolume-form/cephfs-subvolume-form.component.ts @@ -0,0 +1,138 @@ +import { Component, OnInit } from '@angular/core'; +import { FormControl, Validators } from '@angular/forms'; +import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; +import { CephfsSubvolumeService } from '~/app/shared/api/cephfs-subvolume.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-subvolume-form', + templateUrl: './cephfs-subvolume-form.component.html', + styleUrls: ['./cephfs-subvolume-form.component.scss'] +}) +export class CephfsSubvolumeFormComponent implements OnInit { + fsName: string; + pools: Pool[]; + + subvolumeForm: 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 cephFsSubvolumeService: CephfsSubvolumeService, + private formatter: FormatterService + ) { + this.action = this.actionLabels.CREATE; + this.resource = $localize`Subvolume`; + } + + 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.subvolumeForm = new CdFormGroup({ + volumeName: new FormControl({ value: this.fsName, disabled: true }), + subvolumeName: new FormControl('', { + validators: [Validators.required], + asyncValidators: [ + CdValidators.unique( + this.cephFsSubvolumeService.exists, + this.cephFsSubvolumeService, + 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({}), + isolatedNamespace: new FormControl(false) + }); + } + + submit() { + const subVolumeName = this.subvolumeForm.getValue('subvolumeName'); + const pool = this.subvolumeForm.getValue('pool'); + const size = this.formatter.toBytes(this.subvolumeForm.getValue('size')); + const uid = this.subvolumeForm.getValue('uid'); + const gid = this.subvolumeForm.getValue('gid'); + const mode = this.formatter.toOctalPermission(this.subvolumeForm.getValue('mode')); + const isolatedNamespace = this.subvolumeForm.getValue('isolatedNamespace'); + this.taskWrapper + .wrapTaskAroundCall({ + task: new FinishedTask('cephfs/subvolume/' + URLVerbs.CREATE, { + subVolumeName: subVolumeName + }), + call: this.cephFsSubvolumeService.create( + this.fsName, + subVolumeName, + pool, + size, + uid, + gid, + mode, + isolatedNamespace + ) + }) + .subscribe({ + error: () => { + this.subvolumeForm.setErrors({ cdSubmitButton: true }); + }, + complete: () => { + this.activeModal.close(); + } + }); + } +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolume-list/cephfs-subvolume-list.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolume-list/cephfs-subvolume-list.component.html index 7ecb3faae8013..53aa454e4cb6a 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolume-list/cephfs-subvolume-list.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolume-list/cephfs-subvolume-list.component.html @@ -3,7 +3,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-list/cephfs-subvolume-list.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolume-list/cephfs-subvolume-list.component.ts index 59fc488890563..14c0ea724da60 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolume-list/cephfs-subvolume-list.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-subvolume-list/cephfs-subvolume-list.component.ts @@ -1,13 +1,19 @@ import { Component, Input, OnChanges, 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 { CephfsSubvolumeService } from '~/app/shared/api/cephfs-subvolume.service'; +import { ActionLabelsI18n } from '~/app/shared/constants/app.constants'; 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 { CephfsSubvolume } from '~/app/shared/models/cephfs-subvolume.model'; +import { ModalService } from '~/app/shared/services/modal.service'; +import { CephfsSubvolumeFormComponent } from '../cephfs-subvolume-form/cephfs-subvolume-form.component'; +import { AuthStorageService } from '~/app/shared/services/auth-storage.service'; +import { Permissions } from '~/app/shared/models/permissions'; @Component({ selector: 'cd-cephfs-subvolume-list', @@ -31,15 +37,26 @@ export class CephfsSubvolumeListComponent implements OnInit, OnChanges { quotaSizeTpl: any; @Input() fsName: string; + @Input() pools: any[]; columns: CdTableColumn[] = []; + tableActions: CdTableAction[]; context: CdTableFetchDataContext; selection = new CdTableSelection(); icons = Icons; + permissions: Permissions; subVolumes$: Observable; + subject = new ReplaySubject(); - constructor(private cephfsSubVolume: CephfsSubvolumeService) {} + constructor( + private cephfsSubVolume: CephfsSubvolumeService, + private actionLabels: ActionLabelsI18n, + private modalService: ModalService, + private authStorageService: AuthStorageService + ) { + this.permissions = this.authStorageService.getPermissions(); + } ngOnInit(): void { this.columns = [ @@ -84,15 +101,43 @@ export class CephfsSubvolumeListComponent implements OnInit, OnChanges { cellTransformation: CellTemplate.timeAgo } ]; + + this.tableActions = [ + { + name: this.actionLabels.CREATE, + permission: 'create', + icon: Icons.add, + click: () => + this.modalService.show( + CephfsSubvolumeFormComponent, + { + fsName: this.fsName, + pools: this.pools + }, + { size: 'lg' } + ) + } + ]; + + this.subVolumes$ = this.subject.pipe( + switchMap(() => + this.cephfsSubVolume.get(this.fsName).pipe( + catchError(() => { + this.context.error(); + return of(null); + }) + ) + ), + shareReplay(1) + ); + } + + fetchData() { + this.subject.next(); } ngOnChanges() { - this.subVolumes$ = this.cephfsSubVolume.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-tabs/cephfs-tabs.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-tabs/cephfs-tabs.component.html index 50545a1ad65ce..8d49a74dfb8a1 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 @@ -16,7 +16,8 @@ Subvolumes - + 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 3556345662347..892c9058655b1 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 @@ -16,6 +16,7 @@ import { CephfsVolumeFormComponent } from './cephfs-form/cephfs-form.component'; import { CephfsListComponent } from './cephfs-list/cephfs-list.component'; 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'; @NgModule({ imports: [ @@ -38,7 +39,8 @@ import { CephfsSubvolumeListComponent } from './cephfs-subvolume-list/cephfs-sub CephfsTabsComponent, CephfsVolumeFormComponent, CephfsDirectoriesComponent, - CephfsSubvolumeListComponent + CephfsSubvolumeListComponent, + CephfsSubvolumeFormComponent ] }) export class CephfsModule {} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/auth/role-form/role-form.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/role-form/role-form.component.ts index e7876d29464e0..61079945e0431 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/core/auth/role-form/role-form.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/core/auth/role-form/role-form.component.ts @@ -23,7 +23,6 @@ import { RoleFormModel } from './role-form.model'; styleUrls: ['./role-form.component.scss'] }) export class RoleFormComponent extends CdForm implements OnInit { - roleForm: CdFormGroup; response: RoleFormModel; diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/cephfs-subvolume.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/cephfs-subvolume.service.ts index a983f7e2c7a84..bd9a16e0e333b 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/cephfs-subvolume.service.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/cephfs-subvolume.service.ts @@ -1,7 +1,9 @@ import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { CephfsSubvolume } from '../models/cephfs-subvolume.model'; -import { Observable } from 'rxjs'; +import { Observable, of } from 'rxjs'; +import { catchError, mapTo } from 'rxjs/operators'; +import _ from 'lodash'; @Injectable({ providedIn: 'root' @@ -14,4 +16,50 @@ export class CephfsSubvolumeService { get(fsName: string): Observable { return this.http.get(`${this.baseURL}/${fsName}`); } + + create( + fsName: string, + subVolumeName: string, + poolName: string, + size: number, + uid: number, + gid: number, + mode: string, + namespace: boolean + ) { + return this.http.post( + this.baseURL, + { + vol_name: fsName, + subvol_name: subVolumeName, + pool_layout: poolName, + size: size, + uid: uid, + gid: gid, + mode: mode, + namespace_isolated: namespace + }, + { observe: 'response' } + ); + } + + info(fsName: string, subVolumeName: string) { + return this.http.get(`${this.baseURL}/${fsName}/info`, { + params: { + subvol_name: subVolumeName + } + }); + } + + exists(subVolumeName: string, fsName: string) { + return this.info(fsName, subVolumeName).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/api/cephfs.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/cephfs.service.ts index 4e212adeba994..fb5c9e8120b0d 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 @@ -83,4 +83,8 @@ export class CephfsService { } ); } + + isCephFsPool(pool: any) { + return _.indexOf(pool.application_metadata, 'cephfs') !== -1 && !pool.pool_name.includes('/'); + } } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/cd-validators.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/cd-validators.ts index 22371a50f71ec..bea426724e073 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/cd-validators.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/forms/cd-validators.ts @@ -18,7 +18,7 @@ export function isEmptyInputValue(value: any): boolean { return value == null || value.length === 0; } -export type existsServiceFn = (value: any) => Observable; +export type existsServiceFn = (value: any, args?: any) => Observable; export class CdValidators { /** @@ -358,7 +358,8 @@ export class CdValidators { serviceFn: existsServiceFn, serviceFnThis: any = null, usernameFn?: Function, - uidField = false + uidField = false, + extraArgs = '' ): AsyncValidatorFn { let uName: string; return (control: AbstractControl): Observable => { @@ -377,7 +378,7 @@ export class CdValidators { } return observableTimer().pipe( - switchMapTo(serviceFn.call(serviceFnThis, uName)), + switchMapTo(serviceFn.call(serviceFnThis, uName, extraArgs)), map((resp: boolean) => { if (!resp) { return null; diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/formatter.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/formatter.service.ts index eacba3cf16dcc..b5e0b9475a445 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/formatter.service.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/formatter.service.ts @@ -120,4 +120,22 @@ export class FormatterService { return 0; } + + toOctalPermission(modes: any) { + const scopes = ['owner', 'group', 'others']; + let octalMode = ''; + for (const scope of scopes) { + let scopeValue = 0; + const mode = modes[scope]; + + if (mode) { + if (mode.includes('read')) scopeValue += 4; + if (mode.includes('write')) scopeValue += 2; + if (mode.includes('execute')) scopeValue += 1; + } + + octalMode += scopeValue.toString(); + } + return octalMode; + } } 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 bf8f189b4e497..fc5b08be8bd81 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 @@ -355,6 +355,9 @@ export class TaskMessageService { ), 'cephfs/create': this.newTaskMessage(this.commonOperations.create, (metadata) => this.volume(metadata) + ), + 'cephfs/subvolume/create': this.newTaskMessage(this.commonOperations.create, (metadata) => + this.subvolume(metadata) ) }; @@ -415,6 +418,10 @@ export class TaskMessageService { return $localize`'${metadata.volumeName}'`; } + subvolume(metadata: any) { + return $localize`subvolume '${metadata.subVolumeName}'`; + } + crudMessageId(id: string) { return $localize`${id}`; } diff --git a/src/pybind/mgr/dashboard/openapi.yaml b/src/pybind/mgr/dashboard/openapi.yaml index 8afa6a4ba9e93..c8431383e9413 100644 --- a/src/pybind/mgr/dashboard/openapi.yaml +++ b/src/pybind/mgr/dashboard/openapi.yaml @@ -1681,6 +1681,46 @@ paths: - jwt: [] tags: - Cephfs + /api/cephfs/subvolume: + post: + parameters: [] + requestBody: + content: + application/json: + schema: + properties: + subvol_name: + type: string + vol_name: + type: string + required: + - vol_name + - subvol_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: + - CephFSSubvolume /api/cephfs/subvolume/{vol_name}: get: parameters: @@ -1708,6 +1748,38 @@ paths: - jwt: [] tags: - CephFSSubvolume + /api/cephfs/subvolume/{vol_name}/info: + get: + parameters: + - in: path + name: vol_name + required: true + schema: + type: string + - in: query + name: subvol_name + required: true + schema: + type: string + responses: + '200': + content: + application/vnd.ceph.api.v1.0+json: + type: object + description: OK + '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: + - CephFSSubvolume /api/cephfs/{fs_id}: get: parameters: -- 2.39.5