From: Naman Munet Date: Fri, 10 Jan 2025 11:46:39 +0000 (+0530) Subject: mgr/dashboard: Added unit tests and fixed minor issues X-Git-Tag: v20.0.0~305^2 X-Git-Url: http://git-server-git.apps.pok.os.sepia.ceph.com/?a=commitdiff_plain;h=refs%2Fpull%2F61033%2Fhead;p=ceph.git mgr/dashboard: Added unit tests and fixed minor issues Fixes: https://tracker.ceph.com/issues/69140 Signed-off-by: Naman Munet --- diff --git a/src/pybind/mgr/dashboard/controllers/rgw_iam.py b/src/pybind/mgr/dashboard/controllers/rgw_iam.py index 80d4b85862f6..d9a87dc56b81 100644 --- a/src/pybind/mgr/dashboard/controllers/rgw_iam.py +++ b/src/pybind/mgr/dashboard/controllers/rgw_iam.py @@ -9,54 +9,121 @@ from . import APIDoc, APIRouter, EndpointDoc, RESTController, allow_empty_body @APIRouter('rgw/accounts', Scope.RGW) @APIDoc("RGW User Accounts API", "RgwUserAccounts") class RgwUserAccountsController(RESTController): - + @EndpointDoc("Update RGW account info", + parameters={'account_name': (str, 'Account name'), + 'email': (str, 'Email'), + 'tenant': (str, 'Tenant'), + 'max_buckets': (int, 'Max buckets'), + 'max_users': (int, 'Max users'), + 'max_roles': (int, 'Max roles'), + 'max_group': (int, 'Max groups'), + 'max_access_keys': (int, 'Max access keys')}) @allow_empty_body - def create(self, account_name: Optional[str] = None, tenant: str = None, - email: Optional[str] = None, max_buckets: str = None, - max_users: str = None, max_roles: str = None, max_group: str = None, - max_access_keys: str = None): + def create(self, account_name: str, tenant: Optional[str] = None, + email: Optional[str] = None, max_buckets: Optional[int] = None, + max_users: Optional[int] = None, max_roles: Optional[int] = None, + max_group: Optional[int] = None, + max_access_keys: Optional[int] = None): + """ + Create an account + + :param account_name: Account name + :return: Returns account resource. + :rtype: Dict[str, Any] + """ return RgwAccounts.create_account(account_name, tenant, email, max_buckets, max_users, max_roles, max_group, max_access_keys) def list(self, detailed: bool = False): + """ + List all account ids or all detailed account info based on the 'detailed' query parameter. + + - If detailed=True, returns detailed account info. + - If detailed=False, returns only account ids. + """ detailed = str_to_bool(detailed) return RgwAccounts.get_accounts(detailed) @EndpointDoc("Get RGW Account by id", parameters={'account_id': (str, 'Account id')}) def get(self, account_id: str): + """ + Get an account by account id + """ return RgwAccounts.get_account(account_id) @EndpointDoc("Delete RGW Account", parameters={'account_id': (str, 'Account id')}) def delete(self, account_id): + """ + Removes an account + + :param account_id: account identifier + :return: None. + """ return RgwAccounts.delete_account(account_id) @EndpointDoc("Update RGW account info", - parameters={'account_id': (str, 'Account id')}) + parameters={'account_id': (str, 'Account id'), + 'account_name': (str, 'Account name'), + 'email': (str, 'Email'), + 'tenant': (str, 'Tenant'), + 'max_buckets': (int, 'Max buckets'), + 'max_users': (int, 'Max users'), + 'max_roles': (int, 'Max roles'), + 'max_group': (int, 'Max groups'), + 'max_access_keys': (int, 'Max access keys')}) @allow_empty_body - def set(self, account_id: str, account_name: Optional[str] = None, - email: Optional[str] = None, tenant: str = None, - max_buckets: str = None, max_users: str = None, - max_roles: str = None, max_group: str = None, - max_access_keys: str = None): + def set(self, account_id: str, account_name: str, + email: Optional[str] = None, tenant: Optional[str] = None, + max_buckets: Optional[int] = None, max_users: Optional[int] = None, + max_roles: Optional[int] = None, max_group: Optional[int] = None, + max_access_keys: Optional[int] = None): + """ + Modifies an account + + :param account_id: Account identifier + :return: Returns modified account resource. + :rtype: Dict[str, Any] + """ return RgwAccounts.modify_account(account_id, account_name, email, tenant, max_buckets, max_users, max_roles, max_group, max_access_keys) @EndpointDoc("Set RGW Account/Bucket quota", parameters={'account_id': (str, 'Account id'), - 'max_size': (str, 'Max size')}) + 'quota_type': (str, 'Quota type'), + 'max_size': (str, 'Max size'), + 'max_objects': (str, 'Max objects')}) @RESTController.Resource(method='PUT', path='/quota') @allow_empty_body def set_quota(self, quota_type: str, account_id: str, max_size: str, max_objects: str, enabled: bool): + """ + Modifies quota + + :param account_id: Account identifier + :param quota_type: 'account' or 'bucket' + :return: Returns modified quota. + :rtype: Dict[str, Any] + """ return RgwAccounts.set_quota(quota_type, account_id, max_size, max_objects, enabled) @EndpointDoc("Enable/Disable RGW Account/Bucket quota", - parameters={'account_id': (str, 'Account id')}) + parameters={'account_id': (str, 'Account id'), + 'quota_type': (str, 'Quota type'), + 'quota_status': (str, 'Quota status')}) @RESTController.Resource(method='PUT', path='/quota/status') @allow_empty_body def set_quota_status(self, quota_type: str, account_id: str, quota_status: str): + """ + Enable/Disable quota + + :param account_id: Account identifier + :param quota_type: 'account' or 'bucket' + :param quota_status: 'enable' or 'disable' + :return: Returns modified quota. + :rtype: Dict[str, Any] + """ return RgwAccounts.set_quota_status(quota_type, account_id, quota_status) diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-accounts-details/rgw-user-accounts-details.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-accounts-details/rgw-user-accounts-details.component.spec.ts index ac43cfe4aa62..1aad6167fa23 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-accounts-details/rgw-user-accounts-details.component.spec.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-accounts-details/rgw-user-accounts-details.component.spec.ts @@ -1,6 +1,9 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { RgwUserAccountsDetailsComponent } from './rgw-user-accounts-details.component'; +import { DimlessBinaryPipe } from '~/app/shared/pipes/dimless-binary.pipe'; +import { TableKeyValueComponent } from '~/app/shared/datatable/table-key-value/table-key-value.component'; +import { CdDatePipe } from '~/app/shared/pipes/cd-date.pipe'; describe('RgwUserAccountsDetailsComponent', () => { let component: RgwUserAccountsDetailsComponent; @@ -8,11 +11,13 @@ describe('RgwUserAccountsDetailsComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - declarations: [RgwUserAccountsDetailsComponent] + declarations: [RgwUserAccountsDetailsComponent, TableKeyValueComponent], + providers: [DimlessBinaryPipe, CdDatePipe] }).compileComponents(); fixture = TestBed.createComponent(RgwUserAccountsDetailsComponent); component = fixture.componentInstance; + component.selection = { quota: {}, bucket_quota: {} }; fixture.detectChanges(); }); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-accounts-details/rgw-user-accounts-details.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-accounts-details/rgw-user-accounts-details.component.ts index a8062052090d..f18402350572 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-accounts-details/rgw-user-accounts-details.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-accounts-details/rgw-user-accounts-details.component.ts @@ -24,14 +24,16 @@ export class RgwUserAccountsDetailsComponent implements OnChanges { createDisplayValues(quota_type: string) { return { Enabled: this.selection[quota_type].enabled ? 'Yes' : 'No', - 'Maximum Size': - this.selection[quota_type].max_size <= -1 + 'Maximum size': this.selection[quota_type].enabled + ? this.selection[quota_type].max_size <= -1 ? 'Unlimited' - : this.dimlessBinary.transform(this.selection[quota_type].max_size), - 'Maximum objects': - this.selection[quota_type].max_objects <= -1 + : this.dimlessBinary.transform(this.selection[quota_type].max_size) + : '-', + 'Maximum objects': this.selection[quota_type].enabled + ? this.selection[quota_type].max_objects <= -1 ? 'Unlimited' : this.selection[quota_type].max_objects + : '-' }; } } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-accounts-form/rgw-user-accounts-form.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-accounts-form/rgw-user-accounts-form.component.html index b9ccddd0a56c..3856c42a8e6c 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-accounts-form/rgw-user-accounts-form.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-accounts-form/rgw-user-accounts-form.component.html @@ -182,9 +182,10 @@ [name]="formControl" [id]="formControl" label="{{formControl.split('_')[1] | upperFirst}} Mode" - (change)="onModeChange($event.target.value, formControl)"> - + (change)="onModeChange($event.target.value, formControl)" + [helperText]="getHelperTextForMode(formControl)"> + @@ -197,13 +198,16 @@ formControlName="{{formControl}}" label="{{formControl.split('_')[0] | upperFirst}}. {{formControl.split('_').length > 2 ? formControl.split('_')[1]+' '+formControl.split('_')[2]: formControl.split('_')[1]}}" [min]="1" + cdRequiredField="{{formControl.split('_')[0] | upperFirst}}. {{formControl.split('_').length > 2 ? formControl.split('_')[1]+' '+formControl.split('_')[2]: formControl.split('_')[1]}}" [invalid]="!accountForm.controls[formControl].valid && accountForm.controls[formControl].dirty" [invalidText]="maxValError"> This field is required. + Enter number greater than 0 The entered value must be a number greater than 0 + i18n>Enter a valid positive number @@ -213,66 +217,78 @@ [formGroup]="accountForm">
{{quotaType | upperFirst}} Quota +
Set quota on account owned by users.
+ +
+ Set quota on buckets owned by an account. +
+
Enabled - - - Unlimited size - - -
- Max. size - - - - This field is required. - The value is not valid. - Size must be a number or in a valid format. eg: 5 GiB - -
- - - Unlimited objects - - -
- Max. objects - - - - This field is required. - Please enter a valid number - +
+ + + Unlimited size + + +
+ Max. size + + + + This field is required. + Enter a valid value. + Size must be a number or in a valid format. eg: 5 GiB + +
+ + + Unlimited objects + + +
+ Max. objects + + + + This field is required. + Enter a valid positive number + +
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-accounts-form/rgw-user-accounts-form.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-accounts-form/rgw-user-accounts-form.component.scss index e69de29bb2d1..6c6969a7718e 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-accounts-form/rgw-user-accounts-form.component.scss +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-accounts-form/rgw-user-accounts-form.component.scss @@ -0,0 +1,21 @@ +@use '@carbon/layout'; + +::ng-deep .cds--form__helper-text { + white-space: pre-line; +} + +.quota-heading { + margin-bottom: layout.$spacing-03; +} + +.quota-sub-block { + margin: layout.$spacing-03 0 0 layout.$spacing-06; + + .input-wrapper { + margin: layout.$spacing-03 0 0 layout.$spacing-01; + + &:first-of-type { + margin-bottom: layout.$spacing-06; + } + } +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-accounts-form/rgw-user-accounts-form.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-accounts-form/rgw-user-accounts-form.component.spec.ts index 0cd52faffc6c..87499ce2affe 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-accounts-form/rgw-user-accounts-form.component.spec.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-accounts-form/rgw-user-accounts-form.component.spec.ts @@ -9,9 +9,13 @@ import { PipesModule } from '~/app/shared/pipes/pipes.module'; import { of } from 'rxjs'; import { RgwUserAccountsService } from '~/app/shared/api/rgw-user-accounts.service'; import { ModalModule } from 'carbon-components-angular'; +import { ReactiveFormsModule } from '@angular/forms'; +import { RgwUserAccountsComponent } from '../rgw-user-accounts/rgw-user-accounts.component'; class MockRgwUserAccountsService { create = jest.fn().mockReturnValue(of(null)); + modify = jest.fn().mockReturnValue(of(null)); + setQuota = jest.fn().mockReturnValue(of(null)); } describe('RgwUserAccountsFormComponent', () => { @@ -27,8 +31,11 @@ describe('RgwUserAccountsFormComponent', () => { ToastrModule.forRoot(), HttpClientTestingModule, PipesModule, - RouterTestingModule, - ModalModule + RouterTestingModule.withRoutes([ + { path: 'rgw/accounts', component: RgwUserAccountsComponent } + ]), + ModalModule, + ReactiveFormsModule ], providers: [{ provide: RgwUserAccountsService, useClass: MockRgwUserAccountsService }] }).compileComponents(); @@ -47,10 +54,96 @@ describe('RgwUserAccountsFormComponent', () => { it('should call create method of MockRgwUserAccountsService and show success notification', () => { component.editing = false; + component.accountForm.get('name').setValue('test'); + const payload = { + account_name: 'test', + email: '', + tenant: '', + max_users: 1000, + max_buckets: 1000, + max_roles: 1000, + max_group: 1000, + max_access_keys: 4 + }; const spy = jest.spyOn(component, 'submit'); const createDataSpy = jest.spyOn(rgwUserAccountsService, 'create').mockReturnValue(of(null)); component.submit(); + expect(component.accountForm.valid).toBe(true); expect(spy).toHaveBeenCalled(); expect(createDataSpy).toHaveBeenCalled(); + expect(createDataSpy).toHaveBeenCalledWith(payload); + }); + + it('should call modify method of MockRgwUserAccountsService and show success notification', () => { + component.editing = true; + component.accountForm.get('name').setValue('test'); + component.accountForm.get('id').setValue('RGW12312312312312312'); + component.accountForm.get('email').setValue('test@test.com'); + const payload = { + account_id: 'RGW12312312312312312', + account_name: 'test', + email: 'test@test.com', + tenant: '', + max_users: 1000, + max_buckets: 1000, + max_roles: 1000, + max_group: 1000, + max_access_keys: 4 + }; + const spy = jest.spyOn(component, 'submit'); + const modifyDataSpy = jest.spyOn(rgwUserAccountsService, 'modify').mockReturnValue(of(null)); + component.submit(); + expect(component.accountForm.valid).toBe(true); + expect(spy).toHaveBeenCalled(); + expect(modifyDataSpy).toHaveBeenCalled(); + expect(modifyDataSpy).toHaveBeenCalledWith(payload); + }); + + it('should call setQuota for "account" if account quota is dirty', () => { + component.accountForm.get('id').setValue('123'); + component.accountForm.get('account_quota_enabled').setValue(true); + component.accountForm.get('account_quota_max_size_unlimited').setValue(false); + component.accountForm.get('account_quota_max_size').setValue('1 GiB'); + component.accountForm.get('account_quota_max_objects_unlimited').setValue(false); + component.accountForm.get('account_quota_max_objects').setValue('100'); + component.accountForm.get('account_quota_max_size').markAsDirty(); + const accountId = '123'; + + const spySetQuota = jest.spyOn(rgwUserAccountsService, 'setQuota').mockReturnValue(of(null)); + const spyGoToListView = jest.spyOn(component, 'goToListView'); + + component.setQuotaConfig(); + const accountQuotaArgs = { + quota_type: 'account', + enabled: component.accountForm.getValue('account_quota_enabled'), + max_size: '1073741824', + max_objects: component.accountForm.getValue('account_quota_max_objects') + }; + expect(spySetQuota).toHaveBeenCalledWith(accountId, accountQuotaArgs); + expect(spyGoToListView).toHaveBeenCalled(); + }); + + it('should call setQuota for "bucket" if account quota is dirty', () => { + component.accountForm.get('id').setValue('123'); + component.accountForm.get('bucket_quota_enabled').setValue(true); + component.accountForm.get('bucket_quota_max_size_unlimited').setValue(false); + component.accountForm.get('bucket_quota_max_size').setValue('1 GiB'); + component.accountForm.get('bucket_quota_max_objects_unlimited').setValue(false); + component.accountForm.get('bucket_quota_max_objects').setValue('100'); + component.accountForm.get('bucket_quota_max_size').markAsDirty(); + const accountId = '123'; + + const spySetQuota = jest.spyOn(rgwUserAccountsService, 'setQuota').mockReturnValue(of(null)); + const spyGoToListView = jest.spyOn(component, 'goToListView'); + + component.setQuotaConfig(); + const bucketQuotaArgs = { + quota_type: 'bucket', + enabled: component.accountForm.getValue('bucket_quota_enabled'), + max_size: '1073741824', + max_objects: component.accountForm.getValue('bucket_quota_max_objects') + }; + expect(spySetQuota).toHaveBeenCalledWith(accountId, bucketQuotaArgs); + expect(spyGoToListView).toHaveBeenCalled(); }); }); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-accounts-form/rgw-user-accounts-form.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-accounts-form/rgw-user-accounts-form.component.ts index 8e0d81835667..7c082f6867da 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-accounts-form/rgw-user-accounts-form.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-accounts-form/rgw-user-accounts-form.component.ts @@ -94,15 +94,15 @@ export class RgwUserAccountsFormComponent extends CdForm implements OnInit { mapValuesForMode(value: any, formControlName: string) { switch (value[formControlName]) { case -1: - value[`${formControlName}_mode`] = -1; + value[`${formControlName}_mode`] = '-1'; value[formControlName] = ''; break; case 0: - value[`${formControlName}_mode`] = 0; + value[`${formControlName}_mode`] = '0'; value[formControlName] = ''; break; default: - value[`${formControlName}_mode`] = 1; + value[`${formControlName}_mode`] = '1'; break; } } @@ -113,30 +113,50 @@ export class RgwUserAccountsFormComponent extends CdForm implements OnInit { tenant: [''], name: ['', Validators.required], email: ['', CdValidators.email], - max_users_mode: [1], + max_users_mode: ['1'], max_users: [ 1000, - [CdValidators.requiredIf({ max_users_mode: '1' }), CdValidators.number(false)] + [ + CdValidators.requiredIf({ max_users_mode: '1' }), + CdValidators.number(false), + Validators.min(1) + ] ], - max_roles_mode: [1], + max_roles_mode: ['1'], max_roles: [ 1000, - [CdValidators.requiredIf({ max_roles_mode: '1' }), CdValidators.number(false)] + [ + CdValidators.requiredIf({ max_roles_mode: '1' }), + CdValidators.number(false), + Validators.min(1) + ] ], - max_groups_mode: [1], + max_groups_mode: ['1'], max_groups: [ 1000, - [CdValidators.requiredIf({ max_groups_mode: '1' }), CdValidators.number(false)] + [ + CdValidators.requiredIf({ max_groups_mode: '1' }), + CdValidators.number(false), + Validators.min(1) + ] ], - max_access_keys_mode: [1], + max_access_keys_mode: ['1'], max_access_keys: [ 4, - [CdValidators.requiredIf({ max_access_keys_mode: '1' }), CdValidators.number(false)] + [ + CdValidators.requiredIf({ max_access_keys_mode: '1' }), + CdValidators.number(false), + Validators.min(1) + ] ], - max_buckets_mode: [1], + max_buckets_mode: ['1'], max_buckets: [ 1000, - [CdValidators.requiredIf({ max_buckets_mode: '1' }), CdValidators.number(false)] + [ + CdValidators.requiredIf({ max_buckets_mode: '1' }), + CdValidators.number(false), + Validators.min(1) + ] ], account_quota_enabled: [false], account_quota_max_size_unlimited: [true], @@ -307,7 +327,7 @@ export class RgwUserAccountsFormComponent extends CdForm implements OnInit { this.accountForm.getValue(`${quotaType}_quota_max_size`) ); // Finally convert the value to KiB. - result['max_size'] = (bytes / 1024).toFixed(0) as any; + result['max_size'] = bytes.toFixed(0) as any; } if (!this.accountForm.getValue(`${quotaType}_quota_max_objects_unlimited`)) { result['max_objects'] = `${this.accountForm.getValue(`${quotaType}_quota_max_objects`)}`; @@ -354,4 +374,17 @@ export class RgwUserAccountsFormComponent extends CdForm implements OnInit { ? formvalue[formControlName] : formvalue[`${formControlName}_mode`]; } + + getHelperTextForMode(formControl: string) { + const resourceName = + formControl.split('_').length > 3 + ? formControl.split('_')[1] + ' ' + formControl.split('_')[2] + : formControl.split('_')[1]; + if (this.accountForm.getValue(formControl) == -1) { + return `${resourceName[0].toUpperCase() + resourceName.slice(1, -1)} creation is disabled.`; + } else if (this.accountForm.getValue(formControl) == 0) { + return `Unlimited ${resourceName.slice(0, -1)} creation allowed.`; + } + return ''; + } } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-accounts/rgw-user-accounts.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-accounts/rgw-user-accounts.component.html index ab53cd71baad..047b89f621d9 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-accounts/rgw-user-accounts.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-user-accounts/rgw-user-accounts.component.html @@ -2,8 +2,7 @@ User Accounts - This feature allows administrators to assign unique credentials to individual users or applications, - ensuring granular access control and improved security across the cluster. + Administrators can assign unique credentials to users or applications, enabling granular access control and enhancing security across the cluster. + (fetchData)="getAccountsList($event)" + [status]="tableStatus"> { @@ -124,9 +152,11 @@ export class RgwUserAccountsComponent extends ListWithDetails implements OnInit name: this.actionLabels.DELETE }; this.tableActions = [addAction, editAction, deleteAction]; + this.setTableRefreshTimeout(); } getAccountsList(context?: CdTableFetchDataContext) { + this.setTableRefreshTimeout(); this.rgwUserAccountsService.list(true).subscribe({ next: (accounts: Account[]) => { this.accounts = accounts; diff --git a/src/pybind/mgr/dashboard/openapi.yaml b/src/pybind/mgr/dashboard/openapi.yaml index d744eb6c42c0..6a5b9030042f 100644 --- a/src/pybind/mgr/dashboard/openapi.yaml +++ b/src/pybind/mgr/dashboard/openapi.yaml @@ -10810,6 +10810,10 @@ paths: - Prometheus /api/rgw/accounts: get: + description: "\n List all account ids or all detailed account info based\ + \ on the 'detailed' query parameter.\n\n - If detailed=True, returns\ + \ detailed account info.\n - If detailed=False, returns only account\ + \ ids.\n " parameters: - default: false in: query @@ -10836,6 +10840,9 @@ paths: tags: - RgwUserAccounts post: + description: "\n Create an account\n\n :param account_name: Account\ + \ name\n :return: Returns account resource.\n :rtype: Dict[str,\ + \ Any]\n " parameters: [] requestBody: content: @@ -10843,21 +10850,31 @@ paths: schema: properties: account_name: - type: integer + description: Account name + type: string email: + description: Email type: string max_access_keys: - type: string + description: Max access keys + type: integer max_buckets: - type: string + description: Max buckets + type: integer max_group: - type: string + description: Max groups + type: integer max_roles: - type: string + description: Max roles + type: integer max_users: - type: string + description: Max users + type: integer tenant: + description: Tenant type: string + required: + - account_name type: object responses: '201': @@ -10881,10 +10898,13 @@ paths: trace. security: - jwt: [] + summary: Update RGW account info tags: - RgwUserAccounts /api/rgw/accounts/{account_id}: delete: + description: "\n Removes an account\n\n :param account_id: account\ + \ identifier\n :return: None.\n " parameters: - description: Account id in: path @@ -10918,6 +10938,7 @@ paths: tags: - RgwUserAccounts get: + description: "\n Get an account by account id\n " parameters: - description: Account id in: path @@ -10946,6 +10967,9 @@ paths: tags: - RgwUserAccounts put: + description: "\n Modifies an account\n\n :param account_id: Account\ + \ identifier\n :return: Returns modified account resource.\n \ + \ :rtype: Dict[str, Any]\n " parameters: - description: Account id in: path @@ -10959,21 +10983,31 @@ paths: schema: properties: account_name: - type: integer + description: Account name + type: string email: + description: Email type: string max_access_keys: - type: string + description: Max access keys + type: integer max_buckets: - type: string + description: Max buckets + type: integer max_group: - type: string + description: Max groups + type: integer max_roles: - type: string + description: Max roles + type: integer max_users: - type: string + description: Max users + type: integer tenant: + description: Tenant type: string + required: + - account_name type: object responses: '200': @@ -11002,6 +11036,9 @@ paths: - RgwUserAccounts /api/rgw/accounts/{account_id}/quota: put: + description: "\n Modifies quota\n\n :param account_id: Account\ + \ identifier\n :param quota_type: 'account' or 'bucket'\n :return:\ + \ Returns modified quota.\n :rtype: Dict[str, Any]\n " parameters: - description: Account id in: path @@ -11017,11 +11054,13 @@ paths: enabled: type: string max_objects: + description: Max objects type: string max_size: description: Max size type: string quota_type: + description: Quota type type: string required: - quota_type @@ -11056,6 +11095,10 @@ paths: - RgwUserAccounts /api/rgw/accounts/{account_id}/quota/status: put: + description: "\n Enable/Disable quota\n\n :param account_id: Account\ + \ identifier\n :param quota_type: 'account' or 'bucket'\n :param\ + \ quota_status: 'enable' or 'disable'\n :return: Returns modified quota.\n\ + \ :rtype: Dict[str, Any]\n " parameters: - description: Account id in: path @@ -11069,8 +11112,10 @@ paths: schema: properties: quota_status: + description: Quota status type: string quota_type: + description: Quota type type: string required: - quota_type diff --git a/src/pybind/mgr/dashboard/services/rgw_iam.py b/src/pybind/mgr/dashboard/services/rgw_iam.py index 896f1a932396..88730dd96a1d 100644 --- a/src/pybind/mgr/dashboard/services/rgw_iam.py +++ b/src/pybind/mgr/dashboard/services/rgw_iam.py @@ -42,10 +42,10 @@ class RgwAccounts: return cls.send_rgw_cmd(get_account_cmd) @classmethod - def create_account(cls, account_name: Optional[str] = None, tenant: str = None, - email: Optional[str] = None, max_buckets: str = None, - max_users: str = None, max_roles: str = None, - max_group: str = None, max_access_keys: str = None): + def create_account(cls, account_name: str, tenant: Optional[str] = None, + email: Optional[str] = None, max_buckets: Optional[int] = None, + max_users: Optional[int] = None, max_roles: Optional[int] = None, + max_group: Optional[int] = None, max_access_keys: Optional[int] = None): create_accounts_cmd = ['account', 'create'] create_accounts_cmd += cls.get_common_args_list(account_name, email, @@ -56,11 +56,11 @@ class RgwAccounts: return cls.send_rgw_cmd(create_accounts_cmd) @classmethod - def modify_account(cls, account_id: str, account_name: Optional[str] = None, - email: Optional[str] = None, tenant: str = None, - max_buckets: str = None, max_users: str = None, - max_roles: str = None, max_group: str = None, - max_access_keys: str = None): + def modify_account(cls, account_id: str, account_name: str, + email: Optional[str] = None, tenant: Optional[str] = None, + max_buckets: Optional[int] = None, max_users: Optional[int] = None, + max_roles: Optional[int] = None, max_group: Optional[int] = None, + max_access_keys: Optional[int] = None): modify_accounts_cmd = ['account', 'modify', '--account-id', account_id] modify_accounts_cmd += cls.get_common_args_list(account_name, email, @@ -101,11 +101,11 @@ class RgwAccounts: return cls.send_rgw_cmd(set_quota_status_cmd) @classmethod - def get_common_args_list(cls, account_name: Optional[str] = None, - email: Optional[str] = None, tenant: str = None, - max_buckets: str = None, max_users: str = None, - max_roles: str = None, max_group: str = None, - max_access_keys: str = None): + def get_common_args_list(cls, account_name: str, email: Optional[str] = None, + tenant: Optional[str] = None, max_buckets: Optional[int] = None, + max_users: Optional[int] = None, max_roles: Optional[int] = None, + max_group: Optional[int] = None, + max_access_keys: Optional[int] = None): common_cmd_list = [] if account_name: common_cmd_list += ['--account-name', account_name] diff --git a/src/pybind/mgr/dashboard/tests/test_rgw_iam.py b/src/pybind/mgr/dashboard/tests/test_rgw_iam.py index 133b5a0d390c..90bb9ac16c87 100644 --- a/src/pybind/mgr/dashboard/tests/test_rgw_iam.py +++ b/src/pybind/mgr/dashboard/tests/test_rgw_iam.py @@ -39,12 +39,14 @@ class TestRgwUserAccountsController(TestCase): mock_create_account.return_value = mockReturnVal controller = RgwUserAccountsController() - result = controller.create(account_name='test_account', account_id='RGW18661471562806836', - email='test@example.com') + result = controller.create(account_name='test_account', tenant='', + email='test@example.com', max_buckets=1000, + max_users=1000, max_roles=1000, max_group=1000, + max_access_keys=4) # Check if the account creation method was called with the correct parameters - mock_create_account.assert_called_with('test_account', 'RGW18661471562806836', - 'test@example.com') + mock_create_account.assert_called_with('test_account', '', 'test@example.com', + 1000, 1000, 1000, 1000, 4) # Check the returned result self.assertEqual(result, mockReturnVal) @@ -207,10 +209,12 @@ class TestRgwUserAccountsController(TestCase): controller = RgwUserAccountsController() result = controller.set(account_id='RGW59378973811515857', account_name='new_account_name', - email='new_email@example.com') + email='new_email@example.com', tenant='', max_buckets=1000, + max_users=1000, max_roles=1000, max_group=1000, max_access_keys=4) mock_modify_account.assert_called_with('RGW59378973811515857', 'new_account_name', - 'new_email@example.com') + 'new_email@example.com', '', 1000, 1000, 1000, + 1000, 4) self.assertEqual(result, mock_return_value) @@ -246,9 +250,9 @@ class TestRgwUserAccountsController(TestCase): controller = RgwUserAccountsController() result = controller.set_quota(quota_type='account', account_id='RGW11111111111111111', - max_size='10GB', max_objects='1000') + max_size='10GB', max_objects='1000', enabled=True) - mock_set_quota.assert_called_with('account', 'RGW11111111111111111', '10GB', '1000') + mock_set_quota.assert_called_with('account', 'RGW11111111111111111', '10GB', '1000', True) self.assertEqual(result, mock_return_value)