Adds subvolume groups into the subvolume tabs in order to select the subvolumes from the appropiate group.
Also adds the capabilities to manage the subvolume groups of the subvolume in the different actions, create, edit, remove.
Fixes: https://tracker.ceph.com/issues/62675
Signed-off-by: Pedro Gonzalez Gomez <pegonzal@redhat.com>
@APIDoc('CephFS Subvolume Management API', 'CephFSSubvolume')
class CephFSSubvolume(RESTController):
- def get(self, vol_name: str):
+ def get(self, vol_name: str, group_name: str = ""):
+ params = {'vol_name': vol_name}
+ if group_name:
+ params['group_name'] = group_name
error_code, out, err = mgr.remote(
- 'volumes', '_cmd_fs_subvolume_ls', None, {'vol_name': vol_name})
+ 'volumes', '_cmd_fs_subvolume_ls', None, params)
if error_code != 0:
raise DashboardException(
f'Failed to list subvolumes for volume {vol_name}: {err}'
)
subvolumes = json.loads(out)
for subvolume in subvolumes:
- error_code, out, err = mgr.remote('volumes', '_cmd_fs_subvolume_info', None, {
- 'vol_name': vol_name, 'sub_name': subvolume['name']})
+ params['sub_name'] = subvolume['name']
+ error_code, out, err = mgr.remote('volumes', '_cmd_fs_subvolume_info', None,
+ params)
if error_code != 0:
raise DashboardException(
f'Failed to get info for subvolume {subvolume["name"]}: {err}'
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})
+ def info(self, vol_name: str, subvol_name: str, group_name: str = ""):
+ params = {'vol_name': vol_name, 'sub_name': subvol_name}
+ if group_name:
+ params['group_name'] = group_name
+ error_code, out, err = mgr.remote('volumes', '_cmd_fs_subvolume_info', None,
+ params)
if error_code != 0:
raise DashboardException(
f'Failed to get info for subvolume {subvol_name}: {err}'
return f'Subvolume {subvol_name} created successfully'
- def set(self, vol_name: str, subvol_name: str, size: str):
+ def set(self, vol_name: str, subvol_name: str, size: str, group_name: str = ""):
+ params = {'vol_name': vol_name, 'sub_name': subvol_name}
if size:
- error_code, _, err = mgr.remote('volumes', '_cmd_fs_subvolume_resize', None, {
- 'vol_name': vol_name, 'sub_name': subvol_name, 'new_size': size})
+ params['new_size'] = size
+ if group_name:
+ params['group_name'] = group_name
+ error_code, _, err = mgr.remote('volumes', '_cmd_fs_subvolume_resize', None,
+ params)
if error_code != 0:
raise DashboardException(
f'Failed to update subvolume {subvol_name}: {err}'
return f'Subvolume {subvol_name} updated successfully'
- def delete(self, vol_name: str, subvol_name: str, retain_snapshots: bool = False):
+ def delete(self, vol_name: str, subvol_name: str, group_name: str = "",
+ retain_snapshots: bool = False):
params = {'vol_name': vol_name, 'sub_name': subvol_name}
+ if group_name:
+ params['group_name'] = group_name
retain_snapshots = str_to_bool(retain_snapshots)
if retain_snapshots:
params['retain_snapshots'] = 'True'
</div>
</div>
+ <!--Subvolume Group name -->
+ <div class="form-group row">
+ <label class="cd-col-form-label"
+ for="subvolumeGroupName"
+ i18n>Subvolume group
+ </label>
+ <div class="cd-col-form-input">
+ <select class="form-select"
+ id="subvolumeGroupName"
+ name="subvolumeGroupName"
+ formControlName="subvolumeGroupName"
+ *ngIf="subVolumeGroups$ | async as subvolumeGroups">
+ <option value=""
+ i18n>Default</option>
+ <option *ngFor="let subvolumegroup of subvolumeGroups"
+ [value]="subvolumegroup.name">{{ subvolumegroup.name }}</option>
+ </select>
+ </div>
+ </div>
+
<!-- Size -->
<div class="form-group row">
<label class="cd-col-form-label"
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';
import { CephfsSubvolumeInfo } from '~/app/shared/models/cephfs-subvolume.model';
import { DimlessBinaryPipe } from '~/app/shared/pipes/dimless-binary.pipe';
import { OctalToHumanReadablePipe } from '~/app/shared/pipes/octal-to-human-readable.pipe';
import { CdForm } from '~/app/shared/forms/cd-form';
+import { CephfsSubvolumeGroupService } from '~/app/shared/api/cephfs-subvolume-group.service';
+import { CephfsSubvolumeGroup } from '~/app/shared/models/cephfs-subvolume-group.model';
+import { Observable } from 'rxjs';
@Component({
selector: 'cd-cephfs-subvolume-form',
export class CephfsSubvolumeFormComponent extends CdForm implements OnInit {
fsName: string;
subVolumeName: string;
+ subVolumeGroupName: string;
pools: Pool[];
isEdit = false;
action: string;
resource: string;
+ subVolumeGroups$: Observable<CephfsSubvolumeGroup[]>;
+ subVolumeGroups: CephfsSubvolumeGroup[];
dataPools: Pool[];
columns: CdTableColumn[];
private actionLabels: ActionLabelsI18n,
private taskWrapper: TaskWrapperService,
private cephFsSubvolumeService: CephfsSubvolumeService,
+ private cephFsSubvolumeGroupService: CephfsSubvolumeGroupService,
private formatter: FormatterService,
private dimlessBinary: DimlessBinaryPipe,
private octalToHumanReadable: OctalToHumanReadablePipe
}
];
+ this.subVolumeGroups$ = this.cephFsSubvolumeGroupService.get(this.fsName);
this.dataPools = this.pools.filter((pool) => pool.type === 'data');
this.createForm();
)
]
}),
+ subvolumeGroupName: new FormControl(this.subVolumeGroupName),
pool: new FormControl(this.dataPools[0]?.pool, {
validators: [Validators.required]
}),
populateForm() {
this.action = this.actionLabels.EDIT;
this.cephFsSubvolumeService
- .info(this.fsName, this.subVolumeName)
+ .info(this.fsName, this.subVolumeName, this.subVolumeGroupName)
.subscribe((resp: CephfsSubvolumeInfo) => {
// Disabled these fields since its not editable
this.subvolumeForm.get('subvolumeName').disable();
+ this.subvolumeForm.get('subvolumeGroupName').disable();
this.subvolumeForm.get('pool').disable();
this.subvolumeForm.get('uid').disable();
this.subvolumeForm.get('gid').disable();
this.subvolumeForm.get('isolatedNamespace').disable();
this.subvolumeForm.get('subvolumeName').setValue(this.subVolumeName);
+ this.subvolumeForm.get('subvolumeGroupName').setValue(this.subVolumeGroupName);
if (resp.bytes_quota !== 'infinite') {
this.subvolumeForm.get('size').setValue(this.dimlessBinary.transform(resp.bytes_quota));
}
submit() {
const subVolumeName = this.subvolumeForm.getValue('subvolumeName');
+ const subVolumeGroupName = this.subvolumeForm.getValue('subvolumeGroupName');
const pool = this.subvolumeForm.getValue('pool');
const size = this.formatter.toBytes(this.subvolumeForm.getValue('size')) || 0;
const uid = this.subvolumeForm.getValue('uid');
task: new FinishedTask('cephfs/subvolume/' + URLVerbs.EDIT, {
subVolumeName: subVolumeName
}),
- call: this.cephFsSubvolumeService.update(this.fsName, subVolumeName, String(editSize))
+ call: this.cephFsSubvolumeService.update(
+ this.fsName,
+ subVolumeName,
+ String(editSize),
+ subVolumeGroupName
+ )
})
.subscribe({
error: () => {
call: this.cephFsSubvolumeService.create(
this.fsName,
subVolumeName,
+ subVolumeGroupName,
pool,
String(size),
uid,
-<ng-container *ngIf="subVolumes$ | async as subVolumes">
- <cd-table [data]="subVolumes"
- columnMode="flex"
- [columns]="columns"
- selectionType="single"
- [hasDetails]="false"
- (fetchData)="fetchData()"
- (updateSelection)="updateSelection($event)">
+<div class="row">
+ <div class="col-sm-1">
+ <h3 i18n>Groups</h3>
+ <ng-container *ngIf="subVolumeGroups$ | async as subVolumeGroups">
+ <ul class="nav flex-column nav-pills">
+ <li class="nav-item">
+ <a class="nav-link"
+ [class.active]="!activeGroupName"
+ (click)="selectSubVolumeGroup()">Default</a>
+ </li>
+ <li class="nav-item"
+ *ngFor="let subVolumeGroup of subVolumeGroups">
+ <a class="nav-link text-decoration-none text-break"
+ [class.active]="subVolumeGroup.name === activeGroupName"
+ (click)="selectSubVolumeGroup(subVolumeGroup.name)">{{subVolumeGroup.name}}</a>
+ </li>
+ </ul>
+ </ng-container>
+ </div>
+ <div class="col-11 vertical-line">
+ <cd-table [data]="subVolumes$ | async"
+ columnMode="flex"
+ [columns]="columns"
+ selectionType="single"
+ [hasDetails]="false"
+ (fetchData)="fetchData()"
+ (updateSelection)="updateSelection($event)">
- <div class="table-actions btn-toolbar">
- <cd-table-actions [permission]="permissions.cephfs"
- [selection]="selection"
- class="btn-group"
- id="cephfs-subvolume-actions"
- [tableActions]="tableActions">
- </cd-table-actions>
- </div>
- </cd-table>
-</ng-container>
+ <div class="table-actions btn-toolbar">
+ <cd-table-actions [permission]="permissions.cephfs"
+ [selection]="selection"
+ class="btn-group"
+ id="cephfs-subvolume-actions"
+ [tableActions]="tableActions">
+ </cd-table-actions>
+ </div>
+ </cd-table>
+ </div>
+</div>
<ng-template #quotaUsageTpl
let-row="row">
import { CdFormGroup } from '~/app/shared/forms/cd-form-group';
import { CdForm } from '~/app/shared/forms/cd-form';
import { CriticalConfirmationModalComponent } from '~/app/shared/components/critical-confirmation-modal/critical-confirmation-modal.component';
+import { CephfsSubvolumeGroupService } from '~/app/shared/api/cephfs-subvolume-group.service';
+import { CephfsSubvolumeGroup } from '~/app/shared/models/cephfs-subvolumegroup.model';
@Component({
selector: 'cd-cephfs-subvolume-list',
selectedName: string = '';
subVolumes$: Observable<CephfsSubvolume[]>;
+ subVolumeGroups$: Observable<CephfsSubvolumeGroup[]>;
subject = new ReplaySubject<CephfsSubvolume[]>();
+ groupsSubject = new ReplaySubject<CephfsSubvolume[]>();
+
+ activeGroupName: string = '';
constructor(
private cephfsSubVolume: CephfsSubvolumeService,
private actionLabels: ActionLabelsI18n,
private modalService: ModalService,
private authStorageService: AuthStorageService,
- private taskWrapper: TaskWrapperService
+ private taskWrapper: TaskWrapperService,
+ private cephfsSubvolumeGroupService: CephfsSubvolumeGroupService
) {
super();
this.permissions = this.authStorageService.getPermissions();
name: this.actionLabels.CREATE,
permission: 'create',
icon: Icons.add,
- click: () =>
- this.modalService.show(
- CephfsSubvolumeFormComponent,
- {
- fsName: this.fsName,
- pools: this.pools
- },
- { size: 'lg' }
- )
+ click: () => this.openModal()
},
{
name: this.actionLabels.EDIT,
}
];
- this.subVolumes$ = this.subject.pipe(
+ this.getSubVolumes();
+
+ this.subVolumeGroups$ = this.groupsSubject.pipe(
switchMap(() =>
- this.cephfsSubVolume.get(this.fsName).pipe(
+ this.cephfsSubvolumeGroupService.get(this.fsName).pipe(
catchError(() => {
this.context.error();
return of(null);
})
)
- ),
- shareReplay(1)
+ )
);
}
ngOnChanges() {
this.subject.next();
+ this.groupsSubject.next();
}
updateSelection(selection: CdTableSelection) {
{
fsName: this.fsName,
subVolumeName: this.selection?.first()?.name,
+ subVolumeGroupName: this.activeGroupName,
pools: this.pools,
isEdit: edit
},
call: this.cephfsSubVolume.remove(
this.fsName,
this.selectedName,
+ this.activeGroupName,
this.removeForm.getValue('retainSnapshots')
)
})
})
});
}
+
+ selectSubVolumeGroup(subVolumeGroupName: string) {
+ this.activeGroupName = subVolumeGroupName;
+ this.getSubVolumes(subVolumeGroupName);
+ }
+
+ getSubVolumes(subVolumeGroupName = '') {
+ this.subVolumes$ = this.subject.pipe(
+ switchMap(() =>
+ this.cephfsSubVolume.get(this.fsName, subVolumeGroupName).pipe(
+ catchError(() => {
+ this.context.error();
+ return of(null);
+ })
+ )
+ ),
+ shareReplay(1)
+ );
+ }
}
it('should call get', () => {
service.get('testFS').subscribe();
- const req = httpTesting.expectOne('api/cephfs/subvolume/testFS');
+ const req = httpTesting.expectOne('api/cephfs/subvolume/testFS?group_name=');
expect(req.request.method).toBe('GET');
});
it('should call remove', () => {
service.remove('testFS', 'testSubvol').subscribe();
const req = httpTesting.expectOne(
- 'api/cephfs/subvolume/testFS?subvol_name=testSubvol&retain_snapshots=false'
+ 'api/cephfs/subvolume/testFS?subvol_name=testSubvol&group_name=&retain_snapshots=false'
);
expect(req.request.method).toBe('DELETE');
});
constructor(private http: HttpClient) {}
- get(fsName: string): Observable<CephfsSubvolume[]> {
- return this.http.get<CephfsSubvolume[]>(`${this.baseURL}/${fsName}`);
+ get(fsName: string, subVolumeGroupName: string = ''): Observable<CephfsSubvolume[]> {
+ return this.http.get<CephfsSubvolume[]>(`${this.baseURL}/${fsName}`, {
+ params: {
+ group_name: subVolumeGroupName
+ }
+ });
}
create(
fsName: string,
subVolumeName: string,
+ subVolumeGroupName: string,
poolName: string,
size: string,
uid: number,
{
vol_name: fsName,
subvol_name: subVolumeName,
+ group_name: subVolumeGroupName,
pool_layout: poolName,
size: size,
uid: uid,
);
}
- info(fsName: string, subVolumeName: string) {
+ info(fsName: string, subVolumeName: string, subVolumeGroupName: string = '') {
return this.http.get(`${this.baseURL}/${fsName}/info`, {
params: {
- subvol_name: subVolumeName
+ subvol_name: subVolumeName,
+ group_name: subVolumeGroupName
}
});
}
- remove(fsName: string, subVolumeName: string, retainSnapshots: boolean = false) {
+ remove(
+ fsName: string,
+ subVolumeName: string,
+ subVolumeGroupName: string = '',
+ retainSnapshots: boolean = false
+ ) {
return this.http.delete(`${this.baseURL}/${fsName}`, {
params: {
subvol_name: subVolumeName,
+ group_name: subVolumeGroupName,
retain_snapshots: retainSnapshots
},
observe: 'response'
);
}
- update(fsName: string, subVolumeName: string, size: string) {
+ update(fsName: string, subVolumeName: string, size: string, subVolumeGroupName: string = '') {
return this.http.put(`${this.baseURL}/${fsName}`, {
subvol_name: subVolumeName,
- size: size
+ size: size,
+ group_name: subVolumeGroupName
});
}
}
required: true
schema:
type: string
+ - default: ''
+ in: query
+ name: group_name
+ schema:
+ type: string
- default: false
in: query
name: retain_snapshots
required: true
schema:
type: string
+ - default: ''
+ in: query
+ name: group_name
+ schema:
+ type: string
responses:
'200':
content:
application/json:
schema:
properties:
+ group_name:
+ default: ''
+ type: string
size:
type: integer
subvol_name:
required: true
schema:
type: string
+ - default: ''
+ in: query
+ name: group_name
+ schema:
+ type: string
responses:
'200':
content: