Skip to content

Commit 15101a5

Browse files
authored
Merge pull request ceph#61225 from rhcs-dashboard/rgw-ratelimit-integration1
mgr/dashboard: Rgw ratelimit feature for user and bucket Reviewed-by: Aashish Sharma <[email protected]> Reviewed-by: Ankush Behl <[email protected]> Reviewed-by: Mark Nelson <[email protected]> Reviewed-by: Nizamudeen A <[email protected]> Reviewed-by: Naman Munet <[email protected]>
2 parents e11d6ce + 9e73041 commit 15101a5

34 files changed

+2253
-197
lines changed

src/pybind/mgr/dashboard/controllers/rgw.py

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
from ..services.auth import AuthManager, JwtManager
1616
from ..services.ceph_service import CephService
1717
from ..services.rgw_client import _SYNC_GROUP_ID, NoRgwDaemonsException, \
18-
RgwClient, RgwMultisite, RgwMultisiteAutomation
18+
RgwClient, RgwMultisite, RgwMultisiteAutomation, RgwRateLimit
1919
from ..services.rgw_iam import RgwAccounts
2020
from ..services.service import RgwServiceManager, wait_for_daemon_to_start
2121
from ..tools import json_str_to_object, str_to_bool
@@ -722,6 +722,31 @@ def set_lifecycle_policy(self, bucket_name: str = '', lifecycle: str = '', daemo
722722
def get_lifecycle_policy(self, bucket_name: str = '', daemon_name=None, owner=None):
723723
return self._get_lifecycle(bucket_name, daemon_name, owner)
724724

725+
@Endpoint(method='GET', path='/ratelimit')
726+
@EndpointDoc("Get the bucket global rate limit")
727+
@ReadPermission
728+
def get_global_rate_limit(self):
729+
rgwBucketRateLimit_instance = RgwRateLimit()
730+
return rgwBucketRateLimit_instance.get_global_rateLimit()
731+
732+
@Endpoint(method='GET', path='{uid}/ratelimit')
733+
@EndpointDoc("Get the bucket rate limit")
734+
@ReadPermission
735+
def get_rate_limit(self, uid: str):
736+
rgwBucketRateLimit_instance = RgwRateLimit()
737+
return rgwBucketRateLimit_instance.get_rateLimit('bucket', uid)
738+
739+
@Endpoint(method='PUT', path='{uid}/ratelimit')
740+
@UpdatePermission
741+
@allow_empty_body
742+
@EndpointDoc("Update the bucket rate limit")
743+
def set_rate_limit(self, enabled: bool, uid: str, max_read_ops: int,
744+
max_write_ops: int, max_read_bytes: int, max_write_bytes: int):
745+
rgwBucketRateLimit_instance = RgwRateLimit()
746+
return rgwBucketRateLimit_instance.set_rateLimit('bucket', enabled, uid,
747+
max_read_ops, max_write_ops,
748+
max_read_bytes, max_write_bytes)
749+
725750

726751
@UIRouter('/rgw/bucket', Scope.RGW)
727752
class RgwBucketUi(RgwBucket):
@@ -964,6 +989,31 @@ def delete_subuser(self, uid, subuser, purge_keys='true', daemon_name=None):
964989
'purge-keys': purge_keys
965990
}, json_response=False)
966991

992+
@Endpoint(method='GET', path='/ratelimit')
993+
@EndpointDoc("Get the user global rate limit")
994+
@ReadPermission
995+
def get_global_rate_limit(self):
996+
rgwUserRateLimit_instance = RgwRateLimit()
997+
return rgwUserRateLimit_instance.get_global_rateLimit()
998+
999+
@Endpoint(method='GET', path='{uid}/ratelimit')
1000+
@EndpointDoc("Get the user rate limit")
1001+
@ReadPermission
1002+
def get_rate_limit(self, uid: str):
1003+
rgwUserRateLimit_instance = RgwRateLimit()
1004+
return rgwUserRateLimit_instance.get_rateLimit('user', uid)
1005+
1006+
@Endpoint(method='PUT', path='{uid}/ratelimit')
1007+
@UpdatePermission
1008+
@allow_empty_body
1009+
@EndpointDoc("Update the user rate limit")
1010+
def set_rate_limit(self, uid: str, enabled: bool = False, max_read_ops: int = 0,
1011+
max_write_ops: int = 0, max_read_bytes: int = 0, max_write_bytes: int = 0):
1012+
rgwUserRateLimit_instance = RgwRateLimit()
1013+
return rgwUserRateLimit_instance.set_rateLimit('user', enabled,
1014+
uid, max_read_ops, max_write_ops,
1015+
max_read_bytes, max_write_bytes)
1016+
9671017

