from functools import wraps
from inspect import isclass
from typing import Any, Callable, Dict, Generator, Iterable, Iterator, List, \
- NamedTuple, Optional, Union, get_type_hints
+ NamedTuple, Optional, Tuple, Union, get_type_hints
from ._api_router import APIRouter
from ._docs import APIDoc, EndpointDoc
icon: str
routerLink: str = '' # redirect to...
click: str = ''
+ disable: bool = False # disable without selection
+
+
+class SelectionType(Enum):
+ NONE = ''
+ SINGLE = 'single'
+ MULTI = 'multiClick'
class TableComponent(SerializableClass):
self.columns: List[TableColumn] = []
self.columnMode: str = 'flex'
self.toolHeader: bool = True
+ self.selectionType: str = SelectionType.SINGLE.value
+
+ def set_selection_type(self, type_: SelectionType):
+ self.selectionType = type_.value
class Icon(Enum):
- add = 'fa fa-plus'
- destroy = 'fa fa-times'
+ ADD = 'fa fa-plus'
+ DESTROY = 'fa fa-times'
+ IMPORT = 'fa fa-upload'
+ EXPORT = 'fa fa-download'
class Validator(Enum):
JSON = 'json'
RGW_ROLE_NAME = 'rgwRoleName'
RGW_ROLE_PATH = 'rgwRolePath'
+ FILE = 'file'
class FormField(NamedTuple):
_type = 'boolean'
elif self.field_type == 'textarea':
_type = 'textarea'
+ elif self.field_type == "file":
+ _type = 'file'
else:
raise NotImplementedError(f'Unimplemented type {self.field_type}')
return _type
def to_dict(self):
res = self.root_container.to_dict()
res['task_info'] = self.task_info.to_dict()
+ res['path'] = self.path
return res
meta: CRUDMeta = CRUDMeta(), get_all: Optional[CRUDCollectionMethod] = None,
create: Optional[CRUDCollectionMethod] = None,
delete: Optional[CRUDCollectionMethod] = None,
- detail_columns: Optional[List[str]] = None):
+ detail_columns: Optional[List[str]] = None,
+ selection_type: SelectionType = SelectionType.SINGLE,
+ extra_endpoints: Optional[List[Tuple[str, CRUDCollectionMethod]]] = None):
self.router = router
self.doc = doc
self.set_column = set_column
self.permissions = permissions if permissions is not None else []
self.column_key = column_key if column_key is not None else ''
self.detail_columns = detail_columns if detail_columns is not None else []
+ self.extra_endpoints = extra_endpoints if extra_endpoints is not None else []
+ self.selection_type = selection_type
def __call__(self, cls: Any):
self.create_crud_class(cls)
def create_crud_class(self, cls):
outer_self: CRUDEndpoint = self
- @self.router
- @self.doc
- class CRUDClass(RESTController):
-
- if self.get_all:
- @self.get_all.doc
- @wraps(self.get_all.func)
- def list(self, *args, **kwargs):
- items = []
- for item in outer_self.get_all.func(self, *args, **kwargs): # type: ignore
- items.append(serialize(cls(**item)))
- return items
-
- if self.create:
- @self.create.doc
- @wraps(self.create.func)
- def create(self, *args, **kwargs):
- return outer_self.create.func(self, *args, **kwargs) # type: ignore
-
- if self.delete:
- @self.delete.doc
- @wraps(self.delete.func)
- def delete(self, *args, **kwargs):
- return outer_self.delete.func(self, *args, **kwargs) # type: ignore
-
- cls.CRUDClass = CRUDClass
+ funcs = {}
+ if self.get_all:
+ @self.get_all.doc
+ @wraps(self.get_all.func)
+ def _list(self, *args, **kwargs):
+ items = []
+ for item in outer_self.get_all.func(self, *args, **kwargs): # type: ignore
+ items.append(serialize(cls(**item)))
+ return items
+ funcs['list'] = _list
+
+ if self.create:
+ @self.create.doc
+ @wraps(self.create.func)
+ def _create(self, *args, **kwargs):
+ return outer_self.create.func(self, *args, **kwargs) # type: ignore
+ funcs['create'] = _create
+
+ if self.delete:
+ @self.delete.doc
+ @wraps(self.delete.func)
+ def delete(self, *args, **kwargs):
+ return outer_self.delete.func(self, *args, **kwargs) # type: ignore
+ funcs['delete'] = delete
+
+ for extra_endpoint in self.extra_endpoints:
+ funcs[extra_endpoint[0]] = extra_endpoint[1].doc(extra_endpoint[1].func)
+
+ class_name = self.router.path.replace('/', '')
+ crud_class = type(f'{class_name}_CRUDClass',
+ (RESTController,),
+ {
+ **funcs,
+ 'outer_self': self,
+ })
+ self.router(self.doc(crud_class))
+ cls.CRUDClass = crud_class
def create_meta_class(self, cls):
def _list(self):
self.set_permissions()
self.set_column_key()
self.get_detail_columns()
+ selection_type = self.__class__.outer_self.selection_type
+ self.__class__.outer_self.meta.table.set_selection_type(selection_type)
return serialize(self.__class__.outer_self.meta)
def get_detail_columns(self):
import logging
from errno import EINVAL
-from typing import List, NamedTuple
+from typing import List, NamedTuple, Optional
from ..exceptions import DashboardException
from ..security import Scope
from ..services.ceph_service import CephService, SendCommandError
-from . import APIDoc, APIRouter, CRUDCollectionMethod, CRUDEndpoint, EndpointDoc, SecretStr
+from . import APIDoc, APIRouter, CRUDCollectionMethod, CRUDEndpoint, \
+ EndpointDoc, RESTController, SecretStr
from ._crud import ArrayHorizontalContainer, CRUDMeta, Form, FormField, \
- FormTaskInfo, Icon, TableAction, VerticalContainer
+ FormTaskInfo, Icon, SelectionType, TableAction, Validator, \
+ VerticalContainer
logger = logging.getLogger("controllers.ceph_users")
class CephUserEndpoints:
+ @staticmethod
+ def _run_auth_command(command: str, *args, **kwargs):
+ try:
+ return CephService.send_command('mon', command, *args, **kwargs)
+ except SendCommandError as ex:
+ msg = f'{ex} in command {ex.prefix}'
+ if ex.errno == -EINVAL:
+ raise DashboardException(msg, code=400)
+ raise DashboardException(msg, code=500)
+
@staticmethod
def user_list(_):
"""
Get list of ceph users and its respective data
"""
- return CephService.send_command('mon', 'auth ls')["auth_dump"]
+ return CephUserEndpoints._run_auth_command('auth ls')["auth_dump"]
@staticmethod
- def user_create(_, user_entity: str, capabilities: List[Cap]):
+ def user_create(_, user_entity: str = '', capabilities: Optional[List[Cap]] = None,
+ import_data: str = ''):
"""
Add a ceph user with its defined capabilities.
:param user_entity: Entity to change
"""
# Caps are represented as a vector in mon auth add commands.
# Look at AuthMonitor.cc::valid_caps for reference.
+ if import_data:
+ logger.debug("Sending import command 'auth import' \n%s", import_data)
+ CephUserEndpoints._run_auth_command('auth import', inbuf=import_data)
+ return "Successfully imported user"
+
+ assert user_entity
caps = []
for cap in capabilities:
caps.append(cap['entity'])
logger.debug("Sending command 'auth add' of entity '%s' with caps '%s'",
user_entity, str(caps))
- try:
- CephService.send_command('mon', 'auth add', entity=user_entity, caps=caps)
- except SendCommandError as ex:
- msg = f'{ex} in command {ex.prefix}'
- if ex.errno == -EINVAL:
- raise DashboardException(msg, code=400)
- raise DashboardException(msg, code=500)
+ CephUserEndpoints._run_auth_command('auth add', entity=user_entity, caps=caps)
+
return f"Successfully created user '{user_entity}'"
@staticmethod
"""
logger.debug("Sending command 'auth del' of entity '%s'", user_entity)
try:
- CephService.send_command('mon', 'auth del', entity=user_entity)
+ CephUserEndpoints._run_auth_command('auth del', entity=user_entity)
except SendCommandError as ex:
msg = f'{ex} in command {ex.prefix}'
if ex.errno == -EINVAL:
raise DashboardException(msg, code=400)
raise DashboardException(msg, code=500)
- return f"Successfully eleted user '{user_entity}'"
+ return f"Successfully deleted user '{user_entity}'"
+
+ @staticmethod
+ def export(_, entities: List[str]):
+ export_string = ""
+ for entity in entities:
+ out = CephUserEndpoints._run_auth_command('auth export', entity=entity, to_json=False)
+ export_string += f'{out}\n'
+ return export_string
create_cap_container = ArrayHorizontalContainer('Capabilities', 'capabilities', fields=[
task_info=FormTaskInfo("Ceph user '{user_entity}' created successfully",
['user_entity']))
+# pylint: disable=C0301
+import_user_help = (
+ 'The imported file should be a keyring file and it must follow the schema described <a ' # noqa: E501
+ 'href="https://docs.ceph.com/en/latest/rados/operations/user-management/#authorization-capabilities"' # noqa: E501
+ 'target="_blank">here.</a>'
+)
+import_container = VerticalContainer('Import User', 'import_user', fields=[
+ FormField('User file import', 'import_data',
+ field_type="file", validators=[Validator.FILE],
+ help=import_user_help),
+])
+
+import_user_form = Form(path='/cluster/user/import',
+ root_container=import_container,
+ task_info=FormTaskInfo("User imported successfully", []))
+
@CRUDEndpoint(
router=APIRouter('/cluster/user', Scope.CONFIG_OPT),
doc=APIDoc("Get Ceph Users", "Cluster"),
set_column={"caps": {"cellTemplate": "badgeDict"}},
actions=[
- TableAction(name='create', permission='create', icon=Icon.add.value,
+ TableAction(name='Create', permission='create', icon=Icon.ADD.value,
routerLink='/cluster/user/create'),
- TableAction(name='Delete', permission='delete', icon=Icon.destroy.value,
- click='delete')
+ TableAction(name='Delete', permission='delete', icon=Icon.DESTROY.value,
+ click='delete', disable=True),
+ TableAction(name='Import', permission='create', icon=Icon.IMPORT.value,
+ routerLink='/cluster/user/import'),
+ TableAction(name='Export', permission='read', icon=Icon.EXPORT.value,
+ click='authExport', disable=True),
],
- permissions=[Scope.CONFIG_OPT],
- forms=[create_form],
column_key='entity',
+ permissions=[Scope.CONFIG_OPT],
+ forms=[create_form, import_user_form],
get_all=CRUDCollectionMethod(
func=CephUserEndpoints.user_list,
doc=EndpointDoc("Get Ceph Users")
func=CephUserEndpoints.user_delete,
doc=EndpointDoc("Delete Ceph User")
),
+ extra_endpoints=[
+ ('export', CRUDCollectionMethod(
+ func=RESTController.Collection('POST', 'export')(CephUserEndpoints.export),
+ doc=EndpointDoc("Export Ceph Users")
+ ))
+ ],
+ selection_type=SelectionType.MULTI,
meta=CRUDMeta()
)
class CephUser(NamedTuple):
router=APIRouter('/rgw/user/roles', Scope.RGW),
doc=APIDoc("List of RGW roles", "RGW"),
actions=[
- TableAction(name='Create', permission='create', icon=Icon.add.value,
+ TableAction(name='Create', permission='create', icon=Icon.ADD.value,
routerLink='/rgw/user/roles/create')
],
forms=[create_role_form],
resource: 'api.cluster.user@1.0'
}
},
+ {
+ path: 'cluster/user/import',
+ component: CrudFormComponent,
+ data: {
+ breadcrumbs: 'Cluster/Users',
+ resource: 'api.cluster.user@1.0'
+ }
+ },
{
path: 'monitor',
component: MonitorComponent,
--- /dev/null
+import { HttpClient } from '@angular/common/http';
+import { Injectable } from '@angular/core';
+
+@Injectable({
+ providedIn: 'root'
+})
+export class CephUserService {
+ constructor(private http: HttpClient) {}
+
+ export(entities: string[]) {
+ return this.http.post('api/cluster/user/export', { entities: entities });
+ }
+}
(backActionEvent)="boundCancel()"
[form]="confirmationForm"
[submitText]="buttonText"
+ [showCancel]="showCancel"
[showSubmit]="showSubmit"></cd-form-button-panel>
</div>
</form>
onCancel?: Function;
bodyContext?: object;
showSubmit = true;
+ showCancel = true;
// Component only
boundCancel = this.cancel.bind(this);
<div [class]="wrappingClass">
- <cd-back-button class="m-2"
+ <cd-back-button *ngIf="showCancel"
+ class="m-2"
(backAction)="backAction()"
[name]="cancelText"></cd-back-button>
<cd-submit-button *ngIf="showSubmit"
@Input()
showSubmit = true;
@Input()
+ showCancel = true;
+ @Input()
wrappingClass = '';
@Input()
btnClass = '';
[data]="data$ | async"
[columns]="meta.table.columns"
[columnMode]="meta.table.columnMode"
- [toolHeader]="meta.table.toolHeader"
- selectionType="single"
(setExpandedRow)="setExpandedRow($event)"
+ [hasDetails]="meta.detail_columns.length > 0"
+ [selectionType]="meta.table.selectionType"
(updateSelection)="updateSelection($event)"
- [hasDetails]="meta.detail_columns.length > 0">
+ [toolHeader]="meta.table.toolHeader">
<div class="table-actions btn-toolbar">
<cd-table-actions [permission]="permission"
[selection]="selection"
let-value="value">
<span>{{ value | duration }}</span>
</ng-template>
+
+<ng-template #exportDataModalTpl>
+ <div class="d-flex flex-column align-items-center w-100 gap-3">
+ <textarea readonly
+ class="form-control w-100 bg-light height-400"
+ id="authExportArea">{{ modalState.authExportData }}</textarea>
+ <cd-copy-2-clipboard-button class="align-self-end"
+ source="authExportArea">
+
+ </cd-copy-2-clipboard-button>
+ </div>
+</ng-template>
+.height-400 {
+ height: 400px;
+}
import { Component, OnInit, TemplateRef, ViewChild } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
+import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap';
import _ from 'lodash';
import { Observable } from 'rxjs';
import { CrudMetadata } from '~/app/shared/models/crud-table-metadata';
import { DataGatewayService } from '~/app/shared/services/data-gateway.service';
import { TimerService } from '~/app/shared/services/timer.service';
+import { CephUserService } from '../../api/ceph-user.service';
+import { ConfirmationModalComponent } from '../../components/confirmation-modal/confirmation-modal.component';
import { CdTableSelection } from '../../models/cd-table-selection';
import { FinishedTask } from '../../models/finished-task';
import { Permission, Permissions } from '../../models/permissions';
import { AuthStorageService } from '../../services/auth-storage.service';
import { TaskWrapperService } from '../../services/task-wrapper.service';
import { ModalService } from '../../services/modal.service';
-import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap';
import { CriticalConfirmationModalComponent } from '../../components/critical-confirmation-modal/critical-confirmation-modal.component';
@Component({
public dateTpl: TemplateRef<any>;
@ViewChild('durationTpl')
public durationTpl: TemplateRef<any>;
+ @ViewChild('exportDataModalTpl')
+ public authxEportTpl: TemplateRef<any>;
data$: Observable<any>;
meta$: Observable<CrudMetadata>;
permission: Permission;
selection = new CdTableSelection();
expandedRow: any = null;
+ modalRef: NgbModalRef;
tabs = {};
resource: string;
- modalRef: NgbModalRef;
+ modalState = {};
constructor(
private authStorageService: AuthStorageService,
private timerService: TimerService,
private dataGatewayService: DataGatewayService,
- private activatedRoute: ActivatedRoute,
private taskWrapper: TaskWrapperService,
+ private cephUserService: CephUserService,
+ private activatedRoute: ActivatedRoute,
private modalService: ModalService
) {
this.permissions = this.authStorageService.getPermissions();
meta.table.columns = meta.table.columns.filter((col: any) => {
return !col['isHidden'];
});
+
this.meta = meta;
for (let i = 0; i < this.meta.actions.length; i++) {
- if (this.meta.actions[i].click.toString() !== '') {
- this.meta.actions[i].click = this[this.meta.actions[i].click.toString()].bind(this);
+ let action = this.meta.actions[i];
+ if (action.disable) {
+ action.disable = (selection) => !selection.hasSelection;
+ }
+ if (action.click.toString() !== '') {
+ action.click = this[this.meta.actions[i].click.toString()].bind(this);
}
}
}
updateSelection(selection: CdTableSelection) {
this.selection = selection;
}
+
setExpandedRow(event: any) {
this.expandedRow = event;
}
+
+ authExport() {
+ let entities: string[] = [];
+ this.selection.selected.forEach((row) => entities.push(row.entity));
+ this.cephUserService.export(entities).subscribe((data: string) => {
+ const modalVariables = {
+ titleText: $localize`Ceph user export data`,
+ buttonText: $localize`Close`,
+ bodyTpl: this.authxEportTpl,
+ showSubmit: true,
+ showCancel: false,
+ onSubmit: () => {
+ this.modalRef.close();
+ }
+ };
+ this.modalState['authExportData'] = data.trim();
+ this.modalRef = this.modalService.show(ConfirmationModalComponent, modalVariables);
+ });
+ }
}
import { FormlyObjectTypeComponent } from '../forms/crud-form/formly-object-type/formly-object-type.component';
import { FormlyTextareaTypeComponent } from '../forms/crud-form/formly-textarea-type/formly-textarea-type.component';
import { FormlyInputWrapperComponent } from '../forms/crud-form/formly-input-wrapper/formly-input-wrapper.component';
+import { FormlyFileTypeComponent } from '../forms/crud-form/formly-file-type/formly-file-type.component';
+import { FormlyFileValueAccessorDirective } from '../forms/crud-form/formly-file-type/formly-file-type-accessor';
@NgModule({
imports: [
{ name: 'array', component: FormlyArrayTypeComponent },
{ name: 'object', component: FormlyObjectTypeComponent },
{ name: 'input', component: FormlyInputTypeComponent, wrappers: ['input-wrapper'] },
- { name: 'textarea', component: FormlyTextareaTypeComponent, wrappers: ['input-wrapper'] }
+ { name: 'textarea', component: FormlyTextareaTypeComponent, wrappers: ['input-wrapper'] },
+ { name: 'file', component: FormlyFileTypeComponent, wrappers: ['input-wrapper'] }
],
validationMessages: [
{ name: 'required', message: 'This field is required' },
message:
'Role path must start and finish with a slash "/".' +
' (pattern: (\u002F)|(\u002F[\u0021-\u007E]+\u002F))'
- }
+ },
+ { name: 'file_size', message: 'File size must not exceed 4KiB' }
],
wrappers: [{ name: 'input-wrapper', component: FormlyInputWrapperComponent }]
}),
FormlyArrayTypeComponent,
FormlyInputTypeComponent,
FormlyObjectTypeComponent,
- FormlyInputWrapperComponent
+ FormlyInputWrapperComponent,
+ FormlyFileTypeComponent,
+ FormlyFileValueAccessorDirective
],
exports: [
TableComponent,
this.formUISchema$ = this.activatedRoute.data.pipe(
mergeMap((data: any) => {
this.resource = data.resource;
- return this.dataGatewayService.form(`ui-${this.resource}`);
+ const url = '/' + this.activatedRoute.snapshot.url.join('/');
+ return this.dataGatewayService.form(`ui-${this.resource}`, url);
})
);
}
- submit(data: any, taskInfo: CrudTaskInfo) {
+ async readFileAsText(file: File): Promise<string> {
+ let fileReader = new FileReader();
+ let text: string = '';
+ await new Promise((resolve) => {
+ fileReader.onload = (_) => {
+ text = fileReader.result.toString();
+ resolve(true);
+ };
+ fileReader.readAsText(file);
+ });
+ return text;
+ }
+
+ async preSubmit(data: { [name: string]: any }) {
+ for (const [key, value] of Object.entries(data)) {
+ if (value instanceof FileList) {
+ let file = value[0];
+ let text = await this.readFileAsText(file);
+ data[key] = text;
+ }
+ }
+ }
+
+ async submit(data: { [name: string]: any }, taskInfo: CrudTaskInfo) {
if (data) {
let taskMetadata = {};
_.forEach(taskInfo.metadataFields, (field) => {
taskMetadata[field] = data[field];
});
taskMetadata['__message'] = taskInfo.message;
+ await this.preSubmit(data);
this.taskWrapper
.wrapTaskAroundCall({
task: new FinishedTask('crud-component', taskMetadata),
--- /dev/null
+import { Directive } from '@angular/core';
+import { NG_VALUE_ACCESSOR, ControlValueAccessor } from '@angular/forms';
+@Directive({
+ // eslint-disable-next-line
+ selector: 'input[type=file]',
+ // eslint-disable-next-line
+ host: {
+ '(change)': 'onChange($event.target.files)',
+ '(input)': 'onChange($event.target.files)',
+ '(blur)': 'onTouched()'
+ },
+ providers: [
+ { provide: NG_VALUE_ACCESSOR, useExisting: FormlyFileValueAccessorDirective, multi: true }
+ ]
+})
+// https://github.com/angular/angular/issues/7341
+export class FormlyFileValueAccessorDirective implements ControlValueAccessor {
+ value: any;
+ onChange = (_: any) => {};
+ onTouched = () => {};
+
+ writeValue(_value: any) {}
+ registerOnChange(fn: any) {
+ this.onChange = fn;
+ }
+ registerOnTouched(fn: any) {
+ this.onTouched = fn;
+ }
+}
--- /dev/null
+<input type="file"
+ [formControl]="formControl"
+ [formlyAttributes]="field"
+ />
--- /dev/null
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { FormControl } from '@angular/forms';
+import { FormlyModule } from '@ngx-formly/core';
+
+import { FormlyFileTypeComponent } from './formly-file-type.component';
+
+describe('FormlyFileTypeComponent', () => {
+ let component: FormlyFileTypeComponent;
+ let fixture: ComponentFixture<FormlyFileTypeComponent>;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ imports: [FormlyModule.forRoot()],
+ declarations: [FormlyFileTypeComponent]
+ }).compileComponents();
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(FormlyFileTypeComponent);
+ component = fixture.componentInstance;
+
+ const formControl = new FormControl();
+ const field = {
+ key: 'file',
+ type: 'file',
+ templateOptions: {},
+ get formControl() {
+ return formControl;
+ }
+ };
+
+ component.field = field;
+
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
--- /dev/null
+import { Component } from '@angular/core';
+import { FieldType } from '@ngx-formly/core';
+
+@Component({
+ selector: 'cd-formly-file-type',
+ templateUrl: './formly-file-type.component.html',
+ styleUrls: ['./formly-file-type.component.scss']
+})
+export class FormlyFileTypeComponent extends FieldType {}
import { ValidatorFn } from '@angular/forms';
import { FormlyFieldConfig } from '@ngx-formly/core';
import { forEach } from 'lodash';
+import { formlyAsyncFileValidator } from './validators/file-validator';
import { formlyAsyncJsonValidator } from './validators/json-validator';
import { formlyRgwRoleNameValidator, formlyRgwRolePath } from './validators/rgw-role-validator';
validators.push(formlyRgwRolePath);
break;
}
+ case 'file': {
+ validators.push(formlyAsyncFileValidator);
+ break;
+ }
}
});
field.asyncValidators = { validation: validators };
--- /dev/null
+import { AbstractControl } from '@angular/forms';
+
+export function formlyAsyncFileValidator(control: AbstractControl): Promise<any> {
+ return new Promise((resolve, _reject) => {
+ if (control.value instanceof FileList) {
+ control.value;
+ let file = control.value[0];
+ if (file.size > 4096) {
+ resolve({ file_size: true });
+ }
+ resolve(null);
+ }
+ resolve({ not_a_file: true });
+ });
+}
columns: CdTableColumn[];
columnMode: string;
toolHeader: boolean;
+ selectionType: string;
}
export class CrudMetadata {
export class CrudFormAdapterService {
constructor(private formlyJsonschema: FormlyJsonschema) {}
- processJsonSchemaForm(response: any): JsonFormUISchema {
- const title = response.forms[0].control_schema.title;
- const uiSchema = response.forms[0].ui_schema;
- const cSchema = response.forms[0].control_schema;
+ processJsonSchemaForm(response: any, path: string): JsonFormUISchema {
+ let form = 0;
+ while (form < response.forms.length) {
+ if (response.forms[form].path == path) {
+ break;
+ }
+ form++;
+ }
+ form %= response.forms.length;
+ const title = response.forms[form].control_schema.title;
+ const uiSchema = response.forms[form].ui_schema;
+ const cSchema = response.forms[form].control_schema;
let controlSchema = this.formlyJsonschema.toFieldConfig(cSchema).fieldGroup;
for (let i = 0; i < controlSchema.length; i++) {
for (let j = 0; j < uiSchema.length; j++) {
}
}
let taskInfo: CrudTaskInfo = {
- metadataFields: response.forms[0].task_info.metadataFields,
- message: response.forms[0].task_info.message
+ metadataFields: response.forms[form].task_info.metadataFields,
+ message: response.forms[form].task_info.message
};
return { title, uiSchema, controlSchema, taskInfo };
}
});
}
- form(dataPath: string): Observable<JsonFormUISchema> {
+ form(dataPath: string, formPath: string): Observable<JsonFormUISchema> {
const cacheable = this.getCacheable(dataPath, 'get');
if (this.cache[cacheable] === undefined) {
const { url, version } = this.getUrlAndVersion(dataPath);
}
return this.cache[cacheable].pipe(
map((response) => {
- return this.crudFormAdapater.processJsonSchemaForm(response);
+ return this.crudFormAdapater.processJsonSchemaForm(response, formPath);
})
);
}
properties:
capabilities:
type: string
+ import_data:
+ default: ''
+ type: string
user_entity:
+ default: ''
type: string
- required:
- - user_entity
- - capabilities
type: object
responses:
'201':
summary: Create Ceph User
tags:
- Cluster
+ /api/cluster/user/export:
+ post:
+ parameters: []
+ requestBody:
+ content:
+ application/json:
+ schema:
+ properties:
+ entities:
+ type: string
+ required:
+ - entities
+ 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: []
+ summary: Export Ceph Users
+ tags:
+ - Cluster
/api/cluster/user/{user_entity}:
delete:
description: "\n Delete a ceph user and it's defined capabilities.\n\
name: Auth
- description: Cephfs Management API
name: Cephfs
-- description: Get Ceph Users
+- description: Get Cluster Details
name: Cluster
- description: Manage Cluster Configurations
name: ClusterConfiguration
return mgr.get("pg_summary")['by_pool'][pool['pool'].__str__()]
@staticmethod
- def send_command(srv_type, prefix, srv_spec='', **kwargs):
- # type: (str, str, Optional[str], Any) -> Any
+ def send_command(srv_type, prefix, srv_spec='', to_json=True, inbuf='', **kwargs):
+ # type: (str, str, Optional[str], bool, str, Any) -> Any
"""
:type prefix: str
:param srv_type: mon |
:param kwargs: will be added to argdict
:param srv_spec: typically empty. or something like "<fs_id>:0"
+ :param to_json: if true return as json format
:raises PermissionError: See rados.make_ex
:raises ObjectNotFound: See rados.make_ex
"""
argdict = {
"prefix": prefix,
- "format": "json",
}
+ if to_json:
+ argdict["format"] = "json"
argdict.update({k: v for k, v in kwargs.items() if v is not None})
result = CommandResult("")
- mgr.send_command(result, srv_type, srv_spec, json.dumps(argdict), "")
+ mgr.send_command(result, srv_type, srv_spec, json.dumps(argdict), "", inbuf=inbuf)
r, outb, outs = result.wait()
if r != 0:
logger.error("send_command '%s' failed. (r=%s, outs=\"%s\", kwargs=%s)", prefix, r,