Merge pull request #52645 from rhcs-dashboard/cephfs-vol-rm

mgr/dashboard: cephfs volume rm and rename

Reviewed-by: Pegonzal <NOT@FOUND>
Reviewed-by: cloudbehl <NOT@FOUND>
Reviewed-by: Nizamudeen A <nia@redhat.com>
This commit is contained in:
Nizamudeen A 2023-08-12 00:23:00 +05:30 committed by GitHub
commit 91f75abf4c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 306 additions and 6 deletions

View File

@ -15,7 +15,8 @@ from ..services.ceph_service import CephService
from ..services.cephfs import CephFS as CephFS_
from ..services.exception import handle_cephfs_error
from ..tools import ViewCache
from . import APIDoc, APIRouter, EndpointDoc, RESTController, UIRouter, allow_empty_body
from . import APIDoc, APIRouter, DeletePermission, Endpoint, EndpointDoc, \
RESTController, UIRouter, UpdatePermission, allow_empty_body
GET_QUOTAS_SCHEMA = {
'max_bytes': (int, ''),
@ -63,6 +64,41 @@ class CephFS(RESTController):
f'Error creating volume {name} with placement {str(service_spec)}: {err}')
return f'Volume {name} created successfully'
@EndpointDoc("Remove CephFS Volume",
parameters={
'name': (str, 'File System Name'),
})
@allow_empty_body
@Endpoint('DELETE')
@DeletePermission
def remove(self, name):
error_code, _, err = mgr.remote('volumes', '_cmd_fs_volume_rm', None,
{'vol_name': name,
'yes-i-really-mean-it': "--yes-i-really-mean-it"})
if error_code != 0:
raise DashboardException(
msg=f'Error deleting volume {name}: {err}',
component='cephfs')
return f'Volume {name} removed successfully'
@EndpointDoc("Rename CephFS Volume",
parameters={
'name': (str, 'Existing FS Name'),
'new_name': (str, 'New FS Name'),
})
@allow_empty_body
@UpdatePermission
@Endpoint('PUT')
def rename(self, name: str, new_name: str):
error_code, _, err = mgr.remote('volumes', '_cmd_fs_volume_rename', None,
{'vol_name': name, 'new_vol_name': new_name,
'yes_i_really_mean_it': True})
if error_code != 0:
raise DashboardException(
msg=f'Error renaming volume {name} to {new_name}: {err}',
component='cephfs')
return f'Volume {name} renamed successfully to {new_name}'
def get(self, fs_id):
fs_id = self.fs_id_to_int(fs_id)
return self.fs_status(fs_id)

View File