9681018
class RGWRoleEndpoints:
9691019
@staticmethod
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
export interface RgwRateLimitConfig {
2+
enabled: boolean;
3+
name?: string;
4+
max_read_ops: number;
5+
max_write_ops: number;
6+
max_read_bytes: number;
7+
max_write_bytes: number;
8+
}
9+
export interface GlobalRateLimitConfig {
10+
bucket_ratelimit: {
11+
max_read_ops: number;
12+
max_write_ops: number;
13+
max_read_bytes: number;
14+
max_write_bytes: number;
15+
enabled: boolean;
16+
};
17+
user_ratelimit: {
18+
max_read_ops: 1024;
19+
max_write_ops: number;
20+
max_read_bytes: number;
21+
max_write_bytes: number;
22+
enabled: boolean;
23+
};
24+
anonymous_ratelimit: {
25+
max_read_ops: number;
26+
max_write_ops: number;
27+
max_read_bytes: number;
28+
max_write_bytes: number;
29+
enabled: boolean;
30+
};
31+
}

src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-details/rgw-bucket-details.component.html

Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -109,20 +109,26 @@
109109
</tbody>
110110
</table>
111111

112-
<!-- Tags -->
113-
<ng-container *ngIf="(selection.tagset | keyvalue)?.length">
114-
<legend i18n>Tags</legend>
115-
<table class="cds--data-table--sort cds--data-table--no-border cds--data-table cds--data-table--md">
116-
<tbody>
117-
<tr *ngFor="let tag of selection.tagset | keyvalue">
118-
<td i18n
119-
class="bold w-25">{{tag.key}}</td>
120-
<td class="w-75">{{ tag.value }}</td>
121-
</tr>
122-
</tbody>
123-
</table>
124-
</ng-container>
112+
<!-- Tags -->
113+
<ng-container *ngIf="(selection.tagset | keyvalue)?.length">
114+
<legend i18n>Tags</legend>
115+
<table class="cds--data-table--sort cds--data-table--no-border cds--data-table cds--data-table--md">
116+
<tbody>
117+
<tr *ngFor="let tag of selection.tagset | keyvalue">
118+
<td i18n
119+
class="bold w-25">{{tag.key}}</td>
120+
<td class="w-75">{{ tag.value }}</td>
121+
</tr>
122+
</tbody>
123+
</table>
124+
</ng-container>
125+
125126

127+
<!-- Bucket Rate Limit -->
128+
<ng-container *ngIf="!!bucketRateLimit">
129+
<cd-rgw-rate-limit-details [rateLimitConfig]="bucketRateLimit"
130+
[type]="'bucket'"></cd-rgw-rate-limit-details>
131+
</ng-container>
126132
</ng-template>
127133
</ng-container>
128134

