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