component='cephfs')
return f'Volume {name} renamed successfully to {new_name}'
+ @UpdatePermission
+ @Endpoint('PUT')
+ @EndpointDoc("Set Ceph authentication capabilities for the specified user ID in the given path",
+ parameters={
+ 'fs_name': (str, 'File system name'),
+ 'client_id': (str, 'Cephx user ID'),
+ 'caps': (str, 'Path and given capabilities'),
+ 'root_squash': (str, 'File System Identifier'),
+
+ })
+ def auth(self, fs_name: str, client_id: int, caps: List[str], root_squash: bool):
+ if root_squash:
+ caps.insert(2, 'root_squash')
+ error_code, _, err = mgr.mon_command({'prefix': 'fs authorize',
+ 'filesystem': fs_name,
+ 'entity': client_id,
+ 'caps': caps})
+ if error_code != 0:
+ raise DashboardException(
+ msg=f'Error setting authorization for {client_id} with {caps}: {err}',
+ component='cephfs')
+ return f'Updated {client_id} authorization successfully'
+
def get(self, fs_id):
fs_id = self.fs_id_to_int(fs_id)
return self.fs_status(fs_id)
--- /dev/null
+<cd-modal [modalRef]="activeModal">
+ <ng-container i18n="form title"
+ class="modal-title">{{ action | titlecase }} {{ resource | upperFirst }}</ng-container>
+ <ng-container class="modal-content"
+ *cdFormLoading="loading">
+ <form name="form"
+ #formDir="ngForm"
+ [formGroup]="form">
+ <div class="modal-body">
+
+ <!-- FsName -->
+ <div class="form-group row">
+ <label class="cd-col-form-label required"
+ for="userId"
+ i18n>Fs name
+ </label>
+ <div class="cd-col-form-input">
+ <input id="fsName"
+ name="fsName"
+ type="text"
+ class="form-control"
+ formControlName="fsName">
+ <span class="invalid-feedback"
+ *ngIf="form.showError('fsName', formDir, 'required')"
+ i18n>This field is required!</span>
+ </div>
+ </div>
+
+ <!-- UserId -->
+ <div class="form-group row">
+ <label class="cd-col-form-label required"
+ for="userId"
+ i18n>User ID
+ <cd-helper>
+ You can manage users from
+ <a routerLink="/ceph-users"
+ (click)="closeModal()">Ceph Users</a>
+ page
+ </cd-helper>
+ </label>
+ <div class="cd-col-form-input">
+ <div class="input-group">
+ <span class="input-group-text"
+ for="userId"
+ i18n>client.
+ </span>
+ <input id="userId"
+ name="userId"
+ type="text"
+ class="form-control"
+ formControlName="userId">
+ <span class="invalid-feedback"
+ *ngIf="form.showError('userId', formDir, 'required')"
+ i18n>This field is required!</span>
+ </div>
+ </div>
+ </div>
+
+ <!-- Directory -->
+ <div class="form-group row">
+ <label class="cd-col-form-label required"
+ for="directory"
+ i18n>Directory
+ <cd-helper>Path to restrict access to</cd-helper>
+ </label>
+ <div class="cd-col-form-input">
+ <input id="typeahead-http"
+ i18n
+ type="text"
+ class="form-control"
+ disabled="directoryStore.isLoading"
+ formControlName="directory"
+ [ngbTypeahead]="search"
+ [placeholder]="directoryStore.isLoading ? 'Loading directories' : 'Directory search'"
+ i18n-placeholder>
+ <div *ngIf="directoryStore.isLoading">
+ <i [ngClass]="[icons.spinner, icons.spin, 'mt-2', 'me-2']"></i>
+ </div>
+ <span class="invalid-feedback"
+ *ngIf="form.showError('directory', formDir, 'required')"
+ i18n>This field is required!</span>
+ </div>
+ </div>
+
+ <!-- Permissions -->
+ <div class="form-group row">
+ <label i18n
+ class="cd-col-form-label"
+ for="permissions">Permissons</label>
+ <div class="cd-col-form-input">
+
+ <!-- Read -->
+ <div class="custom-control custom-checkbox">
+ <input class="custom-control-input"
+ id="read"
+ formControlName="read"
+ type="checkbox">
+ <label class="custom-control-label"
+ for="read"
+ i18n>Read
+ </label>
+ <cd-helper i18n>Read permission is the minimum givable access</cd-helper>
+ </div>
+
+ <!-- Write -->
+ <div class="custom-control custom-checkbox">
+ <input class="custom-control-input"
+ id="write"
+ formControlName="write"
+ type="checkbox"
+ (change)="toggleFormControl()">
+ <label class="custom-control-label"
+ for="write"
+ i18n>Write
+ </label>
+ </div>
+
+ <!-- Quota -->
+ <div class="custom-control custom-checkbox">
+ <input class="custom-control-input"
+ id="quota"
+ formControlName="quota"
+ type="checkbox">
+ <label class="custom-control-label"
+ for="quota"
+ i18n>Quota
+ </label>
+ <cd-helper i18n>Permission to set layouts or quotas, write access needed</cd-helper>
+ </div>
+
+ <!-- Snapshot -->
+ <div class="custom-control custom-checkbox">
+ <input class="custom-control-input"
+ id="snapshot"
+ formControlName="snapshot"
+ type="checkbox">
+ <label class="custom-control-label"
+ for="snapshot"
+ i18n>Snapshot
+ </label>
+ <cd-helper i18n>Permission to create or delete snapshots, write access needed</cd-helper>
+ </div>
+
+ <!-- Root Squash -->
+ <div class="custom-control custom-checkbox">
+ <input class="custom-control-input"
+ id="rootSquash"
+ formControlName="rootSquash"
+ type="checkbox">
+ <label class="custom-control-label"
+ for="rootSquash"
+ i18n>Root Squash
+ </label>
+ <cd-helper>Safety measure to prevent scenarios such as accidental sudo rm -rf /path</cd-helper>
+ </div>
+ </div>
+ </div>
+ </div>
+ <div class="modal-footer">
+ <cd-form-button-panel (submitActionEvent)="onSubmit()"
+ [form]="form"
+ [submitText]="(action | titlecase)"></cd-form-button-panel>
+ </div>
+ </form>
+ </ng-container>
+</cd-modal>
--- /dev/null
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { CephfsAuthModalComponent } from './cephfs-auth-modal.component';
+import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { ToastrModule } from 'ngx-toastr';
+import { SharedModule } from '~/app/shared/shared.module';
+import { ReactiveFormsModule } from '@angular/forms';
+
+describe('CephfsAuthModalComponent', () => {
+ let component: CephfsAuthModalComponent;
+ let fixture: ComponentFixture<CephfsAuthModalComponent>;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ declarations: [CephfsAuthModalComponent],
+ imports: [HttpClientTestingModule, SharedModule, ReactiveFormsModule, ToastrModule.forRoot()],
+ providers: [NgbActiveModal]
+ }).compileComponents();
+
+ fixture = TestBed.createComponent(CephfsAuthModalComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
--- /dev/null
+import { Component, OnInit } from '@angular/core';
+import { FormControl, Validators } from '@angular/forms';
+import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
+import { OperatorFunction, Observable, of } from 'rxjs';
+import { debounceTime, distinctUntilChanged, switchMap, catchError } from 'rxjs/operators';
+import { CephfsService } from '~/app/shared/api/cephfs.service';
+import { DirectoryStoreService } from '~/app/shared/api/directory-store.service';
+import { ActionLabelsI18n } from '~/app/shared/constants/app.constants';
+import { Icons } from '~/app/shared/enum/icons.enum';
+import { CdForm } from '~/app/shared/forms/cd-form';
+import { CdFormGroup } from '~/app/shared/forms/cd-form-group';
+import { FinishedTask } from '~/app/shared/models/finished-task';
+import { TaskWrapperService } from '~/app/shared/services/task-wrapper.service';
+
+const DEBOUNCE_TIMER = 300;
+
+@Component({
+ selector: 'cd-cephfs-auth-modal',
+ templateUrl: './cephfs-auth-modal.component.html',
+ styleUrls: ['./cephfs-auth-modal.component.scss']
+})
+export class CephfsAuthModalComponent extends CdForm implements OnInit {
+ fsName: string;
+ id: number;
+ subvolumeGroup: string;
+ subvolume: string;
+ isDefaultSubvolumeGroup = false;
+ isSubvolume = false;
+ form: CdFormGroup;
+ action: string;
+ resource: string;
+ icons = Icons;
+
+ constructor(
+ public activeModal: NgbActiveModal,
+ private actionLabels: ActionLabelsI18n,
+ public directoryStore: DirectoryStoreService,
+ private cephfsService: CephfsService,
+ private taskWrapper: TaskWrapperService
+ ) {
+ super();
+ this.action = this.actionLabels.UPDATE;
+ this.resource = $localize`access`;
+ }
+
+ ngOnInit() {
+ this.directoryStore.loadDirectories(this.id, '/', 3);
+ this.createForm();
+ this.loadingReady();
+ }
+
+ createForm() {
+ this.form = new CdFormGroup({
+ fsName: new FormControl(
+ { value: this.fsName, disabled: true },
+ {
+ validators: [Validators.required]
+ }
+ ),
+ directory: new FormControl(undefined, {
+ updateOn: 'blur',
+ validators: [Validators.required]
+ }),
+ userId: new FormControl(undefined, {
+ validators: [Validators.required]
+ }),
+ read: new FormControl(
+ { value: true, disabled: true },
+ {
+ validators: [Validators.required]
+ }
+ ),
+ write: new FormControl(undefined),
+ snapshot: new FormControl({ value: false, disabled: true }),
+ quota: new FormControl({ value: false, disabled: true }),
+ rootSquash: new FormControl(undefined)
+ });
+ }
+
+ search: OperatorFunction<string, readonly string[]> = (input: Observable<string>) =>
+ input.pipe(
+ debounceTime(DEBOUNCE_TIMER),
+ distinctUntilChanged(),
+ switchMap((term) =>
+ this.directoryStore.search(term, this.id).pipe(
+ catchError(() => {
+ return of([]);
+ })
+ )
+ )
+ );
+
+ closeModal() {
+ this.activeModal.close();
+ }
+
+ onSubmit() {
+ const clientId: number = this.form.getValue('userId');
+ const caps: string[] = [this.form.getValue('directory'), this.transformPermissions()];
+ const rootSquash: boolean = this.form.getValue('rootSquash');
+ this.taskWrapper
+ .wrapTaskAroundCall({
+ task: new FinishedTask('cephfs/auth', {
+ clientId: clientId
+ }),
+ call: this.cephfsService.setAuth(this.fsName, clientId, caps, rootSquash)
+ })
+ .subscribe({
+ error: () => this.form.setErrors({ cdSubmitButton: true }),
+ complete: () => {
+ this.activeModal.close();
+ }
+ });
+ }
+
+ transformPermissions(): string {
+ const write = this.form.getValue('write');
+ const snapshot = this.form.getValue('snapshot');
+ const quota = this.form.getValue('quota');
+ return `r${write ? 'w' : ''}${quota ? 'p' : ''}${snapshot ? 's' : ''}`;
+ }
+
+ toggleFormControl() {
+ const snapshot = this.form.get('snapshot');
+ const quota = this.form.get('quota');
+ snapshot.disabled ? snapshot.enable() : snapshot.disable();
+ quota.disabled ? quota.enable() : quota.disable();
+ }
+}
import { CephfsMountDetailsComponent } from '../cephfs-mount-details/cephfs-mount-details.component';
import { map, switchMap } from 'rxjs/operators';
import { HealthService } from '~/app/shared/api/health.service';
+import { CephfsAuthModalComponent } from '~/app/ceph/cephfs/cephfs-auth-modal/cephfs-auth-modal.component';
const BASE_URL = 'cephfs';
click: () =>
this.router.navigate([this.urlBuilder.getEdit(String(this.selection.first().id))])
},
+ {
+ name: this.actionLabels.AUTHORIZE,
+ permission: 'update',
+ icon: Icons.edit,
+ click: () => this.authorizeModal()
+ },
{
name: this.actionLabels.ATTACH,
permission: 'read',
return true;
}
+
+ authorizeModal() {
+ const selectedFileSystem = this.selection?.selected?.[0];
+ this.modalService.show(
+ CephfsAuthModalComponent,
+ {
+ fsName: selectedFileSystem.mdsmap['fs_name'],
+ id: selectedFileSystem.id
+ },
+ { size: 'lg' }
+ );
+ }
}
<cd-cephfs-subvolume-list
[fsName]="selection.mdsmap.fs_name"
[pools]="details.pools"
+ [id]="id"
></cd-cephfs-subvolume-list>
</ng-template>
</ng-container>
import { CephfsSubvolumeSnapshotsFormComponent } from './cephfs-subvolume-snapshots-list/cephfs-subvolume-snapshots-form/cephfs-subvolume-snapshots-form.component';
import { CephfsSnapshotscheduleFormComponent } from './cephfs-snapshotschedule-form/cephfs-snapshotschedule-form.component';
import { CephfsMountDetailsComponent } from './cephfs-mount-details/cephfs-mount-details.component';
+import { CephfsAuthModalComponent } from './cephfs-auth-modal/cephfs-auth-modal.component';
@NgModule({
imports: [
CephfsSnapshotscheduleListComponent,
CephfsSnapshotscheduleFormComponent,
CephfsSubvolumeSnapshotsFormComponent,
- CephfsMountDetailsComponent
+ CephfsMountDetailsComponent,
+ CephfsAuthModalComponent
]
})
export class CephfsModule {}
observe: 'response'
});
}
+
+ setAuth(fsName: string, clientId: number, caps: string[], rootSquash: boolean) {
+ return this.http.put(`${this.baseURL}/auth`, {
+ fs_name: fsName,
+ client_id: `client.${clientId}`,
+ caps: caps,
+ root_squash: rootSquash
+ });
+ }
}
ACTIVATE: string;
DEACTIVATE: string;
ATTACH: string;
+ AUTHORIZE: string;
constructor() {
/* Create a new item */
this.FLAGS = $localize`Flags`;
this.ENTER_MAINTENANCE = $localize`Enter Maintenance`;
this.EXIT_MAINTENANCE = $localize`Exit Maintenance`;
+ this.AUTHORIZE = $localize`Authorize`;
this.START_DRAIN = $localize`Start Drain`;
this.STOP_DRAIN = $localize`Stop Drain`;
'cephfs/edit': this.newTaskMessage(this.commonOperations.update, (metadata) =>
this.volume(metadata)
),
+ 'cephfs/auth': this.newTaskMessage(this.commonOperations.update, (metadata) =>
+ this.auth(metadata)
+ ),
'cephfs/remove': this.newTaskMessage(this.commonOperations.remove, (metadata) =>
this.volume(metadata)
),
return $localize`'${metadata.volumeName}'`;
}
+ auth(metadata: any) {
+ return $localize`client.${metadata.clientId} authorization successfully`;
+ }
+
subvolume(metadata: any) {
return $localize`subvolume '${metadata.subVolumeName}'`;
}
- jwt: []
tags:
- Cephfs
+ /api/cephfs/auth:
+ put:
+ parameters: []
+ requestBody:
+ content:
+ application/json:
+ schema:
+ properties:
+ caps:
+ description: Path and given capabilities
+ type: string
+ client_id:
+ description: Cephx user ID
+ type: string
+ fs_name:
+ description: File system name
+ type: string
+ root_squash:
+ description: File System Identifier
+ type: string
+ required:
+ - fs_name
+ - client_id
+ - caps
+ - root_squash
+ 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: Set Ceph authentication capabilities for the specified user ID in the
+ given path
+ tags:
+ - Cephfs
/api/cephfs/remove/{name}:
delete:
parameters: