Skip to content

Commit 1f5d6d1

Browse files
authored
Merge pull request ceph#57713 from rhcs-dashboard/bucket-replication-form
mgr/dashboard: apply replication policy for a bucket Reviewed-by: Pedro Gonzalez Gomez <[email protected]> Reviewed-by: Ankush Behl <[email protected]>
2 parents e3e7139 + 8ae2032 commit 1f5d6d1

File tree

14 files changed

+198
-13
lines changed

14 files changed

+198
-13
lines changed

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

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
from ..security import Permission, Scope
1515
from ..services.auth import AuthManager, JwtManager
1616
from ..services.ceph_service import CephService
17-
from ..services.rgw_client import NoRgwDaemonsException, RgwClient, RgwMultisite
17+
from ..services.rgw_client import _SYNC_GROUP_ID, NoRgwDaemonsException, RgwClient, RgwMultisite
1818
from ..tools import json_str_to_object, str_to_bool
1919
from . import APIDoc, APIRouter, BaseController, CreatePermission, \
2020
CRUDCollectionMethod, CRUDEndpoint, DeletePermission, Endpoint, \
@@ -242,6 +242,7 @@ def list(self) -> List[dict]:
242242
'server_hostname': hostname,
243243
'realm_name': metadata['realm_name'],
244244
'zonegroup_name': metadata['zonegroup_name'],
245+
'zonegroup_id': metadata['zonegroup_id'],
245246
'zone_name': metadata['zone_name'],
246247
'default': instance.daemon.name == metadata['id'],
247248
'port': int(port) if port else None
@@ -307,6 +308,8 @@ def list(self, query=None, daemon_name=None):
307308
return RgwClient.admin_instance(daemon_name=daemon_name).get_realms()
308309
if query == 'default-realm':
309310
return RgwClient.admin_instance(daemon_name=daemon_name).get_default_realm()
311+
if query == 'default-zonegroup':
312+
return RgwMultisite().get_all_zonegroups_info()['default_zonegroup']
310313

311314
# @TODO: for multisite: by default, retrieve cluster topology/map.
312315
raise DashboardException(http_status_code=501, component='rgw', msg='Not Implemented')
@@ -396,6 +399,16 @@ def _set_acl(self, bucket_name: str, acl: str, owner, daemon_name):
396399
rgw_client = RgwClient.instance(owner, daemon_name)
397400
return rgw_client.set_acl(bucket_name, acl)
398401

402+
def _set_replication(self, bucket_name: str, replication: bool, owner, daemon_name):
403+
multisite = RgwMultisite()
404+
rgw_client = RgwClient.instance(owner, daemon_name)
405+
zonegroup_name = RgwClient.admin_instance(daemon_name=daemon_name).get_default_zonegroup()
406+
407+
policy_exists = multisite.policy_group_exists(_SYNC_GROUP_ID, zonegroup_name)
408+
if replication and not policy_exists:
409+
multisite.create_dashboard_admin_sync_group(zonegroup_name=zonegroup_name)
410+
return rgw_client.set_bucket_replication(bucket_name, replication)
411+
399412
@staticmethod
400413
def strip_tenant_from_bucket_name(bucket_name):
401414
# type (str) -> str
@@ -463,9 +476,11 @@ def create(self, bucket, uid, zonegroup=None, placement_target=None,
463476
lock_retention_period_days=None,
464477
lock_retention_period_years=None, encryption_state='false',
465478
encryption_type=None, key_id=None, tags=None,
466-
bucket_policy=None, canned_acl=None, daemon_name=None):
479+
bucket_policy=None, canned_acl=None, replication='false',
480+
daemon_name=None):
467481
lock_enabled = str_to_bool(lock_enabled)
468482
encryption_state = str_to_bool(encryption_state)
483+
replication = str_to_bool(replication)
469484
try:
470485
rgw_client = RgwClient.instance(uid, daemon_name)
471486
result = rgw_client.create_bucket(bucket, zonegroup,
@@ -488,6 +503,8 @@ def create(self, bucket, uid, zonegroup=None, placement_target=None,
488503
if canned_acl:
489504
self._set_acl(bucket, canned_acl, uid, daemon_name)
490505

506+
if replication:
507+
self._set_replication(bucket, replication, uid, daemon_name)
491508
return result
492509
except RequestException as e: # pragma: no cover - handling is too obvious
493510
raise DashboardException(e, http_status_code=500, component='rgw')

src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/models/rgw-daemon.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ export class RgwDaemon {
55
server_hostname: string;
66
realm_name: string;
77
zonegroup_name: string;
8+
zonegroup_id: string;
89
zone_name: string;
910
default: boolean;
1011
port: number;

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

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -387,6 +387,49 @@
387387
</div>
388388
</fieldset>
389389

390+
<!-- Replication -->
391+
<fieldset>
392+
<legend class="cd-header"
393+
i18n>Replication</legend>
394+
<div class="form-group row">
395+
<label class="cd-col-form-label pt-0"
396+
for="replication"
397+
i18n>
398+
Enable
399+
</label>
400+
<div class="cd-col-form-input"
401+
*ngIf="{status: multisiteStatus$, isDefaultZg: isDefaultZoneGroup$ | async} as multisiteStatus; else loadingTpl">
402+
<input type="checkbox"
403+
class="form-check-input"
404+
id="replication"
405+
name="replication"
406+
formControlName="replication"
407+
[attr.disabled]="!multisiteStatus.isDefaultZg && !multisiteStatus.status.available ? true : null">
408+
<cd-help-text>
409+
<span i18n>Enables replication for the objects in the bucket.</span>
410+
</cd-help-text>
411+
<div class="mt-1">
412+
<cd-alert-panel type="info"
413+
*ngIf="!multisiteStatus.status.available && !multisiteStatus.isDefaultZg"
414+
class="me-1"
415+
id="multisite-configured-info"
416+
i18n>
417+
Multi-site needs to be configured on the current realm or you need to be on
418+
the default zonegroup to enable replication.
419+
</cd-alert-panel>
420+
<cd-alert-panel type="info"
421+
*ngIf="bucketForm.getValue('replication')"
422+
class="me-1"
423+
id="replication-info"
424+
i18n>
425+
A bi-directional sync policy group will be created by the dashboard along with flows and pipes.
426+
The pipe id will then be used for applying the replication policy to the bucket.
427+
</cd-alert-panel>
428+
</div>
429+
</div>
430+
</div>
431+
</fieldset>
432+
390433
<!-- Tags -->
391434
<fieldset>
392435
<legend class="cd-header"
@@ -594,3 +637,9 @@
594637
</button>
595638
</div>
596639
</ng-template>
640+
641+
<ng-template #loadingTpl>
642+
<div class="cd-col-form-input">
643+
<cd-loading-panel i18n>Checking multi-site status...</cd-loading-panel>
644+
</div>
645+
</ng-template>

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -307,4 +307,12 @@ describe('RgwBucketFormComponent', () => {
307307
expectValidLockInputs(false, 'Compliance', '2');
308308
});
309309
});
310+
311+
describe('bucket replication', () => {
312+
it('should validate replication input', () => {
313+
formHelper.setValue('replication', true);
314+
fixture.detectChanges();
315+
formHelper.expectValid('replication');
316+
});
317+
});
310318
});

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

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { AbstractControl, Validators } from '@angular/forms';
1010
import { ActivatedRoute, Router } from '@angular/router';
1111

1212
import _ from 'lodash';
13-
import { forkJoin } from 'rxjs';
13+
import { Observable, forkJoin } from 'rxjs';
1414
import * as xml2js from 'xml2js';
1515

1616
import { RgwBucketService } from '~/app/shared/api/rgw-bucket.service';
@@ -36,6 +36,9 @@ import { RgwBucketVersioning } from '../models/rgw-bucket-versioning';
3636
import { RgwConfigModalComponent } from '../rgw-config-modal/rgw-config-modal.component';
3737
import { BucketTagModalComponent } from '../bucket-tag-modal/bucket-tag-modal.component';
3838
import { TextAreaJsonFormatterService } from '~/app/shared/services/text-area-json-formatter.service';
39+
import { RgwMultisiteService } from '~/app/shared/api/rgw-multisite.service';
40+
import { RgwDaemonService } from '~/app/shared/api/rgw-daemon.service';
41+
import { map, switchMap } from 'rxjs/operators';
3942

4043
@Component({
4144
selector: 'cd-rgw-bucket-form',
@@ -72,6 +75,8 @@ export class RgwBucketFormComponent extends CdForm implements OnInit, AfterViewC
7275
];
7376
grantees: string[] = [Grantee.Owner, Grantee.Everyone, Grantee.AuthenticatedUsers];
7477
aclPermissions: AclPermissionsType[] = [aclPermission.FullControl];
78+
multisiteStatus$: Observable<any>;
79+
isDefaultZoneGroup$: Observable<boolean>;
7580

7681
get isVersioningEnabled(): boolean {
7782
return this.bucketForm.getValue('versioning');
@@ -92,7 +97,9 @@ export class RgwBucketFormComponent extends CdForm implements OnInit, AfterViewC
9297
private rgwEncryptionModal: RgwBucketEncryptionModel,
9398
private textAreaJsonFormatterService: TextAreaJsonFormatterService,
9499
public actionLabels: ActionLabelsI18n,
95-
private readonly changeDetectorRef: ChangeDetectorRef
100+
private readonly changeDetectorRef: ChangeDetectorRef,
101+
private rgwMultisiteService: RgwMultisiteService,
102+
private rgwDaemonService: RgwDaemonService
96103
) {
97104
super();
98105
this.editing = this.router.url.startsWith(`/rgw/bucket/${URLVerbs.EDIT}`);
@@ -154,14 +161,25 @@ export class RgwBucketFormComponent extends CdForm implements OnInit, AfterViewC
154161
lock_retention_period_days: [10, [CdValidators.number(false), lockDaysValidator]],
155162
bucket_policy: ['{}', CdValidators.json()],
156163
grantee: [Grantee.Owner, [Validators.required]],
157-
aclPermission: [[aclPermission.FullControl], [Validators.required]]
164+
aclPermission: [[aclPermission.FullControl], [Validators.required]],
165+
replication: [false]
158166
});
159167
}
160168

