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',
)
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}'
+ )
columnMode="flex"
[columns]="columns"
selectionType="single"
- [hasDetails]="false">
+ [hasDetails]="false"
+ (fetchData)="fetchData()">
+
+ <div class="table-actions btn-toolbar">
+ <cd-table-actions [permission]="permissions.cephfs"
+ [selection]="selection"
+ class="btn-group"
+ id="cephfs-subvolumegropup-actions"
+ [tableActions]="tableActions">
+ </cd-table-actions>
+ </div>
</cd-table>
</ng-container>
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',
@Input()
fsName: any;
+ @Input() pools: any[];
columns: CdTableColumn[];
+ tableActions: CdTableAction[];
context: CdTableFetchDataContext;
selection = new CdTableSelection();
+ icons = Icons;
+ permissions: Permissions;
subvolumeGroup$: Observable<CephfsSubvolumeGroup[]>;
+ subject = new ReplaySubject<CephfsSubvolumeGroup[]>();
- 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 = [
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) {
--- /dev/null
+<cd-modal [modalRef]="activeModal">
+ <ng-container i18n="form title"
+ class="modal-title">{{ action | titlecase }} {{ resource | upperFirst }}</ng-container>
+
+ <ng-container class="modal-content">
+ <form name="subvolumegroupForm"
+ #formDir="ngForm"
+ [formGroup]="subvolumegroupForm"
+ novalidate>
+ <div class="modal-body">
+ <div class="form-group row">
+ <label class="cd-col-form-label required"
+ for="subvolumegroupName"
+ i18n>Name</label>
+ <div class="cd-col-form-input">
+ <input class="form-control"
+ type="text"
+ placeholder="subvolumegroup name..."
+ id="subvolumegroupName"
+ name="subvolumegroupName"
+ formControlName="subvolumegroupName"
+ autofocus>
+ <span class="invalid-feedback"
+ *ngIf="subvolumegroupForm.showError('subvolumegroupName', formDir, 'required')"
+ i18n>This field is required.</span>
+ <span class="invalid-feedback"
+ *ngIf="subvolumegroupForm.showError('subvolumegroupName', formDir, 'notUnique')"
+ i18n>The subvolumegroup already exists.</span>
+ </div>
+ </div>
+
+ <!-- Volume name -->
+ <div class="form-group row">
+ <label class="cd-col-form-label"
+ for="volumeName"
+ i18n>Volume name</label>
+ <div class="cd-col-form-input">
+ <input class="form-control"
+ id="volumeName"
+ name="volumeName"
+ formControlName="volumeName">
+ </div>
+ </div>
+
+ <!-- Size -->
+ <div class="form-group row">
+ <label class="cd-col-form-label"
+ for="size"
+ i18n>Size
+ <cd-helper>The size of the subvolumegropup is specified by setting a quota on it.
+ If left blank or put 0, then quota will be infinite</cd-helper>
+ </label>
+ <div class="cd-col-form-input">
+ <input class="form-control"
+ type="text"
+ id="size"
+ name="size"
+ formControlName="size"
+ i18n-placeholder
+ placeholder="e.g., 10GiB"
+ defaultUnit="GiB"
+ cdDimlessBinary>
+ </div>
+ </div>
+
+ <!-- CephFS Pools -->
+ <div class="form-group row">
+ <label class="cd-col-form-label"
+ for="pool"
+ i18n>Pool
+ <cd-helper>By default, the data_pool_layout of the parent directory is selected.</cd-helper>
+ </label>
+ <div class="cd-col-form-input">
+ <select class="form-select"
+ id="pool"
+ name="pool"
+ formControlName="pool">
+ <option *ngFor="let pool of dataPools"
+ [value]="pool.pool">{{ pool.pool }}</option>
+ </select>
+ </div>
+ </div>
+
+ <!-- UID -->
+ <div class="form-group row">
+ <label class="cd-col-form-label"
+ for="uid"
+ i18n>UID</label>
+ <div class="cd-col-form-input">
+ <input class="form-control"
+ type="number"
+ placeholder="subvolumegroup UID..."
+ id="uid"
+ name="uid"
+ formControlName="uid">
+ </div>
+ </div>
+
+ <!-- GID -->
+ <div class="form-group row">
+ <label class="cd-col-form-label"
+ for="gid"
+ i18n>GID</label>
+ <div class="cd-col-form-input">
+ <input class="form-control"
+ type="number"
+ placeholder="subvolumegroup GID..."
+ id="gid"
+ name="gid"
+ formControlName="gid">
+ </div>
+ </div>
+
+ <!-- Mode -->
+ <div class="form-group row">
+ <label class="cd-col-form-label"
+ for="mode"
+ i18n>Mode
+ <cd-helper>Permissions for the directory. Default mode is 755 which is rwxr-xr-x</cd-helper>
+ </label>
+ <div class="cd-col-form-input">
+ <cd-checked-table-form [data]="scopePermissions"
+ [columns]="columns"
+ [form]="subvolumegroupForm"
+ inputField="mode"
+ [isTableForOctalMode]="true"
+ [scopes]="scopes"></cd-checked-table-form>
+ </div>
+ </div>
+ </div>
+
+ <div class="modal-footer">
+ <cd-form-button-panel (submitActionEvent)="submit()"
+ [form]="subvolumegroupForm"
+ [submitText]="(action | titlecase) + ' ' + (resource | upperFirst)"></cd-form-button-panel>
+ </div>
+ </form>
+ </ng-container>
+</cd-modal>
--- /dev/null
+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<CephfsSubvolumegroupFormComponent>;
+
+ 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();
+ });
+});
--- /dev/null
+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<any> = [];
+ 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();
+ }
+ });
+ }
+}
<a ngbNavLink
i18n>Subvolume groups</a>
<ng-template ngbNavContent>
- <cd-cephfs-subvolume-group [fsName]="selection.mdsmap.fs_name">
+ <cd-cephfs-subvolume-group [fsName]="selection.mdsmap.fs_name"
+ [pools]="details.pools">
</cd-cephfs-subvolume-group>
</ng-template>
</ng-container>
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: [
CephfsSubvolumeListComponent,
CephfsSubvolumeFormComponent,
CephfsDirectoriesComponent,
- CephfsSubvolumeGroupComponent
+ CephfsSubvolumeGroupComponent,
+ CephfsSubvolumegroupFormComponent
]
})
export class CephfsModule {}
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'
get(volName: string): Observable<CephfsSubvolumeGroup[]> {
return this.http.get<CephfsSubvolumeGroup[]>(`${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);
+ })
+ );
+ }
}
),
'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)
)
};
return $localize`subvolume '${metadata.subVolumeName}'`;
}
+ subvolumegroup(metadata: any) {
+ return $localize`subvolume group '${metadata.subvolumegroupName}'`;
+ }
+
crudMessageId(id: string) {
return $localize`${id}`;
}
- 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: