import logging
import os
from collections import defaultdict
+from typing import Any, Dict
import cephfs
import cherrypy
logger = logging.getLogger("controllers.rgw")
+# pylint: disable=R0904
@APIRouter('/cephfs', Scope.CEPHFS)
@APIDoc("Cephfs Management API", "Cephfs")
class CephFS(RESTController):
fsmap = mgr.get("fs_map")
return fsmap['filesystems']
+ def create(self, name: str, service_spec: Dict[str, Any]):
+ service_spec_str = '1 '
+ if 'labels' in service_spec['placement']:
+ for label in service_spec['placement']['labels']:
+ service_spec_str += f'label:{label},'
+ service_spec_str = service_spec_str[:-1]
+ if 'hosts' in service_spec['placement']:
+ for host in service_spec['placement']['hosts']:
+ service_spec_str += f'{host},'
+ service_spec_str = service_spec_str[:-1]
+
+ error_code, _, err = mgr.remote('volumes', '_cmd_fs_volume_create', None,
+ {'name': name, 'placement': service_spec_str})
+ if error_code != 0:
+ raise RuntimeError(
+ f'Error creating volume {name} with placement {str(service_spec)}: {err}')
+ return f'Volume {name} created successfully'
+
def get(self, fs_id):
fs_id = self.fs_id_to_int(fs_id)
return self.fs_status(fs_id)
import { ModuleStatusGuardService } from './shared/services/module-status-guard.service';
import { NoSsoGuardService } from './shared/services/no-sso-guard.service';
import { UpgradeComponent } from './ceph/cluster/upgrade/upgrade.component';
+import { CephfsVolumeFormComponent } from './ceph/cephfs/cephfs-form/cephfs-form.component';
@Injectable()
export class PerformanceCounterBreadcrumbsResolver extends BreadcrumbsResolver {
// File Systems
{
path: 'cephfs',
- component: CephfsListComponent,
canActivate: [FeatureTogglesGuardService],
- data: { breadcrumbs: 'File Systems' }
+ data: { breadcrumbs: 'File Systems' },
+ children: [
+ { path: '', component: CephfsListComponent },
+ {
+ path: URLVerbs.CREATE,
+ component: CephfsVolumeFormComponent,
+ data: { breadcrumbs: ActionLabels.CREATE }
+ }
+ ]
},
// Object Gateway
{
name: $localize`Created`,
flexGrow: 1,
pipe: this.cdDatePipe
+ },
+ {
+ prop: 'created',
+ name: $localize`Capacity`,
+ flexGrow: 1
}
],
selection: new CdTableSelection(),
--- /dev/null
+<div class="cd-col-form"
+ *ngIf="orchStatus$ | async as orchStatus">
+ <form #frm="ngForm"
+ #formDir="ngForm"
+ [formGroup]="form"
+ novalidate>
+ <div class="card">
+ <div i18n="form title|Example: Create Volume@@formTitle"
+ class="card-header">{{ action | titlecase }} {{ resource | upperFirst }}</div>
+
+ <ng-container *ngIf="!orchStatus.available">
+ <cd-alert-panel type="info"
+ class="m-3"
+ spacingClass="mt-3"
+ i18n>Orchestrator is not configured. Deploy MDS daemons manually after creating the volume.</cd-alert-panel>
+ </ng-container>
+ <div class="card-body">
+ <!-- Name -->
+ <div class="form-group row">
+ <label class="cd-col-form-label required"
+ for="name"
+ i18n>Name</label>
+ <div class="cd-col-form-input">
+ <input id="name"
+ name="name"
+ type="text"
+ class="form-control"
+ placeholder="Name..."
+ i18n-placeholder
+ formControlName="name"
+ autofocus>
+ <span class="invalid-feedback"
+ *ngIf="form.showError('name', formDir, 'required')"
+ i18n>This field is required!</span>
+ <span *ngIf="form.showError('name', formDir, 'pattern')"
+ class="invalid-feedback"
+ i18n>Volume name can only contain letters, numbers, '.', '-', '_' or '/'.</span>
+ </div>
+ </div>
+
+ <ng-container *ngIf="orchStatus.available">
+ <!-- Placement -->
+ <div class="form-group row">
+ <label class="cd-col-form-label"
+ for="placement"
+ i18n>Placement</label>
+ <div class="cd-col-form-input">
+ <select id="placement"
+ class="form-select"
+ formControlName="placement">
+ <option i18n
+ value="hosts">Hosts</option>
+ <option i18n
+ value="label">Label</option>
+ </select>
+ </div>
+ </div>
+
+ <!-- Label -->
+ <div *ngIf="form.controls.placement.value === 'label'"
+ class="form-group row">
+ <label i18n
+ class="cd-col-form-label"
+ for="label">Label</label>
+ <div class="cd-col-form-input">
+ <input id="label"
+ class="form-control"
+ type="text"
+ formControlName="label"
+ [ngbTypeahead]="searchLabels"
+ (focus)="labelFocus.next($any($event).target.value)"
+ (click)="labelClick.next($any($event).target.value)">
+ <span class="invalid-feedback"
+ *ngIf="form.showError('label', frm, 'required')"
+ i18n>This field is required.</span>
+ </div>
+ </div>
+
+ <!-- Hosts -->
+ <div *ngIf="form.controls.placement.value === 'hosts'"
+ class="form-group row">
+ <label class="cd-col-form-label"
+ for="hosts"
+ i18n>Hosts</label>
+ <div class="cd-col-form-input">
+ <cd-select-badges id="hosts"
+ [data]="form.controls.hosts.value"
+ [options]="hosts.options"
+ [messages]="hosts.messages">
+ </cd-select-badges>
+ </div>
+ </div>
+ </ng-container>
+ </div>
+ <div class="card-footer">
+ <cd-form-button-panel (submitActionEvent)="submit()"
+ [form]="form"
+ [submitText]="(action | titlecase) + ' ' + (resource | upperFirst)"
+ wrappingClass="text-right"></cd-form-button-panel>
+ </div>
+ </div>
+ </form>
+</div>
--- /dev/null
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
+import { RouterTestingModule } from '@angular/router/testing';
+import { CephfsVolumeFormComponent } from './cephfs-form.component';
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { SharedModule } from '~/app/shared/shared.module';
+import { ToastrModule } from 'ngx-toastr';
+import { ReactiveFormsModule } from '@angular/forms';
+describe('CephfsVolumeFormComponent', () => {
+ let component: CephfsVolumeFormComponent;
+ let fixture: ComponentFixture<CephfsVolumeFormComponent>;
+ configureTestBed({
+ imports: [
+ BrowserAnimationsModule,
+ SharedModule,
+ HttpClientTestingModule,
+ RouterTestingModule,
+ ReactiveFormsModule,
+ ToastrModule.forRoot()
+ ],
+ declarations: [CephfsVolumeFormComponent]
+ });
+ beforeEach(() => {
+ fixture = TestBed.createComponent(CephfsVolumeFormComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
--- /dev/null
+import { Component, OnInit, ViewChild } from '@angular/core';
+import { FormControl, Validators } from '@angular/forms';
+import { Router } from '@angular/router';
+import _ from 'lodash';
+
+import { NgbNav, NgbTooltip, NgbTypeahead } from '@ng-bootstrap/ng-bootstrap';
+import { merge, Observable, Subject } from 'rxjs';
+import { debounceTime, distinctUntilChanged, filter, map } from 'rxjs/operators';
+
+import { CephfsService } from '~/app/shared/api/cephfs.service';
+import { HostService } from '~/app/shared/api/host.service';
+import { OrchestratorService } from '~/app/shared/api/orchestrator.service';
+import { SelectMessages } from '~/app/shared/components/select/select-messages.model';
+import { SelectOption } from '~/app/shared/components/select/select-option.model';
+import { ActionLabelsI18n, URLVerbs } from '~/app/shared/constants/app.constants';
+import { Icons } from '~/app/shared/enum/icons.enum';
+import { CdForm } from '~/app/shared/forms/cd-form';
+import { CdFormBuilder } from '~/app/shared/forms/cd-form-builder';
+import { CdFormGroup } from '~/app/shared/forms/cd-form-group';
+import { CdValidators } from '~/app/shared/forms/cd-validators';
+import { FinishedTask } from '~/app/shared/models/finished-task';
+import { Permission } from '~/app/shared/models/permissions';
+import { TaskWrapperService } from '~/app/shared/services/task-wrapper.service';
+
+@Component({
+ selector: 'cd-cephfs-form',
+ templateUrl: './cephfs-form.component.html',
+ styleUrls: ['./cephfs-form.component.scss']
+})
+export class CephfsVolumeFormComponent extends CdForm implements OnInit {
+ @ViewChild('crushInfoTabs') crushInfoTabs: NgbNav;
+ @ViewChild('crushDeletionBtn') crushDeletionBtn: NgbTooltip;
+ @ViewChild('ecpInfoTabs') ecpInfoTabs: NgbNav;
+ @ViewChild('ecpDeletionBtn') ecpDeletionBtn: NgbTooltip;
+ @ViewChild(NgbTypeahead, { static: false })
+ typeahead: NgbTypeahead;
+
+ labelFocus = new Subject<string>();
+ labelClick = new Subject<string>();
+
+ orchStatus$: Observable<any>;
+
+ permission: Permission;
+ form: CdFormGroup;
+ action: string;
+ resource: string;
+ editing: boolean;
+ icons = Icons;
+ hosts: any;
+ labels: string[];
+ hasOrchestrator: boolean;
+
+ constructor(
+ private router: Router,
+ private taskWrapperService: TaskWrapperService,
+ private orchService: OrchestratorService,
+ private formBuilder: CdFormBuilder,
+ public actionLabels: ActionLabelsI18n,
+ private hostService: HostService,
+ private cephfsService: CephfsService
+ ) {
+ super();
+ this.editing = this.router.url.startsWith(`/pool/${URLVerbs.EDIT}`);
+ this.action = this.editing ? this.actionLabels.EDIT : this.actionLabels.CREATE;
+ this.resource = $localize`volume`;
+ this.hosts = {
+ options: [],
+ messages: new SelectMessages({
+ empty: $localize`There are no hosts.`,
+ filter: $localize`Filter hosts`
+ })
+ };
+ this.createForm();
+ }
+
+ private createForm() {
+ this.orchService.status().subscribe((status) => {
+ this.hasOrchestrator = status.available;
+ });
+ this.form = this.formBuilder.group({
+ name: new FormControl('', {
+ validators: [Validators.pattern(/^[.A-Za-z0-9_/-]+$/), Validators.required]
+ }),
+ placement: ['hosts'],
+ hosts: [[]],
+ label: [
+ null,
+ [
+ CdValidators.requiredIf({
+ placement: 'label',
+ unmanaged: false
+ })
+ ]
+ ],
+ unmanaged: [false]
+ });
+ }
+
+ ngOnInit() {
+ this.hostService.list('false').subscribe((resp: object[]) => {
+ const options: SelectOption[] = [];
+ _.forEach(resp, (host: object) => {
+ if (_.get(host, 'sources.orchestrator', false)) {
+ const option = new SelectOption(false, _.get(host, 'hostname'), '');
+ options.push(option);
+ }
+ });
+ this.hosts.options = [...options];
+ });
+ this.hostService.getLabels().subscribe((resp: string[]) => {
+ this.labels = resp;
+ });
+ this.orchStatus$ = this.orchService.status();
+ }
+
+ searchLabels = (text$: Observable<string>) => {
+ return merge(
+ text$.pipe(debounceTime(200), distinctUntilChanged()),
+ this.labelFocus,
+ this.labelClick.pipe(filter(() => !this.typeahead.isPopupOpen()))
+ ).pipe(
+ map((value) =>
+ this.labels
+ .filter((label: string) => label.toLowerCase().indexOf(value.toLowerCase()) > -1)
+ .slice(0, 10)
+ )
+ );
+ };
+
+ submit() {
+ let values = this.form.getRawValue();
+ const serviceSpec: object = {
+ placement: {},
+ unmanaged: values['unmanaged']
+ };
+ switch (values['placement']) {
+ case 'hosts':
+ if (values['hosts'].length > 0) {
+ serviceSpec['placement']['hosts'] = values['hosts'];
+ }
+ break;
+ case 'label':
+ serviceSpec['placement']['label'] = values['label'];
+ break;
+ }
+
+ const volumeName = this.form.get('name').value;
+ const self = this;
+ let taskUrl = `cephfs/${URLVerbs.CREATE}`;
+ this.taskWrapperService
+ .wrapTaskAroundCall({
+ task: new FinishedTask(taskUrl, {
+ volumeName: volumeName
+ }),
+ call: this.cephfsService.create(this.form.get('name').value, serviceSpec)
+ })
+ .subscribe({
+ error() {
+ self.form.setErrors({ cdSubmitButton: true });
+ },
+ complete: () => {
+ this.router.navigate(['cephfs']);
+ }
+ });
+ }
+}
<cd-cephfs-tabs cdTableDetail
[selection]="expandedRow">
</cd-cephfs-tabs>
+ <div class="table-actions btn-toolbar">
+ <cd-table-actions [permission]="permissions.cephfs"
+ [selection]="selection"
+ class="btn-group"
+ id="cephfs-actions"
+ [tableActions]="tableActions">
+ </cd-table-actions>
+ </div>
</cd-table>
import { Component, Input } from '@angular/core';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
+import { RouterTestingModule } from '@angular/router/testing';
import { CdTableSelection } from '~/app/shared/models/cd-table-selection';
import { SharedModule } from '~/app/shared/shared.module';
import { configureTestBed } from '~/testing/unit-test-helper';
+import { CephfsVolumeFormComponent } from '../cephfs-form/cephfs-form.component';
import { CephfsListComponent } from './cephfs-list.component';
@Component({ selector: 'cd-cephfs-tabs', template: '' })
let fixture: ComponentFixture<CephfsListComponent>;
configureTestBed({
- imports: [BrowserAnimationsModule, SharedModule, HttpClientTestingModule],
- declarations: [CephfsListComponent, CephfsTabsStubComponent]
+ imports: [BrowserAnimationsModule, SharedModule, HttpClientTestingModule, RouterTestingModule],
+ declarations: [CephfsListComponent, CephfsTabsStubComponent, CephfsVolumeFormComponent]
});
beforeEach(() => {
import { Component, OnInit } from '@angular/core';
+import { Permissions } from '~/app/shared/models/permissions';
+import { Router } from '@angular/router';
import { CephfsService } from '~/app/shared/api/cephfs.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 { Icons } from '~/app/shared/enum/icons.enum';
+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 { 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';
+
+const BASE_URL = 'cephfs';
@Component({
selector: 'cd-cephfs-list',
templateUrl: './cephfs-list.component.html',
- styleUrls: ['./cephfs-list.component.scss']
+ styleUrls: ['./cephfs-list.component.scss'],
+ providers: [{ provide: URLBuilderService, useValue: new URLBuilderService(BASE_URL) }]
})
export class CephfsListComponent extends ListWithDetails implements OnInit {
columns: CdTableColumn[];
filesystems: any = [];
selection = new CdTableSelection();
+ tableActions: CdTableAction[];
+ permissions: Permissions;
- constructor(private cephfsService: CephfsService, private cdDatePipe: CdDatePipe) {
+ constructor(
+ private authStorageService: AuthStorageService,
+ private cephfsService: CephfsService,
+ private cdDatePipe: CdDatePipe,
+ public actionLabels: ActionLabelsI18n,
+ private router: Router,
+ private urlBuilder: URLBuilderService
+ ) {
super();
+ this.permissions = this.authStorageService.getPermissions();
}
ngOnInit() {
cellTransformation: CellTemplate.checkIcon
}
];
+ this.tableActions = [
+ {
+ name: this.actionLabels.CREATE,
+ permission: 'create',
+ icon: Icons.add,
+ click: () => this.router.navigate([this.urlBuilder.getCreate()]),
+ canBePrimary: (selection: CdTableSelection) => !selection.hasSelection
+ }
+ ];
}
loadFilesystems(context: CdTableFetchDataContext) {
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
+import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { TreeModule } from '@circlon/angular-tree-component';
-import { NgbNavModule } from '@ng-bootstrap/ng-bootstrap';
+import { NgbNavModule, NgbTypeaheadModule } from '@ng-bootstrap/ng-bootstrap';
import { ChartsModule } from 'ng2-charts';
import { AppRoutingModule } from '~/app/app-routing.module';
import { CephfsClientsComponent } from './cephfs-clients/cephfs-clients.component';
import { CephfsDetailComponent } from './cephfs-detail/cephfs-detail.component';
import { CephfsDirectoriesComponent } from './cephfs-directories/cephfs-directories.component';
+import { CephfsVolumeFormComponent } from './cephfs-form/cephfs-form.component';
import { CephfsListComponent } from './cephfs-list/cephfs-list.component';
import { CephfsTabsComponent } from './cephfs-tabs/cephfs-tabs.component';
@NgModule({
- imports: [CommonModule, SharedModule, AppRoutingModule, ChartsModule, TreeModule, NgbNavModule],
+ imports: [
+ CommonModule,
+ SharedModule,
+ AppRoutingModule,
+ ChartsModule,
+ TreeModule,
+ NgbNavModule,
+ FormsModule,
+ ReactiveFormsModule,
+ NgbTypeaheadModule
+ ],
declarations: [
CephfsDetailComponent,
CephfsClientsComponent,
CephfsChartComponent,
CephfsListComponent,
CephfsTabsComponent,
+ CephfsVolumeFormComponent,
CephfsDirectoriesComponent
]
})
params
});
}
+
+ create(name: string, serviceSpec: object) {
+ return this.http.post(
+ this.baseURL,
+ { name: name, service_spec: serviceSpec },
+ {
+ observe: 'response'
+ }
+ );
+ }
}
),
'crud-component/id': this.newTaskMessage(this.commonOperations.delete, (id) =>
this.crudMessageId(id)
+ ),
+ 'cephfs/create': this.newTaskMessage(this.commonOperations.create, (metadata) =>
+ this.volume(metadata)
)
};
return $localize`${message}`;
}
+ volume(metadata: any) {
+ return $localize`'${metadata.volumeName}'`;
+ }
+
crudMessageId(id: string) {
return $localize`${id}`;
}
- jwt: []
tags:
- Cephfs
+ post:
+ parameters: []
+ requestBody:
+ content:
+ application/json:
+ schema:
+ properties:
+ name:
+ type: string
+ service_spec:
+ type: string
+ required:
+ - name
+ - service_spec
+ type: object
+ responses:
+ '201':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Resource created.
+ '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: []
+ tags:
+ - Cephfs
/api/cephfs/{fs_id}:
get:
parameters: