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
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):
'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
--- /dev/null
+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;
+ };
+}
</tbody>
</table>
- <!-- Tags -->
- <ng-container *ngIf="(selection.tagset | keyvalue)?.length">
- <legend i18n>Tags</legend>
- <table class="cds--data-table--sort cds--data-table--no-border cds--data-table cds--data-table--md">
- <tbody>
- <tr *ngFor="let tag of selection.tagset | keyvalue">
- <td i18n
- class="bold w-25">{{tag.key}}</td>
- <td class="w-75">{{ tag.value }}</td>
- </tr>
- </tbody>
- </table>
- </ng-container>
+ <!-- Tags -->
+ <ng-container *ngIf="(selection.tagset | keyvalue)?.length">
+ <legend i18n>Tags</legend>
+ <table class="cds--data-table--sort cds--data-table--no-border cds--data-table cds--data-table--md">
+ <tbody>
+ <tr *ngFor="let tag of selection.tagset | keyvalue">
+ <td i18n
+ class="bold w-25">{{tag.key}}</td>
+ <td class="w-75">{{ tag.value }}</td>
+ </tr>
+ </tbody>
+ </table>
+ </ng-container>
+
+ <!-- Bucket Rate Limit -->
+ <ng-container *ngIf="!!bucketRateLimit">
+ <cd-rgw-rate-limit-details [rateLimitConfig]="bucketRateLimit"
+ [type]="'bucket'"></cd-rgw-rate-limit-details>
+ </ng-container>
</ng-template>
</ng-container>
component.ngOnChanges();
expect(rgwBucketServiceGetSpy).toHaveBeenCalled();
});
+ it('should retrieve bucket details and set selection when selection is provided', () => {
+ const bucket = { bid: 'bucket', acl: '<xml></xml>', 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: '<xml></xml>', 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: '<xml></xml>', 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: '<xml></xml>',
+ 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 = `
+ <AccessControlPolicy>
+ <AccessControlList>
+ <Grant>
+ </Grant>
+ </AccessControlList>
+ </AccessControlPolicy>
+ `;
+ const result = component.parseXmlAcl(xml, 'owner');
+ expect(result).toEqual({
+ Owner: ['-'],
+ AllUsers: ['-'],
+ AuthenticatedUsers: ['-']
+ });
+ });
+
+ it('should return owner permissions when ACL contains owner ID', () => {
+ const xml = `
+ <AccessControlPolicy>
+ <AccessControlList>
+ <Grant>
+ <Grantee>
+ <ID>owner</ID>
+ </Grantee>
+ <Permission>FULL_CONTROL</Permission>
+ </Grant>
+ </AccessControlList>
+ </AccessControlPolicy>
+ `;
+ const result = component.parseXmlAcl(xml, 'owner');
+ expect(result.Owner).toEqual('FULL_CONTROL');
+ });
+
+ it('should return group permissions when ACL contains group URI', () => {
+ const xml = `
+ <AccessControlPolicy>
+ <AccessControlList>
+ <Grant>
+ <Grantee>
+ <URI>http://acs.amazonaws.com/groups/global/AllUsers</URI>
+ </Grantee>
+ <Permission>READ</Permission>
+ </Grant>
+ </AccessControlList>
+ </AccessControlPolicy>
+ `;
+ const result = component.parseXmlAcl(xml, 'owner');
+ expect(result.AllUsers).toEqual(['-']);
+ });
+
+ it('should handle multiple grants correctly', () => {
+ const xml = `
+ <AccessControlPolicy>
+ <AccessControlList>
+ <Grant>
+ <Grantee>
+ <URI>http://acs.amazonaws.com/groups/global/AllUsers</URI>
+ </Grantee>
+ <Permission>READ</Permission>
+ </Grant>
+ <Grant>
+ <Grantee>
+ <URI>http://acs.amazonaws.com/groups/global/AuthenticatedUsers</URI>
+ </Grantee>
+ <Permission>WRITE</Permission>
+ </Grant>
+ </AccessControlList>
+ </AccessControlPolicy>
+ `;
+ const result = component.parseXmlAcl(xml, 'owner');
+ expect(result.AllUsers).toEqual(['READ']);
+ expect(result.AuthenticatedUsers).toEqual(['WRITE']);
+ });
});
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',
lifecycleFormat: 'json' | 'xml' = 'json';
aclPermissions: Record<string, string[]> = {};
replicationStatus = $localize`Disabled`;
+ bucketRateLimit: RgwRateLimitConfig;
constructor(private rgwBucketService: RgwBucketService) {}
);
}
});
+ this.rgwBucketService.getBucketRateLimit(this.selection.bid).subscribe((resp: any) => {
+ if (resp && resp.bucket_ratelimit !== undefined) {
+ this.bucketRateLimit = resp.bucket_ratelimit;
+ }
+ });
}
}
</select>
<span class="invalid-feedback"
*ngIf="bucketForm.showError('owner', frm, 'required')"
- i18n>This field is required.</span>
- <cd-alert-panel
- type="info"
- *ngIf="bucketForm.get('owner').disabled"
- spacingClass="me-1 mt-1"
- i18n>
- 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.</span>
+ <cd-alert-panel type="info"
+ *ngIf="bucketForm.get('owner').disabled"
+ spacingClass="me-1 mt-1"
+ i18n>The bucket is owned by an account. UI does not support changing the ownership of bucket owned by an account.
</cd-alert-panel>
</div>
</div>
name="versioning"
formControlName="versioning"
(change)="setMfaDeleteValidators()">
- <label class="custom-control-label"
+ <label class="custom-control-label spacing-03"
for="versioning"
i18n>Enabled</label>
<cd-helper>
<!-- Object Locking -->
<fieldset *ngIf="!editing || (editing && bucketForm.getValue('lock_enabled'))">
<legend class="cd-header"
- i18n>
- Object Locking
+ i18n>Object Locking
<cd-help-text>
- 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.
</cd-help-text>
</legend>
<!-- Object Locking enable -->
<label class="cd-col-form-label pt-0"
for="lock_enabled"
i18n>
- Enable
+ Enable
</label>
<div class="cd-col-form-input">
<input class="form-check-input"
id="lock_enabled"
formControlName="lock_enabled"
- type="checkbox"/>
+ type="checkbox" />
<cd-help-text>
<span i18n>Enables locking for the objects in the bucket. Locking can only be enabled while creating a bucket.</span>
</cd-help-text>
name="lock_mode"
id="lock_mode">
<option i18n
- value="COMPLIANCE" >
+ value="COMPLIANCE">
Compliance
</option>
<option i18n
</select>
<cd-help-text>
<span *ngIf="bucketForm.getValue('lock_mode') === 'COMPLIANCE'"
- i18n>
- In COMPLIANCE an object version cannot be overwritten or deleted for the duration of the period.
+ i18n>In COMPLIANCE an object version cannot be overwritten or deleted for the duration of the period.
</span>
<span *ngIf="bucketForm.getValue('lock_mode') === 'GOVERNANCE'"
- i18n>
- In GOVERNANCE mode, users cannot overwrite or delete an object version or alter its lock settings unless they have special permissions.
+ i18n>In GOVERNANCE mode, users cannot overwrite or delete an object version or alter its lock settings unless they have special permissions.
</span>
</cd-help-text>
</div>
formControlName="lock_retention_period_days"
min="1">
<cd-help-text>
- <span i18n>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.</span>
+ <span i18n>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.</span>
</cd-help-text>
<span class="invalid-feedback"
*ngIf="bucketForm.showError('lock_retention_period_days', frm, 'pattern')"
<div class="form-group row">
<div class="cd-col-form-label"></div>
<div class="cd-col-form-input">
- <cd-alert-panel
- type="info"
- *ngIf="bucketForm.getValue('lock_enabled')"
- class="me-1"
- i18n-title>
- Bucket Versioning can't be disabled when Object Locking is enabled.
+ <cd-alert-panel type="info"
+ *ngIf="bucketForm.getValue('lock_enabled')"
+ class="me-1"
+ i18n-title>
+ Bucket Versioning can't be disabled when Object Locking is enabled.
</cd-alert-panel>
- <cd-alert-panel
- type="warning"
- *ngIf="bucketForm.getValue('lock_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.
+ <cd-alert-panel type="warning"
+ *ngIf="bucketForm.getValue('lock_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.
</cd-alert-panel>
</div>
</div>
</fieldset>
- <!-- Encryption -->
- <fieldset>
- <legend class="cd-header"
- i18n>Encryption</legend>
- <div class="form-group row">
- <label class="cd-col-form-label pt-0"
- for="encryption_enabled"
- i18n>
- Enable
- </label>
- <div class="cd-col-form-input">
- <input class="form-check-input"
- id="encryption_enabled"
- name="encryption_enabled"
- formControlName="encryption_enabled"
- type="checkbox"
- [attr.disabled]="!kmsConfigured && !s3Configured ? true : null"/>
- <cd-help-text aria-label="encryption helper">
- <span i18n>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 <a href="#/rgw/configuration"
- aria-label="click here">Click here</a></span>
- </cd-help-text>
- </div>
- </div>
-
- <div *ngIf="bucketForm.getValue('encryption_enabled')">
- <div class="form-group row">
- <div class="cd-col-form-offset">
- <div class="custom-control custom-radio custom-control-inline ps-5">
+ <!-- Encryption -->
+ <fieldset>
+ <legend class="cd-header"
+ i18n>Encryption</legend>
+ <div class="form-group row">
+ <label class="cd-col-form-label pt-0"
+ for="encryption_enabled"
+ i18n>Enable
+ </label>
+ <div class="cd-col-form-input">
+ <input class="form-check-input"
+ id="encryption_enabled"
+ name="encryption_enabled"
+ formControlName="encryption_enabled"
+ type="checkbox"
+ [attr.disabled]="!kmsConfigured && !s3Configured ? true : null" />
+ <cd-help-text aria-label="encryption helper">
+ <span i18n>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
+ <a href="#/rgw/configuration"
+ aria-label="click here">Click here</a></span>
+ </cd-help-text>
+ </div>
+ </div>
+ <div *ngIf="bucketForm.getValue('encryption_enabled')">
+ <div class="form-group row">
+ <div class="cd-col-form-offset">
+ <div class="custom-control custom-radio custom-control-inline ps-5">
<input class="form-check-input"
formControlName="encryption_type"
id="sse_S3_enabled"
[ngClass]="{'text-muted': !s3Configured}"
for="sse_S3_enabled"
i18n>SSE-S3</label>
+ </div>
</div>
</div>
- </div>
<div class="form-group row">
<div class="cd-col-form-offset">
<label class="cd-col-form-label pt-0"
for="replication"
i18n>
- Enable
+ Enable
</label>
<div class="cd-col-form-input"
*ngIf="{status: multisiteStatus$, isDefaultZg: isDefaultZoneGroup$ | async} as multisiteStatus; else loadingTpl">
<div class="row">
<div class="col-12">
<strong *ngIf="tags.length > 19"
- class="text-warning"
- i18n>Maximum of 20 tags reached</strong>
+ class="text-warning"
+ i18n>Maximum of 20 tags reached</strong>
<button type="button"
id="add-tag"
class="btn btn-light float-end my-3"
<!-- Lifecycle -->
<div *ngIf="editing"
class="form-group row">
- <label i18n
- class="cd-col-form-label"
- for="id">Lifecycle
- <cd-helper>JSON or XML formatted document</cd-helper>
- </label>
+ <label i18n
+ class="cd-col-form-label"
+ for="id">Lifecycle
+ <cd-helper>JSON or XML formatted document</cd-helper>
+ </label>
<div class="cd-col-form-input">
<textarea #lifecycleTextArea
class="form-control resize-vertical"
for="aclPermission"
i18n>Permissions
<cd-helper>Select the permision to give to the selected grantee.
- Regardless, the owner of the bucket will always have
- FULL CONTROL access</cd-helper>
- </span>
+ Regardless, the owner of the bucket will always have
+ FULL CONTROL access</cd-helper>
+ </span>
<select id="aclPermission"
name="aclPermission"
class="form-input form-select"
formControlName="aclPermission">
<option *ngFor="let permission of aclPermissions"
[value]="permission"
- i18n>{{ permission }}</option>
+ i18n>{{ permission }}
+ </option>
</select>
<span class="invalid-feedback"
*ngIf="bucketForm.showError('aclPermission', frm, 'required')"
</fieldset>
<!--Advanced-->
- <cd-form-advanced-fieldset *ngIf="!editing">
+ <cd-form-advanced-fieldset>
<!-- Placement target -->
- <div class="form-group row">
+ <div class="form-group row"
+ *ngIf="!editing">
<label class="cd-col-form-label"
for="placement-target"
i18n>Placement target</label>
*ngIf="placementTargets !== null"
[ngValue]="null">-- Select a placement target --</option>
<option *ngFor="let placementTarget of placementTargets"
- [value]="placementTarget.name">{{ placementTarget.description }}</option>
+ [value]="placementTarget.name">{{placementTarget.description }}</option>
</select>
<cd-help-text>
<span i18n>
</cd-help-text>
</div>
</div>
+ <!-- Bucket Rate Limit -->
+
+ <cd-rgw-rate-limit [type]="'bucket'"
+ [isEditing]="this.editing"
+ [allowBid]="this.bucketForm.getValue('bid')"
+ (rateLimitFormGroup)="rateLimitFormInit($event)"></cd-rgw-rate-limit>
</cd-form-advanced-fieldset>
</div>
<cd-loading-panel i18n>Checking multi-site status...</cd-loading-panel>
</div>
</ng-template>
+
import { RgwBucketMfaDelete } from '../models/rgw-bucket-mfa-delete';
import { RgwBucketVersioning } from '../models/rgw-bucket-versioning';
import { RgwBucketFormComponent } from './rgw-bucket-form.component';
+import { RgwRateLimitComponent } from '../rgw-rate-limit/rgw-rate-limit.component';
describe('RgwBucketFormComponent', () => {
let component: RgwBucketFormComponent;
let formHelper: FormHelper;
configureTestBed({
- declarations: [RgwBucketFormComponent],
+ declarations: [RgwBucketFormComponent, RgwRateLimitComponent],
imports: [
HttpClientTestingModule,
ReactiveFormsModule,
'mfa-delete': mfaDeleteChecked
});
fixture.detectChanges();
-
- const mfaTokenSerial = fixture.debugElement.nativeElement.querySelector('#mfa-token-serial');
- const mfaTokenPin = fixture.debugElement.nativeElement.querySelector('#mfa-token-pin');
- if (expectedVisibility) {
- expect(mfaTokenSerial).toBeTruthy();
- expect(mfaTokenPin).toBeTruthy();
- } else {
- expect(mfaTokenSerial).toBeFalsy();
- expect(mfaTokenPin).toBeFalsy();
- }
+ fixture.whenStable().then(() => {
+ const mfaTokenSerial = fixture.debugElement.nativeElement.querySelector(
+ '#mfa-token-serial'
+ );
+ const mfaTokenPin = fixture.debugElement.nativeElement.querySelector('#mfa-token-pin');
+ if (expectedVisibility) {
+ expect(mfaTokenSerial).toBeTruthy();
+ expect(mfaTokenPin).toBeTruthy();
+ } else {
+ expect(mfaTokenSerial).toBeFalsy();
+ expect(mfaTokenPin).toBeFalsy();
+ }
+ });
};
it('inputs should be visible when required', () => {
formHelper.expectValid('replication');
});
});
+
+ it('should call setTag', () => {
+ let tag = { key: 'test', value: 'test' };
+ // jest.spyOn(component.bucketForm,'markAsDirty')
+ component['setTag'](tag, 0);
+ expect(component.tags[0]).toEqual(tag);
+ expect(component.dirtyTags).toEqual(true);
+ });
+ it('should call deleteTag', () => {
+ component.tags = [{ key: 'test', value: 'test' }];
+ const updateValidationSpy = jest.spyOn(component.bucketForm, 'updateValueAndValidity');
+ component.deleteTag(0);
+ expect(updateValidationSpy).toHaveBeenCalled();
+ });
});
ViewChild,
ElementRef
} from '@angular/core';
-import { AbstractControl, Validators } from '@angular/forms';
+import { AbstractControl, FormGroup, Validators } from '@angular/forms';
import { ActivatedRoute, Router } from '@angular/router';
import _ from 'lodash';
import { RgwDaemonService } from '~/app/shared/api/rgw-daemon.service';
import { map, switchMap } from 'rxjs/operators';
import { TextAreaXmlFormatterService } from '~/app/shared/services/text-area-xml-formatter.service';
+import { RgwRateLimitComponent } from '../rgw-rate-limit/rgw-rate-limit.component';
+import { RgwRateLimitConfig } from '../models/rgw-rate-limit';
@Component({
selector: 'cd-rgw-bucket-form',
return this.bucketForm.getValue('mfa-delete');
}
+ @ViewChild(RgwRateLimitComponent, { static: false }) rateLimitComponent!: RgwRateLimitComponent;
+
constructor(
private route: ActivatedRoute,
private router: Router,
goToListView() {
this.router.navigate(['/rgw/bucket']);
}
-
+ rateLimitFormInit(rateLimitForm: FormGroup) {
+ this.bucketForm.addControl('rateLimit', rateLimitForm);
+ }
submit() {
// Exit immediately if the form isn't dirty.
- if (this.bucketForm.pristine) {
+ if (this.bucketForm.pristine && this.rateLimitComponent.form.pristine) {
this.goToListView();
return;
}
NotificationType.success,
$localize`Updated Object Gateway bucket '${values.bid}'.`
);
+ this.updateBucketRateLimit();
this.goToListView();
},
() => {
$localize`Created Object Gateway bucket '${values.bid}'`
);
this.goToListView();
+ this.updateBucketRateLimit();
},
() => {
// Reset the 'Submit' button.
}
}
+ updateBucketRateLimit() {
+ // Check if bucket ratelimit has been modified.
+ const rateLimitConfig: RgwRateLimitConfig = this.rateLimitComponent.getRateLimitFormValue();
+ if (!!rateLimitConfig) {
+ this.rgwBucketService
+ .updateBucketRateLimit(this.bucketForm.getValue('bid'), rateLimitConfig)
+ .subscribe(
+ () => {},
+ (error: any) => {
+ this.notificationService.show(NotificationType.error, error);
+ }
+ );
+ }
+ }
+
areMfaCredentialsRequired() {
return (
this.isMfaDeleteEnabled !== this.isMfaDeleteAlreadyEnabled ||
--- /dev/null
+<legend i18n>{{this.type === 'user'? 'User Rate Limit': 'Bucket Rate Limit'}}</legend>
+<table class="cds--data-table--sort cds--data-table--no-border cds--data-table cds--data-table--md">
+ <tbody>
+ <tr>
+ <td i18n
+ class="bold w-25">Enabled</td>
+ <td class="w-75">{{ rateLimitConfig.enabled | booleanText }}</td>
+ </tr>
+ <tr>
+ <td i18n
+ class="bold">Maximum Read Ops</td>
+ <td *ngIf="!rateLimitConfig.enabled">-</td>
+ <td *ngIf="rateLimitConfig.enabled && rateLimitConfig.max_read_ops <= 0"
+ i18n>Unlimited</td>
+ <td *ngIf="rateLimitConfig.enabled && rateLimitConfig.max_read_ops > 0">
+ {{ rateLimitConfig.max_read_ops}}
+ </td>
+ </tr>
+ <tr>
+ <td i18n
+ class="bold">Maximum Write Ops</td>
+ <td *ngIf="!rateLimitConfig.enabled">-</td>
+ <td *ngIf="rateLimitConfig.enabled && rateLimitConfig.max_write_ops <= 0"
+ i18n>Unlimited</td>
+ <td *ngIf="rateLimitConfig.enabled && rateLimitConfig.max_write_ops > 0">
+ {{ rateLimitConfig.max_write_ops }}
+ </td>
+ </tr>
+ <tr>
+ <td i18n
+ class="bold">Maximum Read Bytes</td>
+ <td *ngIf="!rateLimitConfig.enabled">-</td>
+ <td *ngIf="rateLimitConfig.enabled && rateLimitConfig.max_read_bytes <= 0"
+ i18n>Unlimited</td>
+ <td *ngIf="rateLimitConfig.enabled && rateLimitConfig.max_read_bytes > 0">
+ {{ rateLimitConfig.max_read_bytes | dimlessBinaryPerMinute}}
+ </td>
+ </tr>
+ <tr>
+ <td i18n
+ class="bold">Maximum Write Bytes</td>
+ <td *ngIf="!rateLimitConfig.enabled">-</td>
+ <td *ngIf="rateLimitConfig.enabled && rateLimitConfig.max_write_bytes <= 0"
+ i18n>Unlimited</td>
+ <td *ngIf="rateLimitConfig.enabled && rateLimitConfig.max_write_bytes > 0">
+ {{ rateLimitConfig.max_write_bytes | dimlessBinaryPerMinute}}
+ </td>
+ </tr>
+ </tbody>
+</table>
+
--- /dev/null
+import { SharedModule } from '~/app/shared/shared.module';
+import { RgwRateLimitDetailsComponent } from './rgw-rate-limit-details.component';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { configureTestBed } from '~/testing/unit-test-helper';
+
+describe('RgwRateLimitDetailsComponent', () => {
+ let component: RgwRateLimitDetailsComponent;
+ let fixture: ComponentFixture<RgwRateLimitDetailsComponent>;
+ configureTestBed({
+ declarations: [RgwRateLimitDetailsComponent],
+ imports: [HttpClientTestingModule, SharedModule]
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(RgwRateLimitDetailsComponent);
+ component = fixture.componentInstance;
+ component.type = 'user';
+ component.rateLimitConfig = {
+ enabled: true,
+ max_read_bytes: 987648,
+ max_read_ops: 0,
+ max_write_bytes: 0,
+ max_write_ops: 9876543212
+ };
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
--- /dev/null
+import { Component, Input } from '@angular/core';
+import { RgwRateLimitConfig } from '../models/rgw-rate-limit';
+
+@Component({
+ selector: 'cd-rgw-rate-limit-details',
+ templateUrl: './rgw-rate-limit-details.component.html',
+ styleUrls: ['./rgw-rate-limit-details.component.scss']
+})
+export class RgwRateLimitDetailsComponent {
+ @Input() rateLimitConfig: RgwRateLimitConfig;
+ @Input() type: string;
+}
--- /dev/null
+<fieldset *ngIf="type">
+ <legend i18n>
+ Rate Limit
+ <cd-help-text *ngIf="type === 'user'">
+ The User Rate Limit controls the max read/write operations and data per minute for each user.
+ </cd-help-text>
+ <cd-help-text *ngIf="type === 'bucket'">
+ The Bucket Rate Limit controls the max read/write operations and data per minute for each
+ bucket.
+ </cd-help-text>
+ </legend>
+ <form name="form"
+ #frm="ngForm"
+ [formGroup]="form"
+ novalidate>
+ <div class="row"
+ *ngIf="!!globalRateLimit && globalRateLimit.enabled">
+ <div class="col-3"></div>
+ <div class="col-9">
+ <div>
+ <cd-alert-panel type="info"
+ class="me-1"
+ id="global-ratelimit-info"
+ i18n>
+ <div>
+ <span class="bold">Global Rate Limit</span> <br />Max. read bytes :
+ {{ globalRateLimit.max_read_bytes | dimlessBinaryPerMinute }} <br />Max. read ops :
+ {{ globalRateLimit.max_read_ops }} <br />Max. write bytes :
+ {{ globalRateLimit.max_write_bytes | dimlessBinaryPerMinute }} <br />Max. write ops :
+ {{ globalRateLimit.max_write_ops }}
+ </div>
+ </cd-alert-panel>
+ </div>
+ </div>
+ </div>
+ <!-- Enabled -->
+ <div class="form-group row">
+ <div class="cd-col-form-offset">
+ <div class="custom-control custom-checkbox">
+ <input
+ class="custom-control-input"
+ id="rate_limit_enabled"
+ type="checkbox"
+ formControlName="rate_limit_enabled"
+ />
+ <label class="custom-control-label"
+ for="rate_limit_enabled"
+ i18n>Enabled</label>
+ <cd-help-text i18n>Toggle to enable or disable the rate limit settings.</cd-help-text>
+ </div>
+ </div>
+ </div>
+ <!-- Unlimited size -->
+ <div class="form-group row"
+ *ngIf="form.controls.rate_limit_enabled.value">
+ <div class="cd-col-form-offset">
+ <div class="custom-control custom-checkbox">
+ <input
+ class="custom-control-input"
+ id="rate_limit_max_readOps_unlimited"
+ type="checkbox"
+ formControlName="rate_limit_max_readOps_unlimited"
+ />
+ <label class="custom-control-label"
+ for="rate_limit_max_readOps_unlimited"
+ i18n>Unlimited read ops
+ </label>
+ <cd-help-text i18n>Select this box to allow unlimited read operations.</cd-help-text>
+ </div>
+ </div>
+ </div>
+
+ <!-- Maximum size -->
+ <div
+ class="form-group row"
+ *ngIf="
+ form.controls.rate_limit_enabled.value && !form.getValue('rate_limit_max_readOps_unlimited')
+ "
+ >
+ <label class="cd-col-form-label required"
+ for="rate_limit_max_readOps"
+ i18n>Max. read ops</label>
+ <div class="cd-col-form-input">
+ <input
+ id="rate_limit_max_readOps"
+ class="form-control"
+ type="number"
+ formControlName="rate_limit_max_readOps"
+ />
+ <cd-help-text>Limits the number of read operations per minute for a user.</cd-help-text>
+ <span
+ class="invalid-feedback"
+ *ngIf="form.showError('rate_limit_max_readOps', frm, 'required')"
+ i18n
+ >This field is required.</span
+ >
+ <span
+ class="invalid-feedback"
+ *ngIf="form.showError('rate_limit_max_readOps', frm, 'rateOpsMaxSize')"
+ i18n
+ >The value is not valid.</span
+ >
+ <span
+ class="invalid-feedback"
+ *ngIf="form.showError('rate_limit_max_readOps', frm, 'min')"
+ i18n
+ >Enter a positive number.</span
+ >
+ </div>
+ </div>
+ <!-- Unlimited Write Ops -->
+ <div class="form-group row"
+ *ngIf="form.controls.rate_limit_enabled.value">
+ <div class="cd-col-form-offset">
+ <div class="custom-control custom-checkbox">
+ <input
+ class="custom-control-input"
+ id="rate_limit_max_writeOps_unlimited"
+ type="checkbox"
+ formControlName="rate_limit_max_writeOps_unlimited"
+ />
+ <label class="custom-control-label"
+ for="rate_limit_max_writeOps_unlimited"
+ i18n>Unlimited write ops
+ </label>
+ <cd-help-text i18n>Select this box to allow unlimited write operations.</cd-help-text>
+ </div>
+ </div>
+ </div>
+ <!-- Maximum Write Ops -->
+ <div
+ class="form-group row"
+ *ngIf="
+ form.controls.rate_limit_enabled.value &&
+ !form.getValue('rate_limit_max_writeOps_unlimited')
+ "
+ >
+ <label class="cd-col-form-label required"
+ for="rate_limit_max_writeOps"
+ i18n>Max. write ops</label>
+ <div class="cd-col-form-input">
+ <input
+ id="rate_limit_max_writeOps"
+ class="form-control"
+ type="number"
+ formControlName="rate_limit_max_writeOps"
+ />
+ <cd-help-text>Limits the number of write operations per minute for a user.</cd-help-text>
+ <span
+ class="invalid-feedback"
+ *ngIf="form.showError('rate_limit_max_writeOps', frm, 'required')"
+ i18n
+ >This field is required.</span
+ >
+ <span
+ class="invalid-feedback"
+ *ngIf="form.showError('rate_limit_max_writeOps', frm, 'rateOpsMaxSize')"
+ i18n
+ >The value is not valid.</span
+ >
+ <span
+ class="invalid-feedback"
+ *ngIf="form.showError('rate_limit_max_writeOps', frm, 'min')"
+ i18n
+ >Enter a positive number.</span
+ >
+ </div>
+ </div>
+ <!-- Unlimited Read Bytes -->
+ <div class="form-group row"
+ *ngIf="form.controls.rate_limit_enabled.value">
+ <div class="cd-col-form-offset">
+ <div class="custom-control custom-checkbox">
+ <input
+ class="custom-control-input"
+ id="rate_limit_max_readBytes_unlimited"
+ type="checkbox"
+ formControlName="rate_limit_max_readBytes_unlimited"
+ />
+ <label class="custom-control-label"
+ for="rate_limit_max_readBytes_unlimited"
+ i18n>Unlimited read bytes
+ </label>
+ <cd-help-text i18n>Select this box to allow unlimited read bytes.</cd-help-text>
+ </div>
+ </div>
+ </div>
+ <!-- Maximum Read Bytes -->
+ <div
+ class="form-group row"
+ *ngIf="
+ form.controls.rate_limit_enabled.value &&
+ !form.getValue('rate_limit_max_readBytes_unlimited')
+ "
+ >
+ <label class="cd-col-form-label required"
+ for="rate_limit_max_readBytes"
+ i18n>Max. read bytes</label
+ >
+ <div class="cd-col-form-input">
+ <input
+ id="rate_limit_max_readBytes"
+ class="form-control"
+ type="text"
+ defaultUnit="b"
+ formControlName="rate_limit_max_readBytes"
+ cdDimlessBinaryPerMinute
+ />
+ <cd-help-text>Limits the number of read bytes per minute for a user.</cd-help-text>
+ <span
+ class="invalid-feedback"
+ *ngIf="form.showError('rate_limit_max_readBytes', frm, 'required')"
+ i18n>This field is required.</span>
+ <span
+ class="invalid-feedback"
+ *ngIf="form.showError('rate_limit_max_readBytes', frm, 'rateByteMaxSize')"
+ i18n>The value is not valid.</span>
+ <span
+ class="invalid-feedback"
+ *ngIf="form.showError('rate_limit_max_readBytes', frm, 'min')"
+ i18n>Enter a positive number.</span>
+ </div>
+ </div>
+ <!-- Unlimited Write Bytes -->
+ <div class="form-group row"
+ *ngIf="form.controls.rate_limit_enabled.value">
+ <div class="cd-col-form-offset">
+ <div class="custom-control custom-checkbox">
+ <input
+ class="custom-control-input"
+ id="rate_limit_max_writeBytes_unlimited"
+ type="checkbox"
+ formControlName="rate_limit_max_writeBytes_unlimited"
+ />
+ <label class="custom-control-label"
+ for="rate_limit_max_writeBytes_unlimited"
+ i18n>Unlimited write bytes
+ </label>
+ <cd-help-text i18n>Select this box to allow unlimited write bytes.</cd-help-text>
+ </div>
+ </div>
+ </div>
+ <!-- Maximum Write Bytes -->
+ <div
+ class="form-group row"
+ *ngIf="
+ form.controls.rate_limit_enabled.value &&
+ !form.getValue('rate_limit_max_writeBytes_unlimited')
+ ">
+ <label class="cd-col-form-label required"
+ for="rate_limit_max_writeBytes"
+ i18n>Max. write bytes</label>
+ <div class="cd-col-form-input">
+ <input
+ id="rate_limit_max_writeBytes"
+ class="form-control"
+ type="text"
+ defaultUnit="b"
+ formControlName="rate_limit_max_writeBytes"
+ cdDimlessBinaryPerMinute
+ />
+ <cd-help-text>Limits the number of write bytes per minute for a user.</cd-help-text>
+ <span
+ class="invalid-feedback"
+ *ngIf="form.showError('rate_limit_max_writeBytes', frm, 'required')"
+ i18n>This field is required.</span>
+ <span
+ class="invalid-feedback"
+ *ngIf="form.showError('rate_limit_max_writeBytes', frm, 'rateByteMaxSize')"
+ i18n>The value is not valid.</span>
+ <span
+ class="invalid-feedback"
+ *ngIf="form.showError('rate_limit_max_writeBytes', frm, 'min')"
+ i18n>Enter a positive number.</span>
+ </div>
+ </div>
+ </form>
+</fieldset>
--- /dev/null
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { RgwRateLimitComponent } from './rgw-rate-limit.component';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { AbstractControl, ReactiveFormsModule, ValidationErrors } from '@angular/forms';
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { FormatterService } from '~/app/shared/services/formatter.service';
+import { CheckboxModule, InputModule, NotificationService } from 'carbon-components-angular';
+import { ToastrModule } from 'ngx-toastr';
+import { RouterTestingModule } from '@angular/router/testing';
+import { SharedModule } from '~/app/shared/shared.module';
+
+describe('RgwRateLimitComponent', () => {
+ let component: RgwRateLimitComponent;
+ let fixture: ComponentFixture<RgwRateLimitComponent>;
+ configureTestBed({
+ imports: [
+ ReactiveFormsModule,
+ ToastrModule.forRoot(),
+ InputModule,
+ CheckboxModule,
+ HttpClientTestingModule,
+ ReactiveFormsModule,
+ RouterTestingModule,
+ SharedModule,
+ ToastrModule.forRoot(),
+ InputModule
+ ],
+ declarations: [RgwRateLimitComponent],
+ providers: [NotificationService]
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(RgwRateLimitComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+
+ it('should call populateFormValues', () => {
+ let data: any = {
+ enabled: true,
+ max_read_ops: 100,
+ max_write_ops: 200,
+ max_read_bytes: 10485760,
+ max_write_bytes: 10485760
+ };
+ const ratelimitPropSpy = spyOn(component as any, '_setRateLimitProperty');
+ component['populateFormValues'](data);
+ expect(ratelimitPropSpy).toHaveBeenCalled();
+ });
+
+ describe('test case for _getRateLimitArgs', () => {
+ it('should return expected result when all rate limits are specified', () => {
+ // Using patchValue to set form values
+ component.form.patchValue({
+ rate_limit_enabled: true,
+ rate_limit_max_readOps: 100,
+ rate_limit_max_writeOps: 200,
+ rate_limit_max_readBytes: '10Mb',
+ rate_limit_max_writeBytes: '10Mb',
+ rate_limit_max_readOps_unlimited: false,
+ rate_limit_max_writeOps_unlimited: false,
+ rate_limit_max_readBytes_unlimited: false,
+ rate_limit_max_writeBytes_unlimited: false
+ });
+ jest.spyOn(FormatterService.prototype, 'toBytes').mockReturnValue(10485760); // 10MB in bytes
+
+ const result = component['_getRateLimitArgs']();
+
+ expect(result).toEqual({
+ enabled: true,
+ max_read_ops: 100,
+ max_write_ops: 200,
+ max_read_bytes: 10485760, // Converted by FormatterService.toBytes
+ max_write_bytes: 10485760 // Converted by FormatterService.toBytes
+ });
+ });
+ it('should return default values for unlimited options', () => {
+ // Using patchValue to set unlimited flags
+ component.form.patchValue({
+ rate_limit_enabled: true,
+ rate_limit_max_readOps: 100,
+ rate_limit_max_writeOps: 200,
+ rate_limit_max_readBytes: '10MB',
+ rate_limit_max_writeBytes: '20MB',
+ rate_limit_max_readOps_unlimited: true, // Unlimited
+ rate_limit_max_writeOps_unlimited: true, // Unlimited
+ rate_limit_max_readBytes_unlimited: true, // Unlimited
+ rate_limit_max_writeBytes_unlimited: true // Unlimited
+ });
+
+ // Set return values for the spied methods (not used for unlimited cases)
+ jest.spyOn(FormatterService.prototype, 'toBytes').mockReturnValue(10485760); // 10MB in bytes
+
+ const result = component['_getRateLimitArgs']();
+
+ expect(result).toEqual({
+ enabled: true,
+ max_read_ops: 0, // Default value when unlimited
+ max_write_ops: 0, // Default value when unlimited
+ max_read_bytes: 0, // Default value when unlimited
+ max_write_bytes: 0 // Default value when unlimited
+ });
+ });
+ });
+
+ it('should call _setRateLimitProperty when value is equal to 0 ', () => {
+ const mockrateLimitKey = 'rate_limit_max_readBytes';
+ const mockunlimitedKey = 'rate_limit_max_readBytes_unlimited';
+ const mockproperty = 0;
+
+ component['_setRateLimitProperty'](mockrateLimitKey, mockunlimitedKey, mockproperty);
+ expect(component.form.getValue('rate_limit_max_readBytes_unlimited')).toEqual(true);
+ expect(component.form.getValue('rate_limit_max_readBytes')).toEqual('');
+ });
+ it('should call rateLimitIopmMaxSizeValidator and return result', () => {
+ const mockResult: ValidationErrors = { someError: true };
+ FormatterService.prototype.iopmMaxSizeValidator = jest.fn().mockReturnValue(mockResult);
+ const control: AbstractControl = { value: 'testValue' } as AbstractControl;
+ const result = component.rateLimitIopmMaxSizeValidator(control);
+ expect(FormatterService.prototype.iopmMaxSizeValidator).toHaveBeenCalledWith(control);
+ expect(result).toEqual(mockResult);
+ });
+ it('should call rateLimitIopmMaxSizeValidator and return null', () => {
+ FormatterService.prototype.iopmMaxSizeValidator = jest.fn().mockReturnValue(null);
+ const control: AbstractControl = { value: 'testValue' } as AbstractControl;
+ const result = component.rateLimitIopmMaxSizeValidator(control);
+ expect(FormatterService.prototype.iopmMaxSizeValidator).toHaveBeenCalledWith(control);
+ expect(result).toBeNull();
+ });
+ it('should call the rateLimitBytesMaxSizeValidator and FormatterService method with the correct parameters', () => {
+ const control: AbstractControl = {
+ value: '1000 K'
+ } as AbstractControl;
+
+ const serviceSpy = jest
+ .spyOn(FormatterService.prototype, 'performValidation')
+ .mockReturnValue(null);
+ const result = component.rateLimitBytesMaxSizeValidator(control);
+ expect(serviceSpy).toHaveBeenCalledWith(
+ control,
+ '^(\\d+(\\.\\d+)?)\\s*(B/m|K(B|iB/m)?|M(B|iB/m)?|G(B|iB/m)?|T(B|iB/m)?|P(B|iB/m)?)?$',
+ { rateByteMaxSize: true }
+ );
+ expect(result).toBeNull();
+ });
+});
--- /dev/null
+import { AfterViewInit, Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
+import { GlobalRateLimitConfig, RgwRateLimitConfig } from '../models/rgw-rate-limit';
+import { CdFormGroup } from '~/app/shared/forms/cd-form-group';
+import { CdFormBuilder } from '~/app/shared/forms/cd-form-builder';
+import { CdValidators } from '~/app/shared/forms/cd-validators';
+import { AbstractControl, FormGroup, ValidationErrors, Validators } from '@angular/forms';
+import { FormatterService } from '~/app/shared/services/formatter.service';
+import { RgwUserService } from '~/app/shared/api/rgw-user.service';
+import { RgwBucketService } from '~/app/shared/api/rgw-bucket.service';
+import _ from 'lodash';
+import { NotificationService } from '~/app/shared/services/notification.service';
+import { NotificationType } from '~/app/shared/enum/notification-type.enum';
+
+@Component({
+ selector: 'cd-rgw-rate-limit',
+ templateUrl: './rgw-rate-limit.component.html',
+ styleUrls: ['./rgw-rate-limit.component.scss']
+})
+export class RgwRateLimitComponent implements OnInit, AfterViewInit {
+ globalRateLimit: GlobalRateLimitConfig['user_ratelimit' | 'bucket_ratelimit'];
+ form: CdFormGroup;
+ @Input() type: string;
+
+ @Output() formValue = new EventEmitter();
+ @Output() rateLimitFormGroup = new EventEmitter<FormGroup>();
+
+ @Input()
+ isEditing: boolean;
+
+ @Input() id: string;
+
+ bid: string;
+ @Input() set allowBid(value: string) {
+ this.bid = value;
+ if (this.isEditing && !!this.bid && this.type == 'bucket') {
+ this.getRateLimitFormValues();
+ }
+ }
+ constructor(
+ private formBuilder: CdFormBuilder,
+ private rgwUserService: RgwUserService,
+ private rgwBucketService: RgwBucketService,
+ private notificationService: NotificationService
+ ) {}
+
+ ngOnInit(): void {
+ // get the global rate Limit
+ if (this.type === 'user') {
+ this.rgwUserService.getGlobalUserRateLimit().subscribe(
+ (data: GlobalRateLimitConfig) => {
+ if (data && data.user_ratelimit !== undefined) {
+ this.globalRateLimit = data.user_ratelimit;
+ }
+ },
+ (error: any) => {
+ this.notificationService.show(NotificationType.error, error);
+ }
+ );
+ this.isEditing ? this.getRateLimitFormValues() : '';
+ } else {
+ this.rgwBucketService.getGlobalBucketRateLimit().subscribe(
+ (data: GlobalRateLimitConfig) => {
+ if (data && data.bucket_ratelimit !== undefined) {
+ this.globalRateLimit = data.bucket_ratelimit;
+ }
+ },
+ (error: any) => {
+ this.notificationService.show(NotificationType.error, error);
+ }
+ );
+ }
+ // rate limit form
+ this.form = this.formBuilder.group({
+ rate_limit_enabled: [false],
+ rate_limit_max_readOps_unlimited: [true],
+ rate_limit_max_readOps: [
+ null,
+ [
+ CdValidators.composeIf(
+ {
+ rate_limit_enabled: true,
+ rate_limit_max_readOps_unlimited: false
+ },
+ [Validators.required, this.rateLimitIopmMaxSizeValidator]
+ )
+ ]
+ ],
+ rate_limit_max_writeOps_unlimited: [true],
+ rate_limit_max_writeOps: [
+ null,
+ [
+ CdValidators.composeIf(
+ {
+ rate_limit_enabled: true,
+ rate_limit_max_writeOps_unlimited: false
+ },
+ [Validators.required, this.rateLimitIopmMaxSizeValidator]
+ )
+ ]
+ ],
+ rate_limit_max_readBytes_unlimited: [true],
+ rate_limit_max_readBytes: [
+ null,
+ [
+ CdValidators.composeIf(
+ {
+ rate_limit_enabled: true,
+ rate_limit_max_readBytes_unlimited: false
+ },
+ [Validators.required, this.rateLimitBytesMaxSizeValidator]
+ )
+ ]
+ ],
+ rate_limit_max_writeBytes_unlimited: [true],
+ rate_limit_max_writeBytes: [
+ null,
+ [
+ CdValidators.composeIf(
+ {
+ rate_limit_enabled: true,
+ rate_limit_max_writeBytes_unlimited: false
+ },
+ [Validators.required, this.rateLimitBytesMaxSizeValidator]
+ )
+ ]
+ ]
+ });
+ }
+ /**
+ * Helper function to populate Form Values
+ * when edit rate limit edit is called.
+ */
+ private populateFormValues(data: RgwRateLimitConfig) {
+ this.form.get('rate_limit_enabled').setValue(data.enabled);
+ this._setRateLimitProperty(
+ 'rate_limit_max_readBytes',
+ 'rate_limit_max_readBytes_unlimited',
+ data.max_read_bytes
+ );
+ this._setRateLimitProperty(
+ 'rate_limit_max_writeBytes',
+ 'rate_limit_max_writeBytes_unlimited',
+ data.max_write_bytes
+ );
+ this._setRateLimitProperty(
+ 'rate_limit_max_readOps',
+ 'rate_limit_max_readOps_unlimited',
+ data.max_read_ops
+ );
+ this._setRateLimitProperty(
+ 'rate_limit_max_writeOps',
+ 'rate_limit_max_writeOps_unlimited',
+ data.max_write_ops
+ );
+ }
+ /**
+ * Helper function to call api and get Rate Limit Values
+ * on load for user and bucket
+ */
+ private getRateLimitFormValues() {
+ if (this.type === 'user') {
+ this.rgwUserService.getUserRateLimit(this.id).subscribe(
+ (resp: GlobalRateLimitConfig) => {
+ this.populateFormValues(resp.user_ratelimit);
+ },
+ (error: any) => {
+ this.notificationService.show(NotificationType.error, error);
+ }
+ );
+ } else {
+ this.rgwBucketService.getBucketRateLimit(this.bid).subscribe(
+ (resp: GlobalRateLimitConfig) => {
+ this.populateFormValues(resp.bucket_ratelimit);
+ },
+ (error: any) => {
+ this.notificationService.show(NotificationType.error, error);
+ }
+ );
+ }
+ }
+ /**
+ * Validate the rate limit bytes maximum size, e.g. 30, 1K, 30 PiB/m or 1.9 MiB/m.
+ */
+ rateLimitBytesMaxSizeValidator(control: AbstractControl): ValidationErrors | null {
+ return new FormatterService().performValidation(
+ control,
+ '^(\\d+(\\.\\d+)?)\\s*(B/m|K(B|iB/m)?|M(B|iB/m)?|G(B|iB/m)?|T(B|iB/m)?|P(B|iB/m)?)?$',
+ { rateByteMaxSize: true }
+ );
+ }
+ /**
+ * Validate the rate limit operations maximum size
+ */
+ rateLimitIopmMaxSizeValidator(control: AbstractControl): ValidationErrors | null {
+ return new FormatterService().iopmMaxSizeValidator(control);
+ }
+ getRateLimitFormValue() {
+ if (this._isRateLimitFormDirty()) return this._getRateLimitArgs();
+ return null;
+ }
+ ngAfterViewInit() {
+ this.rateLimitFormGroup.emit(this.form);
+ }
+ /**
+ * Check if the user rate limit has been modified.
+ * @return {Boolean} Returns TRUE if the user rate limit has been modified.
+ */
+ private _isRateLimitFormDirty(): boolean {
+ return [
+ 'rate_limit_enabled',
+ 'rate_limit_max_readOps_unlimited',
+ 'rate_limit_max_readOps',
+ 'rate_limit_max_writeOps_unlimited',
+ 'rate_limit_max_writeOps',
+ 'rate_limit_max_readBytes_unlimited',
+ 'rate_limit_max_readBytes',
+ 'rate_limit_max_writeBytes_unlimited',
+ 'rate_limit_max_writeBytes'
+ ].some((path) => {
+ return this.form.get(path).dirty;
+ });
+ }
+ /**
+ * Helper function to get the arguments for the API request when the user
+ * rate limit configuration has been modified.
+ */
+ private _getRateLimitArgs(): RgwRateLimitConfig {
+ const result: RgwRateLimitConfig = {
+ enabled: this.form.getValue('rate_limit_enabled'),
+ max_read_ops: 0,
+ max_write_ops: 0,
+ max_read_bytes: 0,
+ max_write_bytes: 0
+ };
+ if (!this.form.getValue('rate_limit_max_readOps_unlimited')) {
+ result['max_read_ops'] = this.form.getValue('rate_limit_max_readOps');
+ }
+ if (!this.form.getValue('rate_limit_max_writeOps_unlimited')) {
+ result['max_write_ops'] = this.form.getValue('rate_limit_max_writeOps');
+ }
+ if (!this.form.getValue('rate_limit_max_readBytes_unlimited')) {
+ // Convert the given value to bytes.
+ result['max_read_bytes'] = new FormatterService().toBytes(
+ this.form.getValue('rate_limit_max_readBytes')
+ );
+ }
+ if (!this.form.getValue('rate_limit_max_writeBytes_unlimited')) {
+ result['max_write_bytes'] = new FormatterService().toBytes(
+ this.form.getValue('rate_limit_max_writeBytes')
+ );
+ }
+ return result;
+ }
+
+ /**
+ * Helper function to map the values for the Rate Limit when the user
+ * rate limit gets loaded for first time or edited.
+ */
+
+ private _setRateLimitProperty(rateLimitKey: string, unlimitedKey: string, property: number) {
+ if (property === 0) {
+ this.form.get(unlimitedKey).setValue(true);
+ this.form.get(rateLimitKey).setValue('');
+ } else {
+ this.form.get(unlimitedKey).setValue(false);
+ this.form.get(rateLimitKey).setValue(property);
+ }
+ }
+}
<div *ngIf="user">
<div *ngIf="keys.length">
<legend i18n>Keys</legend>
- <cd-table [data]="keys"
- [columns]="keysColumns"
- columnMode="flex"
- selectionType="single"
- forceIdentifier="true"
- (updateSelection)="updateKeysSelection($event)">
- <cd-table-actions class="table-actions"
- [permission]="{read: true}"
- [selection]="selection"
- [tableActions]="tableAction"></cd-table-actions>
+ <cd-table
+ [data]="keys"
+ [columns]="keysColumns"
+ columnMode="flex"
+ selectionType="single"
+ forceIdentifier="true"
+ (updateSelection)="updateKeysSelection($event)"
+ >
+ <cd-table-actions
+ class="table-actions"
+ [permission]="{ read: true }"
+ [selection]="selection"
+ [tableActions]="tableAction"
+ ></cd-table-actions>
</cd-table>
</div>
<legend i18n>Details</legend>
- <table class="cds--data-table--sort cds--data-table--no-border cds--data-table cds--data-table--md">
+ <table
+ class="cds--data-table--sort cds--data-table--no-border cds--data-table cds--data-table--md"
+ >
<tbody>
<tr>
<td i18n
<tr>
<td i18n
class="bold">Maximum buckets</td>
- <td>{{ user.max_buckets | map:maxBucketsMap }}</td>
+ <td>{{ user.max_buckets | map: maxBucketsMap }}</td>
</tr>
<tr *ngIf="user.subusers && user.subusers.length">
<td i18n
<td i18n
class="bold">Capabilities</td>
<td>
- <div *ngFor="let cap of user.caps">
- {{ cap.type }} ({{ cap.perm }})
- </div>
+ <div *ngFor="let cap of user.caps">{{ cap.type }} ({{ cap.perm }})</div>
</td>
</tr>
<tr *ngIf="user.mfa_ids?.length">
<td i18n
class="bold">MFAs(Id)</td>
- <td>{{ user.mfa_ids | join}}</td>
+ <td>{{ user.mfa_ids | join }}</td>
</tr>
</tbody>
</table>
<!-- User quota -->
<div *ngIf="user.user_quota">
<legend i18n>User quota</legend>
- <table class="cds--data-table--sort cds--data-table--no-border cds--data-table cds--data-table--md">
+ <table
+ class="cds--data-table--sort cds--data-table--no-border cds--data-table cds--data-table--md"
+ >
<tbody>
<tr>
<td i18n
class="bold">Maximum size</td>
<td *ngIf="!user.user_quota.enabled">-</td>
<td *ngIf="user.user_quota.enabled && user.user_quota.max_size <= -1"
- i18n>Unlimited</td>
+ i18n>
+ Unlimited
+ </td>
<td *ngIf="user.user_quota.enabled && user.user_quota.max_size > -1">
{{ user.user_quota.max_size | dimlessBinary }}
</td>
class="bold">Maximum objects</td>
<td *ngIf="!user.user_quota.enabled">-</td>
<td *ngIf="user.user_quota.enabled && user.user_quota.max_objects <= -1"
- i18n>Unlimited</td>
+ i18n>
+ Unlimited
+ </td>
<td *ngIf="user.user_quota.enabled && user.user_quota.max_objects > -1">
{{ user.user_quota.max_objects }}
</td>
<!-- Bucket quota -->
<div *ngIf="user.bucket_quota">
<legend i18n>Bucket quota</legend>
- <table class="cds--data-table--sort cds--data-table--no-border cds--data-table cds--data-table--md">
+ <table
+ class="cds--data-table--sort cds--data-table--no-border cds--data-table cds--data-table--md"
+ >
<tbody>
<tr>
<td i18n
class="bold">Maximum size</td>
<td *ngIf="!user.bucket_quota.enabled">-</td>
<td *ngIf="user.bucket_quota.enabled && user.bucket_quota.max_size <= -1"
- i18n>Unlimited</td>
+ i18n>
+ Unlimited
+ </td>
<td *ngIf="user.bucket_quota.enabled && user.bucket_quota.max_size > -1">
{{ user.bucket_quota.max_size | dimlessBinary }}
</td>
class="bold">Maximum objects</td>
<td *ngIf="!user.bucket_quota.enabled">-</td>
<td *ngIf="user.bucket_quota.enabled && user.bucket_quota.max_objects <= -1"
- i18n>Unlimited</td>
+ i18n>
+ Unlimited
+ </td>
<td *ngIf="user.bucket_quota.enabled && user.bucket_quota.max_objects > -1">
{{ user.bucket_quota.max_objects }}
</td>
</tbody>
</table>
</div>
+ <!-- User Rate Limit -->
+ <div *ngIf="user.user_ratelimit">
+ <cd-rgw-rate-limit-details
+ [rateLimitConfig]="user.user_ratelimit"
+ [type]="'user'"
+ ></cd-rgw-rate-limit-details>
+ </div>
</div>
</ng-container>
import { SharedModule } from '~/app/shared/shared.module';
import { configureTestBed } from '~/testing/unit-test-helper';
import { RgwUserDetailsComponent } from './rgw-user-details.component';
+import { ModalService } from 'carbon-components-angular';
describe('RgwUserDetailsComponent', () => {
let component: RgwUserDetailsComponent;
let fixture: ComponentFixture<RgwUserDetailsComponent>;
-
+ let modalRef: any;
configureTestBed({
declarations: [RgwUserDetailsComponent],
- imports: [BrowserAnimationsModule, HttpClientTestingModule, SharedModule, NgbNavModule]
+ imports: [BrowserAnimationsModule, HttpClientTestingModule, SharedModule, NgbNavModule],
+ provider: [ModalService]
});
beforeEach(() => {
expect(detailsTab[14].textContent).toEqual('MFAs(Id)');
expect(detailsTab[15].textContent).toEqual('testMFA1, testMFA2');
});
+ it('should test updateKeysSelection', () => {
+ component.selection = {
+ hasMultiSelection: false,
+ hasSelection: false,
+ hasSingleSelection: false,
+ _selected: []
+ };
+ component.updateKeysSelection(component.selection);
+ expect(component.keysSelection).toEqual(component.selection);
+ });
+ it('should call showKeyModal when key selection is of type S3', () => {
+ component.keysSelection.first = () => {
+ return { type: 'S3', ref: { user: '', access_key: '', secret_key: '' } };
+ };
+ const modalShowSpy = spyOn(component['modalService'], 'show').and.callFake(() => {
+ modalRef = {
+ componentInstance: {
+ setValues: jest.fn(),
+ setViewing: jest.fn()
+ }
+ };
+ return modalRef;
+ });
+ component.showKeyModal();
+ expect(modalShowSpy).toHaveBeenCalled();
+ // expect(s).toHaveBeenCalledWith( modalRef.componentInstance.setViewing);
+ });
+ it('should call showKeyModal when key selection is of type Swift', () => {
+ component.keysSelection.first = () => {
+ return { type: 'Swift', ref: { user: '', access_key: '', secret_key: '' } };
+ };
+ const modalShowSpy = spyOn(component['modalService'], 'show').and.callFake(() => {
+ modalRef = {
+ componentInstance: {
+ setValues: jest.fn(),
+ setViewing: jest.fn()
+ }
+ };
+ return modalRef;
+ });
+ component.showKeyModal();
+ expect(modalShowSpy).toHaveBeenCalled();
+ // expect(s).toHaveBeenCalledWith( modalRef.componentInstance.setViewing);
+ });
});
import { RgwUserSwiftKeyModalComponent } from '../rgw-user-swift-key-modal/rgw-user-swift-key-modal.component';
import { CdTableAction } from '~/app/shared/models/cd-table-action';
import { Permissions } from '~/app/shared/models/permissions';
+import { RgwRateLimitConfig } from '../models/rgw-rate-limit';
@Component({
selector: 'cd-rgw-user-details',
_.extend(this.user, resp);
});
+ // Load the user rate limit of the selected user.
+ this.rgwUserService.getUserRateLimit(this.user.uid).subscribe((resp: RgwRateLimitConfig) => {
+ _.extend(this.user, resp);
+ });
+
// Process the keys.
this.keys = [];
if (this.user.keys) {
type="text"
formControlName="user_id"
[readonly]="editing">
- <span class="invalid-feedback"
- *ngIf="userForm.showError('user_id', frm, 'required')"
- i18n>This field is required.</span>
+ <span class="invalid-feedback"
+ *ngIf="userForm.showError('user_id', frm, 'required')"
+ i18n>This field is required.</span>
<span class="invalid-feedback"
*ngIf="userForm.showError('user_id', frm, 'pattern')"
i18n>The value is not valid.</span>
</div>
</div>
- <!-- Show Tenant -->
- <div class="form-group row">
- <div class="cd-col-form-offset">
- <div class="custom-control custom-checkbox">
- <input class="custom-control-input"
- id="show_tenant"
- type="checkbox"
- (click)="updateFieldsWhenTenanted()"
- formControlName="show_tenant"
- [readonly]="true">
- <label class="custom-control-label"
- for="show_tenant"
- i18n>Show Tenant</label>
- </div>
+ <!-- Show Tenant -->
+ <div class="form-group row">
+ <div class="cd-col-form-offset">
+ <div class="custom-control custom-checkbox">
+ <input class="custom-control-input"
+ id="show_tenant"
+ type="checkbox"
+ (click)="updateFieldsWhenTenanted()"
+ formControlName="show_tenant"
+ [readonly]="true">
+ <label class="custom-control-label spacing-03"
+ for="show_tenant"
+ i18n>Show Tenant</label>
</div>
</div>
+ </div>
<!-- Tenant -->
<div class="form-group row"
id="suspended"
type="checkbox"
formControlName="suspended">
- <label class="custom-control-label"
+ <label class="custom-control-label spacing-03"
for="suspended"
i18n>Suspended</label>
<cd-helper i18n>Suspending the user disables the user and subuser.</cd-helper>
id="system"
type="checkbox"
formControlName="system">
- <label class="custom-control-label"
+ <label class="custom-control-label spacing-03"
for="system"
i18n>System user</label>
- <cd-helper i18n>System users are distinct from regular users, they are used by the RGW service to perform administrative tasks, manage buckets and objects</cd-helper>
+ <cd-helper i18n>System users are distinct from regular users, they are used by the RGW service to perform
+ administrative tasks, manage buckets and objects</cd-helper>
</div>
</div>
</div>
id="generate_key"
type="checkbox"
formControlName="generate_key">
- <label class="custom-control-label"
+ <label class="custom-control-label spacing-03"
for="generate_key"
i18n>Auto-generate key</label>
</div>
</div>
<span class="invalid-feedback"
*ngIf="userForm.showError('secret_key', frm, 'required')"
- i18n>This field is required.</span>
+ i18n>This field
+ is required.</span>
</div>
</div>
</fieldset>
id="user_quota_enabled"
type="checkbox"
formControlName="user_quota_enabled">
- <label class="custom-control-label"
+ <label class="custom-control-label spacing-03"
for="user_quota_enabled"
i18n>Enabled</label>
</div>
id="user_quota_max_size_unlimited"
type="checkbox"
formControlName="user_quota_max_size_unlimited">
- <label class="custom-control-label"
+ <label class="custom-control-label spacing-03"
for="user_quota_max_size_unlimited"
i18n>Unlimited size</label>
</div>
id="user_quota_max_objects_unlimited"
type="checkbox"
formControlName="user_quota_max_objects_unlimited">
- <label class="custom-control-label"
+ <label class="custom-control-label spacing-03"
for="user_quota_max_objects_unlimited"
i18n>Unlimited objects</label>
</div>
id="bucket_quota_enabled"
type="checkbox"
formControlName="bucket_quota_enabled">
- <label class="custom-control-label"
+ <label class="custom-control-label spacing-03"
for="bucket_quota_enabled"
i18n>Enabled</label>
</div>
id="bucket_quota_max_size_unlimited"
type="checkbox"
formControlName="bucket_quota_max_size_unlimited">
- <label class="custom-control-label"
+ <label class="custom-control-label spacing-03"
for="bucket_quota_max_size_unlimited"
i18n>Unlimited size</label>
</div>
id="bucket_quota_max_objects_unlimited"
type="checkbox"
formControlName="bucket_quota_max_objects_unlimited">
- <label class="custom-control-label"
+ <label class="custom-control-label spacing-03"
for="bucket_quota_max_objects_unlimited"
i18n>Unlimited objects</label>
</div>
i18n>This field is required.</span>
<span class="invalid-feedback"
*ngIf="userForm.showError('bucket_quota_max_objects', frm, 'min')"
- i18n>The entered value must be >= 0.</span>
+ i18n>Enter a positive number.</span>
</div>
</div>
</fieldset>
+
+ <!-- Advanced Section -->
+ <cd-form-advanced-fieldset>
+ <!-- User Rate Limit -->
+ <cd-rgw-rate-limit [type]="'user'"
+ [isEditing]="this.editing"
+ [id]="uid"
+ (rateLimitFormGroup)="rateLimitFormInit($event)">
+ </cd-rgw-rate-limit>
+ </cd-form-advanced-fieldset>
</div>
<div class="card-footer">
import { RgwUserS3Key } from '../models/rgw-user-s3-key';
import { RgwUserFormComponent } from './rgw-user-form.component';
import { DUE_TIMER } from '~/app/shared/forms/cd-validators';
+import { FormatterService } from '~/app/shared/services/formatter.service';
+import { RgwRateLimitComponent } from '../rgw-rate-limit/rgw-rate-limit.component';
+import { By } from '@angular/platform-browser';
describe('RgwUserFormComponent', () => {
let component: RgwUserFormComponent;
let fixture: ComponentFixture<RgwUserFormComponent>;
let rgwUserService: RgwUserService;
let formHelper: FormHelper;
-
+ let modalRef: any;
+ let childComponent: any;
configureTestBed({
- declarations: [RgwUserFormComponent],
+ declarations: [RgwUserFormComponent, RgwRateLimitComponent],
imports: [
HttpClientTestingModule,
ReactiveFormsModule,
]
});
- beforeEach(() => {
+ beforeEach(async () => {
fixture = TestBed.createComponent(RgwUserFormComponent);
component = fixture.componentInstance;
- fixture.detectChanges();
rgwUserService = TestBed.inject(RgwUserService);
formHelper = new FormHelper(component.userForm);
+ await fixture.whenStable();
});
it('should create', () => {
});
describe('max buckets', () => {
+ beforeEach(() => {
+ fixture.detectChanges();
+ childComponent = fixture.debugElement.query(By.directive(RgwRateLimitComponent))
+ .componentInstance;
+ });
it('disable creation (create)', () => {
spyOn(rgwUserService, 'create');
formHelper.setValue('max_buckets_mode', -1, true);
+ let spyRateLimit = jest.spyOn(childComponent, 'getRateLimitFormValue');
component.onSubmit();
expect(rgwUserService.create).toHaveBeenCalledWith({
access_key: '',
system: false,
uid: null
});
+ expect(spyRateLimit).toHaveBeenCalled();
});
it('disable creation (edit)', () => {
spyOn(rgwUserService, 'update');
component.editing = true;
formHelper.setValue('max_buckets_mode', -1, true);
+ let spyRateLimit = jest.spyOn(childComponent, 'getRateLimitFormValue');
component.onSubmit();
expect(rgwUserService.update).toHaveBeenCalledWith(null, {
display_name: null,
suspended: false,
system: false
});
+ expect(spyRateLimit).toHaveBeenCalled();
});
it('unlimited buckets (create)', () => {
spyOn(rgwUserService, 'create');
formHelper.setValue('max_buckets_mode', 0, true);
+ let spyRateLimit = jest.spyOn(childComponent, 'getRateLimitFormValue');
component.onSubmit();
expect(rgwUserService.create).toHaveBeenCalledWith({
access_key: '',
system: false,
uid: null
});
+ expect(spyRateLimit).toHaveBeenCalled();
});
it('unlimited buckets (edit)', () => {
spyOn(rgwUserService, 'update');
component.editing = true;
formHelper.setValue('max_buckets_mode', 0, true);
+ let spyRateLimit = jest.spyOn(childComponent, 'getRateLimitFormValue');
component.onSubmit();
expect(rgwUserService.update).toHaveBeenCalledWith(null, {
display_name: null,
suspended: false,
system: false
});
+ expect(spyRateLimit).toHaveBeenCalled();
});
it('custom (create)', () => {
spyOn(rgwUserService, 'create');
formHelper.setValue('max_buckets_mode', 1, true);
formHelper.setValue('max_buckets', 100, true);
+ let spyRateLimit = jest.spyOn(childComponent, 'getRateLimitFormValue');
component.onSubmit();
expect(rgwUserService.create).toHaveBeenCalledWith({
access_key: '',
system: false,
uid: null
});
+ expect(spyRateLimit).toHaveBeenCalled();
});
it('custom (edit)', () => {
component.editing = true;
formHelper.setValue('max_buckets_mode', 1, true);
formHelper.setValue('max_buckets', 100, true);
+ let spyRateLimit = jest.spyOn(childComponent, 'getRateLimitFormValue');
component.onSubmit();
expect(rgwUserService.update).toHaveBeenCalledWith(null, {
display_name: null,
suspended: false,
system: false
});
+ expect(spyRateLimit).toHaveBeenCalled();
});
});
spyOn(TestBed.inject(Router), 'navigate').and.stub();
notificationService = TestBed.inject(NotificationService);
spyOn(notificationService, 'show');
+ fixture.detectChanges();
+ let childComponent = fixture.debugElement.query(By.directive(RgwRateLimitComponent))
+ .componentInstance;
+ jest.spyOn(childComponent, 'getRateLimitFormValue');
});
it('should be able to clear the mail field on update', () => {
expect(capabilityButton.disabled).toBeFalsy();
});
});
+ it('should not modify max_buckets if mode is not "1"', () => {
+ formHelper.setValue('max_buckets', '', false);
+ component.onMaxBucketsModeChange('2');
+ const patchVal = spyOn(component.userForm, 'patchValue');
+ expect(patchVal).not.toHaveBeenCalled();
+ });
+
+ describe('updateFieldsWhenTenanted()', () => {
+ it('should reset the form when showTenant is falsy', () => {
+ component.showTenant = false;
+
+ component.previousTenant = 'true';
+
+ component.userForm.get('tenant').setValue('test1');
+ component.userForm.get('user_id').setValue('user-123');
+
+ component.updateFieldsWhenTenanted();
+ expect(component.userForm.get('user_id').untouched).toBeTruthy(); // user_id should be untouched
+ expect(component.userForm.get('tenant').value).toBe('true'); // tenant should be reset to
+ });
+ });
+
+ it('should call deletecapab', () => {
+ component.capabilities = [{ type: 'users', perm: 'read' }];
+ component.deleteCapability(0);
+ expect(component.capabilities.length).toBe(0);
+ expect(component.userForm.dirty).toBeTruthy();
+ });
+ it('should call deleteS3Key', () => {
+ component.s3Keys = [
+ { user: 'test5$test11', access_key: 'A009', secret_key: 'ABCKEY', generate_key: true }
+ ];
+ component.deleteS3Key(0);
+ expect(component.userForm.dirty).toBeTruthy();
+ });
+
+ it('should call showCapabilityModal', () => {
+ const modalShowSpy = spyOn(component['modalService'], 'show').and.callFake(() => {
+ modalRef = {
+ componentInstance: {
+ setEditing: jest.fn(),
+ setValues: jest.fn(),
+ setCapabilities: jest.fn(),
+ submitAction: { subscribe: jest.fn() }
+ }
+ };
+ return modalRef;
+ });
+ component.capabilities = [{ type: 'users', perm: 'read' }];
+ component.showCapabilityModal(0);
+ expect(modalShowSpy).toHaveBeenCalled();
+ });
+ it('should call showSwiftKeyModal', () => {
+ const modalShowSpy = spyOn(component['modalService'], 'show').and.callFake(() => {
+ modalRef = {
+ componentInstance: {
+ setValues: jest.fn()
+ }
+ };
+ return modalRef;
+ });
+ component.swiftKeys = [
+ { user: 'user1', secret_key: 'secret1' },
+ { user: 'user2', secret_key: 'secret2' }
+ ];
+ component.showSwiftKeyModal(0);
+ expect(modalShowSpy).toHaveBeenCalled();
+ });
+ it('should call showS3KeyModal', () => {
+ const modalShowSpy = spyOn(component['modalService'], 'show').and.callFake(() => {
+ modalRef = {
+ componentInstance: {
+ setValues: jest.fn(),
+ setViewing: jest.fn(),
+ setUserCandidates: jest.fn(),
+ submitAction: { subscribe: jest.fn() }
+ }
+ };
+ return modalRef;
+ });
+ component.s3Keys = [
+ { user: 'test5$test11', access_key: 'A009', secret_key: 'ABCKEY', generate_key: true }
+ ];
+ component.showS3KeyModal(0);
+ expect(modalShowSpy).toHaveBeenCalled();
+ });
+
+ it('should call _getS3KeyUserCandidates', () => {
+ spyOn(component, 'getUID').and.returnValue('mockUID');
+ component.s3Keys = [
+ { user: 'mockUID', access_key: 'test', secret_key: 'TestKey', generate_key: true }
+ ];
+ const keycandidates = component['_getS3KeyUserCandidates']();
+ const result = ['mockUID'];
+ expect(keycandidates).toEqual(result);
+ });
+
+ describe('test case for _getBucketQuotaArgs', () => {
+ it('should return correct result when quota values are specified', () => {
+ // Using patchValue to set form values
+ component.userForm.patchValue({
+ bucket_quota_enabled: true,
+ bucket_quota_max_size: 2048, // 2MB
+ bucket_quota_max_objects: 10000,
+ bucket_quota_max_size_unlimited: false, // Not unlimited
+ bucket_quota_max_objects_unlimited: false // Not unlimited
+ });
+ const formatterServiceSpy = jest.spyOn(FormatterService.prototype, 'toBytes');
+ // Mock the toBytes function to return 2048 (bytes)
+ formatterServiceSpy.mockReturnValue(2048); // Convert 2MB to bytes (2048 KB)
+
+ const result = component['_getBucketQuotaArgs']();
+
+ expect(result).toEqual({
+ quota_type: 'bucket',
+ enabled: true,
+ max_size_kb: '2', // 2048 bytes = 2KB
+ max_objects: 10000
+ });
+ });
+ it('should return correct result when quota values are specified', () => {
+ // Using patchValue to set form values
+ component.userForm.patchValue({
+ bucket_quota_enabled: true,
+ bucket_quota_max_size: 2048, // 2MB
+ bucket_quota_max_objects: 10000,
+ bucket_quota_max_size_unlimited: false, // Not unlimited
+ bucket_quota_max_objects_unlimited: false // Not unlimited
+ });
+
+ const formatterServiceSpy = jest.spyOn(FormatterService.prototype, 'toBytes');
+ // Mock the toBytes function to return 2048 (bytes)
+ formatterServiceSpy.mockReturnValue(2048); // Convert 2MB to bytes (2048 KB)
+
+ const result = component['_getBucketQuotaArgs']();
+
+ expect(result).toEqual({
+ quota_type: 'bucket',
+ enabled: true,
+ max_size_kb: '2', // 2048 bytes = 2KB
+ max_objects: 10000
+ });
+ });
+ it('should return default values for unlimited size and objects', () => {
+ component.userForm.patchValue({
+ bucket_quota_enabled: true,
+ bucket_quota_max_size: 2048, // 2MB
+ bucket_quota_max_objects: 10000,
+ bucket_quota_max_size_unlimited: true, // Unlimited
+ bucket_quota_max_objects_unlimited: true // Unlimited
+ });
+
+ const formatterServiceSpy = jest.spyOn(FormatterService.prototype, 'toBytes');
+ formatterServiceSpy.mockReturnValue(2048); // Convert 2MB to bytes (2048 KB)
+
+ const result = component['_getBucketQuotaArgs']();
+
+ expect(result).toEqual({
+ quota_type: 'bucket',
+ enabled: true,
+ max_size_kb: -1, // Default value when unlimited
+ max_objects: -1 // Default value when unlimited
+ });
+ });
+ });
+ describe('test case for _getUserQuotaArgs', () => {
+ it('should return quota info with default values when no quota is set', () => {
+ // Use patchValue to set form values
+ component.userForm.patchValue({
+ user_quota_enabled: true,
+ user_quota_max_size_unlimited: true,
+ user_quota_max_objects_unlimited: true,
+ user_quota_max_size: null,
+ user_quota_max_objects: null
+ });
+
+ // Call the method
+ const result = component._getUserQuotaArgs();
+
+ // Assertions
+ expect(result).toEqual({
+ quota_type: 'user',
+ enabled: true,
+ max_size_kb: -1,
+ max_objects: -1
+ });
+ });
+
+ it('should calculate max_size_kb when quota size is specified and not unlimited', () => {
+ // Use patchValue to set form values
+ component.userForm.patchValue({
+ user_quota_enabled: true,
+ user_quota_max_size_unlimited: false,
+ user_quota_max_objects_unlimited: true,
+ user_quota_max_size: 2048, // Example quota size in KB
+ user_quota_max_objects: null
+ });
+
+ const toBytesSpy = jest
+ .spyOn(FormatterService.prototype, 'toBytes')
+ .mockReturnValue(2048 * 1024);
+
+ const result = component._getUserQuotaArgs();
+ expect(toBytesSpy).toHaveBeenCalledWith(2048);
+ expect(result).toEqual({
+ quota_type: 'user',
+ enabled: true,
+ max_size_kb: '2048', // Expect the converted KB value
+ max_objects: -1
+ });
+ });
+
+ it('should set max_objects when quota is specified and not unlimited', () => {
+ // Use patchValue to set form values
+ component.userForm.patchValue({
+ user_quota_enabled: true,
+ user_quota_max_size_unlimited: true,
+ user_quota_max_objects_unlimited: false,
+ user_quota_max_size: null,
+ user_quota_max_objects: 1000 // Example quota size
+ });
+
+ const result = component._getUserQuotaArgs();
+
+ expect(result).toEqual({
+ quota_type: 'user',
+ enabled: true,
+ max_size_kb: -1,
+ max_objects: 1000
+ });
+ });
+ });
+
+ it('should call showSubuserModal', () => {
+ const modalShowSpy = spyOn(component['modalService'], 'show').and.callFake(() => {
+ modalRef = {
+ componentInstance: {
+ setValues: jest.fn(),
+ setViewing: jest.fn(),
+ setEditing: jest.fn(),
+ setUserCandidates: jest.fn(),
+ submitAction: { subscribe: jest.fn() }
+ }
+ };
+ return modalRef;
+ });
+ component.subusers = [
+ { id: 'test', permissions: 'true', generate_secret: true, secret_key: '' }
+ ];
+ // component.s3Keys= [{user: 'test5$test11', access_key: 'A009', secret_key: 'ABCKEY', generate_key: true}];
+ component.showSubuserModal(0);
+ expect(modalShowSpy).toHaveBeenCalled();
+ });
+ describe('test case for showSubuserModal', () => {
+ it('should handle "Edit" scenario when index is provided', () => {
+ let index = 0;
+ component.subusers = [
+ { id: 'test', permissions: 'true', generate_secret: true, secret_key: '' }
+ ];
+ let spy = spyOn(component['modalService'], 'show').and.callFake(() => {
+ return (modalRef = {
+ componentInstance: {
+ setEditing: jest.fn(),
+ setValues: jest.fn(),
+ setCapabilities: jest.fn(),
+ setSubusers: jest.fn(),
+ setUserCandidates: jest.fn(),
+ submitAction: { subscribe: jest.fn() }
+ }
+ });
+ });
+ spyOn(component, 'getUID').and.returnValue('dashboard');
+ component.showSubuserModal(index);
+ expect(spy).toHaveBeenCalledTimes(1);
+ expect(modalRef.componentInstance.setEditing).toHaveBeenCalledTimes(1);
+ expect(modalRef.componentInstance.setValues).toHaveBeenCalledWith(
+ 'dashboard',
+ component.subusers[index].id,
+ component.subusers[index].permissions
+ );
+ expect(modalRef.componentInstance.submitAction.subscribe).toHaveBeenCalled();
+ });
+
+ it('should handle "Add" scenario when index is not provided', () => {
+ let spy = spyOn(component['modalService'], 'show').and.callFake(() => {
+ return (modalRef = {
+ componentInstance: {
+ setEditing: jest.fn(),
+ setValues: jest.fn(),
+ setCapabilities: jest.fn(),
+ setSubusers: jest.fn(),
+ setUserCandidates: jest.fn(),
+ submitAction: { subscribe: jest.fn() }
+ }
+ });
+ });
+ component.subusers = [
+ { id: 'test', permissions: 'true', generate_secret: true, secret_key: '' }
+ ];
+ spyOn(component, 'getUID').and.returnValue('dashboard');
+ component.showSubuserModal();
+ expect(spy).toHaveBeenCalledTimes(1);
+ expect(modalRef.componentInstance.setEditing).toHaveBeenCalledWith(false);
+ expect(modalRef.componentInstance.setValues).toHaveBeenCalledWith('dashboard');
+ expect(modalRef.componentInstance.setSubusers).toHaveBeenCalledWith(component.subusers);
+ expect(modalRef.componentInstance.submitAction.subscribe).toHaveBeenCalled();
+ });
+ });
});
-import { Component, OnInit } from '@angular/core';
-import { AbstractControl, ValidationErrors, Validators } from '@angular/forms';
+import { Component, OnInit, ViewChild } from '@angular/core';
+import { AbstractControl, FormGroup, ValidationErrors, Validators } from '@angular/forms';
import { ActivatedRoute, Router } from '@angular/router';
import _ from 'lodash';
import { CdForm } from '~/app/shared/forms/cd-form';
import { CdFormBuilder } from '~/app/shared/forms/cd-form-builder';
import { CdFormGroup } from '~/app/shared/forms/cd-form-group';
-import { CdValidators, isEmptyInputValue } from '~/app/shared/forms/cd-validators';
+import { CdValidators } from '~/app/shared/forms/cd-validators';
import { FormatterService } from '~/app/shared/services/formatter.service';
import { ModalService } from '~/app/shared/services/modal.service';
import { NotificationService } from '~/app/shared/services/notification.service';
import { RgwUserS3KeyModalComponent } from '../rgw-user-s3-key-modal/rgw-user-s3-key-modal.component';
import { RgwUserSubuserModalComponent } from '../rgw-user-subuser-modal/rgw-user-subuser-modal.component';
import { RgwUserSwiftKeyModalComponent } from '../rgw-user-swift-key-modal/rgw-user-swift-key-modal.component';
+import { RgwRateLimitComponent } from '../rgw-rate-limit/rgw-rate-limit.component';
+import { RgwRateLimitConfig } from '../models/rgw-rate-limit';
@Component({
selector: 'cd-rgw-user-form',
s3Keys: RgwUserS3Key[] = [];
swiftKeys: RgwUserSwiftKey[] = [];
capabilities: RgwUserCapability[] = [];
-
+ uid: string;
action: string;
resource: string;
subuserLabel: string;
usernameExists: boolean;
showTenant = false;
previousTenant: string = null;
+ @ViewChild(RgwRateLimitComponent, { static: false }) rateLimitComponent!: RgwRateLimitComponent;
constructor(
private formBuilder: CdFormBuilder,
const observables = [];
observables.push(this.rgwUserService.get(uid));
observables.push(this.rgwUserService.getQuota(uid));
+ observables.push(this.rgwUserService.getUserRateLimit(uid));
observableForkJoin(observables).subscribe(
(resp: any[]) => {
// Get the default values.
value[type + '_quota_max_objects'] = quota.max_objects;
}
});
+
// Merge with default values.
value = _.merge(defaults, value);
// Update the form.
}
});
this.capabilities = resp[0].caps;
-
+ this.uid = this.getUID();
this.loadingReady();
},
() => {
});
}
+ rateLimitFormInit(rateLimitForm: FormGroup) {
+ this.userForm.addControl('rateLimit', rateLimitForm);
+ }
+
goToListView() {
this.router.navigate(['/rgw/user']);
}
onSubmit() {
+ this.uid = this.getUID();
let notificationTitle: string;
// Exit immediately if the form isn't dirty.
- if (this.userForm.pristine) {
+ if (this.userForm.pristine && this.rateLimitComponent.form.pristine) {
this.goToListView();
return;
}
- const uid = this.getUID();
if (this.editing) {
// Edit
if (this._isGeneralDirty()) {
const args = this._getUpdateArgs();
- this.submitObservables.push(this.rgwUserService.update(uid, args));
+ this.submitObservables.push(this.rgwUserService.update(this.uid, args));
}
- notificationTitle = $localize`Updated Object Gateway user '${uid}'`;
+ notificationTitle = $localize`Updated Object Gateway user '${this.uid}'`;
} else {
// Add
const args = this._getCreateArgs();
this.submitObservables.push(this.rgwUserService.create(args));
- notificationTitle = $localize`Created Object Gateway user '${uid}'`;
+ notificationTitle = $localize`Created Object Gateway user '${this.uid}'`;
}
// Check if user quota has been modified.
if (this._isUserQuotaDirty()) {
const userQuotaArgs = this._getUserQuotaArgs();
- this.submitObservables.push(this.rgwUserService.updateQuota(uid, userQuotaArgs));
+ this.submitObservables.push(this.rgwUserService.updateQuota(this.uid, userQuotaArgs));
}
// Check if bucket quota has been modified.
if (this._isBucketQuotaDirty()) {
const bucketQuotaArgs = this._getBucketQuotaArgs();
- this.submitObservables.push(this.rgwUserService.updateQuota(uid, bucketQuotaArgs));
+ this.submitObservables.push(this.rgwUserService.updateQuota(this.uid, bucketQuotaArgs));
+ }
+
+ // Check if user ratelimit has been modified.
+ const ratelimitvalue: RgwRateLimitConfig = this.rateLimitComponent.getRateLimitFormValue();
+ if (!!ratelimitvalue) {
+ this.submitObservables.push(
+ this.rgwUserService.updateUserRateLimit(this.userForm.getValue('user_id'), ratelimitvalue)
+ );
}
// Finally execute all observables one by one in serial.
observableConcat(...this.submitObservables).subscribe({
* Validate the quota maximum size, e.g. 1096, 1K, 30M or 1.9MiB.
*/
quotaMaxSizeValidator(control: AbstractControl): ValidationErrors | null {
- if (isEmptyInputValue(control.value)) {
- return null;
- }
- const m = RegExp('^(\\d+(\\.\\d+)?)\\s*(B|K(B|iB)?|M(B|iB)?|G(B|iB)?|T(B|iB)?)?$', 'i').exec(
- control.value
+ return new FormatterService().performValidation(
+ control,
+ '^(\\d+(\\.\\d+)?)\\s*(B|K(B|iB)?|M(B|iB)?|G(B|iB)?|T(B|iB)?)?$',
+ { quotaMaxSize: true },
+ 'quota'
);
- if (m === null) {
- return { quotaMaxSize: true };
- }
- const bytes = new FormatterService().toBytes(control.value);
- return bytes < 1024 ? { quotaMaxSize: true } : null;
}
/**
* Helper function to get the arguments for the API request when the user
* quota configuration has been modified.
*/
- private _getUserQuotaArgs(): Record<string, any> {
+ _getUserQuotaArgs(): Record<string, any> {
const result = {
quota_type: 'user',
enabled: this.userForm.getValue('user_quota_enabled'),
import { RgwStorageClassFormComponent } from './rgw-storage-class-form/rgw-storage-class-form.component';
import { RgwBucketTieringFormComponent } from './rgw-bucket-tiering-form/rgw-bucket-tiering-form.component';
import { RgwBucketLifecycleListComponent } from './rgw-bucket-lifecycle-list/rgw-bucket-lifecycle-list.component';
+import { RgwRateLimitComponent } from './rgw-rate-limit/rgw-rate-limit.component';
+import { RgwRateLimitDetailsComponent } from './rgw-rate-limit-details/rgw-rate-limit-details.component';
@NgModule({
imports: [
RgwStorageClassListComponent
],
declarations: [
+ RgwRateLimitComponent,
RgwDaemonListComponent,
RgwDaemonDetailsComponent,
RgwBucketFormComponent,
RgwStorageClassDetailsComponent,
RgwStorageClassFormComponent,
RgwBucketTieringFormComponent,
- RgwBucketLifecycleListComponent
+ RgwBucketLifecycleListComponent,
+ RgwRateLimitDetailsComponent
],
providers: [TitleCasePipe]
})
import { BehaviorSubject, of as observableOf } from 'rxjs';
import { catchError, map, mapTo } from 'rxjs/operators';
import { Bucket } from '~/app/ceph/rgw/models/rgw-bucket';
+import { RgwRateLimitConfig } from '~/app/ceph/rgw/models/rgw-rate-limit';
import { ApiClient } from '~/app/shared/api/api-client';
import { RgwDaemonService } from '~/app/shared/api/rgw-daemon.service';
return this.http.get(`${this.url}/lifecycle`, { params: params });
});
}
+ updateBucketRateLimit(bid: string, bucketRateLimitArgs: RgwRateLimitConfig) {
+ return this.http.put(`${this.url}/${bid}/ratelimit`, bucketRateLimitArgs);
+ }
+ getBucketRateLimit(uid: string) {
+ return this.http.get(`${this.url}/${uid}/ratelimit`);
+ }
+ getGlobalBucketRateLimit() {
+ return this.http.get(`${this.url}/ratelimit`);
+ }
}
import _ from 'lodash';
import { forkJoin as observableForkJoin, Observable, of as observableOf } from 'rxjs';
import { catchError, mapTo, mergeMap } from 'rxjs/operators';
+import { RgwRateLimitConfig } from '~/app/ceph/rgw/models/rgw-rate-limit';
import { RgwDaemonService } from '~/app/shared/api/rgw-daemon.service';
import { cdEncode } from '~/app/shared/decorators/cd-encode';
})
);
}
+
+ updateUserRateLimit(uid: string, rateLimitArgs: RgwRateLimitConfig) {
+ return this.http.put(`${this.url}/${uid}/ratelimit`, rateLimitArgs);
+ }
+
+ getUserRateLimit(uid: string) {
+ return this.http.get(`${this.url}/${uid}/ratelimit`);
+ }
+ getGlobalUserRateLimit() {
+ return this.http.get(`${this.url}/ratelimit`);
+ }
}
--- /dev/null
+import { DimlessBinaryPerMinuteDirective } from './dimless-binary-per-minute.directive';
+
+export class MockElementRef {
+ nativeElement: {};
+}
+
+describe('DimlessBinaryPerMinuteDirective', () => {
+ it('should create an instance', () => {
+ const directive = new DimlessBinaryPerMinuteDirective(new MockElementRef(), null, null, null);
+ expect(directive).toBeTruthy();
+ });
+});
--- /dev/null
+import {
+ Directive,
+ ElementRef,
+ EventEmitter,
+ HostListener,
+ Input,
+ OnInit,
+ Output
+} from '@angular/core';
+import { NgControl } from '@angular/forms';
+
+import _ from 'lodash';
+import { FormatterService } from '../services/formatter.service';
+import { DimlessBinaryPerMinutePipe } from '../pipes/dimless-binary-per-minute.pipe';
+
+@Directive({
+ selector: '[cdDimlessBinaryPerMinute]'
+})
+export class DimlessBinaryPerMinuteDirective implements OnInit {
+ @Output()
+ ngModelChange: EventEmitter<any> = new EventEmitter();
+
+ /**
+ * Event emitter for letting this directive know that the data has (asynchronously) been loaded
+ * and the value needs to be adapted by this directive.
+ */
+ @Input()
+ ngDataReady: EventEmitter<any>;
+
+ /**
+ * Minimum size in bytes.
+ * If user enter a value lower than <minBytes>,
+ * the model will automatically be update to <minBytes>.
+ *
+ * If <roundPower> is used, this value should be a power of <roundPower>.
+ *
+ * Example:
+ * Given minBytes=4096 (4KiB), if user type 1KiB, then model will be updated to 4KiB
+ */
+ @Input()
+ minBytes: number;
+
+ /**
+ * Maximum size in bytes.
+ * If user enter a value greater than <maxBytes>,
+ * the model will automatically be update to <maxBytes>.
+ *
+ * If <roundPower> is used, this value should be a power of <roundPower>.
+ *
+ * Example:
+ * Given maxBytes=3145728 (3MiB), if user type 4MiB, then model will be updated to 3MiB
+ */
+ @Input()
+ maxBytes: number;
+
+ /**
+ * Value will be rounded up the nearest power of <roundPower>
+ *
+ * Example:
+ * Given roundPower=2, if user type 7KiB, then model will be updated to 8KiB
+ * Given roundPower=2, if user type 5KiB, then model will be updated to 4KiB
+ */
+ @Input()
+ roundPower: number;
+
+ /**
+ * Default unit that should be used when user do not type a unit.
+ * By default, "MiB" will be used.
+ *
+ * Example:
+ * Given defaultUnit=null, if user type 7, then model will be updated to 7MiB
+ * Given defaultUnit=k, if user type 7, then model will be updated to 7KiB
+ */
+ @Input()
+ defaultUnit: string;
+
+ private el: HTMLInputElement;
+
+ constructor(
+ private elementRef: ElementRef,
+ private control: NgControl,
+ private dimlessBinaryPerMinutePipe: DimlessBinaryPerMinutePipe,
+ private formatter: FormatterService
+ ) {
+ this.el = this.elementRef.nativeElement;
+ }
+
+ ngOnInit() {
+ this.setValue(this.el.value);
+ if (this.ngDataReady) {
+ this.ngDataReady.subscribe(() => this.setValue(this.el.value));
+ }
+ }
+
+ setValue(value: string) {
+ if (value === '') {
+ this.ngModelChange.emit(value);
+ this.control.control.setValue(value);
+ return;
+ }
+ if (/^[\d.]+$/.test(value)) {
+ value += this.defaultUnit || 'm';
+ }
+ const size = this.formatter.toBytes(value, 0);
+ const roundedSize = this.round(size);
+ this.el.value = this.dimlessBinaryPerMinutePipe.transform(roundedSize);
+ if (size !== null) {
+ this.ngModelChange.emit(this.el.value);
+ this.control.control.setValue(this.el.value);
+ } else {
+ this.ngModelChange.emit(null);
+ this.control.control.setValue(null);
+ }
+ }
+
+ round(size: number) {
+ if (size !== null && size !== 0) {
+ if (!_.isUndefined(this.minBytes) && size < this.minBytes) {
+ return this.minBytes;
+ }
+ if (!_.isUndefined(this.maxBytes) && size > this.maxBytes) {
+ return this.maxBytes;
+ }
+ if (!_.isUndefined(this.roundPower)) {
+ const power = Math.round(Math.log(size) / Math.log(this.roundPower));
+ return Math.pow(this.roundPower, power);
+ }
+ }
+ return size;
+ }
+
+ @HostListener('blur', ['$event.target.value'])
+ onBlur(value: string) {
+ this.setValue(value);
+ }
+}
import { RequiredFieldDirective } from './required-field.directive';
import { ReactiveFormsModule } from '@angular/forms';
import { OptionalFieldDirective } from './optional-field.directive';
+import { DimlessBinaryPerMinuteDirective } from './dimless-binary-per-minute.directive';
@NgModule({
imports: [ReactiveFormsModule],
CdFormValidationDirective,
AuthStorageDirective,
RequiredFieldDirective,
- OptionalFieldDirective
+ OptionalFieldDirective,
+ DimlessBinaryPerMinuteDirective
],
exports: [
AutofocusDirective,
CdFormValidationDirective,
AuthStorageDirective,
RequiredFieldDirective,
- OptionalFieldDirective
+ OptionalFieldDirective,
+ DimlessBinaryPerMinuteDirective
]
})
export class DirectivesModule {}
--- /dev/null
+import { Pipe, PipeTransform } from '@angular/core';
+
+import { FormatterService } from '../services/formatter.service';
+
+@Pipe({
+ name: 'dimlessBinaryPerMinute'
+})
+export class DimlessBinaryPerMinutePipe implements PipeTransform {
+ constructor(private formatter: FormatterService) {}
+
+ transform(value: any, decimals: number = 1): any {
+ return this.formatter.format_number(
+ value,
+ 1024,
+ ['B/m', 'KiB/m', 'MiB/m', 'GiB/m', 'TiB/m', 'PiB/m', 'EiB/m', 'ZiB/m', 'YiB/m'],
+ decimals
+ );
+ }
+}
import { XmlPipe } from './xml.pipe';
import { MbpersecondPipe } from './mbpersecond.pipe';
import { PipeFunctionPipe } from './pipe-function.pipe';
+import { DimlessBinaryPerMinutePipe } from './dimless-binary-per-minute.pipe';
@NgModule({
imports: [CommonModule],
PluralizePipe,
XmlPipe,
MbpersecondPipe,
- PipeFunctionPipe
+ PipeFunctionPipe,
+ DimlessBinaryPerMinutePipe
],
exports: [
ArrayPipe,
PluralizePipe,
XmlPipe,
MbpersecondPipe,
- PipeFunctionPipe
+ PipeFunctionPipe,
+ DimlessBinaryPerMinutePipe
],
providers: [
ArrayPipe,
MdsSummaryPipe,
OsdSummaryPipe,
OctalToHumanReadablePipe,
- MbpersecondPipe
+ MbpersecondPipe,
+ DimlessBinaryPerMinutePipe
]
})
export class PipesModule {}
import { Injectable } from '@angular/core';
-
+import { AbstractControl, ValidationErrors } from '@angular/forms';
import _ from 'lodash';
+import { isEmptyInputValue } from '../forms/cd-validators';
@Injectable({
providedIn: 'root'
toBytes(value: string, error_value: number = null): number | null {
const base = 1024;
const units = ['b', 'k', 'm', 'g', 't', 'p', 'e', 'z', 'y'];
- const m = RegExp('^(\\d+(.\\d+)?) ?([' + units.join('') + ']?(b|ib|B/s)?)?$', 'i').exec(value);
- if (m === null) {
+ const bytesRegexMatch = RegExp(
+ '^(\\d+(.\\d+)?) ?([' + units.join('') + ']?(b|ib|B/s|B/m|iB/m)?)?$',
+ 'i'
+ ).exec(value);
+ if (bytesRegexMatch === null) {
return error_value;
}
- let bytes = parseFloat(m[1]);
- if (_.isString(m[3])) {
- bytes = bytes * Math.pow(base, units.indexOf(m[3].toLowerCase()[0]));
+ let bytes = parseFloat(bytesRegexMatch[1]);
+ if (_.isString(bytesRegexMatch[3])) {
+ bytes = bytes * Math.pow(base, units.indexOf(bytesRegexMatch[3].toLowerCase()[0]));
}
return Math.round(bytes);
}
}
return octalMode;
}
+ /**
+ * Validate the input maximum size as per regrex passed.
+ */
+ performValidation(
+ control: AbstractControl,
+ regex: string,
+ errorObject: object,
+ type?: string
+ ): ValidationErrors | null {
+ if (isEmptyInputValue(control.value)) {
+ return null;
+ }
+ const matchResult = RegExp(regex, 'i').exec(control.value);
+ if (matchResult === null) {
+ return errorObject;
+ }
+ if (type == 'quota') {
+ const bytes = new FormatterService().toBytes(control.value);
+ return bytes < 1024 ? errorObject : null;
+ }
+ return null;
+ }
+
+ iopmMaxSizeValidator(control: AbstractControl): ValidationErrors | null {
+ const pattern = /^\s*(\d+)$/i;
+ const testResult = pattern.exec(control.value);
+ if (isEmptyInputValue(control.value)) {
+ return null;
+ }
+ if (testResult == null) {
+ return { rateOpsMaxSize: true };
+ }
+ return control.value.toString()?.length > 18 ? { rateOpsMaxSize: true } : null;
+ }
}
- jwt: []
tags:
- RgwBucket
+ /api/rgw/bucket/ratelimit:
+ get:
+ parameters: []
+ responses:
+ '200':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: OK
+ '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: Get the bucket global rate limit
+ tags:
+ - RgwBucket
/api/rgw/bucket/setEncryptionConfig:
put:
parameters: []
- jwt: []
tags:
- RgwBucket
+ /api/rgw/bucket/{uid}/ratelimit:
+ get:
+ parameters:
+ - in: path
+ name: uid
+ required: true
+ schema:
+ type: string
+ responses:
+ '200':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: OK
+ '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: Get the bucket rate limit
+ tags:
+ - RgwBucket
+ put:
+ parameters:
+ - in: path
+ name: uid
+ required: true
+ schema:
+ type: string
+ requestBody:
+ content:
+ application/json:
+ schema:
+ properties:
+ enabled:
+ type: string
+ max_read_bytes:
+ type: string
+ max_read_ops:
+ type: string
+ max_write_bytes:
+ type: string
+ max_write_ops:
+ type: string
+ required:
+ - enabled
+ - max_read_ops
+ - max_write_ops
+ - max_read_bytes
+ - max_write_bytes
+ type: object
+ responses:
+ '200':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Resource updated.
+ '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: Update the bucket rate limit
+ tags:
+ - RgwBucket
/api/rgw/daemon:
get:
parameters: []
- jwt: []
tags:
- RgwUser
+ /api/rgw/user/ratelimit:
+ get:
+ parameters: []
+ responses:
+ '200':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: OK
+ '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: Get the user global rate limit
+ tags:
+ - RgwUser
/api/rgw/user/{uid}:
delete:
parameters:
- jwt: []
tags:
- RgwUser
+ /api/rgw/user/{uid}/ratelimit:
+ get:
+ parameters:
+ - in: path
+ name: uid
+ required: true
+ schema:
+ type: string
+ responses:
+ '200':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: OK
+ '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: Get the user rate limit
+ tags:
+ - RgwUser
+ put:
+ parameters:
+ - in: path
+ name: uid
+ required: true
+ schema:
+ type: string
+ requestBody:
+ content:
+ application/json:
+ schema:
+ properties:
+ enabled:
+ default: false
+ type: boolean
+ max_read_bytes:
+ default: 0
+ type: integer
+ max_read_ops:
+ default: 0
+ type: integer
+ max_write_bytes:
+ default: 0
+ type: integer
+ max_write_ops:
+ default: 0
+ type: integer
+ type: object
+ responses:
+ '200':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Resource updated.
+ '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: Update the user rate limit
+ tags:
+ - RgwUser
/api/rgw/user/{uid}/subuser:
post:
parameters:
time.sleep(5)
+class RgwRateLimit:
+ def get_global_rateLimit(self):
+ rate_limit_cmd = ['global', 'ratelimit', 'get']
+ try:
+ exit_code, out, err = mgr.send_rgwadmin_command(rate_limit_cmd)
+ if exit_code > 0:
+ raise DashboardException(f'Unable to get rate limit: {err}',
+ http_status_code=500, component='rgw')
+ return out
+ except SubprocessError as error:
+ raise DashboardException(error, http_status_code=500, component='rgw')
+
+ def get_rateLimit(self, scope: str, name: str):
+ rate_limit_cmd = ['ratelimit', 'get', '--ratelimit-scope', scope]
+ if scope == 'user':
+ rate_limit_cmd.extend(['--uid', name])
+ if scope == 'bucket':
+ rate_limit_cmd.extend(['--bucket', name])
+ try:
+ exit_code, out, err = mgr.send_rgwadmin_command(rate_limit_cmd)
+ if exit_code > 0:
+ raise DashboardException(f'Unable to get rate limit: {err}',
+ http_status_code=500, component='rgw')
+ return out
+ except SubprocessError as error:
+ raise DashboardException(error, http_status_code=500, component='rgw')
+
+ def set_rateLimit(self, scope: str, enabled: bool, name: str,
+ max_read_ops: int, max_write_ops: int,
+ max_read_bytes: int, max_write_bytes: int):
+ enabled = str(enabled)
+ rgw_rate_limit_cmd = ['ratelimit', 'set', '--ratelimit-scope', scope,
+ '--max-read-ops', str(max_read_ops), '--max-write-ops',
+ str(max_write_ops), '--max-write-bytes', str(max_write_bytes),
+ '--max-read-bytes', str(max_read_bytes)]
+
+ rgw_rate_limit_enable_cmd = ['ratelimit', 'enable' if enabled == 'True' else 'disable',
+ '--ratelimit-scope', scope]
+ if scope == 'user':
+ rgw_rate_limit_cmd.extend(['--uid', name])
+ rgw_rate_limit_enable_cmd.extend(['--uid', name])
+
+ if scope == 'bucket':
+ rgw_rate_limit_cmd.extend(['--bucket', name, ])
+ rgw_rate_limit_enable_cmd.extend(['--bucket', name])
+ try:
+ if enabled == 'True':
+ exit_code, _, err = mgr.send_rgwadmin_command(rgw_rate_limit_cmd)
+ if exit_code > 0:
+ raise DashboardException(f'Unable to set rate limit: {err}',
+ http_status_code=500, component='rgw')
+ exit_code1, _, err = mgr.send_rgwadmin_command(rgw_rate_limit_enable_cmd)
+
+ if exit_code1 > 0:
+ raise DashboardException(f'Unable to enable rate limit: {err}',
+ http_status_code=500, component='rgw')
+ except SubprocessError as error:
+ raise DashboardException(error, http_status_code=500, component='rgw')
+
+
class RgwMultisite:
def migrate_to_multisite(self, realm_name: str, zonegroup_name: str, zone_name: str,
zonegroup_endpoints: str, zone_endpoints: str, username: str):
self.assertStatus(200)
self.assertNotIn('keys', self.json_body())
self.assertNotIn('swift_keys', self.json_body())
+
+ @patch('dashboard.services.rgw_client.mgr.send_rgwadmin_command')
+ @patch('dashboard.controllers.rgw.RgwRESTController.proxy')
+ def test_get_rate_limit(self, mock_proxy, send_rgwadmin_command):
+ mock_proxy.side_effect = [{
+ 'count': 3,
+ 'keys': ['test1', 'test2', 'test3'],
+ 'truncated': False
+ }]
+ send_rgwadmin_command.return_value = (
+ 0,
+ {
+ "user_ratelimit": {
+ "max_read_ops": 100,
+ "max_write_ops": 50
+ }
+ },
+ "" # empty error msg
+ )
+
+ self._get('/test/api/rgw/user/testuser/ratelimit')
+ self.assertStatus(200)
+ self.assertInJsonBody('user_ratelimit')
+
+ @patch('dashboard.services.rgw_client.mgr.send_rgwadmin_command')
+ @patch('dashboard.controllers.rgw.RgwRESTController.proxy')
+ def test_get_global_rate_limit(self, mock_proxy, send_rgwadmin_command):
+ mock_proxy.side_effect = [{
+ 'count': 3,
+ 'keys': ['test1', 'test2', 'test3'],
+ 'truncated': False
+ }]
+ mock_return_value = {
+ "bucket_ratelimit": {
+ "max_read_ops": 2024,
+ "max_write_ops": 0,
+ "max_read_bytes": 0,
+ "max_write_bytes": 0,
+ "enabled": True
+ },
+ "user_ratelimit": {
+ "max_read_ops": 1024,
+ "max_write_ops": 0,
+ "max_read_bytes": 0,
+ "max_write_bytes": 0,
+ "enabled": True
+ },
+ "anonymous_ratelimit": {
+ "max_read_ops": 0,
+ "max_write_ops": 0,
+ "max_read_bytes": 0,
+ "max_write_bytes": 0,
+ "enabled": True
+ }
+ }
+ send_rgwadmin_command.return_value = (
+ 0,
+ mock_return_value,
+ "" # empty error msg
+ )
+
+ self._get('/test/api/rgw/user/testuser/ratelimit')
+ self.assertStatus(200)
+ self.assertJsonBody(mock_return_value)