mgr/dashboard: create subvolumegroup

Fixes: https://tracker.ceph.com/issues/62363
Signed-off-by: Pedro Gonzalez Gomez <pegonzal@redhat.com>
This commit is contained in:
Pedro Gonzalez Gomez 2023-08-09 02:05:40 +02:00
parent 75389ba061
commit c5e39cb921
12 changed files with 489 additions and 15 deletions

View File

@ -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}'
)

View File

@ -5,7 +5,17 @@
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>

View File

@ -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<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 = [
@ -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) {

View File

@ -0,0 +1,139 @@
<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>

View File

@ -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<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();
});
});

View File

@ -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<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();
}
});
}
}

View File

@ -24,7 +24,8 @@
<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>

View File

@ -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 {}

View File

@ -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<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);
})
);
}
}

View File

@ -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}`;
}

View File

@ -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: