add = 'fa fa-plus'
+class Validator(Enum):
+ JSON = 'json'
+ RGW_ROLE_NAME = 'rgwRoleName'
+ RGW_ROLE_PATH = 'rgwRolePath'
+
+
class FormField(NamedTuple):
"""
The key of a FromField is then used to send the data related to that key into the
field_type: Any = str
default_value: Optional[Any] = None
optional: bool = False
+ help: str = ''
+ validators: List[Validator] = []
def get_type(self):
_type = ''
_type = 'int'
elif self.field_type == bool:
_type = 'boolean'
+ elif self.field_type == 'textarea':
+ _type = 'textarea'
else:
raise NotImplementedError(f'Unimplemented type {self.field_type}')
return _type
# include fields in this container's schema
for field in self.fields:
- field_ui_schema = {}
+ field_ui_schema: Dict[str, Any] = {}
properties[field.key] = {}
field_key = field.key
if key:
properties[field.key]['type'] = _type
properties[field.key]['title'] = field.name
field_ui_schema['key'] = field_key
+ field_ui_schema['help'] = f'{field.help}'
+ field_ui_schema['validators'] = [i.value for i in field.validators]
items.append(field_ui_schema)
elif isinstance(field, Container):
container_schema = field.to_dict(key+'.'+field.key if key else field.key)
return 'array'
+class FormTaskInfo:
+ def __init__(self, message: str, metadata_fields: List[str]) -> None:
+ self.message = message
+ self.metadata_fields = metadata_fields
+
+ def to_dict(self):
+ return {'message': self.message, 'metadataFields': self.metadata_fields}
+
+
class Form:
- def __init__(self, path, root_container):
+ def __init__(self, path, root_container,
+ task_info: FormTaskInfo = FormTaskInfo("Unknown task", [])):
self.path = path
- self.root_container = root_container
+ self.root_container: Container = root_container
+ self.task_info = task_info
def to_dict(self):
- return self.root_container.to_dict()
+ res = self.root_container.to_dict()
+ res['task_info'] = self.task_info.to_dict()
+ return res
class CRUDMeta(SerializableClass):
self.permissions = []
self.actions = []
self.forms = []
+ self.detail_columns = []
class CRUDCollectionMethod(NamedTuple):
actions: Optional[List[TableAction]] = None,
permissions: Optional[List[str]] = None, forms: Optional[List[Form]] = None,
meta: CRUDMeta = CRUDMeta(), get_all: Optional[CRUDCollectionMethod] = None,
- create: Optional[CRUDCollectionMethod] = None):
+ create: Optional[CRUDCollectionMethod] = None,
+ detail_columns: Optional[List[str]] = None):
self.router = router
self.doc = doc
self.set_column = set_column
- if actions:
- self.actions = actions
- else:
- self.actions = []
-
- if forms:
- self.forms = forms
- else:
- self.forms = []
+ self.actions = actions if actions is not None else []
+ self.forms = forms if forms is not None else []
self.meta = meta
self.get_all = get_all
self.create = create
- if permissions:
- self.permissions = permissions
- else:
- self.permissions = []
+ self.permissions = permissions if permissions is not None else []
+ self.detail_columns = detail_columns if detail_columns is not None else []
def __call__(self, cls: Any):
self.create_crud_class(cls)
self.generate_actions()
self.generate_forms()
self.set_permissions()
+ self.get_detail_columns()
return serialize(self.__class__.outer_self.meta)
+ def get_detail_columns(self):
+ columns = self.__class__.outer_self.detail_columns
+ self.__class__.outer_self.meta.detail_columns = columns
+
def update_columns(self):
if self.__class__.outer_self.set_column:
for i, column in enumerate(self.__class__.outer_self.meta.table.columns):
'generate_actions': generate_actions,
'generate_forms': generate_forms,
'set_permissions': set_permissions,
+ 'get_detail_columns': get_detail_columns,
'outer_self': self,
})
UIRouter(self.router.path, self.router.security_scope)(meta_class)
from ..security import Scope
from ..services.ceph_service import CephService, SendCommandError
from . import APIDoc, APIRouter, CRUDCollectionMethod, CRUDEndpoint, EndpointDoc, SecretStr
-from ._crud import ArrayHorizontalContainer, CRUDMeta, Form, FormField, Icon, \
- TableAction, VerticalContainer
+from ._crud import ArrayHorizontalContainer, CRUDMeta, Form, FormField, \
+ FormTaskInfo, Icon, TableAction, VerticalContainer
logger = logging.getLogger("controllers.ceph_users")
])
create_form = Form(path='/cluster/user/create',
- root_container=create_container)
+ root_container=create_container,
+ task_info=FormTaskInfo("Ceph user '{user_entity}' created successfully",
+ ['user_entity']))
@CRUDEndpoint(
from . import APIDoc, APIRouter, BaseController, CRUDCollectionMethod, \
CRUDEndpoint, Endpoint, EndpointDoc, ReadPermission, RESTController, \
UIRouter, allow_empty_body
-from ._crud import CRUDMeta
+from ._crud import CRUDMeta, Form, FormField, FormTaskInfo, Icon, TableAction, \
+ Validator, VerticalContainer
from ._version import APIVersion
logger = logging.getLogger("controllers.rgw")
roles = rgw_client.list_roles()
return roles
+ @staticmethod
+ def role_create(_, role_name: str = '', role_path: str = '', role_assume_policy_doc: str = ''):
+ assert role_name
+ assert role_path
+ rgw_client = RgwClient.admin_instance()
+ rgw_client.create_role(role_name, role_path, role_assume_policy_doc)
+ return f'Role {role_name} created successfully'
+
+
+# pylint: disable=C0301
+assume_role_policy_help = (
+ 'Paste a json assume role policy document, to find more information on how to get this document, <a ' # noqa: E501
+ 'href="https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iam-role.html#cfn-iam-role-assumerolepolicydocument"' # noqa: E501
+ 'target="_blank">click here.</a>'
+)
+
+create_container = VerticalContainer('Create Role', 'create_role', fields=[
+ FormField('Role name', 'role_name', validators=[Validator.RGW_ROLE_NAME]),
+ FormField('Path', 'role_path', validators=[Validator.RGW_ROLE_PATH]),
+ FormField('Assume Role Policy Document',
+ 'role_assume_policy_doc',
+ help=assume_role_policy_help,
+ field_type='textarea',
+ validators=[Validator.JSON]),
+])
+create_role_form = Form(path='/rgw/user/roles/create',
+ root_container=create_container,
+ task_info=FormTaskInfo("IAM RGW Role '{role_name}' created successfully",
+ ['role_name']))
+
@CRUDEndpoint(
router=APIRouter('/rgw/user/roles', Scope.RGW),
doc=APIDoc("List of RGW roles", "RGW"),
- actions=[],
+ actions=[
+ TableAction(name='Create', permission='create', icon=Icon.add.value,
+ routerLink='/rgw/user/roles/create')
+ ],
+ forms=[create_role_form],
permissions=[Scope.CONFIG_OPT],
get_all=CRUDCollectionMethod(
func=RGWRoleEndpoints.role_list,
doc=EndpointDoc("List RGW roles")
),
+ create=CRUDCollectionMethod(
+ func=RGWRoleEndpoints.role_create,
+ doc=EndpointDoc("Create Ceph User")
+ ),
set_column={
"CreateDate": {'cellTemplate': 'date'},
"MaxSessionDuration": {'cellTemplate': 'duration'},
+ "RoleId": {'isHidden': True},
"AssumeRolePolicyDocument": {'isHidden': True}
},
+ detail_columns=['RoleId', 'AssumeRolePolicyDocument'],
meta=CRUDMeta()
)
class RgwUserRole(NamedTuple):
import { RgwMultisiteRealmFormComponent } from './rgw-multisite-realm-form/rgw-multisite-realm-form.component';
import { RgwMultisiteZonegroupFormComponent } from './rgw-multisite-zonegroup-form/rgw-multisite-zonegroup-form.component';
import { RgwMultisiteZoneFormComponent } from './rgw-multisite-zone-form/rgw-multisite-zone-form.component';
+import { CrudFormComponent } from '~/app/shared/forms/crud-form/crud-form.component';
@NgModule({
imports: [
},
{
path: 'roles',
- component: CRUDTableComponent,
data: {
breadcrumbs: 'Roles',
resource: 'api.rgw.user.roles@1.0',
url: '/rgw/user/roles'
}
]
- }
+ },
+ children: [
+ {
+ path: '',
+ component: CRUDTableComponent
+ },
+ {
+ path: URLVerbs.CREATE,
+ component: CrudFormComponent,
+ data: {
+ breadcrumbs: ActionLabels.CREATE
+ }
+ }
+ ]
}
]
},
[data]="data$ | async"
[columns]="meta.table.columns"
[columnMode]="meta.table.columnMode"
- [toolHeader]="meta.table.toolHeader">
+ [toolHeader]="meta.table.toolHeader"
+ selectionType="single"
+ (setExpandedRow)="setExpandedRow($event)"
+ (updateSelection)="updateSelection($event)"
+ [hasDetails]="meta.detail_columns.length > 0">
<div class="table-actions btn-toolbar">
<cd-table-actions [permission]="permission"
[selection]="selection"
permissions: Permissions;
permission: Permission;
selection = new CdTableSelection();
+ expandedRow: any = null;
tabs = {};
constructor(
});
this.meta = meta;
}
+
+ updateSelection(selection: CdTableSelection) {
+ this.selection = selection;
+ }
+ setExpandedRow(event: any) {
+ this.expandedRow = event;
+ }
}
import { FormlyArrayTypeComponent } from '../forms/crud-form/formly-array-type/formly-array-type.component';
import { FormlyInputTypeComponent } from '../forms/crud-form/formly-input-type/formly-input-type.component';
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';
@NgModule({
imports: [
types: [
{ name: 'array', component: FormlyArrayTypeComponent },
{ name: 'object', component: FormlyObjectTypeComponent },
- { name: 'input', component: FormlyInputTypeComponent }
+ { name: 'input', component: FormlyInputTypeComponent, wrappers: ['input-wrapper'] },
+ { name: 'textarea', component: FormlyTextareaTypeComponent, wrappers: ['input-wrapper'] }
],
- validationMessages: [{ name: 'required', message: 'This field is required' }]
+ validationMessages: [
+ { name: 'required', message: 'This field is required' },
+ { name: 'json', message: 'This field is not a valid json document' },
+ {
+ name: 'rgwRoleName',
+ message:
+ 'Role name must contain letters, numbers or the ' +
+ 'following valid special characters "_+=,.@-]+" (pattern: [0-9a-zA-Z_+=,.@-]+)'
+ },
+ {
+ name: 'rgwRolePath',
+ message:
+ 'Role path must start and finish with a slash "/".' +
+ ' (pattern: (\u002F)|(\u002F[\u0021-\u007E]+\u002F))'
+ }
+ ],
+ wrappers: [{ name: 'input-wrapper', component: FormlyInputWrapperComponent }]
}),
FormlyBootstrapModule
],
CrudFormComponent,
FormlyArrayTypeComponent,
FormlyInputTypeComponent,
- FormlyObjectTypeComponent
+ FormlyObjectTypeComponent,
+ FormlyInputWrapperComponent
],
exports: [
TableComponent,
class="card-header">{{ formUISchema.title }}</div>
<form *ngIf="formUISchema.uiSchema"
[formGroup]="form"
- (ngSubmit)="submit(model)">
+ (ngSubmit)="submit(model, formUISchema.taskInfo)">
<div class="card-body position-relative">
<formly-form [form]="form"
[fields]="formUISchema.controlSchema"
[model]="model"
- [options]="options"></formly-form>
+ [options]="{formState: formUISchema.uiSchema}"></formly-form>
</div>
<div class="card-footer">
- <cd-form-button-panel (submitActionEvent)="submit(model)"
+ <cd-form-button-panel (submitActionEvent)="submit(model, formUISchema.taskInfo)"
[form]="formDir"
[submitText]="formUISchema.title"
[disabled]="!form.valid"
import { FinishedTask } from '~/app/shared/models/finished-task';
import { Location } from '@angular/common';
import { FormGroup } from '@angular/forms';
-import { FormlyFormOptions } from '@ngx-formly/core';
import { mergeMap } from 'rxjs/operators';
-import { JsonFormUISchema } from './crud-form.model';
+import { CrudTaskInfo, JsonFormUISchema } from './crud-form.model';
import { Observable } from 'rxjs';
+import _ from 'lodash';
@Component({
selector: 'cd-crud-form',
})
export class CrudFormComponent implements OnInit {
model: any = {};
- options: FormlyFormOptions = {};
resource: string;
+ task: { message: string; id: string } = { message: '', id: '' };
form = new FormGroup({});
formUISchema$: Observable<JsonFormUISchema>;
);
}
- submit(data: any) {
+ submit(data: any, taskInfo: CrudTaskInfo) {
if (data) {
+ let taskMetadata = {};
+ _.forEach(taskInfo.metadataFields, (field) => {
+ taskMetadata[field] = data[field];
+ });
+ taskMetadata['__message'] = taskInfo.message;
this.taskWrapper
.wrapTaskAroundCall({
- task: new FinishedTask('ceph-user/create', {
- user_entity: data.user_entity
- }),
+ task: new FinishedTask('crud-component', taskMetadata),
call: this.dataGatewayService.create(this.resource, data)
})
.subscribe({
import { FormlyFieldConfig } from '@ngx-formly/core';
+export interface CrudTaskInfo {
+ metadataFields: string[];
+ message: string;
+}
+
export interface JsonFormUISchema {
title: string;
controlSchema: FormlyFieldConfig[];
uiSchema: any;
+ taskInfo: CrudTaskInfo;
}
<input [formControl]="formControl"
- class="form-control cd-col-form-input"
- [formlyAttributes]="field">
+ [formlyAttributes]="field"
+ class="form-control col-form-input"/>
--- /dev/null
+<ng-template #labelTemplate>
+ <div class="d-flex align-items-center">
+ <label *ngIf="props.label && props.hideLabel !== true"
+ [attr.for]="id"
+ class="form-label">
+ {{ props.label }}
+ <span *ngIf="props.required && props.hideRequiredMarker !== true"
+ aria-hidden="true">*</span>
+ <cd-helper *ngIf="helper">
+ <span [innerHTML]="helper"></span>
+ </cd-helper>
+ </label>
+ </div>
+</ng-template>
+
+<div class="mb-3"
+ [class.form-floating]="props.labelPosition === 'floating'"
+ [class.has-error]="showError">
+ <ng-container *ngIf="props.labelPosition !== 'floating'">
+ <ng-container [ngTemplateOutlet]="labelTemplate"></ng-container>
+ </ng-container>
+
+ <ng-container #fieldComponent></ng-container>
+
+ <ng-container *ngIf="props.labelPosition === 'floating'">
+ <ng-container [ngTemplateOutlet]="labelTemplate"></ng-container>
+ </ng-container>
+
+ <div *ngIf="showError"
+ class="invalid-feedback"
+ [style.display]="'block'">
+ <formly-validation-message [field]="field"></formly-validation-message>
+ </div>
+
+ <small *ngIf="props.description"
+ class="form-text text-muted">{{ props.description }}</small>
+</div>
--- /dev/null
+import { Component } from '@angular/core';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { FormGroup } from '@angular/forms';
+import { FormlyFieldConfig, FormlyModule } from '@ngx-formly/core';
+
+import { FormlyInputWrapperComponent } from './formly-input-wrapper.component';
+
+@Component({
+ template: ` <form [formGroup]="form">
+ <formly-form [model]="{}" [fields]="fields" [options]="{}" [form]="form"></formly-form>
+ </form>`
+})
+class MockFormComponent {
+ form = new FormGroup({});
+ fields: FormlyFieldConfig[] = [
+ {
+ wrappers: ['input'],
+ defaultValue: {}
+ }
+ ];
+}
+
+describe('FormlyInputWrapperComponent', () => {
+ let component: MockFormComponent;
+ let fixture: ComponentFixture<MockFormComponent>;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ declarations: [FormlyInputWrapperComponent],
+ imports: [
+ FormlyModule.forRoot({
+ types: [{ name: 'input', component: FormlyInputWrapperComponent }]
+ })
+ ]
+ }).compileComponents();
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(MockFormComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
--- /dev/null
+import { Component } from '@angular/core';
+import { FieldWrapper } from '@ngx-formly/core';
+import { getFieldState } from '../helpers';
+
+@Component({
+ selector: 'cd-formly-input-wrapper',
+ templateUrl: './formly-input-wrapper.component.html',
+ styleUrls: ['./formly-input-wrapper.component.scss']
+})
+export class FormlyInputWrapperComponent extends FieldWrapper {
+ get helper(): string {
+ const fieldState = getFieldState(this.field);
+ return fieldState?.help || '';
+ }
+}
--- /dev/null
+<textarea #textArea
+ [formControl]="formControl"
+ [cols]="props.cols"
+ [rows]="props.rows"
+ class="form-control"
+ [class.is-invalid]="showError"
+ [formlyAttributes]="field"
+ (change)="onChange()">
+</textarea>
--- /dev/null
+import { Component } from '@angular/core';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { FormGroup } from '@angular/forms';
+import { FormlyFieldConfig, FormlyModule } from '@ngx-formly/core';
+
+import { FormlyTextareaTypeComponent } from './formly-textarea-type.component';
+
+@Component({
+ template: ` <form [formGroup]="form">
+ <formly-form [model]="{}" [fields]="fields" [options]="{}" [form]="form"></formly-form>
+ </form>`
+})
+class MockFormComponent {
+ options = {};
+ form = new FormGroup({});
+ fields: FormlyFieldConfig[] = [
+ {
+ wrappers: ['input'],
+ defaultValue: {}
+ }
+ ];
+}
+describe('FormlyTextareaTypeComponent', () => {
+ let component: MockFormComponent;
+ let fixture: ComponentFixture<MockFormComponent>;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ declarations: [FormlyTextareaTypeComponent],
+ imports: [
+ FormlyModule.forRoot({
+ types: [{ name: 'input', component: FormlyTextareaTypeComponent }]
+ })
+ ]
+ }).compileComponents();
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(MockFormComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
--- /dev/null
+import { Component, ViewChild, ElementRef } from '@angular/core';
+import { FieldType, FieldTypeConfig } from '@ngx-formly/core';
+
+@Component({
+ selector: 'cd-formly-textarea-type',
+ templateUrl: './formly-textarea-type.component.html',
+ styleUrls: ['./formly-textarea-type.component.scss']
+})
+export class FormlyTextareaTypeComponent extends FieldType<FieldTypeConfig> {
+ @ViewChild('textArea')
+ public textArea: ElementRef<any>;
+
+ onChange() {
+ const value = this.textArea.nativeElement.value;
+ try {
+ const formatted = JSON.stringify(JSON.parse(value), null, 2);
+ this.textArea.nativeElement.value = formatted;
+ this.textArea.nativeElement.style.height = 'auto';
+ const lineNumber = formatted.split('\n').length;
+ const pixelPerLine = 25;
+ const pixels = lineNumber * pixelPerLine;
+ this.textArea.nativeElement.style.height = pixels + 'px';
+ } catch (e) {}
+ }
+}
--- /dev/null
+import { ValidatorFn } from '@angular/forms';
+import { FormlyFieldConfig } from '@ngx-formly/core';
+import { forEach } from 'lodash';
+import { formlyAsyncJsonValidator } from './validators/json-validator';
+import { formlyRgwRoleNameValidator, formlyRgwRolePath } from './validators/rgw-role-validator';
+
+export function getFieldState(field: FormlyFieldConfig, uiSchema: any[] = undefined) {
+ const formState: any[] = uiSchema || field.options?.formState;
+ if (formState) {
+ return formState.find((element) => element.key == field.key);
+ }
+ return {};
+}
+
+export function setupValidators(field: FormlyFieldConfig, uiSchema: any[]) {
+ const fieldState = getFieldState(field, uiSchema);
+ let validators: ValidatorFn[] = [];
+ forEach(fieldState.validators, (validatorStr) => {
+ switch (validatorStr) {
+ case 'json': {
+ validators.push(formlyAsyncJsonValidator);
+ break;
+ }
+ case 'rgwRoleName': {
+ validators.push(formlyRgwRoleNameValidator);
+ break;
+ }
+ case 'rgwRolePath': {
+ validators.push(formlyRgwRolePath);
+ break;
+ }
+ }
+ });
+ field.asyncValidators = { validation: validators };
+}
--- /dev/null
+import { AbstractControl } from '@angular/forms';
+
+export function formlyAsyncJsonValidator(control: AbstractControl): Promise<any> {
+ return new Promise((resolve, _reject) => {
+ try {
+ JSON.parse(control.value);
+ resolve(null);
+ } catch (e) {
+ resolve({ json: true });
+ }
+ });
+}
--- /dev/null
+import { AbstractControl } from '@angular/forms';
+
+export function formlyRgwRolePath(control: AbstractControl): Promise<any> {
+ return new Promise((resolve, _reject) => {
+ if (control.value.match('^((\u002F)|(\u002F[\u0021-\u007E]+\u002F))$')) {
+ resolve(null);
+ }
+ resolve({ rgwRolePath: true });
+ });
+}
+
+export function formlyRgwRoleNameValidator(control: AbstractControl): Promise<any> {
+ return new Promise((resolve, _reject) => {
+ if (control.value.match('^[0-9a-zA-Z_+=,.@-]+$')) {
+ resolve(null);
+ }
+ resolve({ rgwRoleName: true });
+ });
+}
import { Injectable } from '@angular/core';
import { FormlyJsonschema } from '@ngx-formly/core/json-schema';
-import { JsonFormUISchema } from '../forms/crud-form/crud-form.model';
+import { CrudTaskInfo, JsonFormUISchema } from '../forms/crud-form/crud-form.model';
+import { setupValidators } from '../forms/crud-form/helpers';
@Injectable({
providedIn: 'root'
for (let i = 0; i < controlSchema.length; i++) {
for (let j = 0; j < uiSchema.length; j++) {
if (controlSchema[i].key == uiSchema[j].key) {
- controlSchema[i].className = uiSchema[j].className;
controlSchema[i].props.templateOptions = uiSchema[j].templateOptions;
+ setupValidators(controlSchema[i], uiSchema);
}
}
}
- return { title, uiSchema, controlSchema };
+ let taskInfo: CrudTaskInfo = {
+ metadataFields: response.forms[0].task_info.metadataFields,
+ message: response.forms[0].task_info.message
+ };
+ return { title, uiSchema, controlSchema, taskInfo };
}
}
import { Injectable } from '@angular/core';
+import _ from 'lodash';
import { Components } from '../enum/components.enum';
import { FinishedTask } from '../models/finished-task';
'service/delete': this.newTaskMessage(this.commonOperations.delete, (metadata) =>
this.service(metadata)
),
- 'ceph-users/create': this.newTaskMessage(this.commonOperations.create, (metadata) =>
- this.cephUser(metadata)
+ 'crud-component': this.newTaskMessage(this.commonOperations.create, (metadata) =>
+ this.crudMessage(metadata)
)
};
return $localize`Service '${metadata.service_name}'`;
}
- cephUser(metadata: any) {
- return $localize`Ceph User '${metadata.user_entity}'`;
+ crudMessage(metadata: any) {
+ let message = metadata.__message;
+ _.forEach(metadata, (value, key) => {
+ if (key != '__message') {
+ let regex = '{' + key + '}';
+ message = message.replace(regex, value);
+ }
+ });
+ return $localize`${message}`;
}
_getTaskTitle(task: Task) {
import { FormlyArrayTypeComponent } from './forms/crud-form/formly-array-type/formly-array-type.component';
import { FormlyObjectTypeComponent } from './forms/crud-form/formly-object-type/formly-object-type.component';
import { FormlyInputTypeComponent } from './forms/crud-form/formly-input-type/formly-input-type.component';
+import { FormlyTextareaTypeComponent } from './forms/crud-form/formly-textarea-type/formly-textarea-type.component';
@NgModule({
imports: [
}),
FormlyBootstrapModule
],
- declarations: [],
+ declarations: [FormlyTextareaTypeComponent],
exports: [ComponentsModule, PipesModule, DataTableModule, DirectivesModule],
providers: [AuthStorageService, AuthGuardService, FormatterService, CssHelper]
})
.form-label {
@extend .cd-col-form-label;
text-align: start;
- width: 50%;
+ white-space: nowrap;
+ width: fit-content;
span[aria-hidden='true'] {
color: $danger;
summary: List RGW roles
tags:
- RGW
+ post:
+ parameters: []
+ requestBody:
+ content:
+ application/json:
+ schema:
+ properties:
+ role_assume_policy_doc:
+ default: ''
+ type: string
+ role_name:
+ default: ''
+ type: string
+ role_path:
+ default: ''
+ type: string
+ 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: Create Ceph User
+ tags:
+ - RGW
/api/rgw/user/{uid}:
delete:
parameters:
import ipaddress
import json
import logging
+import os
import re
import xml.etree.ElementTree as ET # noqa: N814
from subprocess import SubprocessError
return roles
+ def create_role(self, role_name: str, role_path: str, role_assume_policy_doc: str) -> None:
+ try:
+ json.loads(role_assume_policy_doc)
+ except: # noqa: E722
+ raise DashboardException('Assume role policy document is not a valid json')
+
+ # valid values:
+ # pylint: disable=C0301
+ # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iam-role.html#cfn-iam-role-path # noqa: E501
+ if len(role_name) > 64:
+ raise DashboardException(
+ f'Role name "{role_name}" is invalid. Should be 64 characters or less')
+
+ role_name_regex = '[0-9a-zA-Z_+=,.@-]+'
+ if not re.fullmatch(role_name_regex, role_name):
+ raise DashboardException(
+ f'Role name "{role_name}" is invalid. Valid characters are "{role_name_regex}"')
+
+ if not os.path.isabs(role_path):
+ raise DashboardException(
+ f'Role path "{role_path}" is invalid. It should be an absolute path')
+ if role_path[-1] != '/':
+ raise DashboardException(
+ f'Role path "{role_path}" is invalid. It should start and end with a slash')
+ path_regex = '(\u002F)|(\u002F[\u0021-\u007E]+\u002F)'
+ if not re.fullmatch(path_regex, role_path):
+ raise DashboardException(
+ (f'Role path "{role_path}" is invalid.'
+ f'Role path should follow the pattern "{path_regex}"'))
+
+ rgw_create_role_command = ['role', 'create', '--role-name', role_name, '--path', role_path]
+ if role_assume_policy_doc:
+ rgw_create_role_command += ['--assume-role-policy-doc', f"{role_assume_policy_doc}"]
+
+ code, _roles, _err = mgr.send_rgwadmin_command(rgw_create_role_command,
+ stdout_as_json=False)
+ if code != 0:
+ # pylint: disable=C0301
+ link = 'https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iam-role.html#cfn-iam-role-path' # noqa: E501
+ msg = (f'Error creating role with code {code}: '
+ 'Looks like the document has a wrong format.'
+ f' For more information about the format look at {link}')
+ raise DashboardException(msg=msg, component='rgw')
+
def perform_validations(self, retention_period_days, retention_period_years, mode):
try:
retention_period_days = int(retention_period_days) if retention_period_days else 0