mirror of
https://github.com/ceph/ceph
synced 2025-02-21 01:47:25 +00:00
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:
commit
91f75abf4c
@ -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)
|
||||
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -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}'`
|
||||
);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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');
|
||||
});
|
||||
});
|
||||
|
@ -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'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
)
|
||||
};
|
||||
|
||||
|
@ -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: []
|
||||
|
Loading…
Reference in New Issue
Block a user