From 9e73041aa30b5132766b33817e3032aa5607281b Mon Sep 17 00:00:00 2001 From: Achintk1491 Date: Fri, 13 Dec 2024 13:08:33 +0530 Subject: [PATCH] mgr/dashboard: Rgw ratelimit feature for user and bucket Fixes: https://tracker.ceph.com/issues/69233 Signed-off-by: Achint Kaur Signed-off-by: Achintk1491 --- src/pybind/mgr/dashboard/controllers/rgw.py | 52 ++- .../src/app/ceph/rgw/models/rgw-rate-limit.ts | 31 ++ .../rgw-bucket-details.component.html | 32 +- .../rgw-bucket-details.component.spec.ts | 122 +++++++ .../rgw-bucket-details.component.ts | 7 + .../rgw-bucket-form.component.html | 158 ++++---- .../rgw-bucket-form.component.spec.ts | 40 +- .../rgw-bucket-form.component.ts | 29 +- .../rgw-rate-limit-details.component.html | 51 +++ .../rgw-rate-limit-details.component.scss | 0 .../rgw-rate-limit-details.component.spec.ts | 32 ++ .../rgw-rate-limit-details.component.ts | 12 + .../rgw-rate-limit.component.html | 278 ++++++++++++++ .../rgw-rate-limit.component.scss | 0 .../rgw-rate-limit.component.spec.ts | 150 ++++++++ .../rgw-rate-limit.component.ts | 269 ++++++++++++++ .../rgw-user-details.component.html | 67 ++-- .../rgw-user-details.component.spec.ts | 50 ++- .../rgw-user-details.component.ts | 6 + .../rgw-user-form.component.html | 70 ++-- .../rgw-user-form.component.spec.ts | 341 +++++++++++++++++- .../rgw-user-form/rgw-user-form.component.ts | 58 +-- .../frontend/src/app/ceph/rgw/rgw.module.ts | 6 +- .../src/app/shared/api/rgw-bucket.service.ts | 10 + .../src/app/shared/api/rgw-user.service.ts | 12 + ...imless-binary-per-minute.directive.spec.ts | 12 + .../dimless-binary-per-minute.directive.ts | 136 +++++++ .../shared/directives/directives.module.ts | 7 +- .../pipes/dimless-binary-per-minute.pipe.ts | 19 + .../src/app/shared/pipes/pipes.module.ts | 10 +- .../app/shared/services/formatter.service.ts | 50 ++- src/pybind/mgr/dashboard/openapi.yaml | 209 +++++++++++ .../mgr/dashboard/services/rgw_client.py | 60 +++ src/pybind/mgr/dashboard/tests/test_rgw.py | 64 ++++ 34 files changed, 2253 insertions(+), 197 deletions(-) create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/models/rgw-rate-limit.ts create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-rate-limit-details/rgw-rate-limit-details.component.html create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-rate-limit-details/rgw-rate-limit-details.component.scss create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-rate-limit-details/rgw-rate-limit-details.component.spec.ts create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-rate-limit-details/rgw-rate-limit-details.component.ts create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-rate-limit/rgw-rate-limit.component.html create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-rate-limit/rgw-rate-limit.component.scss create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-rate-limit/rgw-rate-limit.component.spec.ts create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-rate-limit/rgw-rate-limit.component.ts create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/shared/directives/dimless-binary-per-minute.directive.spec.ts create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/shared/directives/dimless-binary-per-minute.directive.ts create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/dimless-binary-per-minute.pipe.ts mode change 100644 => 100755 src/pybind/mgr/dashboard/openapi.yaml diff --git a/src/pybind/mgr/dashboard/controllers/rgw.py b/src/pybind/mgr/dashboard/controllers/rgw.py index 1a694b4734146..2cdcf7124f1ec 100755 --- a/src/pybind/mgr/dashboard/controllers/rgw.py +++ b/src/pybind/mgr/dashboard/controllers/rgw.py @@ -15,7 +15,7 @@ from ..security import Permission, Scope from ..services.auth import AuthManager, JwtManager from ..services.ceph_service import CephService from ..services.rgw_client import _SYNC_GROUP_ID, NoRgwDaemonsException, \ - RgwClient, RgwMultisite, RgwMultisiteAutomation + RgwClient, RgwMultisite, RgwMultisiteAutomation, RgwRateLimit from ..services.rgw_iam import RgwAccounts from ..services.service import RgwServiceManager, wait_for_daemon_to_start from ..tools import json_str_to_object, str_to_bool @@ -722,6 +722,31 @@ class RgwBucket(RgwRESTController): def get_lifecycle_policy(self, bucket_name: str = '', daemon_name=None, owner=None): return self._get_lifecycle(bucket_name, daemon_name, owner) + @Endpoint(method='GET', path='/ratelimit') + @EndpointDoc("Get the bucket global rate limit") + @ReadPermission + def get_global_rate_limit(self): + rgwBucketRateLimit_instance = RgwRateLimit() + return rgwBucketRateLimit_instance.get_global_rateLimit() + + @Endpoint(method='GET', path='{uid}/ratelimit') + @EndpointDoc("Get the bucket rate limit") + @ReadPermission + def get_rate_limit(self, uid: str): + rgwBucketRateLimit_instance = RgwRateLimit() + return rgwBucketRateLimit_instance.get_rateLimit('bucket', uid) + + @Endpoint(method='PUT', path='{uid}/ratelimit') + @UpdatePermission + @allow_empty_body + @EndpointDoc("Update the bucket rate limit") + def set_rate_limit(self, enabled: bool, uid: str, max_read_ops: int, + max_write_ops: int, max_read_bytes: int, max_write_bytes: int): + rgwBucketRateLimit_instance = RgwRateLimit() + return rgwBucketRateLimit_instance.set_rateLimit('bucket', enabled, uid, + max_read_ops, max_write_ops, + max_read_bytes, max_write_bytes) + @UIRouter('/rgw/bucket', Scope.RGW) class RgwBucketUi(RgwBucket): @@ -964,6 +989,31 @@ class RgwUser(RgwRESTController): 'purge-keys': purge_keys }, json_response=False) + @Endpoint(method='GET', path='/ratelimit') + @EndpointDoc("Get the user global rate limit") + @ReadPermission + def get_global_rate_limit(self): + rgwUserRateLimit_instance = RgwRateLimit() + return rgwUserRateLimit_instance.get_global_rateLimit() + + @Endpoint(method='GET', path='{uid}/ratelimit') + @EndpointDoc("Get the user rate limit") + @ReadPermission + def get_rate_limit(self, uid: str): + rgwUserRateLimit_instance = RgwRateLimit() + return rgwUserRateLimit_instance.get_rateLimit('user', uid) + + @Endpoint(method='PUT', path='{uid}/ratelimit') + @UpdatePermission + @allow_empty_body + @EndpointDoc("Update the user rate limit") + def set_rate_limit(self, uid: str, enabled: bool = False, max_read_ops: int = 0, + max_write_ops: int = 0, max_read_bytes: int = 0, max_write_bytes: int = 0): + rgwUserRateLimit_instance = RgwRateLimit() + return rgwUserRateLimit_instance.set_rateLimit('user', enabled, + uid, max_read_ops, max_write_ops, + max_read_bytes, max_write_bytes) + class RGWRoleEndpoints: @staticmethod diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/models/rgw-rate-limit.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/models/rgw-rate-limit.ts new file mode 100644 index 0000000000000..668f00ad283e0 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/models/rgw-rate-limit.ts @@ -0,0 +1,31 @@ +export interface RgwRateLimitConfig { + enabled: boolean; + name?: string; + max_read_ops: number; + max_write_ops: number; + max_read_bytes: number; + max_write_bytes: number; +} +export interface GlobalRateLimitConfig { + bucket_ratelimit: { + max_read_ops: number; + max_write_ops: number; + max_read_bytes: number; + max_write_bytes: number; + enabled: boolean; + }; + user_ratelimit: { + max_read_ops: 1024; + max_write_ops: number; + max_read_bytes: number; + max_write_bytes: number; + enabled: boolean; + }; + anonymous_ratelimit: { + max_read_ops: number; + max_write_ops: number; + max_read_bytes: number; + max_write_bytes: number; + enabled: boolean; + }; +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-details/rgw-bucket-details.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-details/rgw-bucket-details.component.html index 1a422e5396f5c..1e02f6b357d65 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-details/rgw-bucket-details.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-details/rgw-bucket-details.component.html @@ -109,20 +109,26 @@ - - - Tags - - - - - - - -
{{tag.key}}{{ tag.value }}
-
+ + + Tags + + + + + + + +
{{tag.key}}{{ tag.value }}
+
+ + + + + diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-details/rgw-bucket-details.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-details/rgw-bucket-details.component.spec.ts index be6aa09182ca6..efef91826d116 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-details/rgw-bucket-details.component.spec.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-details/rgw-bucket-details.component.spec.ts @@ -41,4 +41,126 @@ describe('RgwBucketDetailsComponent', () => { component.ngOnChanges(); expect(rgwBucketServiceGetSpy).toHaveBeenCalled(); }); + it('should retrieve bucket details and set selection when selection is provided', () => { + const bucket = { bid: 'bucket', acl: '', owner: 'owner' }; + rgwBucketServiceGetSpy.and.returnValue(of(bucket)); + component.selection = { bid: 'bucket' }; + component.ngOnChanges(); + expect(rgwBucketServiceGetSpy).toHaveBeenCalledWith('bucket'); + expect(component.selection).toEqual(jasmine.objectContaining(bucket)); + }); + + it('should set default lifecycle when lifecycleFormat is json and lifecycle is not provided', () => { + const bucket = { bid: 'bucket', acl: '', owner: 'owner' }; + rgwBucketServiceGetSpy.and.returnValue(of(bucket)); + component.selection = { bid: 'bucket' }; + component.lifecycleFormat = 'json'; + component.ngOnChanges(); + expect(component.selection.lifecycle).toEqual({}); + }); + + it('should parse ACL and set aclPermissions', () => { + const bucket = { bid: 'bucket', acl: '', owner: 'owner' }; + rgwBucketServiceGetSpy.and.returnValue(of(bucket)); + spyOn(component, 'parseXmlAcl').and.returnValue({ Owner: ['READ'] }); + component.selection = { bid: 'bucket' }; + component.ngOnChanges(); + expect(component.aclPermissions).toEqual({ Owner: ['READ'] }); + }); + + it('should set replicationStatus when replication status is provided', () => { + const bucket = { + bid: 'bucket', + acl: '', + owner: 'owner', + replication: { Rule: { Status: 'Enabled' } } + }; + rgwBucketServiceGetSpy.and.returnValue(of(bucket)); + component.selection = { bid: 'bucket' }; + component.ngOnChanges(); + expect(component.replicationStatus).toBe('Disabled'); + }); + + it('should set bucketRateLimit when getBucketRateLimit is called', () => { + const rateLimit = { bucket_ratelimit: { max_size: 1000 } }; + spyOn(rgwBucketService, 'getBucketRateLimit').and.returnValue(of(rateLimit)); + component.selection = { bid: 'bucket' }; + component.ngOnChanges(); + expect(component.bucketRateLimit).toEqual(rateLimit.bucket_ratelimit); + }); + + it('should return default permissions when ACL is empty', () => { + const xml = ` + + + + + + + `; + const result = component.parseXmlAcl(xml, 'owner'); + expect(result).toEqual({ + Owner: ['-'], + AllUsers: ['-'], + AuthenticatedUsers: ['-'] + }); + }); + + it('should return owner permissions when ACL contains owner ID', () => { + const xml = ` + + + + + owner + + FULL_CONTROL + + + + `; + const result = component.parseXmlAcl(xml, 'owner'); + expect(result.Owner).toEqual('FULL_CONTROL'); + }); + + it('should return group permissions when ACL contains group URI', () => { + const xml = ` + + + + + http://acs.amazonaws.com/groups/global/AllUsers + + READ + + + + `; + const result = component.parseXmlAcl(xml, 'owner'); + expect(result.AllUsers).toEqual(['-']); + }); + + it('should handle multiple grants correctly', () => { + const xml = ` + + + + + http://acs.amazonaws.com/groups/global/AllUsers + + READ + + + + http://acs.amazonaws.com/groups/global/AuthenticatedUsers + + WRITE + + + + `; + const result = component.parseXmlAcl(xml, 'owner'); + expect(result.AllUsers).toEqual(['READ']); + expect(result.AuthenticatedUsers).toEqual(['WRITE']); + }); }); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-details/rgw-bucket-details.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-details/rgw-bucket-details.component.ts index 79e25808b93ea..c970a4eee12e7 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-details/rgw-bucket-details.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-details/rgw-bucket-details.component.ts @@ -3,6 +3,7 @@ import { Component, Input, OnChanges } from '@angular/core'; import { RgwBucketService } from '~/app/shared/api/rgw-bucket.service'; import * as xml2js from 'xml2js'; +import { RgwRateLimitConfig } from '../models/rgw-rate-limit'; @Component({ selector: 'cd-rgw-bucket-details', @@ -21,6 +22,7 @@ export class RgwBucketDetailsComponent implements OnChanges { lifecycleFormat: 'json' | 'xml' = 'json'; aclPermissions: Record = {}; replicationStatus = $localize`Disabled`; + bucketRateLimit: RgwRateLimitConfig; constructor(private rgwBucketService: RgwBucketService) {} @@ -46,6 +48,11 @@ export class RgwBucketDetailsComponent implements OnChanges { ); } }); + this.rgwBucketService.getBucketRateLimit(this.selection.bid).subscribe((resp: any) => { + if (resp && resp.bucket_ratelimit !== undefined) { + this.bucketRateLimit = resp.bucket_ratelimit; + } + }); } } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-form/rgw-bucket-form.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-form/rgw-bucket-form.component.html index 9c07182a0e597..df6150d028a6b 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-form/rgw-bucket-form.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-form/rgw-bucket-form.component.html @@ -90,14 +90,12 @@ This field is required. - - The bucket is owned by an account. UI does not support changing - the ownership of bucket owned by an account. + i18n>This field is + required. + The bucket is owned by an account. UI does not support changing the ownership of bucket owned by an account. @@ -116,7 +114,7 @@ name="versioning" formControlName="versioning" (change)="setMfaDeleteValidators()"> - @@ -188,11 +186,11 @@
- Object Locking + i18n>Object Locking - Store objects using a write-once-read-many (WORM) model to prevent objects from being deleted or overwritten for a fixed amount of time or indefinitely. - Object Locking works only in versioned buckets. + Store objects using a write-once-read-many (WORM) model to prevent objects from being deleted or + overwritten for a fixed amount of time or indefinitely. + Object Locking works only in versioned buckets. @@ -200,13 +198,13 @@
+ type="checkbox" /> Enables locking for the objects in the bucket. Locking can only be enabled while creating a bucket. @@ -224,7 +222,7 @@ name="lock_mode" id="lock_mode">
@@ -258,7 +254,8 @@ formControlName="lock_retention_period_days" min="1"> - The number of days that you want to specify for the default retention period that will be applied to new objects placed in this bucket. + The number of days that you want to specify for the default retention period that will be + applied to new objects placed in this bucket.
- - Bucket Versioning can't be disabled when Object Locking is enabled. + + Bucket Versioning can't be disabled when Object Locking is enabled. - - Enabling Object Locking will allow the configuration of GOVERNANCE or COMPLIANCE modes, which will help ensure that an object version cannot be overwritten or deleted for the specified period. + + Enabling Object Locking will allow the configuration of GOVERNANCE or COMPLIANCE modes, which will help + ensure that an object version cannot be overwritten or deleted for the specified period.
- -
- Encryption -
- -
- - - Enables encryption for the objects in the bucket. - To enable encryption on a bucket you need to set the configuration values for SSE-S3 or SSE-KMS. - To set the configuration values Click here - -
-
- -
-
-
-
+ +
+ Encryption +
+ +
+ + + Enables encryption for the objects in the bucket. + To enable encryption on a bucket you need to set the configuration values for SSE-S3 or SSE-KMS. + To set the configuration values + Click here + +
+
+
+
+
+
SSE-S3 +
-
@@ -404,7 +399,7 @@
@@ -444,8 +439,8 @@
Maximum of 20 tags reached + class="text-warning" + i18n>Maximum of 20 tags reached