src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-details/rgw-bucket-details.component.spec.ts

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,4 +41,126 @@ describe('RgwBucketDetailsComponent', () => {
4141
component.ngOnChanges();
4242
expect(rgwBucketServiceGetSpy).toHaveBeenCalled();
4343
});
44+
it('should retrieve bucket details and set selection when selection is provided', () => {
45+
const bucket = { bid: 'bucket', acl: '<xml></xml>', owner: 'owner' };
46+
rgwBucketServiceGetSpy.and.returnValue(of(bucket));
47+
component.selection = { bid: 'bucket' };
48+
component.ngOnChanges();
49+
expect(rgwBucketServiceGetSpy).toHaveBeenCalledWith('bucket');
50+
expect(component.selection).toEqual(jasmine.objectContaining(bucket));
51+
});
52+
53+
it('should set default lifecycle when lifecycleFormat is json and lifecycle is not provided', () => {
54+
const bucket = { bid: 'bucket', acl: '<xml></xml>', owner: 'owner' };
55+
rgwBucketServiceGetSpy.and.returnValue(of(bucket));
56+
component.selection = { bid: 'bucket' };
57+
component.lifecycleFormat = 'json';
58+
component.ngOnChanges();
59+
expect(component.selection.lifecycle).toEqual({});
60+
});
61+
62+
it('should parse ACL and set aclPermissions', () => {
63+
const bucket = { bid: 'bucket', acl: '<xml></xml>', owner: 'owner' };
64+
rgwBucketServiceGetSpy.and.returnValue(of(bucket));
65+
spyOn(component, 'parseXmlAcl').and.returnValue({ Owner: ['READ'] });
66+
component.selection = { bid: 'bucket' };
67+
component.ngOnChanges();
68+
expect(component.aclPermissions).toEqual({ Owner: ['READ'] });
69+
});
70+
71+
it('should set replicationStatus when replication status is provided', () => {
72+
const bucket = {
73+
bid: 'bucket',
74+
acl: '<xml></xml>',
75+
owner: 'owner',
76+
replication: { Rule: { Status: 'Enabled' } }
77+
};
78+
rgwBucketServiceGetSpy.and.returnValue(of(bucket));
79+
component.selection = { bid: 'bucket' };
80+
component.ngOnChanges();
81+
expect(component.replicationStatus).toBe('Disabled');
82+
});
83+
84+
it('should set bucketRateLimit when getBucketRateLimit is called', () => {
85+
const rateLimit = { bucket_ratelimit: { max_size: 1000 } };
86+
spyOn(rgwBucketService, 'getBucketRateLimit').and.returnValue(of(rateLimit));
87+
component.selection = { bid: 'bucket' };
88+
component.ngOnChanges();
89+
expect(component.bucketRateLimit).toEqual(rateLimit.bucket_ratelimit);
90+
});
91+
92+
it('should return default permissions when ACL is empty', () => {
93+
const xml = `
94+
<AccessControlPolicy>
95+
<AccessControlList>
96+
<Grant>
97+
</Grant>
98+
</AccessControlList>
99+
</AccessControlPolicy>
100+
`;
101+
const result = component.parseXmlAcl(xml, 'owner');
102+
expect(result).toEqual({
103+
Owner: ['-'],
104+
AllUsers: ['-'],
105+
AuthenticatedUsers: ['-']
106+
});
107+
});
108+
109+
it('should return owner permissions when ACL contains owner ID', () => {
110+
const xml = `
111+
<AccessControlPolicy>
112+
<AccessControlList>
113+
<Grant>
114+
<Grantee>
115+
<ID>owner</ID>
116+
</Grantee>
117+
<Permission>FULL_CONTROL</Permission>
118+
</Grant>
119+
</AccessControlList>
120+
</AccessControlPolicy>
121+
`;
122+
const result = component.parseXmlAcl(xml, 'owner');
123+
expect(result.Owner).toEqual('FULL_CONTROL');
124+
});
125+
126+
it('should return group permissions when ACL contains group URI', () => {
127+
const xml = `
128+
<AccessControlPolicy>
129+
<AccessControlList>
130+
<Grant>
131+
<Grantee>
132+
<URI>http://acs.amazonaws.com/groups/global/AllUsers</URI>
133+
</Grantee>
134+
<Permission>READ</Permission>
135+
</Grant>
136+
</AccessControlList>
137+
</AccessControlPolicy>
138+
`;
139+
const result = component.parseXmlAcl(xml, 'owner');
140+
expect(result.AllUsers).toEqual(['-']);
141+
});
142+
143+
it('should handle multiple grants correctly', () => {
144+
const xml = `
145+
<AccessControlPolicy>
146+
<AccessControlList>
147+
<Grant>
148+
<Grantee>
149+
<URI>http://acs.amazonaws.com/groups/global/AllUsers</URI>
150+
</Grantee>
151+
<Permission>READ</Permission>
152+
</Grant>
153+
<Grant>
154+
<Grantee>
155+
<URI>http://acs.amazonaws.com/groups/global/AuthenticatedUsers</URI>
156+
</Grantee>
157+
<Permission>WRITE</Permission>
158+
</Grant>
159+
</AccessControlList>
160+
</AccessControlPolicy>
161+
`;
162+
const result = component.parseXmlAcl(xml, 'owner');
163+
expect(result.AllUsers).toEqual(['READ']);
164+
expect(result.AuthenticatedUsers).toEqual(['WRITE']);
165+
});
44166
});

src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-details/rgw-bucket-details.component.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { Component, Input, OnChanges } from '@angular/core';
33
import { RgwBucketService } from '~/app/shared/api/rgw-bucket.service';
44

55
import * as xml2js from 'xml2js';
6+
import { RgwRateLimitConfig } from '../models/rgw-rate-limit';
67

78
@Component({
89
selector: 'cd-rgw-bucket-details',
@@ -21,6 +22,7 @@ export class RgwBucketDetailsComponent implements OnChanges {
2122
lifecycleFormat: 'json' | 'xml' = 'json';
2223
aclPermissions: Record<string, string[]> = {};
2324
replicationStatus = $localize`Disabled`;
25+
bucketRateLimit: RgwRateLimitConfig;
2426

2527
constructor(private rgwBucketService: RgwBucketService) {}
2628

@@ -46,6 +48,11 @@ export class RgwBucketDetailsComponent implements OnChanges {
4648
);
4749
}
4850
});
51+
this.rgwBucketService.getBucketRateLimit(this.selection.bid).subscribe((resp: any) => {
52+
if (resp && resp.bucket_ratelimit !== undefined) {
53+
this.bucketRateLimit = resp.bucket_ratelimit;
54+
}
55+
});
4956
}
5057
}
5158

0 commit comments

Comments
 (0)