from ..security import Permission, Scope
from ..services.auth import AuthManager, JwtManager
from ..services.ceph_service import CephService
-from ..services.rgw_client import NoRgwDaemonsException, RgwClient, RgwMultisite
+from ..services.rgw_client import _SYNC_GROUP_ID, NoRgwDaemonsException, RgwClient, RgwMultisite
from ..tools import json_str_to_object, str_to_bool
from . import APIDoc, APIRouter, BaseController, CreatePermission, \
CRUDCollectionMethod, CRUDEndpoint, DeletePermission, Endpoint, \
'server_hostname': hostname,
'realm_name': metadata['realm_name'],
'zonegroup_name': metadata['zonegroup_name'],
+ 'zonegroup_id': metadata['zonegroup_id'],
'zone_name': metadata['zone_name'],
'default': instance.daemon.name == metadata['id'],
'port': int(port) if port else None
return RgwClient.admin_instance(daemon_name=daemon_name).get_realms()
if query == 'default-realm':
return RgwClient.admin_instance(daemon_name=daemon_name).get_default_realm()
+ if query == 'default-zonegroup':
+ return RgwMultisite().get_all_zonegroups_info()['default_zonegroup']
# @TODO: for multisite: by default, retrieve cluster topology/map.
raise DashboardException(http_status_code=501, component='rgw', msg='Not Implemented')
rgw_client = RgwClient.instance(owner, daemon_name)
return rgw_client.set_acl(bucket_name, acl)
+ def _set_replication(self, bucket_name: str, replication: bool, owner, daemon_name):
+ multisite = RgwMultisite()
+ rgw_client = RgwClient.instance(owner, daemon_name)
+ zonegroup_name = RgwClient.admin_instance(daemon_name=daemon_name).get_default_zonegroup()
+
+ policy_exists = multisite.policy_group_exists(_SYNC_GROUP_ID, zonegroup_name)
+ if replication and not policy_exists:
+ multisite.create_dashboard_admin_sync_group(zonegroup_name=zonegroup_name)
+ return rgw_client.set_bucket_replication(bucket_name, replication)
+
@staticmethod
def strip_tenant_from_bucket_name(bucket_name):
# type (str) -> str
lock_retention_period_days=None,
lock_retention_period_years=None, encryption_state='false',
encryption_type=None, key_id=None, tags=None,
- bucket_policy=None, canned_acl=None, daemon_name=None):
+ bucket_policy=None, canned_acl=None, replication='false',
+ daemon_name=None):
lock_enabled = str_to_bool(lock_enabled)
encryption_state = str_to_bool(encryption_state)
+ replication = str_to_bool(replication)
try:
rgw_client = RgwClient.instance(uid, daemon_name)
result = rgw_client.create_bucket(bucket, zonegroup,
if canned_acl:
self._set_acl(bucket, canned_acl, uid, daemon_name)
+ if replication:
+ self._set_replication(bucket, replication, uid, daemon_name)
return result
except RequestException as e: # pragma: no cover - handling is too obvious
raise DashboardException(e, http_status_code=500, component='rgw')
server_hostname: string;
realm_name: string;
zonegroup_name: string;
+ zonegroup_id: string;
zone_name: string;
default: boolean;
port: number;
</div>
</fieldset>
+ <!-- Replication -->
+ <fieldset>
+ <legend class="cd-header"
+ i18n>Replication</legend>
+ <div class="form-group row">
+ <label class="cd-col-form-label pt-0"
+ for="replication"
+ i18n>
+ Enable
+ </label>
+ <div class="cd-col-form-input"
+ *ngIf="{status: multisiteStatus$, isDefaultZg: isDefaultZoneGroup$ | async} as multisiteStatus; else loadingTpl">
+ <input type="checkbox"
+ class="form-check-input"
+ id="replication"
+ name="replication"
+ formControlName="replication"
+ [attr.disabled]="!multisiteStatus.isDefaultZg && !multisiteStatus.status.available ? true : null">
+ <cd-help-text>
+ <span i18n>Enables replication for the objects in the bucket.</span>
+ </cd-help-text>
+ <div class="mt-1">
+ <cd-alert-panel type="info"
+ *ngIf="!multisiteStatus.status.available && !multisiteStatus.isDefaultZg"
+ class="me-1"
+ id="multisite-configured-info"
+ i18n>
+ Multi-site needs to be configured on the current realm or you need to be on
+ the default zonegroup to enable replication.
+ </cd-alert-panel>
+ <cd-alert-panel type="info"
+ *ngIf="bucketForm.getValue('replication')"
+ class="me-1"
+ id="replication-info"
+ i18n>
+ A bi-directional sync policy group will be created by the dashboard along with flows and pipes.
+ The pipe id will then be used for applying the replication policy to the bucket.
+ </cd-alert-panel>
+ </div>
+ </div>
+ </div>
+ </fieldset>
+
<!-- Tags -->
<fieldset>
<legend class="cd-header"
</button>
</div>
</ng-template>
+
+<ng-template #loadingTpl>
+ <div class="cd-col-form-input">
+ <cd-loading-panel i18n>Checking multi-site status...</cd-loading-panel>
+ </div>
+</ng-template>
expectValidLockInputs(false, 'Compliance', '2');
});
});
+
+ describe('bucket replication', () => {
+ it('should validate replication input', () => {
+ formHelper.setValue('replication', true);
+ fixture.detectChanges();
+ formHelper.expectValid('replication');
+ });
+ });
});
import { ActivatedRoute, Router } from '@angular/router';
import _ from 'lodash';
-import { forkJoin } from 'rxjs';
+import { Observable, forkJoin } from 'rxjs';
import * as xml2js from 'xml2js';
import { RgwBucketService } from '~/app/shared/api/rgw-bucket.service';
import { RgwConfigModalComponent } from '../rgw-config-modal/rgw-config-modal.component';
import { BucketTagModalComponent } from '../bucket-tag-modal/bucket-tag-modal.component';
import { TextAreaJsonFormatterService } from '~/app/shared/services/text-area-json-formatter.service';
+import { RgwMultisiteService } from '~/app/shared/api/rgw-multisite.service';
+import { RgwDaemonService } from '~/app/shared/api/rgw-daemon.service';
+import { map, switchMap } from 'rxjs/operators';
@Component({
selector: 'cd-rgw-bucket-form',
];
grantees: string[] = [Grantee.Owner, Grantee.Everyone, Grantee.AuthenticatedUsers];
aclPermissions: AclPermissionsType[] = [aclPermission.FullControl];
+ multisiteStatus$: Observable<any>;
+ isDefaultZoneGroup$: Observable<boolean>;
get isVersioningEnabled(): boolean {
return this.bucketForm.getValue('versioning');
private rgwEncryptionModal: RgwBucketEncryptionModel,
private textAreaJsonFormatterService: TextAreaJsonFormatterService,
public actionLabels: ActionLabelsI18n,
- private readonly changeDetectorRef: ChangeDetectorRef
+ private readonly changeDetectorRef: ChangeDetectorRef,
+ private rgwMultisiteService: RgwMultisiteService,
+ private rgwDaemonService: RgwDaemonService
) {
super();
this.editing = this.router.url.startsWith(`/rgw/bucket/${URLVerbs.EDIT}`);
lock_retention_period_days: [10, [CdValidators.number(false), lockDaysValidator]],
bucket_policy: ['{}', CdValidators.json()],
grantee: [Grantee.Owner, [Validators.required]],
- aclPermission: [[aclPermission.FullControl], [Validators.required]]
+ aclPermission: [[aclPermission.FullControl], [Validators.required]],
+ replication: [false]
});
}
const promises = {
owners: this.rgwUserService.enumerate()
};
+ this.multisiteStatus$ = this.rgwMultisiteService.status();
+ this.isDefaultZoneGroup$ = this.rgwDaemonService.selectedDaemon$.pipe(
+ switchMap((daemon) =>
+ this.rgwSiteService.get('default-zonegroup').pipe(
+ map((defaultZoneGroup) => {
+ return daemon.zonegroup_id === defaultZoneGroup;
+ })
+ )
+ )
+ );
this.kmsProviders = this.rgwEncryptionModal.kmsProviders;
this.rgwBucketService.getEncryptionConfig().subscribe((data) => {
values['keyId'],
xmlStrTags,
bucketPolicy,
- cannedAcl
+ cannedAcl,
+ values['replication']
)
.subscribe(
() => {
server_hostname: 'ceph',
realm_name: 'realm1',
zonegroup_name: 'zg1-realm1',
+ zonegroup_id: 'zg1-id',
zone_name: 'zone1-zg1-realm1',
default: true,
port: 80
server_hostname: 'ceph',
realm_name: 'realm1',
zonegroup_name: 'zg1-realm1',
+ zonegroup_id: 'zg1-id',
zone_name: 'zone1-zg1-realm1',
default: true,
port: 80
CommonModule,
SharedModule,
FormsModule,
- ReactiveFormsModule,
+ ReactiveFormsModule.withConfig({ callSetDisabledState: 'whenDisabledForLegacyCode' }),
PerformanceCounterModule,
NgbNavModule,
RouterModule,
'qwerty1',
null,
null,
- 'private'
+ 'private',
+ 'true'
)
.subscribe();
const req = httpTesting.expectOne(
- `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}`
+ `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}`
);
expect(req.request.method).toBe('POST');
});
key_id: string,
tags: string,
bucketPolicy: string,
- cannedAcl: string
+ cannedAcl: string,
+ replication: string
) {
return this.rgwDaemonService.request((params: HttpParams) => {
const paramsObject = {
tags: tags,
bucket_policy: bucketPolicy,
canned_acl: cannedAcl,
+ replication: replication,
daemon_name: params.get('daemon_name')
};
getSyncStatus() {
return this.http.get(`${this.url}/sync_status`);
}
+
+ status() {
+ return this.http.get(`${this.uiUrl}/status`);
+ }
}
type: string
placement_target:
type: string
+ replication:
+ default: 'false'
+ type: string
tags:
type: string
uid:
logger = logging.getLogger('rgw_client')
+_SYNC_GROUP_ID = 'dashboard_admin_group'
+_SYNC_FLOW_ID = 'dashboard_admin_flow'
+_SYNC_PIPE_ID = 'dashboard_admin_pipe'
+
class NoRgwDaemonsException(Exception):
def __init__(self):
return realm_info['name']
return None
+ def get_default_zonegroup(self):
+ return self.daemon.zonegroup_name
+
@RestClient.api_get('/{bucket_name}?versioning')
def get_bucket_versioning(self, bucket_name, request=None):
"""
raise DashboardException(msg=msg, component='rgw')
return retention_period_days, retention_period_years
+ @RestClient.api_put('/{bucket_name}?replication')
+ def set_bucket_replication(self, bucket_name, replication: bool, request=None):
+ # pGenerate the minimum replication configuration
+ # required for enabling the replication
+ root = ET.Element('ReplicationConfiguration',
+ xmlns='http://s3.amazonaws.com/doc/2006-03-01/')
+ role = ET.SubElement(root, 'Role')
+ role.text = f'{bucket_name}_replication_role'
+
+ rule = ET.SubElement(root, 'Rule')
+ rule_id = ET.SubElement(rule, 'ID')
+ rule_id.text = _SYNC_PIPE_ID
+
+ status = ET.SubElement(rule, 'Status')
+ status.text = 'Enabled' if replication else 'Disabled'
+
+ filter_elem = ET.SubElement(rule, 'Filter')
+ prefix = ET.SubElement(filter_elem, 'Prefix')
+ prefix.text = ''
+
+ destination = ET.SubElement(rule, 'Destination')
+
+ bucket = ET.SubElement(destination, 'Bucket')
+ bucket.text = bucket_name
+
+ replication_config = ET.tostring(root, encoding='utf-8', method='xml').decode()
+
+ try:
+ request = request(data=replication_config)
+ except RequestException as e:
+ raise DashboardException(msg=str(e), component='rgw')
+
class SyncStatus(Enum):
enabled = 'enabled'
rgw_realm_list = self.list_realms()
rgw_zonegroup_list = self.list_zonegroups()
rgw_zone_list = self.list_zones()
- if len(rgw_realm_list['realms']) < 1 and len(rgw_zonegroup_list['zonegroups']) < 1 \
- and len(rgw_zone_list['zones']) < 1:
+ if len(rgw_realm_list['realms']) < 1 and len(rgw_zonegroup_list['zonegroups']) <= 1 \
+ and len(rgw_zone_list['zones']) <= 1:
is_multisite_configured = False
return is_multisite_configured
except SubprocessError as error:
raise DashboardException(error, http_status_code=500, component='rgw')
- def get_sync_policy_group(self, group_id: str, bucket_name: str = ''):
+ def get_sync_policy_group(self, group_id: str, bucket_name: str = '',
+ zonegroup_name: str = ''):
rgw_sync_policy_cmd = ['sync', 'group', 'get', '--group-id', group_id]
if bucket_name:
rgw_sync_policy_cmd += ['--bucket', bucket_name]
+ if zonegroup_name:
+ rgw_sync_policy_cmd += ['--rgw-zonegroup', zonegroup_name]
try:
exit_code, out, err = mgr.send_rgwadmin_command(rgw_sync_policy_cmd)
if exit_code > 0:
http_status_code=500, component='rgw')
except SubprocessError as error:
raise DashboardException(error, http_status_code=500, component='rgw')
+
+ def create_dashboard_admin_sync_group(self, zonegroup_name: str = ''):
+
+ zonegroup_info = self.get_zonegroup(zonegroup_name)
+ zone_names = []
+ for zones in zonegroup_info['zones']:
+ zone_names.append(zones['name'])
+
+ # create a sync policy group with status allowed
+ self.create_sync_policy_group(_SYNC_GROUP_ID, SyncStatus.allowed.value)
+ # create a sync flow with source and destination zones
+ self.create_sync_flow(_SYNC_GROUP_ID, _SYNC_FLOW_ID,
+ SyncFlowTypes.symmetrical.value,
+ zones=zone_names)
+ # create a sync pipe with source and destination zones
+ self.create_sync_pipe(_SYNC_GROUP_ID, _SYNC_PIPE_ID, source_zones=['*'],
+ destination_zones=['*'], destination_buckets=['*'])
+ # period update --commit
+ self.update_period()
+
+ def policy_group_exists(self, group_name: str, zonegroup_name: str):
+ try:
+ _ = self.get_sync_policy_group(
+ group_id=group_name, zonegroup_name=zonegroup_name)
+ return True
+ except DashboardException:
+ return False
'id': 'daemon1',
'realm_name': 'realm1',
'zonegroup_name': 'zg1',
+ 'zonegroup_id': 'zg1-id',
'zone_name': 'zone1',
'frontend_config#0': 'beast port=80'
},
'id': 'daemon2',
'realm_name': 'realm2',
'zonegroup_name': 'zg2',
+ 'zonegroup_id': 'zg2-id',
'zone_name': 'zone2',
'frontend_config#0': 'beast ssl_port=443 ssl_certificate=config:/config'
},
'id': 'daemon3',
'realm_name': 'realm3',
'zonegroup_name': 'zg3',
+ 'zonegroup_id': 'zg3-id',
'zone_name': 'zone3',
'frontend_config#0':
'beast ssl_endpoint=0.0.0.0:8080 ssl_certificate=config:/config'
'id': 'daemon4',
'realm_name': 'realm4',
'zonegroup_name': 'zg4',
+ 'zonegroup_id': 'zg4-id',
'zone_name': 'zone4',
'frontend_config#0': 'beast ssl_certificate=config:/config'
},
'id': 'daemon5',
'realm_name': 'realm5',
'zonegroup_name': 'zg5',
+ 'zonegroup_id': 'zg5-id',
'zone_name': 'zone5',
'frontend_config#0':
'beast endpoint=0.0.0.0:8445 ssl_certificate=config:/config'
'server_hostname': 'host1',
'realm_name': 'realm1',
'zonegroup_name': 'zg1',
+ 'zonegroup_id': 'zg1-id',
'zone_name': 'zone1', 'default': True,
'port': 80
},
'server_hostname': 'host1',
'realm_name': 'realm2',
'zonegroup_name': 'zg2',
+ 'zonegroup_id': 'zg2-id',
'zone_name': 'zone2',
'default': False,
'port': 443,
'server_hostname': 'host1',
'realm_name': 'realm3',
'zonegroup_name': 'zg3',
+ 'zonegroup_id': 'zg3-id',
'zone_name': 'zone3',
'default': False,
'port': 8080,
'server_hostname': 'host1',
'realm_name': 'realm4',
'zonegroup_name': 'zg4',
+ 'zonegroup_id': 'zg4-id',
'zone_name': 'zone4',
'default': False,
'port': None,
'server_hostname': 'host1',
'realm_name': 'realm5',
'zonegroup_name': 'zg5',
+ 'zonegroup_id': 'zg5-id',
'zone_name': 'zone5',
'default': False,
'port': 8445,