161169
ngOnInit() {
162170
const promises = {
163171
owners: this.rgwUserService.enumerate()
164172
};
173+
this.multisiteStatus$ = this.rgwMultisiteService.status();
174+
this.isDefaultZoneGroup$ = this.rgwDaemonService.selectedDaemon$.pipe(
175+
switchMap((daemon) =>
176+
this.rgwSiteService.get('default-zonegroup').pipe(
177+
map((defaultZoneGroup) => {
178+
return daemon.zonegroup_id === defaultZoneGroup;
179+
})
180+
)
181+
)
182+
);
165183

166184
this.kmsProviders = this.rgwEncryptionModal.kmsProviders;
167185
this.rgwBucketService.getEncryptionConfig().subscribe((data) => {
@@ -331,7 +349,8 @@ export class RgwBucketFormComponent extends CdForm implements OnInit, AfterViewC
331349
values['keyId'],
332350
xmlStrTags,
333351
bucketPolicy,
334-
cannedAcl
352+
cannedAcl,
353+
values['replication']
335354
)
336355
.subscribe(
337356
() => {

src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-daemon-list/rgw-daemon-list.component.spec.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ describe('RgwDaemonListComponent', () => {
3232
server_hostname: 'ceph',
3333
realm_name: 'realm1',
3434
zonegroup_name: 'zg1-realm1',
35+
zonegroup_id: 'zg1-id',
3536
zone_name: 'zone1-zg1-realm1',
3637
default: true,
3738
port: 80

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ describe('RgwOverviewDashboardComponent', () => {
2626
server_hostname: 'ceph',
2727
realm_name: 'realm1',
2828
zonegroup_name: 'zg1-realm1',
29+
zonegroup_id: 'zg1-id',
2930
zone_name: 'zone1-zg1-realm1',
3031
default: true,
3132
port: 80

src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw.module.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ import { BucketTagModalComponent } from './bucket-tag-modal/bucket-tag-modal.com
5151
CommonModule,
5252
SharedModule,
5353
FormsModule,
54-
ReactiveFormsModule,
54+
ReactiveFormsModule.withConfig({ callSetDisabledState: 'whenDisabledForLegacyCode' }),
5555
PerformanceCounterModule,
5656
NgbNavModule,
5757
RouterModule,

src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-bucket.service.spec.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,11 +62,12 @@ describe('RgwBucketService', () => {
6262
'qwerty1',
6363
null,
6464
null,
65-
'private'
65+
'private',
66+
'true'
6667
)
6768
.subscribe();
6869
const req = httpTesting.expectOne(
69-
`api/rgw/bucket?bucket=foo&uid=bar&zonegroup=default&lock_enabled=false&lock_mode=COMPLIANCE&lock_retention_period_days=5&encryption_state=true&encryption_type=aws%253Akms&key_id=qwerty1&tags=null&bucket_policy=null&canned_acl=private&${RgwHelper.DAEMON_QUERY_PARAM}`
70+
`api/rgw/bucket?bucket=foo&uid=bar&zonegroup=default&lock_enabled=false&lock_mode=COMPLIANCE&lock_retention_period_days=5&encryption_state=true&encryption_type=aws%253Akms&key_id=qwerty1&tags=null&bucket_policy=null&canned_acl=private&replication=true&${RgwHelper.DAEMON_QUERY_PARAM}`
7071
);
7172
expect(req.request.method).toBe('POST');
7273
});

src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-bucket.service.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,8 @@ export class RgwBucketService extends ApiClient {
6262
key_id: string,
6363
tags: string,
6464
bucketPolicy: string,
65-
cannedAcl: string
65+
cannedAcl: string,
66+
replication: string
6667
) {
6768
return this.rgwDaemonService.request((params: HttpParams) => {
6869
const paramsObject = {
@@ -78,6 +79,7 @@ export class RgwBucketService extends ApiClient {
7879
tags: tags,
7980
bucket_policy: bucketPolicy,
8081
canned_acl: cannedAcl,
82+
replication: replication,
8183
daemon_name: params.get('daemon_name')
8284
};
8385

0 commit comments

Comments
 (0)