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, ''),
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)
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 {
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');
+ });
+ });
});
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';
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';
selection = new CdTableSelection();
tableActions: CdTableAction[];
permissions: Permissions;
+ icons = Icons;
+ monAllowPoolDelete = false;
constructor(
private authStorageService: AuthStorageService,
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();
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) {
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}'`
+ );
+ });
+ }
+ });
+ }
}
-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';
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;
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');
+ });
});
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'
+ });
+ }
}
),
'cephfs/subvolume/group/create': this.newTaskMessage(this.commonOperations.create, (metadata) =>
this.subvolumegroup(metadata)
+ ),
+ 'cephfs/remove': this.newTaskMessage(this.commonOperations.remove, (metadata) =>
+ this.volume(metadata)
)
};
- 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: []