@ -4,11 +4,17 @@ import { ComponentFixture, TestBed } from '@angular/core/testing';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { RouterTestingModule } from '@angular/router/testing';
import { ToastrModule } from 'ngx-toastr';
import { CdTableSelection } from '~/app/shared/models/cd-table-selection';
import { SharedModule } from '~/app/shared/shared.module';
import { configureTestBed } from '~/testing/unit-test-helper';
import { CriticalConfirmationModalComponent } from '~/app/shared/components/critical-confirmation-modal/critical-confirmation-modal.component';
import { CephfsVolumeFormComponent } from '../cephfs-form/cephfs-form.component';
import { ModalService } from '~/app/shared/services/modal.service';
import { TaskWrapperService } from '~/app/shared/services/task-wrapper.service';
import { CephfsListComponent } from './cephfs-list.component';
import { CephfsService } from '~/app/shared/api/cephfs.service';
@Component({ selector: 'cd-cephfs-tabs', template: '' })
class CephfsTabsStubComponent {
@ -19,19 +25,73 @@ class CephfsTabsStubComponent {
describe('CephfsListComponent', () => {
let component: CephfsListComponent;
let fixture: ComponentFixture<CephfsListComponent>;
let cephfsService: CephfsService;
configureTestBed({
imports: [BrowserAnimationsModule, SharedModule, HttpClientTestingModule, RouterTestingModule],
imports: [
BrowserAnimationsModule,
SharedModule,
HttpClientTestingModule,
ToastrModule.forRoot(),
RouterTestingModule
],
declarations: [CephfsListComponent, CephfsTabsStubComponent, CephfsVolumeFormComponent]
});
beforeEach(() => {
fixture = TestBed.createComponent(CephfsListComponent);
component = fixture.componentInstance;
cephfsService = TestBed.inject(CephfsService);
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
describe('volume deletion', () => {
let taskWrapper: TaskWrapperService;
let modalRef: any;
const setSelectedVolume = (volName: string) =>
(component.selection.selected = [{ mdsmap: { fs_name: volName } }]);
const callDeletion = () => {
component.removeVolumeModal();
expect(modalRef).toBeTruthy();
const deletion: CriticalConfirmationModalComponent = modalRef && modalRef.componentInstance;
deletion.submitActionObservable();
};
const testVolumeDeletion = (volName: string) => {
setSelectedVolume(volName);
callDeletion();
expect(cephfsService.remove).toHaveBeenCalledWith(volName);
expect(taskWrapper.wrapTaskAroundCall).toHaveBeenCalledWith({
task: {
name: 'cephfs/remove',
metadata: {
volumeName: volName
}
},
call: undefined // because of stub
});
};
beforeEach(() => {
spyOn(TestBed.inject(ModalService), 'show').and.callFake((deletionClass, initialState) => {
modalRef = {
componentInstance: Object.assign(new deletionClass(), initialState)
};
return modalRef;
});
spyOn(cephfsService, 'remove').and.stub();
taskWrapper = TestBed.inject(TaskWrapperService);
spyOn(taskWrapper, 'wrapTaskAroundCall').and.callThrough();
});
it('should delete cephfs volume', () => {
testVolumeDeletion('somevolumeName');
});
});
});

View File

@ -2,11 +2,17 @@ import { Component, OnInit } from '@angular/core';
import { Permissions } from '~/app/shared/models/permissions';
import { Router } from '@angular/router';
import _ from 'lodash';
import { CephfsService } from '~/app/shared/api/cephfs.service';
import { ConfigurationService } from '~/app/shared/api/configuration.service';
import { ListWithDetails } from '~/app/shared/classes/list-with-details.class';
import { ActionLabelsI18n } from '~/app/shared/constants/app.constants';
import { CellTemplate } from '~/app/shared/enum/cell-template.enum';
import { ActionLabelsI18n } from '~/app/shared/constants/app.constants';
import { Icons } from '~/app/shared/enum/icons.enum';
import { CriticalConfirmationModalComponent } from '~/app/shared/components/critical-confirmation-modal/critical-confirmation-modal.component';
import { NotificationType } from '~/app/shared/enum/notification-type.enum';
import { FormModalComponent } from '~/app/shared/components/form-modal/form-modal.component';
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';
@ -14,6 +20,10 @@ import { CdTableSelection } from '~/app/shared/models/cd-table-selection';
import { CdDatePipe } from '~/app/shared/pipes/cd-date.pipe';
import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
import { URLBuilderService } from '~/app/shared/services/url-builder.service';
import { ModalService } from '~/app/shared/services/modal.service';
import { TaskWrapperService } from '~/app/shared/services/task-wrapper.service';
import { FinishedTask } from '~/app/shared/models/finished-task';
import { NotificationService } from '~/app/shared/services/notification.service';
const BASE_URL = 'cephfs';
@ -29,6 +39,8 @@ export class CephfsListComponent extends ListWithDetails implements OnInit {
selection = new CdTableSelection();
tableActions: CdTableAction[];
permissions: Permissions;
icons = Icons;
monAllowPoolDelete = false;
constructor(
private authStorageService: AuthStorageService,
@ -36,7 +48,11 @@ export class CephfsListComponent extends ListWithDetails implements OnInit {
private cdDatePipe: CdDatePipe,
public actionLabels: ActionLabelsI18n,
private router: Router,
private urlBuilder: URLBuilderService
private urlBuilder: URLBuilderService,
private configurationService: ConfigurationService,
private modalService: ModalService,
private taskWrapper: TaskWrapperService,
public notificationService: NotificationService
) {
super();
this.permissions = this.authStorageService.getPermissions();
@ -69,8 +85,32 @@ export class CephfsListComponent extends ListWithDetails implements OnInit {
icon: Icons.add,
click: () => this.router.navigate([this.urlBuilder.getCreate()]),
canBePrimary: (selection: CdTableSelection) => !selection.hasSelection
},
{
name: this.actionLabels.EDIT,
permission: 'update',
icon: Icons.edit,
click: () => this.editAction()
},
{
permission: 'delete',
icon: Icons.destroy,
click: () => this.removeVolumeModal(),
name: this.actionLabels.REMOVE,
disable: this.getDisableDesc.bind(this)
}
];
if (this.permissions.configOpt.read) {
this.configurationService.get('mon_allow_pool_delete').subscribe((data: any) => {
if (_.has(data, 'value')) {
const monSection = _.find(data.value, (v) => {
return v.section === 'mon';
}) || { value: false };
this.monAllowPoolDelete = monSection.value === 'true' ? true : false;
}
});
}
}
loadFilesystems(context: CdTableFetchDataContext) {
@ -87,4 +127,56 @@ export class CephfsListComponent extends ListWithDetails implements OnInit {
updateSelection(selection: CdTableSelection) {
this.selection = selection;
}
removeVolumeModal() {
const volName = this.selection.first().mdsmap['fs_name'];
this.modalService.show(CriticalConfirmationModalComponent, {
itemDescription: 'Volume',
itemNames: [volName],
actionDescription: 'remove',
submitActionObservable: () =>
this.taskWrapper.wrapTaskAroundCall({
task: new FinishedTask('cephfs/remove', { volumeName: volName }),
call: this.cephfsService.remove(volName)
})
});
}
getDisableDesc(): boolean | string {
if (this.selection?.hasSelection) {
if (!this.monAllowPoolDelete) {
return $localize`Volume deletion is disabled by the mon_allow_pool_delete configuration setting.`;
}
return false;
}
return true;
}
editAction() {
const selectedVolume = this.selection.first().mdsmap['fs_name'];
this.modalService.show(FormModalComponent, {
titleText: $localize`Edit Volume: ${selectedVolume}`,
fields: [
{
type: 'text',
name: 'volumeName',
value: selectedVolume,
label: $localize`Name`,
required: true
}
],
submitButtonText: $localize`Edit Volume`,
onSubmit: (values: any) => {
this.cephfsService.rename(selectedVolume, values.volumeName).subscribe(() => {
this.notificationService.show(
NotificationType.success,
$localize`Updated Volume '${selectedVolume}'`
);
});
}
});
}
}

View File

@ -1,4 +1,4 @@
import { Component, Input, OnInit, ViewChild } from '@angular/core';
import { Component, Input, OnChanges, OnInit, ViewChild } from '@angular/core';
import { Observable, ReplaySubject, of } from 'rxjs';
import { catchError, shareReplay, switchMap } from 'rxjs/operators';
@ -21,7 +21,7 @@ import { Permissions } from '~/app/shared/models/permissions';
templateUrl: './cephfs-subvolume-group.component.html',
styleUrls: ['./cephfs-subvolume-group.component.scss']
})
export class CephfsSubvolumeGroupComponent implements OnInit {
export class CephfsSubvolumeGroupComponent implements OnInit, OnChanges {
@ViewChild('quotaUsageTpl', { static: true })
quotaUsageTpl: any;

View File

@ -95,4 +95,20 @@ describe('CephfsService', () => {
expect(req.request.method).toBe('PUT');
expect(req.request.body).toEqual({ max_bytes: 1024, max_files: 10 });
});
it('should rename the cephfs volume', () => {
const volName = 'testvol';
const newVolName = 'newtestvol';
service.rename(volName, newVolName).subscribe();
const req = httpTesting.expectOne('api/cephfs/rename');
expect(req.request.method).toBe('PUT');
expect(req.request.body).toEqual({ name: 'testvol', new_name: 'newtestvol' });
});
it('should remove the cephfs volume', () => {
const volName = 'testvol';
service.remove(volName).subscribe();
const req = httpTesting.expectOne(`api/cephfs/remove/${volName}`);
expect(req.request.method).toBe('DELETE');
});
});

View File

@ -87,4 +87,20 @@ export class CephfsService {
isCephFsPool(pool: any) {
return _.indexOf(pool.application_metadata, 'cephfs') !== -1 && !pool.pool_name.includes('/');
}
remove(name: string) {
return this.http.delete(`${this.baseURL}/remove/${name}`, {
observe: 'response'
});
}
rename(vol_name: string, new_vol_name: string) {
let requestBody = {
name: vol_name,
new_name: new_vol_name
};
return this.http.put(`${this.baseURL}/rename`, requestBody, {
observe: 'response'
});
}
}

View File

@ -361,6 +361,9 @@ export class TaskMessageService {
),
'cephfs/subvolume/group/create': this.newTaskMessage(this.commonOperations.create, (metadata) =>
this.subvolumegroup(metadata)
),
'cephfs/remove': this.newTaskMessage(this.commonOperations.remove, (metadata) =>
this.volume(metadata)
)
};

View File

@ -1681,6 +1681,83 @@ paths:
- jwt: []
tags:
- Cephfs
/api/cephfs/remove/{name}:
delete:
parameters:
- description: File System Name
in: path
name: name
required: true
schema:
type: string
responses:
'202':
content:
application/vnd.ceph.api.v1.0+json:
type: object
description: Operation is still executing. Please check the task queue.
'204':
content:
application/vnd.ceph.api.v1.0+json:
type: object
description: Resource deleted.
'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: []
summary: Remove CephFS Volume
tags:
- Cephfs
/api/cephfs/rename:
put:
parameters: []
requestBody:
content:
application/json:
schema:
properties:
name:
description: Existing FS Name
type: string
new_name:
description: New FS Name
type: string
required:
- name
- new_name
type: object
responses:
'200':
content:
application/vnd.ceph.api.v1.0+json:
type: object
description: Resource updated.
'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: []
summary: Rename CephFS Volume
tags:
- Cephfs
/api/cephfs/subvolume:
post:
parameters: []