Skip to content

Commit 8ae2032

Browse files
committed
mgr/dashboard: apply replication policy for a bucket
On a normal multisite configured cluster, you can create a bucket with this replication enabled which will stop the normal syncing and starts doing the granular bucket syncing; meaning only the bucket with the replication enabled will be syncing to the secondary site. To enable replication, there should be a group policy created in the primary site. If no group policy is there, the dashboard will create one with bidirectional rule and add all the zones in the zonegroup for syncing. Fixes: https://tracker.ceph.com/issues/66239 Signed-off-by: Nizamudeen A <[email protected]>
1 parent 27e97f2 commit 8ae2032

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)