diff options
author | Aashish Sharma <aasharma@redhat.com> | 2023-03-28 10:04:25 +0200 |
---|---|---|
committer | Aashish Sharma <aasharma@li-e74156cc-2f67-11b2-a85c-e98659a63c5c.ibm.com> | 2023-07-04 13:20:32 +0200 |
commit | 5415a2611de7c50cbf429e92b253ed7bbd2ab7ea (patch) | |
tree | c7d5dce136ea823724540afe24e212c5b06a1f27 /src/pybind/mgr | |
parent | Merge pull request #52194 from zdover23/wip-doc-2023-06-26-radosgw-s3select-o... (diff) | |
download | ceph-5415a2611de7c50cbf429e92b253ed7bbd2ab7ea.tar.xz ceph-5415a2611de7c50cbf429e92b253ed7bbd2ab7ea.zip |
mgr/dashboard: Allow the user to import and export multi-site configuration
Fixes: https://tracker.ceph.com/issues/61776
Signed-off-by: Aashish Sharma <aasharma@redhat.com>
Diffstat (limited to 'src/pybind/mgr')
42 files changed, 2090 insertions, 1185 deletions
diff --git a/src/pybind/mgr/dashboard/controllers/rgw.py b/src/pybind/mgr/dashboard/controllers/rgw.py index 2dc5874ea22..3ba4cf4923e 100644 --- a/src/pybind/mgr/dashboard/controllers/rgw.py +++ b/src/pybind/mgr/dashboard/controllers/rgw.py @@ -12,12 +12,11 @@ from ..rest_client import RequestException from ..security import Permission, Scope from ..services.auth import AuthManager, JwtManager from ..services.ceph_service import CephService -from ..services.orchestrator import OrchClient -from ..services.rgw_client import NoRgwDaemonsException, RgwClient +from ..services.rgw_client import NoRgwDaemonsException, RgwClient, RgwMultisite from ..tools import json_str_to_object, str_to_bool from . import APIDoc, APIRouter, BaseController, CreatePermission, \ CRUDCollectionMethod, CRUDEndpoint, Endpoint, EndpointDoc, ReadPermission, \ - RESTController, UIRouter, allow_empty_body + RESTController, UIRouter, UpdatePermission, allow_empty_body from ._crud import CRUDMeta, Form, FormField, FormTaskInfo, Icon, MethodType, \ TableAction, Validator, VerticalContainer from ._version import APIVersion @@ -82,49 +81,31 @@ class Rgw(BaseController): @UIRouter('/rgw/multisite') -class RgwStatus(BaseController): +class RgwMultisiteStatus(RESTController): @Endpoint() @ReadPermission # pylint: disable=R0801 def status(self): status = {'available': True, 'message': None} - try: - instance = RgwClient.admin_instance() - is_multisite_configured = instance.get_multisite_status() - if not is_multisite_configured: - status['available'] = False - status['message'] = 'Multi-site provides disaster recovery and may also \ - serve as a foundation for content delivery networks' # type: ignore - except NoRgwDaemonsException as e: - raise DashboardException(e, http_status_code=404, component='rgw') + multisite_instance = RgwMultisite() + is_multisite_configured = multisite_instance.get_multisite_status() + if not is_multisite_configured: + status['available'] = False + status['message'] = 'Multi-site provides disaster recovery and may also \ + serve as a foundation for content delivery networks' # type: ignore return status - @Endpoint() - @ReadPermission - # pylint: disable=R0801 - def sync_status(self): - try: - instance = RgwClient.admin_instance() - result = instance.get_multisite_sync_status() - except NoRgwDaemonsException as e: - raise DashboardException(e, http_status_code=404, component='rgw') # noqa: E501 pylint: disable=line-too-long - return result - - @Endpoint(method='PUT') - # pylint: disable=W0102 - def migrate(self, realm_name=None, zonegroup_name=None, zone_name=None, - zonegroup_endpoints: List[str] = [], zone_endpoints: List[str] = [], - user=None, daemon_name: Optional[str] = None): - try: - instance = RgwClient.admin_instance(daemon_name=daemon_name) - result = instance.migrate_to_multisite(realm_name, zonegroup_name, zone_name, - zonegroup_endpoints, zone_endpoints, user) - orch = OrchClient.instance() - daemons = orch.services.list_daemons(service_name='rgw') - for daemon in daemons: - orch.daemons.action(action='reload', daemon_name=daemon.daemon_id) - except NoRgwDaemonsException as e: - raise DashboardException(e, http_status_code=404, component='rgw') + @RESTController.Collection(method='PUT', path='/migrate') + @allow_empty_body + # pylint: disable=W0102,W0613 + def migrate(self, daemon_name=None, realm_name=None, zonegroup_name=None, zone_name=None, + zonegroup_endpoints=None, zone_endpoints=None, access_key=None, + secret_key=None): + multisite_instance = RgwMultisite() + result = multisite_instance.migrate_to_multisite(realm_name, zonegroup_name, + zone_name, zonegroup_endpoints, + zone_endpoints, access_key, + secret_key) return result @@ -187,6 +168,12 @@ class RgwDaemon(RESTController): daemon['rgw_status'] = status return daemon + @RESTController.Collection(method='PUT', path='/set_multisite_config') + @allow_empty_body + def set_multisite_config(self, realm_name=None, zonegroup_name=None, + zone_name=None, daemon_name=None): + CephService.set_multisite_config(realm_name, zonegroup_name, zone_name, daemon_name) + class RgwRESTController(RESTController): def proxy(self, daemon_name, method, path, params=None, json_response=True): @@ -738,132 +725,120 @@ class RgwUserRole(NamedTuple): class RgwRealm(RESTController): @allow_empty_body # pylint: disable=W0613 - def create(self, realm_name, default, daemon_name=None): - try: - instance = RgwClient.admin_instance(daemon_name=daemon_name) - result = instance.create_realm(realm_name, default) - return result - except NoRgwDaemonsException as e: - raise DashboardException(e, http_status_code=404, component='rgw') + def create(self, realm_name, default): + multisite_instance = RgwMultisite() + result = multisite_instance.create_realm(realm_name, default) + return result @allow_empty_body # pylint: disable=W0613 - def list(self, daemon_name=None): - try: - instance = RgwClient.admin_instance(daemon_name=daemon_name) - result = instance.list_realms() - return result - except NoRgwDaemonsException as e: - raise DashboardException(e, http_status_code=404, component='rgw') + def list(self): + multisite_instance = RgwMultisite() + result = multisite_instance.list_realms() + return result @allow_empty_body # pylint: disable=W0613 - def get(self, realm_name, daemon_name=None): - try: - instance = RgwClient.admin_instance(daemon_name=daemon_name) - result = instance.get_realm(realm_name) - return result - except NoRgwDaemonsException as e: - raise DashboardException(e, http_status_code=404, component='rgw') + def get(self, realm_name): + multisite_instance = RgwMultisite() + result = multisite_instance.get_realm(realm_name) + return result @Endpoint() @ReadPermission def get_all_realms_info(self): - try: - instance = RgwClient.admin_instance() - result = instance.get_all_realms_info() - return result - except NoRgwDaemonsException as e: - raise DashboardException(e, http_status_code=404, component='rgw') + multisite_instance = RgwMultisite() + result = multisite_instance.get_all_realms_info() + return result @allow_empty_body # pylint: disable=W0613 - def set(self, realm_name: str, new_realm_name: str, default: str = '', daemon_name=None): + def set(self, realm_name: str, new_realm_name: str, default: str = ''): + multisite_instance = RgwMultisite() + result = multisite_instance.edit_realm(realm_name, new_realm_name, default) + return result + + @Endpoint() + @ReadPermission + def get_realm_tokens(self): try: - instance = RgwClient.admin_instance(daemon_name=daemon_name) - result = instance.edit_realm(realm_name, new_realm_name, default) + result = CephService.get_realm_tokens() return result except NoRgwDaemonsException as e: raise DashboardException(e, http_status_code=404, component='rgw') - def delete(self, realm_name, daemon_name=None): + @Endpoint(method='POST') + @UpdatePermission + @allow_empty_body + # pylint: disable=W0613 + def import_realm_token(self, realm_token, zone_name, daemon_name=None): try: - instance = RgwClient.admin_instance(daemon_name) - result = instance.delete_realm(realm_name) + multisite_instance = RgwMultisite() + result = CephService.import_realm_token(realm_token, zone_name) + multisite_instance.update_period() return result except NoRgwDaemonsException as e: raise DashboardException(e, http_status_code=404, component='rgw') + def delete(self, realm_name): + multisite_instance = RgwMultisite() + result = multisite_instance.delete_realm(realm_name) + return result + @APIRouter('/rgw/zonegroup', Scope.RGW) class RgwZonegroup(RESTController): @allow_empty_body # pylint: disable=W0613 def create(self, realm_name, zonegroup_name, default=None, master=None, - zonegroup_endpoints=None, daemon_name=None): - try: - instance = RgwClient.admin_instance(daemon_name=daemon_name) - result = instance.create_zonegroup(realm_name, zonegroup_name, default, - master, zonegroup_endpoints) - return result - except NoRgwDaemonsException as e: - raise DashboardException(e, http_status_code=404, component='rgw') + zonegroup_endpoints=None): + multisite_instance = RgwMultisite() + result = multisite_instance.create_zonegroup(realm_name, zonegroup_name, default, + master, zonegroup_endpoints) + return result @allow_empty_body # pylint: disable=W0613 - def list(self, daemon_name=None): - try: - instance = RgwClient.admin_instance(daemon_name=daemon_name) - result = instance.list_zonegroups() - return result - except NoRgwDaemonsException as e: - raise DashboardException(e, http_status_code=404, component='rgw') + def list(self): + multisite_instance = RgwMultisite() + result = multisite_instance.list_zonegroups() + return result @allow_empty_body # pylint: disable=W0613 - def get(self, zonegroup_name, daemon_name=None): - try: - instance = RgwClient.admin_instance(daemon_name=daemon_name) - result = instance.get_zonegroup(zonegroup_name) - return result - except NoRgwDaemonsException as e: - raise DashboardException(e, http_status_code=404, component='rgw') + def get(self, zonegroup_name): + multisite_instance = RgwMultisite() + result = multisite_instance.get_zonegroup(zonegroup_name) + return result @Endpoint() @ReadPermission def get_all_zonegroups_info(self): - try: - instance = RgwClient.admin_instance() - result = instance.get_all_zonegroups_info() - return result - except NoRgwDaemonsException as e: - raise DashboardException(e, http_status_code=404, component='rgw') + multisite_instance = RgwMultisite() + result = multisite_instance.get_all_zonegroups_info() + return result - def delete(self, zonegroup_name, delete_pools, pools: Optional[List[str]] = None, - daemon_name=None): + def delete(self, zonegroup_name, delete_pools, pools: Optional[List[str]] = None): if pools is None: pools = [] try: - instance = RgwClient.admin_instance(daemon_name) - result = instance.delete_zonegroup(zonegroup_name, delete_pools, pools) + multisite_instance = RgwMultisite() + result = multisite_instance.delete_zonegroup(zonegroup_name, delete_pools, pools) return result except NoRgwDaemonsException as e: raise DashboardException(e, http_status_code=404, component='rgw') @allow_empty_body # pylint: disable=W0613,W0102 - def set(self, zonegroup_name: str, realm_name: str, new_zonegroup_name: str, default: str = '', - master: str = '', zonegroup_endpoints: List[str] = [], add_zones: List[str] = [], - remove_zones: List[str] = [], placement_targets: List[Dict[str, str]] = [], - daemon_name=None): - try: - instance = RgwClient.admin_instance() - result = instance.edit_zonegroup(realm_name, zonegroup_name, new_zonegroup_name, - default, master, zonegroup_endpoints, add_zones, - remove_zones, placement_targets) - return result - except NoRgwDaemonsException as e: - raise DashboardException(e, http_status_code=404, component='rgw') + def set(self, zonegroup_name: str, realm_name: str, new_zonegroup_name: str, + default: str = '', master: str = '', zonegroup_endpoints: str = '', + add_zones: List[str] = [], remove_zones: List[str] = [], + placement_targets: List[Dict[str, str]] = []): + multisite_instance = RgwMultisite() + result = multisite_instance.edit_zonegroup(realm_name, zonegroup_name, new_zonegroup_name, + default, master, zonegroup_endpoints, add_zones, + remove_zones, placement_targets) + return result @APIRouter('/rgw/zone', Scope.RGW) @@ -871,56 +846,43 @@ class RgwZone(RESTController): @allow_empty_body # pylint: disable=W0613 def create(self, zone_name, zonegroup_name=None, default=False, master=False, - zone_endpoints=None, user=None, createSystemUser=False, daemon_name=None, - master_zone_of_master_zonegroup=None): - try: - instance = RgwClient.admin_instance(daemon_name=daemon_name) - result = instance.create_zone(zone_name, zonegroup_name, default, - master, zone_endpoints, user, createSystemUser, - master_zone_of_master_zonegroup) - return result - except NoRgwDaemonsException as e: - raise DashboardException(e, http_status_code=404, component='rgw') + zone_endpoints=None, access_key=None, secret_key=None): + multisite_instance = RgwMultisite() + result = multisite_instance.create_zone(zone_name, zonegroup_name, default, + master, zone_endpoints, access_key, + secret_key) + return result @allow_empty_body # pylint: disable=W0613 - def list(self, daemon_name=None): - try: - instance = RgwClient.admin_instance(daemon_name=daemon_name) - result = instance.list_zones() - return result - except NoRgwDaemonsException as e: - raise DashboardException(e, http_status_code=404, component='rgw') + def list(self): + multisite_instance = RgwMultisite() + result = multisite_instance.list_zones() + return result @allow_empty_body # pylint: disable=W0613 - def get(self, zone_name, daemon_name=None): - try: - instance = RgwClient.admin_instance(daemon_name=daemon_name) - result = instance.get_zone(zone_name) - return result - except NoRgwDaemonsException as e: - raise DashboardException(e, http_status_code=404, component='rgw') + def get(self, zone_name): + multisite_instance = RgwMultisite() + result = multisite_instance.get_zone(zone_name) + return result @Endpoint() @ReadPermission def get_all_zones_info(self): - try: - instance = RgwClient.admin_instance() - result = instance.get_all_zones_info() - return result - except NoRgwDaemonsException as e: - raise DashboardException(e, http_status_code=404, component='rgw') + multisite_instance = RgwMultisite() + result = multisite_instance.get_all_zones_info() + return result def delete(self, zone_name, delete_pools, pools: Optional[List[str]] = None, - zonegroup_name=None, daemon_name=None): + zonegroup_name=None): if pools is None: pools = [] if zonegroup_name is None: zonegroup_name = '' try: - instance = RgwClient.admin_instance(daemon_name) - result = instance.delete_zone(zone_name, delete_pools, pools, zonegroup_name) + multisite_instance = RgwMultisite() + result = multisite_instance.delete_zone(zone_name, delete_pools, pools, zonegroup_name) return result except NoRgwDaemonsException as e: raise DashboardException(e, http_status_code=404, component='rgw') @@ -928,20 +890,17 @@ class RgwZone(RESTController): @allow_empty_body # pylint: disable=W0613,W0102 def set(self, zone_name: str, new_zone_name: str, zonegroup_name: str, default: str = '', - master: str = '', zone_endpoints: List[str] = [], user: str = '', + master: str = '', zone_endpoints: str = '', access_key: str = '', secret_key: str = '', placement_target: str = '', data_pool: str = '', index_pool: str = '', data_extra_pool: str = '', storage_class: str = '', data_pool_class: str = '', - compression: str = '', daemon_name=None, master_zone_of_master_zonegroup=None): - try: - instance = RgwClient.admin_instance(daemon_name=daemon_name) - result = instance.edit_zone(zone_name, new_zone_name, zonegroup_name, default, - master, zone_endpoints, user, placement_target, - data_pool, index_pool, data_extra_pool, storage_class, - data_pool_class, compression, - master_zone_of_master_zonegroup) - return result - except NoRgwDaemonsException as e: - raise DashboardException(e, http_status_code=404, component='rgw') + compression: str = ''): + multisite_instance = RgwMultisite() + result = multisite_instance.edit_zone(zone_name, new_zone_name, zonegroup_name, default, + master, zone_endpoints, access_key, secret_key, + placement_target, data_pool, index_pool, + data_extra_pool, storage_class, data_pool_class, + compression) + return result @Endpoint() @ReadPermission @@ -957,20 +916,14 @@ class RgwZone(RESTController): @Endpoint('PUT') @CreatePermission - def create_system_user(self, userName: str, zoneName: str, daemon_name=None): - try: - instance = RgwClient.admin_instance(daemon_name=daemon_name) - result = instance.create_system_user(userName, zoneName) - return result - except NoRgwDaemonsException as e: - raise DashboardException(e, http_status_code=404, component='rgw') + def create_system_user(self, userName: str, zoneName: str): + multisite_instance = RgwMultisite() + result = multisite_instance.create_system_user(userName, zoneName) + return result @Endpoint() @ReadPermission - def get_user_list(self, daemon_name=None, zoneName=None): - try: - instance = RgwClient.admin_instance(daemon_name=daemon_name) - result = instance.get_user_list(zoneName) - return result - except NoRgwDaemonsException as e: - raise DashboardException(e, http_status_code=404, component='rgw') + def get_user_list(self, zoneName=None): + multisite_instance = RgwMultisite() + result = multisite_instance.get_user_list(zoneName) + return result diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/service-form/service-form.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/service-form/service-form.component.html index 6c465d46998..0c2392fce41 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/service-form/service-form.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/service-form/service-form.component.html @@ -7,7 +7,14 @@ [formGroup]="serviceForm" novalidate> <div class="modal-body"> - + <cd-alert-panel *ngIf="serviceForm.controls.service_type.value === 'rgw' && showRealmCreationForm" + type="info" + spacingClass="mb-3" + i18n> + <a class="text-decoration-underline" + (click)="createMultisiteSetup()"> + Click here</a> to create a new realm/zonegroup/zone + </cd-alert-panel> <!-- Service type --> <div class="form-group row"> <label class="cd-col-form-label required" @@ -84,14 +91,71 @@ *ngIf="serviceForm.showError('service_id', frm, 'uniqueName')" i18n>This service id is already in use.</span> <span class="invalid-feedback" - *ngIf="serviceForm.showError('service_id', frm, 'rgwPattern')" - i18n>The value does not match the pattern <strong><service_id>[.<realm_name>.<zone_name>]</strong>.</span> - <span class="invalid-feedback" *ngIf="serviceForm.showError('service_id', frm, 'mdsPattern')" i18n>MDS service id must start with a letter and contain alphanumeric characters or '.', '-', and '_'</span> </div> </div> + <div class="form-group row" + *ngIf="serviceForm.controls.service_type.value === 'rgw'"> + <label class="cd-col-form-label" + for="realm_name" + i18n>Realm</label> + <div class="cd-col-form-input"> + <select class="form-select" + id="realm_name" + formControlName="realm_name" + name="realm_name" + [attr.disabled]="realmList.length === 0 || editing ? true : null"> + <option *ngIf="realmList.length === 0" + i18n + selected>-- No realm available --</option> + <option *ngFor="let realm of realmList" + [value]="realm.name"> + {{ realm.name }} + </option> + </select> + </div> + </div> + + <div class="form-group row" + *ngIf="serviceForm.controls.service_type.value === 'rgw'"> + <label class="cd-col-form-label" + for="zonegroup_name" + i18n>Zonegroup</label> + <div class="cd-col-form-input"> + <select class="form-select" + id="zonegroup_name" + formControlName="zonegroup_name" + name="zonegroup_name" + [attr.disabled]="zonegroupList.length === 0 || editing ? true : null"> + <option *ngFor="let zonegroup of zonegroupList" + [value]="zonegroup.name"> + {{ zonegroup.name }} + </option> + </select> + </div> + </div> + + <div class="form-group row" + *ngIf="serviceForm.controls.service_type.value === 'rgw'"> + <label class="cd-col-form-label" + for="zone_name" + i18n>Zone</label> + <div class="cd-col-form-input"> + <select class="form-select" + id="zone_name" + formControlName="zone_name" + name="zone_name" + [attr.disabled]="zoneList.length === 0 || editing ? true : null"> + <option *ngFor="let zone of zoneList" + [value]="zone.name"> + {{ zone.name }} + </option> + </select> + </div> + </div> + <!-- unmanaged --> <div class="form-group row"> <div class="cd-col-form-offset"> diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/service-form/service-form.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/service-form/service-form.component.spec.ts index b2c965ee71b..ebecec5cc38 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/service-form/service-form.component.spec.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/service-form/service-form.component.spec.ts @@ -191,29 +191,18 @@ describe('ServiceFormComponent', () => { formHelper.expectValid('service_id'); }); - it('should test rgw invalid service id', () => { - formHelper.setValue('service_id', '.'); - formHelper.expectError('service_id', 'rgwPattern'); - formHelper.setValue('service_id', 'svc.'); - formHelper.expectError('service_id', 'rgwPattern'); - formHelper.setValue('service_id', 'svc.realm'); - formHelper.expectError('service_id', 'rgwPattern'); - formHelper.setValue('service_id', 'svc.realm.'); - formHelper.expectError('service_id', 'rgwPattern'); - formHelper.setValue('service_id', '.svc.realm'); - formHelper.expectError('service_id', 'rgwPattern'); - formHelper.setValue('service_id', 'svc.realm.zone.'); - formHelper.expectError('service_id', 'rgwPattern'); - }); - - it('should submit rgw with realm and zone', () => { - formHelper.setValue('service_id', 'svc.my-realm.my-zone'); + it('should submit rgw with realm, zonegroup and zone', () => { + formHelper.setValue('service_id', 'svc'); + formHelper.setValue('realm_name', 'my-realm'); + formHelper.setValue('zone_name', 'my-zone'); + formHelper.setValue('zonegroup_name', 'my-zonegroup'); component.onSubmit(); expect(cephServiceService.create).toHaveBeenCalledWith({ service_type: 'rgw', service_id: 'svc', rgw_realm: 'my-realm', rgw_zone: 'my-zone', + rgw_zonegroup: 'my-zonegroup', placement: {}, unmanaged: false, ssl: false @@ -227,6 +216,9 @@ describe('ServiceFormComponent', () => { expect(cephServiceService.create).toHaveBeenCalledWith({ service_type: 'rgw', service_id: 'svc', + rgw_realm: null, + rgw_zone: null, + rgw_zonegroup: null, placement: {}, unmanaged: false, rgw_frontend_port: 1234, @@ -271,6 +263,9 @@ describe('ServiceFormComponent', () => { expect(cephServiceService.create).toHaveBeenCalledWith({ service_type: 'rgw', service_id: 'svc', + rgw_realm: null, + rgw_zone: null, + rgw_zonegroup: null, placement: {}, unmanaged: false, ssl: false diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/service-form/service-form.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/service-form/service-form.component.ts index 5ae2dfa50b4..00276d771bb 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/service-form/service-form.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/service-form/service-form.component.ts @@ -3,24 +3,36 @@ import { Component, Input, OnInit, ViewChild } from '@angular/core'; import { AbstractControl, Validators } from '@angular/forms'; import { ActivatedRoute, Router } from '@angular/router'; -import { NgbActiveModal, NgbTypeahead } from '@ng-bootstrap/ng-bootstrap'; +import { NgbActiveModal, NgbModalRef, NgbTypeahead } from '@ng-bootstrap/ng-bootstrap'; import _ from 'lodash'; -import { merge, Observable, Subject } from 'rxjs'; +import { forkJoin, merge, Observable, Subject, Subscription } from 'rxjs'; import { debounceTime, distinctUntilChanged, filter, map } from 'rxjs/operators'; +import { CreateRgwServiceEntitiesComponent } from '~/app/ceph/rgw/create-rgw-service-entities/create-rgw-service-entities.component'; +import { RgwRealm, RgwZonegroup, RgwZone } from '~/app/ceph/rgw/models/rgw-multisite'; import { CephServiceService } from '~/app/shared/api/ceph-service.service'; import { HostService } from '~/app/shared/api/host.service'; import { PoolService } from '~/app/shared/api/pool.service'; +import { RgwMultisiteService } from '~/app/shared/api/rgw-multisite.service'; +import { RgwRealmService } from '~/app/shared/api/rgw-realm.service'; +import { RgwZoneService } from '~/app/shared/api/rgw-zone.service'; +import { RgwZonegroupService } from '~/app/shared/api/rgw-zonegroup.service'; import { SelectMessages } from '~/app/shared/components/select/select-messages.model'; import { SelectOption } from '~/app/shared/components/select/select-option.model'; -import { ActionLabelsI18n, URLVerbs } from '~/app/shared/constants/app.constants'; +import { + ActionLabelsI18n, + TimerServiceInterval, + URLVerbs +} from '~/app/shared/constants/app.constants'; 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 } from '~/app/shared/forms/cd-validators'; import { FinishedTask } from '~/app/shared/models/finished-task'; import { CephServiceSpec } from '~/app/shared/models/service.interface'; +import { ModalService } from '~/app/shared/services/modal.service'; import { TaskWrapperService } from '~/app/shared/services/task-wrapper.service'; +import { TimerService } from '~/app/shared/services/timer.service'; @Component({ selector: 'cd-service-form', @@ -28,7 +40,8 @@ import { TaskWrapperService } from '~/app/shared/services/task-wrapper.service'; styleUrls: ['./service-form.component.scss'] }) export class ServiceFormComponent extends CdForm implements OnInit { - readonly RGW_SVC_ID_PATTERN = /^([^.]+)(\.([^.]+)\.([^.]+))?$/; + public sub = new Subscription(); + readonly MDS_SVC_ID_PATTERN = /^[a-zA-Z_.-][a-zA-Z0-9_.-]*$/; readonly SNMP_DESTINATION_PATTERN = /^[^\:]+:[0-9]/; readonly SNMP_ENGINE_ID_PATTERN = /^[0-9A-Fa-f]{10,64}/g; @@ -57,6 +70,20 @@ export class ServiceFormComponent extends CdForm implements OnInit { services: Array<CephServiceSpec> = []; pageURL: string; serviceList: CephServiceSpec[]; + multisiteInfo: object[] = []; + defaultRealmId = ''; + defaultZonegroupId = ''; + defaultZoneId = ''; + realmList: RgwRealm[] = []; + zonegroupList: RgwZonegroup[] = []; + zoneList: RgwZone[] = []; + bsModalRef: NgbModalRef; + defaultZonegroup: RgwZonegroup; + showRealmCreationForm = false; + defaultsInfo: { defaultRealmName: string; defaultZonegroupName: string; defaultZoneName: string }; + realmNames: string[]; + zonegroupNames: string[]; + zoneNames: string[]; constructor( public actionLabels: ActionLabelsI18n, @@ -66,8 +93,15 @@ export class ServiceFormComponent extends CdForm implements OnInit { private poolService: PoolService, private router: Router, private taskWrapperService: TaskWrapperService, + public timerService: TimerService, + public timerServiceVariable: TimerServiceInterval, + public rgwRealmService: RgwRealmService, + public rgwZonegroupService: RgwZonegroupService, + public rgwZoneService: RgwZoneService, + public rgwMultisiteService: RgwMultisiteService, private route: ActivatedRoute, - public activeModal: NgbActiveModal + public activeModal: NgbActiveModal, + public modalService: ModalService ) { super(); this.resource = $localize`service`; @@ -115,15 +149,7 @@ export class ServiceFormComponent extends CdForm implements OnInit { { service_type: 'rgw' }, - [ - Validators.required, - CdValidators.custom('rgwPattern', (value: string) => { - if (_.isEmpty(value)) { - return false; - } - return !this.RGW_SVC_ID_PATTERN.test(value); - }) - ] + [Validators.required] ), CdValidators.custom('uniqueName', (service_id: string) => { return this.serviceIds && this.serviceIds.includes(service_id); @@ -154,6 +180,9 @@ export class ServiceFormComponent extends CdForm implements OnInit { ], // RGW rgw_frontend_port: [null, [CdValidators.number(false)]], + realm_name: [null], + zonegroup_name: [null], + zone_name: [null], // iSCSI trusted_ip_list: [null], api_port: [null, [CdValidators.number(false)]], @@ -425,6 +454,12 @@ export class ServiceFormComponent extends CdForm implements OnInit { this.serviceForm .get('rgw_frontend_port') .setValue(response[0].spec?.rgw_frontend_port); + this.getServiceIds( + 'rgw', + response[0].spec?.rgw_realm, + response[0].spec?.rgw_zonegroup, + response[0].spec?.rgw_zone + ); this.serviceForm.get('ssl').setValue(response[0].spec?.ssl); if (response[0].spec?.ssl) { this.serviceForm @@ -493,10 +528,131 @@ export class ServiceFormComponent extends CdForm implements OnInit { } } - getServiceIds(selectedServiceType: string) { + getDefaultsEntities( + defaultRealmId: string, + defaultZonegroupId: string, + defaultZoneId: string + ): { defaultRealmName: string; defaultZonegroupName: string; defaultZoneName: string } { + const defaultRealm = this.realmList.find((x: { id: string }) => x.id === defaultRealmId); + const defaultZonegroup = this.zonegroupList.find( + (x: { id: string }) => x.id === defaultZonegroupId + ); + const defaultZone = this.zoneList.find((x: { id: string }) => x.id === defaultZoneId); + const defaultRealmName = defaultRealm !== undefined ? defaultRealm.name : null; + const defaultZonegroupName = defaultZonegroup !== undefined ? defaultZonegroup.name : 'default'; + const defaultZoneName = defaultZone !== undefined ? defaultZone.name : 'default'; + if (defaultZonegroupName === 'default' && !this.zonegroupNames.includes(defaultZonegroupName)) { + const defaultZonegroup = new RgwZonegroup(); + defaultZonegroup.name = 'default'; + this.zonegroupList.push(defaultZonegroup); + } + if (defaultZoneName === 'default' && !this.zoneNames.includes(defaultZoneName)) { + const defaultZone = new RgwZone(); + defaultZone.name = 'default'; + this.zoneList.push(defaultZone); + } + return { + defaultRealmName: defaultRealmName, + defaultZonegroupName: defaultZonegroupName, + defaultZoneName: defaultZoneName + }; + } + + getServiceIds( + selectedServiceType: string, + realm_name?: string, + zonegroup_name?: string, + zone_name?: string + ) { this.serviceIds = this.serviceList ?.filter((service) => service['service_type'] === selectedServiceType) .map((service) => service['service_id']); + + if (selectedServiceType === 'rgw') { + const observables = [ + this.rgwRealmService.getAllRealmsInfo(), + this.rgwZonegroupService.getAllZonegroupsInfo(), + this.rgwZoneService.getAllZonesInfo() + ]; + this.sub = forkJoin(observables).subscribe( + (multisiteInfo: [object, object, object]) => { + this.multisiteInfo = multisiteInfo; + this.realmList = + this.multisiteInfo[0] !== undefined && this.multisiteInfo[0].hasOwnProperty('realms') + ? this.multisiteInfo[0]['realms'] + : []; + this.zonegroupList = + this.multisiteInfo[1] !== undefined && + this.multisiteInfo[1].hasOwnProperty('zonegroups') + ? this.multisiteInfo[1]['zonegroups'] + : []; + this.zoneList = + this.multisiteInfo[2] !== undefined && this.multisiteInfo[2].hasOwnProperty('zones') + ? this.multisiteInfo[2]['zones'] + : []; + this.realmNames = this.realmList.map((realm) => { + return realm['name']; + }); + this.zonegroupNames = this.zonegroupList.map((zonegroup) => { + return zonegroup['name']; + }); + this.zoneNames = this.zoneList.map((zone) => { + return zone['name']; + }); + this.defaultRealmId = multisiteInfo[0]['default_realm']; + this.defaultZonegroupId = multisiteInfo[1]['default_zonegroup']; + this.defaultZoneId = multisiteInfo[2]['default_zone']; + this.defaultsInfo = this.getDefaultsEntities( + this.defaultRealmId, + this.defaultZonegroupId, + this.defaultZoneId + ); + if (!this.editing) { + this.serviceForm.get('realm_name').setValue(this.defaultsInfo['defaultRealmName']); + this.serviceForm + .get('zonegroup_name') + .setValue(this.defaultsInfo['defaultZonegroupName']); + this.serviceForm.get('zone_name').setValue(this.defaultsInfo['defaultZoneName']); + } else { + if (realm_name && !this.realmNames.includes(realm_name)) { + const realm = new RgwRealm(); + realm.name = realm_name; + this.realmList.push(realm); + } + if (zonegroup_name && !this.zonegroupNames.includes(zonegroup_name)) { + const zonegroup = new RgwZonegroup(); + zonegroup.name = zonegroup_name; + this.zonegroupList.push(zonegroup); + } + if (zone_name && !this.zoneNames.includes(zone_name)) { + const zone = new RgwZone(); + zone.name = zone_name; + this.zoneList.push(zone); + } + if (zonegroup_name === undefined && zone_name === undefined) { + zonegroup_name = 'default'; + zone_name = 'default'; + } + this.serviceForm.get('realm_name').setValue(realm_name); + this.serviceForm.get('zonegroup_name').setValue(zonegroup_name); + this.serviceForm.get('zone_name').setValue(zone_name); + } + if (this.realmList.length === 0) { + this.showRealmCreationForm = true; + } else { + this.showRealmCreationForm = false; + } + }, + (_error) => { + const defaultZone = new RgwZone(); + defaultZone.name = 'default'; + const defaultZonegroup = new RgwZonegroup(); + defaultZonegroup.name = 'default'; + this.zoneList.push(defaultZone); + this.zonegroupList.push(defaultZonegroup); + } + ); + } } disableForEditing(serviceType: string) { @@ -559,12 +715,11 @@ export class ServiceFormComponent extends CdForm implements OnInit { }; let svcId: string; if (serviceType === 'rgw') { - const svcIdMatch = values['service_id'].match(this.RGW_SVC_ID_PATTERN); - svcId = svcIdMatch[1]; - if (svcIdMatch[3]) { - serviceSpec['rgw_realm'] = svcIdMatch[3]; - serviceSpec['rgw_zone'] = svcIdMatch[4]; - } + serviceSpec['rgw_realm'] = values['realm_name'] ? values['realm_name'] : null; + serviceSpec['rgw_zonegroup'] = + values['zonegroup_name'] !== 'default' ? values['zonegroup_name'] : null; + serviceSpec['rgw_zone'] = values['zone_name'] !== 'default' ? values['zone_name'] : null; + svcId = values['service_id']; } else { svcId = values['service_id']; } @@ -705,4 +860,13 @@ export class ServiceFormComponent extends CdForm implements OnInit { this.serviceForm.get('snmp_v3_priv_password').clearValidators(); } } + + createMultisiteSetup() { + this.bsModalRef = this.modalService.show(CreateRgwServiceEntitiesComponent, { + size: 'lg' + }); + this.bsModalRef.componentInstance.submitAction.subscribe(() => { + this.getServiceIds('rgw'); + }); + } } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/services.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/services.component.ts index 1234a684e6c..82a975c9df4 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/services.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/services.component.ts @@ -90,8 +90,8 @@ export class ServicesComponent extends ListWithDetails implements OnChanges, OnI icon: Icons.add, click: () => this.openModal(), name: this.actionLabels.CREATE, - canBePrimary: (selection: CdTableSelection) => !selection.hasSelection, - disable: (selection: CdTableSelection) => this.getDisable('create', selection) + canBePrimary: (selection: CdTableSelection) => !selection.hasSelection + // disable: (selection: CdTableSelection) => this.getDisable('create', selection) }, { permission: 'update', diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/create-rgw-service-entities/create-rgw-service-entities.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/create-rgw-service-entities/create-rgw-service-entities.component.html new file mode 100644 index 00000000000..b6e4f805d0c --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/create-rgw-service-entities/create-rgw-service-entities.component.html @@ -0,0 +1,70 @@ +<cd-modal [modalRef]="activeModal"> + <ng-container i18n="form title" + class="modal-title">Create Realm/Zonegroup/Zone + </ng-container> + + <ng-container class="modal-content"> + <form name="createMultisiteEntitiesForm" + #formDir="ngForm" + [formGroup]="createMultisiteEntitiesForm" + novalidate> + <div class="modal-body"> + <cd-alert-panel type="info" + spacingClass="mb-3">The realm/zonegroup/zone created will be set as default and master. + </cd-alert-panel> + <div class="form-group row"> + <label class="cd-col-form-label required" + for="realmName" + i18n>Realm Name</label> + <div class="cd-col-form-input"> + <input class="form-control" + type="text" + placeholder="Realm name..." + id="realmName" + name="realmName" + formControlName="realmName"> + <span class="invalid-feedback" + *ngIf="createMultisiteEntitiesForm.showError('realmName', formDir, 'required')" + i18n>This field is required.</span> + </div> + </div> + <div class="form-group row"> + <label class="cd-col-form-label required" + for="zonegroupName" + i18n>Zonegroup Name</label> + <div class="cd-col-form-input"> + <input class="form-control" + type="text" + placeholder="Zonegroup name..." + id="zonegroupName" + name="zonegroupName" + formControlName="zonegroupName"> + <span class="invalid-feedback" + *ngIf="createMultisiteEntitiesForm.showError('zonegroupName', formDir, 'required')" + i18n>This field is required.</span> + </div> + </div> + <div class="form-group row"> + <label class="cd-col-form-label required" + for="zoneName" + i18n>Zone Name</label> + <div class="cd-col-form-input"> + <input class="form-control" + type="text" + placeholder="Zone name..." + id="zoneName" + name="zoneName" + formControlName="zoneName"> + <span class="invalid-feedback" + *ngIf="createMultisiteEntitiesForm.showError('zoneName', formDir, 'required')" + i18n>This field is required.</span> + </div> + </div> + </div> + <div class="modal-footer"> + <cd-form-button-panel (submitActionEvent)="submit()" + [form]="createMultisiteEntitiesForm"></cd-form-button-panel> + </div> + </form> + </ng-container> +</cd-modal> diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/create-rgw-service-entities/create-rgw-service-entities.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/create-rgw-service-entities/create-rgw-service-entities.component.scss new file mode 100644 index 00000000000..e69de29bb2d --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/create-rgw-service-entities/create-rgw-service-entities.component.scss diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/create-rgw-service-entities/create-rgw-service-entities.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/create-rgw-service-entities/create-rgw-service-entities.component.spec.ts new file mode 100644 index 00000000000..5e6621b3b83 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/create-rgw-service-entities/create-rgw-service-entities.component.spec.ts @@ -0,0 +1,38 @@ +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ReactiveFormsModule } from '@angular/forms'; +import { RouterTestingModule } from '@angular/router/testing'; +import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; +import { ToastrModule } from 'ngx-toastr'; +import { SharedModule } from '~/app/shared/shared.module'; + +import { CreateRgwServiceEntitiesComponent } from './create-rgw-service-entities.component'; + +describe('CreateRgwServiceEntitiesComponent', () => { + let component: CreateRgwServiceEntitiesComponent; + let fixture: ComponentFixture<CreateRgwServiceEntitiesComponent>; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ + SharedModule, + ReactiveFormsModule, + RouterTestingModule, + HttpClientTestingModule, + ToastrModule.forRoot() + ], + providers: [NgbActiveModal], + declarations: [CreateRgwServiceEntitiesComponent] + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(CreateRgwServiceEntitiesComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/create-rgw-service-entities/create-rgw-service-entities.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/create-rgw-service-entities/create-rgw-service-entities.component.ts new file mode 100644 index 00000000000..041915186c0 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/create-rgw-service-entities/create-rgw-service-entities.component.ts @@ -0,0 +1,99 @@ +import { Component, EventEmitter, Output } from '@angular/core'; +import { FormControl, Validators } from '@angular/forms'; +import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; +import { RgwMultisiteService } from '~/app/shared/api/rgw-multisite.service'; +import { RgwRealmService } from '~/app/shared/api/rgw-realm.service'; +import { RgwZoneService } from '~/app/shared/api/rgw-zone.service'; +import { RgwZonegroupService } from '~/app/shared/api/rgw-zonegroup.service'; +import { ActionLabelsI18n } from '~/app/shared/constants/app.constants'; +import { CdFormGroup } from '~/app/shared/forms/cd-form-group'; +import { ModalService } from '~/app/shared/services/modal.service'; +import { NotificationService } from '~/app/shared/services/notification.service'; +import { RgwRealm, RgwZonegroup, RgwZone, SystemKey } from '../models/rgw-multisite'; +import { NotificationType } from '~/app/shared/enum/notification-type.enum'; +import { Subscription } from 'rxjs'; + +@Component({ + selector: 'cd-create-rgw-service-entities', + templateUrl: './create-rgw-service-entities.component.html', + styleUrls: ['./create-rgw-service-entities.component.scss'] +}) +export class CreateRgwServiceEntitiesComponent { + public sub = new Subscription(); + createMultisiteEntitiesForm: CdFormGroup; + realm: RgwRealm; + zonegroup: RgwZonegroup; + zone: RgwZone; + + @Output() + submitAction = new EventEmitter(); + + constructor( + public activeModal: NgbActiveModal, + public actionLabels: ActionLabelsI18n, + public rgwMultisiteService: RgwMultisiteService, + public rgwZoneService: RgwZoneService, + public notificationService: NotificationService, + public rgwZonegroupService: RgwZonegroupService, + public rgwRealmService: RgwRealmService, + public modalService: ModalService + ) { + this.createForm(); + } + + createForm() { + this.createMultisiteEntitiesForm = new CdFormGroup({ + realmName: new FormControl(null, { + validators: [Validators.required] + }), + zonegroupName: new FormControl(null, { + validators: [Validators.required] + }), + zoneName: new FormControl(null, { + validators: [Validators.required] + }) + }); + } + + submit() { + const values = this.createMultisiteEntitiesForm.value; + this.realm = new RgwRealm(); + this.realm.name = values['realmName']; + this.zonegroup = new RgwZonegroup(); + this.zonegroup.name = values['zonegroupName']; + this.zonegroup.endpoints = ''; + this.zone = new RgwZone(); + this.zone.name = values['zoneName']; + this.zone.endpoints = ''; + this.zone.system_key = new SystemKey(); + this.zone.system_key.access_key = ''; + this.zone.system_key.secret_key = ''; + this.rgwRealmService + .create(this.realm, true) + .toPromise() + .then(() => { + this.rgwZonegroupService + .create(this.realm, this.zonegroup, true, true) + .toPromise() + .then(() => { + this.rgwZoneService + .create(this.zone, this.zonegroup, true, true, this.zone.endpoints) + .toPromise() + .then(() => { + this.notificationService.show( + NotificationType.success, + $localize`Realm/Zonegroup/Zone created successfully` + ); + this.submitAction.emit(); + this.activeModal.close(); + }) + .catch(() => { + this.notificationService.show( + NotificationType.error, + $localize`Realm/Zonegroup/Zone creation failed` + ); + }); + }); + }); + } +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/models/rgw-multisite-zone-deletion-form/rgw-multisite-zone-deletion-form.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/models/rgw-multisite-zone-deletion-form/rgw-multisite-zone-deletion-form.component.spec.ts index 947ef3cbfbc..8cdd79e6549 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/models/rgw-multisite-zone-deletion-form/rgw-multisite-zone-deletion-form.component.spec.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/models/rgw-multisite-zone-deletion-form/rgw-multisite-zone-deletion-form.component.spec.ts @@ -5,6 +5,7 @@ import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; import { ToastrModule } from 'ngx-toastr'; import { SharedModule } from '~/app/shared/shared.module'; import { configureTestBed } from '~/testing/unit-test-helper'; +import { RgwZone } from '../rgw-multisite'; import { RgwMultisiteZoneDeletionFormComponent } from './rgw-multisite-zone-deletion-form.component'; @@ -21,6 +22,7 @@ describe('RgwMultisiteZoneDeletionFormComponent', () => { beforeEach(() => { fixture = TestBed.createComponent(RgwMultisiteZoneDeletionFormComponent); component = fixture.componentInstance; + component.zone = new RgwZone(); fixture.detectChanges(); }); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/models/rgw-multisite-zonegroup-deletion-form/rgw-multisite-zonegroup-deletion-form.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/models/rgw-multisite-zonegroup-deletion-form/rgw-multisite-zonegroup-deletion-form.component.spec.ts index 3ee39f2e9fb..2c4059f251a 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/models/rgw-multisite-zonegroup-deletion-form/rgw-multisite-zonegroup-deletion-form.component.spec.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/models/rgw-multisite-zonegroup-deletion-form/rgw-multisite-zonegroup-deletion-form.component.spec.ts @@ -5,6 +5,7 @@ import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; import { ToastrModule } from 'ngx-toastr'; import { SharedModule } from '~/app/shared/shared.module'; import { configureTestBed } from '~/testing/unit-test-helper'; +import { RgwZonegroup } from '../rgw-multisite'; import { RgwMultisiteZonegroupDeletionFormComponent } from './rgw-multisite-zonegroup-deletion-form.component'; @@ -21,6 +22,7 @@ describe('RgwMultisiteZonegroupDeletionFormComponent', () => { beforeEach(() => { fixture = TestBed.createComponent(RgwMultisiteZonegroupDeletionFormComponent); component = fixture.componentInstance; + component.zonegroup = new RgwZonegroup(); fixture.detectChanges(); }); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/models/rgw-multisite.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/models/rgw-multisite.ts index fb0ce154900..1729f6418b2 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/models/rgw-multisite.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/models/rgw-multisite.ts @@ -10,7 +10,7 @@ export class RgwZonegroup { name: string; api_name: string; is_master: boolean; - endpoints: string[]; + endpoints: string; hostnames: string[]; hostnames_s3website: string[]; master_zone: string; @@ -39,9 +39,14 @@ export class RgwZone { user_swift_pool: string; user_uid_pool: string; otp_pool: string; - system_key: object; + system_key: SystemKey; placement_pools: any[]; realm_id: string; notif_pool: string; - endpoints: string[]; + endpoints: string; +} + +export class SystemKey { + access_key: string; + secret_key: string; } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-details/rgw-multisite-details.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-details/rgw-multisite-details.component.html index 757d5f78ae3..331ce22e9a2 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-details/rgw-multisite-details.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-details/rgw-multisite-details.component.html @@ -1,6 +1,14 @@ <div class="row"> <div class="col-sm-12 col-lg-12"> <div> + <cd-alert-panel *ngIf="!rgwModuleStatus" + type="info" + spacingClass="mb-3" + i18n>You need to enable the rgw module to access the import/export feature. + <a class="text-decoration-underline" + (click)="enableRgwModule()"> + Enable RGW Module</a> + </cd-alert-panel> <cd-table-actions class="btn-group mb-4 me-2" [permission]="permission" [selection]="selection" @@ -14,6 +22,18 @@ [tableActions]="migrateTableAction"> </cd-table-actions> </span> + <cd-table-actions class="btn-group mb-4 me-2" + [permission]="permission" + [btnColor]="'light'" + [selection]="selection" + [tableActions]="importAction"> + </cd-table-actions> + <cd-table-actions class="btn-group mb-4 me-2" + [permission]="permission" + [btnColor]="'light'" + [selection]="selection" + [tableActions]="exportAction"> + </cd-table-actions> </div> <div class="card"> <div class="card-header" @@ -31,6 +51,12 @@ let-node> <span *ngIf="node.data.name" class="me-3"> + <span *ngIf="(node.data.show_warning)"> + <i class="text-danger" + i18n-title + [title]="node.data.warning_message" + [ngClass]="icons.danger"></i> + </span> <i [ngClass]="node.data.icon"></i> {{ node.data.name }} </span> @@ -42,6 +68,10 @@ *ngIf="node.data.is_master"> master </span> + <span class="badge badge-warning me-2" + *ngIf="node.data.secondary_zone"> + secondary-zone + </span> <div class="btn-group align-inline-btns" *ngIf="node.isFocused" role="group"> @@ -50,7 +80,7 @@ <button type="button" class="btn btn-light dropdown-toggle-split ms-1" (click)="openModal(node, true)" - [disabled]="getDisable()"> + [disabled]="getDisable() || node.data.secondary_zone"> <i [ngClass]="[icons.edit]"></i> </button> </div> @@ -58,7 +88,7 @@ i18n-title> <button type="button" class="btn btn-light ms-1" - [disabled]="isDeleteDisabled(node)" + [disabled]="isDeleteDisabled(node) || node.data.secondary_zone" (click)="delete(node)"> <i [ngClass]="[icons.destroy]"></i> </button> @@ -71,9 +101,7 @@ *ngIf="metadata"> <legend>{{ metadataTitle }}</legend> <cd-table-key-value cdTableDetail - [data]="metadata" - [renderObjects]="true" - [customCss]="customCss"> + [data]="metadata"> </cd-table-key-value> </div> </div> diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-details/rgw-multisite-details.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-details/rgw-multisite-details.component.scss index 537b53a519c..4eefc8892b8 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-details/rgw-multisite-details.component.scss +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-details/rgw-multisite-details.component.scss @@ -11,3 +11,9 @@ .btn:disabled { pointer-events: none; } + +cd-table-key-value { + ::ng-deep .table-scroller { + overflow: unset; + } +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-details/rgw-multisite-details.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-details/rgw-multisite-details.component.spec.ts index 7f1c0b19769..a2fd6c6f331 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-details/rgw-multisite-details.component.spec.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-details/rgw-multisite-details.component.spec.ts @@ -6,6 +6,7 @@ import { ToastrModule } from 'ngx-toastr'; import { SharedModule } from '~/app/shared/shared.module'; import { RgwMultisiteDetailsComponent } from './rgw-multisite-details.component'; +import { RouterTestingModule } from '@angular/router/testing'; describe('RgwMultisiteDetailsComponent', () => { let component: RgwMultisiteDetailsComponent; @@ -15,7 +16,13 @@ describe('RgwMultisiteDetailsComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ declarations: [RgwMultisiteDetailsComponent], - imports: [HttpClientTestingModule, TreeModule, SharedModule, ToastrModule.forRoot()] + imports: [ + HttpClientTestingModule, + TreeModule, + SharedModule, + ToastrModule.forRoot(), + RouterTestingModule + ] }).compileComponents(); }); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-details/rgw-multisite-details.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-details/rgw-multisite-details.component.ts index 29f9b9d9e9c..5a667cbe41f 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-details/rgw-multisite-details.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-details/rgw-multisite-details.component.ts @@ -8,7 +8,8 @@ import { } from '@circlon/angular-tree-component'; import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; import _ from 'lodash'; -import { forkJoin, Subscription } from 'rxjs'; + +import { forkJoin, Subscription, timer as observableTimer } from 'rxjs'; import { RgwRealmService } from '~/app/shared/api/rgw-realm.service'; import { RgwZoneService } from '~/app/shared/api/rgw-zone.service'; import { RgwZonegroupService } from '~/app/shared/api/rgw-zonegroup.service'; @@ -27,9 +28,15 @@ import { RgwRealm, RgwZone, RgwZonegroup } from '../models/rgw-multisite'; import { RgwMultisiteMigrateComponent } from '../rgw-multisite-migrate/rgw-multisite-migrate.component'; import { RgwMultisiteZoneDeletionFormComponent } from '../models/rgw-multisite-zone-deletion-form/rgw-multisite-zone-deletion-form.component'; import { RgwMultisiteZonegroupDeletionFormComponent } from '../models/rgw-multisite-zonegroup-deletion-form/rgw-multisite-zonegroup-deletion-form.component'; +import { RgwMultisiteExportComponent } from '../rgw-multisite-export/rgw-multisite-export.component'; +import { RgwMultisiteImportComponent } from '../rgw-multisite-import/rgw-multisite-import.component'; import { RgwMultisiteRealmFormComponent } from '../rgw-multisite-realm-form/rgw-multisite-realm-form.component'; import { RgwMultisiteZoneFormComponent } from '../rgw-multisite-zone-form/rgw-multisite-zone-form.component'; import { RgwMultisiteZonegroupFormComponent } from '../rgw-multisite-zonegroup-form/rgw-multisite-zonegroup-form.component'; +import { RgwDaemonService } from '~/app/shared/api/rgw-daemon.service'; +import { MgrModuleService } from '~/app/shared/api/mgr-module.service'; +import { BlockUI, NgBlockUI } from 'ng-block-ui'; +import { Router } from '@angular/router'; @Component({ selector: 'cd-rgw-multisite-details', @@ -43,15 +50,21 @@ export class RgwMultisiteDetailsComponent implements OnDestroy, OnInit { messages = { noDefaultRealm: $localize`Please create a default realm first to enable this feature`, - noMasterZone: $localize`Please create a master zone for each zonegroup to enable this feature`, - disableMigrate: $localize`Deployment is already migrated to multi-site system.` + noMasterZone: $localize`Please create a master zone for each zonegroups to enable this feature`, + noRealmExists: $localize`No realm exists`, + disableExport: $localize`Please create master zonegroup and master zone for each of the realms` }; + @BlockUI() + blockUI: NgBlockUI; + icons = Icons; permission: Permission; selection = new CdTableSelection(); createTableActions: CdTableAction[]; migrateTableAction: CdTableAction[]; + importAction: CdTableAction[]; + exportAction: CdTableAction[]; loadingIndicator = true; nodes: object[] = []; treeOptions: ITreeOptions = { @@ -82,6 +95,9 @@ export class RgwMultisiteDetailsComponent implements OnDestroy, OnInit { showMigrateAction: boolean = false; editTitle: string = 'Edit'; deleteTitle: string = 'Delete'; + disableExport = true; + rgwModuleStatus: boolean; + rgwModuleData: string | any[] = []; constructor( private modalService: ModalService, @@ -89,39 +105,15 @@ export class RgwMultisiteDetailsComponent implements OnDestroy, OnInit { private authStorageService: AuthStorageService, public actionLabels: ActionLabelsI18n, public timerServiceVariable: TimerServiceInterval, + public router: Router, public rgwRealmService: RgwRealmService, public rgwZonegroupService: RgwZonegroupService, public rgwZoneService: RgwZoneService, + public rgwDaemonService: RgwDaemonService, + public mgrModuleService: MgrModuleService, private notificationService: NotificationService ) { this.permission = this.authStorageService.getPermissions().rgw; - const createRealmAction: CdTableAction = { - permission: 'create', - icon: Icons.add, - name: this.actionLabels.CREATE + ' Realm', - click: () => this.openModal('realm') - }; - const createZonegroupAction: CdTableAction = { - permission: 'create', - icon: Icons.add, - name: this.actionLabels.CREATE + ' Zonegroup', - click: () => this.openModal('zonegroup'), - disable: () => this.getDisable() - }; - const createZoneAction: CdTableAction = { - permission: 'create', - icon: Icons.add, - name: this.actionLabels.CREATE + ' Zone', - click: () => this.openModal('zone') - }; - const migrateMultsiteAction: CdTableAction = { - permission: 'read', - icon: Icons.exchange, - name: this.actionLabels.MIGRATE, - click: () => this.openMigrateModal() - }; - this.createTableActions = [createRealmAction, createZonegroupAction, createZoneAction]; - this.migrateTableAction = [migrateMultsiteAction]; } openModal(entity: any, edit = false) { @@ -158,7 +150,100 @@ export class RgwMultisiteDetailsComponent implements OnDestroy, OnInit { }); } + openImportModal() { + const initialState = { + multisiteInfo: this.multisiteInfo + }; + this.bsModalRef = this.modalService.show(RgwMultisiteImportComponent, initialState, { + size: 'lg' + }); + } + + openExportModal() { + const initialState = { + defaultsInfo: this.defaultsInfo, + multisiteInfo: this.multisiteInfo + }; + this.bsModalRef = this.modalService.show(RgwMultisiteExportComponent, initialState, { + size: 'lg' + }); + } + + getDisableExport() { + this.realms.forEach((realm: any) => { + this.zonegroups.forEach((zonegroup) => { + if (realm.id === zonegroup.realm_id) { + if (zonegroup.is_master && zonegroup.master_zone !== '') { + this.disableExport = false; + } + } + }); + }); + if (!this.rgwModuleStatus) { + return true; + } + if (this.realms.length < 1) { + return this.messages.noRealmExists; + } else if (this.disableExport) { + return this.messages.disableExport; + } else { + return false; + } + } + + getDisableImport() { + if (!this.rgwModuleStatus) { + return true; + } else { + return false; + } + } + ngOnInit() { + const createRealmAction: CdTableAction = { + permission: 'create', + icon: Icons.add, + name: this.actionLabels.CREATE + ' Realm', + click: () => this.openModal('realm') + }; + const createZonegroupAction: CdTableAction = { + permission: 'create', + icon: Icons.add, + name: this.actionLabels.CREATE + ' Zonegroup', + click: () => this.openModal('zonegroup'), + disable: () => this.getDisable() + }; + const createZoneAction: CdTableAction = { + permission: 'create', + icon: Icons.add, + name: this.actionLabels.CREATE + ' Zone', + click: () => this.openModal('zone') + }; + const migrateMultsiteAction: CdTableAction = { + permission: 'read', + icon: Icons.exchange, + name: this.actionLabels.MIGRATE, + click: () => this.openMigrateModal() + }; + const importMultsiteAction: CdTableAction = { + permission: 'read', + icon: Icons.download, + name: this.actionLabels.IMPORT, + click: () => this.openImportModal(), + disable: () => this.getDisableImport() + }; + const exportMultsiteAction: CdTableAction = { + permission: 'read', + icon: Icons.upload, + name: this.actionLabels.EXPORT, + click: () => this.openExportModal(), + disable: () => this.getDisableExport() + }; + this.createTableActions = [createRealmAction, createZonegroupAction, createZoneAction]; + this.migrateTableAction = [migrateMultsiteAction]; + this.importAction = [importMultsiteAction]; + this.exportAction = [exportMultsiteAction]; + const observables = [ this.rgwRealmService.getAllRealmsInfo(), this.rgwZonegroupService.getAllZonegroupsInfo(), @@ -174,8 +259,24 @@ export class RgwMultisiteDetailsComponent implements OnDestroy, OnInit { }, (_error) => {} ); + this.mgrModuleService.list().subscribe((moduleData: any) => { + this.rgwModuleData = moduleData.filter((module: object) => module['name'] === 'rgw'); + if (this.rgwModuleData.length > 0) { + this.rgwModuleStatus = this.rgwModuleData[0].enabled; + } + }); } + /* setConfigValues() { + this.rgwDaemonService + .setMultisiteConfig( + this.defaultsInfo['defaultRealmName'], + this.defaultsInfo['defaultZonegroupName'], + this.defaultsInfo['defaultZoneName'] + ) + .subscribe(() => {}); + }*/ + ngOnDestroy() { this.sub.unsubscribe(); } @@ -215,6 +316,7 @@ export class RgwMultisiteDetailsComponent implements OnDestroy, OnInit { const zoneResult = this.rgwZoneService.getZoneTree( zone, this.defaultZoneId, + this.zones, zonegroup, realm ); @@ -244,7 +346,12 @@ export class RgwMultisiteDetailsComponent implements OnDestroy, OnInit { if (!this.realmIds.includes(zonegroup.realm_id)) { rootNodes = this.rgwZonegroupService.getZonegroupTree(zonegroup, this.defaultZonegroupId); for (const zone of zonegroup.zones) { - const zoneResult = this.rgwZoneService.getZoneTree(zone, this.defaultZoneId, zonegroup); + const zoneResult = this.rgwZoneService.getZoneTree( + zone, + this.defaultZoneId, + this.zones, + zonegroup + ); firstChildNodes = zoneResult['nodes']; this.zoneIds = this.zoneIds.concat(zoneResult['zoneIds']); allFirstChildNodes.push(firstChildNodes); @@ -262,7 +369,7 @@ export class RgwMultisiteDetailsComponent implements OnDestroy, OnInit { // get tree for standalone zones(zones that do not belong to a zonegroup) for (const zone of this.zones) { if (this.zoneIds.length > 0 && !this.zoneIds.includes(zone.id)) { - const zoneResult = this.rgwZoneService.getZoneTree(zone, this.defaultZoneId); + const zoneResult = this.rgwZoneService.getZoneTree(zone, this.defaultZoneId, this.zones); rootNodes = zoneResult['nodes']; allNodes.push(rootNodes); rootNodes = {}; @@ -428,4 +535,46 @@ export class RgwMultisiteDetailsComponent implements OnDestroy, OnInit { }); } } + + enableRgwModule() { + let $obs; + const fnWaitUntilReconnected = () => { + observableTimer(2000).subscribe(() => { + // Trigger an API request to check if the connection is + // re-established. + this.mgrModuleService.list().subscribe( + () => { + // Resume showing the notification toasties. + this.notificationService.suspendToasties(false); + // Unblock the whole UI. + this.blockUI.stop(); + // Reload the data table content. + this.notificationService.show(NotificationType.success, $localize`Enabled RGW Module`); + this.router.navigateByUrl('/', { skipLocationChange: true }).then(() => { + this.router.navigate(['/rgw/multisite']); + }); + // Reload the data table content. + }, + () => { + fnWaitUntilReconnected(); + } + ); + }); + }; + + if (!this.rgwModuleStatus) { + $obs = this.mgrModuleService.enable('rgw'); + } + $obs.subscribe( + () => undefined, + () => { + // Suspend showing the notification toasties. + this.notificationService.suspendToasties(true); + // Block the whole UI to prevent user interactions until + // the connection to the backend is reestablished + this.blockUI.start($localize`Reconnecting, please wait ...`); + fnWaitUntilReconnected(); + } + ); + } } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-export/rgw-multisite-export.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-export/rgw-multisite-export.component.html new file mode 100644 index 00000000000..77988493693 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-export/rgw-multisite-export.component.html @@ -0,0 +1,65 @@ +<cd-modal [modalRef]="activeModal"> + <ng-container i18n="form title" + class="modal-title">Export Multi-site Realm Token</ng-container> + + <ng-container class="modal-content"> + <form name="exportTokenForm" + #frm="ngForm" + [formGroup]="exportTokenForm"> + <span *ngIf="loading" + class="d-flex justify-content-center"> + <i [ngClass]="[icons.large3x, icons.spinner, icons.spin]"></i></span> + <div class="modal-body" + *ngIf="!loading"> + <cd-alert-panel *ngIf="!tokenValid" + type="warning" + class="mx-3" + i18n> + <div *ngFor="let realminfo of realms"> + <b>{{realminfo.realm}}</b> - + {{realminfo.token}} + </div> + </cd-alert-panel> + <div *ngFor="let realminfo of realms"> + <div class="form-group row"> + <label class="cd-col-form-label" + for="realmName" + i18n>Realm Name + </label> + <div class="cd-col-form-input"> + <input id="realmName" + name="realmName" + type="text" + value="{{ realminfo.realm }}" + readonly> + </div> + </div> + <div class="form-group row"> + <label class="cd-col-form-label" + for="token" + i18n>Token + </label> + <div class="cd-col-form-input"> + <input id="realmToken" + name="realmToken" + type="text" + value="{{ realminfo.token }}" + class="me-2 mb-4" + readonly> + <cd-copy-2-clipboard-button + source="{{ realminfo.token }}" + [byId]="false"> + </cd-copy-2-clipboard-button> + </div> + <hr *ngIf="realms.length > 1"> + </div> + </div> + </div> + <div class="modal-footer"> + <cd-back-button class="m-2 float-end" + aria-label="Close" + (backAction)="activeModal.close()"></cd-back-button> + </div> + </form> + </ng-container> +</cd-modal> diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-export/rgw-multisite-export.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-export/rgw-multisite-export.component.scss new file mode 100644 index 00000000000..e69de29bb2d --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-export/rgw-multisite-export.component.scss diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-export/rgw-multisite-export.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-export/rgw-multisite-export.component.spec.ts new file mode 100644 index 00000000000..13c33d339d9 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-export/rgw-multisite-export.component.spec.ts @@ -0,0 +1,38 @@ +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ReactiveFormsModule } from '@angular/forms'; +import { RouterTestingModule } from '@angular/router/testing'; +import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; +import { ToastrModule } from 'ngx-toastr'; +import { SharedModule } from '~/app/shared/shared.module'; + +import { RgwMultisiteExportComponent } from './rgw-multisite-export.component'; + +describe('RgwMultisiteExportComponent', () => { + let component: RgwMultisiteExportComponent; + let fixture: ComponentFixture<RgwMultisiteExportComponent>; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ + SharedModule, + ReactiveFormsModule, + RouterTestingModule, + HttpClientTestingModule, + ToastrModule.forRoot() + ], + declarations: [RgwMultisiteExportComponent], + providers: [NgbActiveModal] + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(RgwMultisiteExportComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-export/rgw-multisite-export.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-export/rgw-multisite-export.component.ts new file mode 100644 index 00000000000..0b1b2428729 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-export/rgw-multisite-export.component.ts @@ -0,0 +1,62 @@ +import { AfterViewChecked, ChangeDetectorRef, Component, OnInit } from '@angular/core'; +import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; +import { RgwRealmService } from '~/app/shared/api/rgw-realm.service'; +import { ActionLabelsI18n } from '~/app/shared/constants/app.constants'; +import { CdFormGroup } from '~/app/shared/forms/cd-form-group'; +import { NotificationService } from '~/app/shared/services/notification.service'; +import { RgwRealm } from '../models/rgw-multisite'; +import { Icons } from '~/app/shared/enum/icons.enum'; + +@Component({ + selector: 'cd-rgw-multisite-export', + templateUrl: './rgw-multisite-export.component.html', + styleUrls: ['./rgw-multisite-export.component.scss'] +}) +export class RgwMultisiteExportComponent implements OnInit, AfterViewChecked { + exportTokenForm: CdFormGroup; + realms: any; + realmList: RgwRealm[]; + multisiteInfo: any; + tokenValid = false; + loading = true; + icons = Icons; + + constructor( + public activeModal: NgbActiveModal, + public rgwRealmService: RgwRealmService, + public actionLabels: ActionLabelsI18n, + public notificationService: NotificationService, + private readonly changeDetectorRef: ChangeDetectorRef + ) { + this.createForm(); + } + + createForm() { + this.exportTokenForm = new CdFormGroup({}); + } + + onSubmit() { + this.activeModal.close(); + } + + ngOnInit(): void { + this.rgwRealmService.getRealmTokens().subscribe((data: object[]) => { + this.loading = false; + this.realms = data; + var base64Matcher = new RegExp( + '^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=|[A-Za-z0-9+/]{4})$' + ); + this.realms.forEach((realmInfo: any) => { + if (base64Matcher.test(realmInfo.token)) { + this.tokenValid = true; + } else { + this.tokenValid = false; + } + }); + }); + } + + ngAfterViewChecked(): void { + this.changeDetectorRef.detectChanges(); + } +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-import/rgw-multisite-import.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-import/rgw-multisite-import.component.html new file mode 100644 index 00000000000..823e8e8d4f3 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-import/rgw-multisite-import.component.html @@ -0,0 +1,56 @@ +<cd-modal [modalRef]="activeModal"> + <ng-container i18n="form title" + class="modal-title">Import Multi-site Token</ng-container> + + <ng-container class="modal-content"> + <form name="importTokenForm" + #frm="ngForm" + [formGroup]="importTokenForm"> + <div class="modal-body"> + <cd-alert-panel type="info" + spacingClass="mb-3">Please create a rgw service using the secondary zone(created after submitting this form) to start the replication between zones. + </cd-alert-panel> + <div class="form-group row"> + <label class="cd-col-form-label required" + for="realmToken" + i18n>Token + </label> + <div class="cd-col-form-input"> + <input id="realmToken" + name="realmToken" + class="form-control" + type="text" + formControlName="realmToken"> + <span class="invalid-feedback" + *ngIf="importTokenForm.showError('realmToken', frm, 'required')" + i18n>This field is required.</span> + </div> + </div> + <div class="form-group row"> + <label class="cd-col-form-label required" + for="zoneName" + i18n>Secondary Zone Name</label> + <div class="cd-col-form-input"> + <input class="form-control" + type="text" + placeholder="Zone name..." + id="zoneName" + name="zoneName" + formControlName="zoneName"> + <span class="invalid-feedback" + *ngIf="importTokenForm.showError('zoneName', frm, 'required')" + i18n>This field is required.</span> + <span class="invalid-feedback" + *ngIf="importTokenForm.showError('zoneName', frm, 'uniqueName')" + i18n>The chosen zone name is already in use.</span> + </div> + </div> + </div> + <div class="modal-footer"> + <cd-form-button-panel (submitActionEvent)="onSubmit()" + [submitText]="actionLabels.IMPORT" + [form]="importTokenForm"></cd-form-button-panel> + </div> + </form> + </ng-container> +</cd-modal> diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-import/rgw-multisite-import.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-import/rgw-multisite-import.component.scss new file mode 100644 index 00000000000..e69de29bb2d --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-import/rgw-multisite-import.component.scss diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-import/rgw-multisite-import.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-import/rgw-multisite-import.component.spec.ts new file mode 100644 index 00000000000..8ceb42087ea --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-import/rgw-multisite-import.component.spec.ts @@ -0,0 +1,38 @@ +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ReactiveFormsModule } from '@angular/forms'; +import { RouterTestingModule } from '@angular/router/testing'; +import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; +import { ToastrModule } from 'ngx-toastr'; +import { SharedModule } from '~/app/shared/shared.module'; + +import { RgwMultisiteImportComponent } from './rgw-multisite-import.component'; + +describe('RgwMultisiteImportComponent', () => { + let component: RgwMultisiteImportComponent; + let fixture: ComponentFixture<RgwMultisiteImportComponent>; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ + SharedModule, + ReactiveFormsModule, + RouterTestingModule, + HttpClientTestingModule, + ToastrModule.forRoot() + ], + declarations: [RgwMultisiteImportComponent], + providers: [NgbActiveModal] + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(RgwMultisiteImportComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-import/rgw-multisite-import.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-import/rgw-multisite-import.component.ts new file mode 100644 index 00000000000..5581a80bfe1 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-import/rgw-multisite-import.component.ts @@ -0,0 +1,77 @@ +import { Component, OnInit } from '@angular/core'; +import { FormControl, Validators } from '@angular/forms'; +import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; +import { RgwRealmService } from '~/app/shared/api/rgw-realm.service'; +import { ActionLabelsI18n } from '~/app/shared/constants/app.constants'; +import { NotificationType } from '~/app/shared/enum/notification-type.enum'; +import { CdFormGroup } from '~/app/shared/forms/cd-form-group'; +import { CdValidators } from '~/app/shared/forms/cd-validators'; +import { NotificationService } from '~/app/shared/services/notification.service'; +import { RgwZone } from '../models/rgw-multisite'; +import _ from 'lodash'; + +@Component({ + selector: 'cd-rgw-multisite-import', + templateUrl: './rgw-multisite-import.component.html', + styleUrls: ['./rgw-multisite-import.component.scss'] +}) +export class RgwMultisiteImportComponent implements OnInit { + readonly endpoints = /^((https?:\/\/)|(www.))(?:([a-zA-Z]+)|(\d+\.\d+.\d+.\d+)):\d{2,4}$/; + readonly ipv4Rgx = /^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/i; + readonly ipv6Rgx = /^(?:[a-f0-9]{1,4}:){7}[a-f0-9]{1,4}$/i; + + importTokenForm: CdFormGroup; + multisiteInfo: object[] = []; + zoneList: RgwZone[] = []; + zoneNames: string[]; + + constructor( + public activeModal: NgbActiveModal, + public rgwRealmService: RgwRealmService, + public actionLabels: ActionLabelsI18n, + public notificationService: NotificationService + ) { + this.createForm(); + } + ngOnInit(): void { + this.zoneList = + this.multisiteInfo[2] !== undefined && this.multisiteInfo[2].hasOwnProperty('zones') + ? this.multisiteInfo[2]['zones'] + : []; + this.zoneNames = this.zoneList.map((zone) => { + return zone['name']; + }); + } + + createForm() { + this.importTokenForm = new CdFormGroup({ + realmToken: new FormControl('', { + validators: [Validators.required] + }), + zoneName: new FormControl(null, { + validators: [ + Validators.required, + CdValidators.custom('uniqueName', (zoneName: string) => { + return this.zoneNames && this.zoneNames.indexOf(zoneName) !== -1; + }) + ] + }) + }); + } + + onSubmit() { + const values = this.importTokenForm.value; + this.rgwRealmService.importRealmToken(values['realmToken'], values['zoneName']).subscribe( + () => { + this.notificationService.show( + NotificationType.success, + $localize`Realm token import successfull` + ); + this.activeModal.close(); + }, + () => { + this.importTokenForm.setErrors({ cdSubmitButton: true }); + } + ); + } +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-migrate/rgw-multisite-migrate.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-migrate/rgw-multisite-migrate.component.html index ceb0afc5d57..4e0cef63322 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-migrate/rgw-multisite-migrate.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-migrate/rgw-multisite-migrate.component.html @@ -108,28 +108,35 @@ </div> </div> <div class="form-group row"> - <label class="cd-col-form-label" - for="users" - i18n>System User</label> + <label class="cd-col-form-label required" + for="access_key" + i18n>Access key</label> + <div class="cd-col-form-input"> + <input class="form-control" + type="text" + placeholder="e.g." + id="access_key" + name="access_key" + formControlName="access_key"> + </div> + </div> + <div class="form-group row"> + <label class="cd-col-form-label required" + for="access_key" + i18n>Secret key</label> <div class="cd-col-form-input"> - <select id="users" - name="users" - class="form-select" - formControlName="users"> - <option i18n - *ngIf="users === null" - [ngValue]="null">Loading...</option> - <option i18n - *ngIf="users !== null" - [ngValue]="null">-- Select a user --</option> - <option *ngFor="let user of users" - [value]="user.user_id">{{ user.user_id }}</option> - </select> + <input class="form-control" + type="text" + placeholder="e.g." + id="secret_key" + name="secret_key" + formControlName="secret_key"> </div> </div> </div> <div class="modal-footer"> <cd-form-button-panel (submitActionEvent)="submit()" + [submitText]="actionLabels.MIGRATE" [form]="multisiteMigrateForm"></cd-form-button-panel> </div> </form> diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-migrate/rgw-multisite-migrate.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-migrate/rgw-multisite-migrate.component.ts index 786a30fc119..4ede615a2e4 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-migrate/rgw-multisite-migrate.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-migrate/rgw-multisite-migrate.component.ts @@ -11,8 +11,9 @@ import { NotificationType } from '~/app/shared/enum/notification-type.enum'; import { CdFormGroup } from '~/app/shared/forms/cd-form-group'; import { CdValidators } from '~/app/shared/forms/cd-validators'; import { NotificationService } from '~/app/shared/services/notification.service'; -import { RgwRealm, RgwZone, RgwZonegroup } from '../models/rgw-multisite'; +import { RgwRealm, RgwZone, RgwZonegroup, SystemKey } from '../models/rgw-multisite'; import { ModalService } from '~/app/shared/services/modal.service'; +import { RgwDaemonService } from '~/app/shared/api/rgw-daemon.service'; @Component({ selector: 'cd-rgw-multisite-migrate', @@ -51,6 +52,7 @@ export class RgwMultisiteMigrateComponent implements OnInit { public notificationService: NotificationService, public rgwZonegroupService: RgwZonegroupService, public rgwRealmService: RgwRealmService, + public rgwDaemonService: RgwDaemonService, public modalService: ModalService ) { this.createForm(); @@ -133,7 +135,8 @@ export class RgwMultisiteMigrateComponent implements OnInit { Validators.required ] ), - users: new FormControl(null) + access_key: new FormControl(null), + secret_key: new FormControl(null) }); } @@ -159,9 +162,6 @@ export class RgwMultisiteMigrateComponent implements OnInit { this.zoneNames = this.zoneList.map((zone) => { return zone['name']; }); - this.rgwZoneService.getUserList('default').subscribe((users: any) => { - this.users = users.filter((user: any) => user['system'] === true); - }); } submit() { @@ -170,17 +170,20 @@ export class RgwMultisiteMigrateComponent implements OnInit { this.realm.name = values['realmName']; this.zonegroup = new RgwZonegroup(); this.zonegroup.name = values['zonegroupName']; - this.zonegroup.endpoints = this.checkUrlArray(values['zonegroup_endpoints']); + this.zonegroup.endpoints = values['zonegroup_endpoints']; this.zone = new RgwZone(); this.zone.name = values['zoneName']; - this.zone.endpoints = this.checkUrlArray(values['zone_endpoints']); - const user = values['users']; - this.rgwMultisiteService.migrate(this.realm, this.zonegroup, this.zone, user).subscribe( + this.zone.endpoints = values['zone_endpoints']; + this.zone.system_key = new SystemKey(); + this.zone.system_key.access_key = values['access_key']; + this.zone.system_key.secret_key = values['secret_key']; + this.rgwMultisiteService.migrate(this.realm, this.zonegroup, this.zone).subscribe( () => { this.notificationService.show( NotificationType.success, $localize`${this.actionLabels.MIGRATE} done successfully` ); + this.notificationService.show(NotificationType.success, `Daemon restart scheduled`); this.submitAction.emit(); this.activeModal.close(); }, @@ -189,14 +192,4 @@ export class RgwMultisiteMigrateComponent implements OnInit { } ); } - - checkUrlArray(endpoints: string) { - let endpointsArray = []; - if (endpoints.includes(',')) { - endpointsArray = endpoints.split(','); - } else { - endpointsArray.push(endpoints); - } - return endpointsArray; - } } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-zone-form/rgw-multisite-zone-form.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-zone-form/rgw-multisite-zone-form.component.html index 0c4ec560d38..8f4422051c4 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-zone-form/rgw-multisite-zone-form.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-zone-form/rgw-multisite-zone-form.component.html @@ -109,33 +109,34 @@ i18n>Please enter a valid IP address.</span> </div> </div> - <div class="form-group row" - *ngIf="action === 'edit'"> - <label class="cd-col-form-label" - for="users" - i18n>System User</label> + <div class="form-group row"> + <label class="cd-col-form-label required" + for="access_key" + i18n>Access key</label> <div class="cd-col-form-input"> - <select id="users" - name="users" - class="form-select" - formControlName="users"> - <option i18n - *ngIf="users === null" - [ngValue]="null">Loading...</option> - <option i18n - *ngIf="users !== null" - [ngValue]="null">-- Select a user --</option> - <option *ngFor="let user of users" - [value]="user.user_id">{{ user.user_id }}</option> - </select><br><br> - <div *ngIf="info.data.zone_zonegroup.is_master && info.data.is_master"> - <button type="button" - class="btn btn-light" - (click)="CreateSystemUser()"> - Create System User - </button> - </div> + <input class="form-control" + type="text" + placeholder="DiPt4V7WWvy2njL1z6aC" + id="access_key" + name="access_key" + formControlName="access_key"> + </div> + </div> + <div class="form-group row"> + <label class="cd-col-form-label required" + for="access_key" + i18n>Secret key</label> + <div class="cd-col-form-input"> + <input class="form-control" + type="text" + placeholder="xSZUdYky0bTctAdCEEW8ikhfBVKsBV5LFYL82vvh" + id="secret_key" + name="secret_key" + formControlName="secret_key"> </div> + </div> + <div class="form-group row" + *ngIf="action === 'edit'"> <div *ngIf="action === 'edit'"> <legend>Placement Targets</legend> <div class="form-group row"> diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-zone-form/rgw-multisite-zone-form.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-zone-form/rgw-multisite-zone-form.component.ts index 1fe980e4cd5..1fb9c178da1 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-zone-form/rgw-multisite-zone-form.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-zone-form/rgw-multisite-zone-form.component.ts @@ -11,9 +11,8 @@ import { NotificationType } from '~/app/shared/enum/notification-type.enum'; import { CdFormGroup } from '~/app/shared/forms/cd-form-group'; import { CdValidators } from '~/app/shared/forms/cd-validators'; import { NotificationService } from '~/app/shared/services/notification.service'; -import { RgwRealm, RgwZone, RgwZonegroup } from '../models/rgw-multisite'; +import { RgwRealm, RgwZone, RgwZonegroup, SystemKey } from '../models/rgw-multisite'; import { ModalService } from '~/app/shared/services/modal.service'; -import { RgwSystemUserComponent } from '../rgw-system-user/rgw-system-user.component'; @Component({ selector: 'cd-rgw-multisite-zone-form', @@ -55,6 +54,7 @@ export class RgwMultisiteZoneFormComponent implements OnInit { access_key: any; master_zonegroup_of_realm: RgwZonegroup; compressionTypes = ['lz4', 'zlib', 'snappy']; + userListReady: boolean = false; constructor( public activeModal: NgbActiveModal, @@ -87,7 +87,7 @@ export class RgwMultisiteZoneFormComponent implements OnInit { default_zone: new FormControl(false), master_zone: new FormControl(false), selectedZonegroup: new FormControl(null), - zone_endpoints: new FormControl([], { + zone_endpoints: new FormControl(null, { validators: [ CdValidators.custom('endpoint', (value: string) => { if (_.isEmpty(value)) { @@ -112,7 +112,8 @@ export class RgwMultisiteZoneFormComponent implements OnInit { Validators.required ] }), - users: new FormControl(null), + access_key: new FormControl(null), + secret_key: new FormControl(null), placementTarget: new FormControl(null), placementDataPool: new FormControl(''), placementIndexPool: new FormControl(null), @@ -136,24 +137,6 @@ export class RgwMultisiteZoneFormComponent implements OnInit { this.multisiteZoneForm.get('master_zone').disable(); this.disableMaster = true; } - const zonegroupInfo = this.zonegroupList.filter((zgroup: any) => zgroup.name === zg.name)[0]; - if (zonegroupInfo) { - const realm_id = zonegroupInfo.realm_id; - this.master_zonegroup_of_realm = this.zonegroupList.filter( - (zg: any) => zg.realm_id === realm_id && zg.is_master === true - )[0]; - } - if (this.master_zonegroup_of_realm) { - this.master_zone_of_master_zonegroup = this.zoneList.filter( - (zone: any) => zone.id === this.master_zonegroup_of_realm.master_zone - )[0]; - } - if (this.master_zone_of_master_zonegroup) { - this.getUserInfo(this.master_zone_of_master_zonegroup); - } - if (zonegroupInfo.is_master && this.multisiteZoneForm.getValue('master_zone') === true) { - this.createSystemUser = true; - } }); if ( this.multisiteZoneForm.getValue('selectedZonegroup') !== @@ -193,7 +176,9 @@ export class RgwMultisiteZoneFormComponent implements OnInit { this.multisiteZoneForm.get('selectedZonegroup').setValue(this.info.data.parent); this.multisiteZoneForm.get('default_zone').setValue(this.info.data.is_default); this.multisiteZoneForm.get('master_zone').setValue(this.info.data.is_master); - this.multisiteZoneForm.get('zone_endpoints').setValue(this.info.data.endpoints); + this.multisiteZoneForm.get('zone_endpoints').setValue(this.info.data.endpoints.toString()); + this.multisiteZoneForm.get('access_key').setValue(this.info.data.access_key); + this.multisiteZoneForm.get('secret_key').setValue(this.info.data.secret_key); this.multisiteZoneForm .get('placementTarget') .setValue(this.info.parent.data.default_placement); @@ -209,9 +194,6 @@ export class RgwMultisiteZoneFormComponent implements OnInit { const zone = new RgwZone(); zone.name = this.info.data.name; this.onZoneGroupChange(this.info.data.parent); - setTimeout(() => { - this.getUserInfo(zone); - }, 1000); } if ( this.multisiteZoneForm.getValue('selectedZonegroup') !== @@ -222,22 +204,6 @@ export class RgwMultisiteZoneFormComponent implements OnInit { } } - getUserInfo(zone: RgwZone) { - this.rgwZoneService - .getUserList(this.master_zone_of_master_zonegroup.name) - .subscribe((users: any) => { - this.users = users.filter((user: any) => user.keys.length !== 0); - this.rgwZoneService.get(zone).subscribe((zone: RgwZone) => { - const access_key = zone.system_key['access_key']; - const user = this.users.filter((user: any) => user.keys[0].access_key === access_key); - if (user.length > 0) { - this.multisiteZoneForm.get('users').setValue(user[0].user_id); - } - return user[0].user_id; - }); - }); - } - getZonePlacementData(placementTarget: string) { this.zone = new RgwZone(); this.zone.name = this.info.data.name; @@ -296,20 +262,17 @@ export class RgwMultisiteZoneFormComponent implements OnInit { this.zonegroup.name = values['selectedZonegroup']; this.zone = new RgwZone(); this.zone.name = values['zoneName']; - this.zone.endpoints = this.checkUrlArray(values['zone_endpoints']); - if (this.createSystemUser) { - values['users'] = values['zoneName'] + '_User'; - } + this.zone.endpoints = values['zone_endpoints']; + this.zone.system_key = new SystemKey(); + this.zone.system_key.access_key = values['access_key']; + this.zone.system_key.secret_key = values['secret_key']; this.rgwZoneService .create( this.zone, this.zonegroup, values['default_zone'], values['master_zone'], - this.zone.endpoints, - values['users'], - this.createSystemUser, - this.master_zone_of_master_zonegroup + this.zone.endpoints ) .subscribe( () => { @@ -328,10 +291,10 @@ export class RgwMultisiteZoneFormComponent implements OnInit { this.zonegroup.name = values['selectedZonegroup']; this.zone = new RgwZone(); this.zone.name = this.info.data.name; - this.zone.endpoints = - values['zone_endpoints'] === this.info.data.endpoints - ? values['zone_endpoints'] - : this.checkUrlArray(values['zone_endpoints']); + this.zone.endpoints = values['zone_endpoints']; + this.zone.system_key = new SystemKey(); + this.zone.system_key.access_key = values['access_key']; + this.zone.system_key.secret_key = values['secret_key']; this.rgwZoneService .update( this.zone, @@ -340,15 +303,13 @@ export class RgwMultisiteZoneFormComponent implements OnInit { values['default_zone'], values['master_zone'], this.zone.endpoints, - values['users'], values['placementTarget'], values['placementDataPool'], values['placementIndexPool'], values['placementDataExtraPool'], values['storageClass'], values['storageDataPool'], - values['storageCompression'], - this.master_zone_of_master_zonegroup + values['storageCompression'] ) .subscribe( () => { @@ -374,14 +335,4 @@ export class RgwMultisiteZoneFormComponent implements OnInit { } return endpointsArray; } - - CreateSystemUser() { - const initialState = { - zoneName: this.master_zone_of_master_zonegroup.name - }; - this.bsModalRef = this.modalService.show(RgwSystemUserComponent, initialState); - this.bsModalRef.componentInstance.submitAction.subscribe(() => { - this.getUserInfo(this.master_zone_of_master_zonegroup); - }); - } } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-zonegroup-form/rgw-multisite-zonegroup-form.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-zonegroup-form/rgw-multisite-zonegroup-form.component.ts index be9e3f64859..01955cd49a1 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-zonegroup-form/rgw-multisite-zonegroup-form.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-zonegroup-form/rgw-multisite-zonegroup-form.component.ts @@ -217,7 +217,7 @@ export class RgwMultisiteZonegroupFormComponent implements OnInit { this.realm.name = values['selectedRealm']; this.zonegroup = new RgwZonegroup(); this.zonegroup.name = values['zonegroupName']; - this.zonegroup.endpoints = this.checkUrlArray(values['zonegroup_endpoints']); + this.zonegroup.endpoints = values['zonegroup_endpoints']; this.rgwZonegroupService .create(this.realm, this.zonegroup, values['default_zonegroup'], values['master_zonegroup']) .subscribe( @@ -248,10 +248,7 @@ export class RgwMultisiteZonegroupFormComponent implements OnInit { this.zonegroup = new RgwZonegroup(); this.zonegroup.name = this.info.data.name; this.newZonegroupName = values['zonegroupName']; - this.zonegroup.endpoints = - values['zonegroup_endpoints'] === this.info.data.endpoints - ? values['zonegroup_endpoints'] - : this.checkUrlArray(values['zonegroup_endpoints']); + this.zonegroup.endpoints = values['zonegroup_endpoints']; this.zonegroup.placement_targets = values['placementTargets']; this.rgwZonegroupService .update( diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw.module.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw.module.ts index 0ecbd3eb53f..a0082209b96 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw.module.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw.module.ts @@ -38,6 +38,9 @@ import { RgwMultisiteZoneDeletionFormComponent } from './models/rgw-multisite-zo import { RgwMultisiteZonegroupDeletionFormComponent } from './models/rgw-multisite-zonegroup-deletion-form/rgw-multisite-zonegroup-deletion-form.component'; import { RgwSystemUserComponent } from './rgw-system-user/rgw-system-user.component'; import { RgwMultisiteMigrateComponent } from './rgw-multisite-migrate/rgw-multisite-migrate.component'; +import { RgwMultisiteImportComponent } from './rgw-multisite-import/rgw-multisite-import.component'; +import { RgwMultisiteExportComponent } from './rgw-multisite-export/rgw-multisite-export.component'; +import { CreateRgwServiceEntitiesComponent } from './create-rgw-service-entities/create-rgw-service-entities.component'; @NgModule({ imports: [ @@ -85,7 +88,10 @@ import { RgwMultisiteMigrateComponent } from './rgw-multisite-migrate/rgw-multis RgwMultisiteZoneDeletionFormComponent, RgwMultisiteZonegroupDeletionFormComponent, RgwSystemUserComponent, - RgwMultisiteMigrateComponent + RgwMultisiteMigrateComponent, + RgwMultisiteImportComponent, + RgwMultisiteExportComponent, + CreateRgwServiceEntitiesComponent ] }) export class RgwModule {} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-daemon.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-daemon.service.ts index 5c513c7f1fa..a6007404681 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-daemon.service.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-daemon.service.ts @@ -79,4 +79,15 @@ export class RgwDaemonService { }) ); } + + setMultisiteConfig(realm_name: string, zonegroup_name: string, zone_name: string) { + return this.request((params: HttpParams) => { + params = params.appendAll({ + realm_name: realm_name, + zonegroup_name: zonegroup_name, + zone_name: zone_name + }); + return this.http.put(`${this.url}/set_multisite_config`, null, { params: params }); + }); + } } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-multisite.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-multisite.service.ts index 0a601bf0fa1..fbd2ad64ec4 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-multisite.service.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-multisite.service.ts @@ -1,7 +1,7 @@ -import { HttpClient } from '@angular/common/http'; +import { HttpClient, HttpParams } from '@angular/common/http'; import { Injectable } from '@angular/core'; -import { RgwDaemonService } from './rgw-daemon.service'; import { RgwRealm, RgwZone, RgwZonegroup } from '~/app/ceph/rgw/models/rgw-multisite'; +import { RgwDaemonService } from './rgw-daemon.service'; @Injectable({ providedIn: 'root' @@ -9,25 +9,20 @@ import { RgwRealm, RgwZone, RgwZonegroup } from '~/app/ceph/rgw/models/rgw-multi export class RgwMultisiteService { private url = 'ui-api/rgw/multisite'; - constructor(private http: HttpClient, private rgwDaemonService: RgwDaemonService) {} - - getMultisiteSyncStatus() { - return this.rgwDaemonService.request(() => { - return this.http.get(`${this.url}/sync_status`); - }); - } + constructor(private http: HttpClient, public rgwDaemonService: RgwDaemonService) {} - migrate(realm: RgwRealm, zonegroup: RgwZonegroup, zone: RgwZone, user: string) { - return this.rgwDaemonService.request((requestBody: any) => { - requestBody = { + migrate(realm: RgwRealm, zonegroup: RgwZonegroup, zone: RgwZone) { + return this.rgwDaemonService.request((params: HttpParams) => { + params = params.appendAll({ realm_name: realm.name, zonegroup_name: zonegroup.name, zone_name: zone.name, zonegroup_endpoints: zonegroup.endpoints, zone_endpoints: zone.endpoints, - user: user - }; - return this.http.put(`${this.url}/migrate`, requestBody); + access_key: zone.system_key.access_key, + secret_key: zone.system_key.secret_key + }); + return this.http.put(`${this.url}/migrate`, null, { params: params }); }); } } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-realm.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-realm.service.ts index 63bb7b8c657..efa882c8b34 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-realm.service.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-realm.service.ts @@ -11,54 +11,43 @@ import { RgwDaemonService } from './rgw-daemon.service'; export class RgwRealmService { private url = 'api/rgw/realm'; - constructor(private http: HttpClient, private rgwDaemonService: RgwDaemonService) {} + constructor(private http: HttpClient, public rgwDaemonService: RgwDaemonService) {} create(realm: RgwRealm, defaultRealm: boolean) { - return this.rgwDaemonService.request((params: HttpParams) => { - params = params.appendAll({ - realm_name: realm.name, - default: defaultRealm - }); - return this.http.post(`${this.url}`, null, { params: params }); - }); + let requestBody = { + realm_name: realm.name, + default: defaultRealm + }; + return this.http.post(`${this.url}`, requestBody); } update(realm: RgwRealm, defaultRealm: boolean, newRealmName: string) { - return this.rgwDaemonService.request((requestBody: any) => { - requestBody = { - realm_name: realm.name, - default: defaultRealm, - new_realm_name: newRealmName - }; - return this.http.put(`${this.url}/${realm.name}`, requestBody); - }); + let requestBody = { + realm_name: realm.name, + default: defaultRealm, + new_realm_name: newRealmName + }; + return this.http.put(`${this.url}/${realm.name}`, requestBody); } list(): Observable<object> { - return this.rgwDaemonService.request(() => { - return this.http.get<object>(`${this.url}`); - }); + return this.http.get<object>(`${this.url}`); } - get(realm: RgwRealm): Observable<RgwRealm> { - return this.rgwDaemonService.request(() => { - return this.http.get(`${this.url}/${realm.name}`); - }); + get(realm: RgwRealm): Observable<object> { + return this.http.get(`${this.url}/${realm.name}`); } getAllRealmsInfo(): Observable<object> { - return this.rgwDaemonService.request(() => { - return this.http.get(`${this.url}/get_all_realms_info`); - }); + return this.http.get(`${this.url}/get_all_realms_info`); } delete(realmName: string): Observable<any> { - return this.rgwDaemonService.request((params: HttpParams) => { - params = params.appendAll({ - realm_name: realmName - }); - return this.http.delete(`${this.url}/${realmName}`, { params: params }); + let params = new HttpParams(); + params = params.appendAll({ + realm_name: realmName }); + return this.http.delete(`${this.url}/${realmName}`, { params: params }); } getRealmTree(realm: RgwRealm, defaultRealmId: string) { @@ -76,4 +65,20 @@ export class RgwRealmService { realmIds: realmIds }; } + + importRealmToken(realm_token: string, zone_name: string) { + return this.rgwDaemonService.request((params: HttpParams) => { + params = params.appendAll({ + realm_token: realm_token, + zone_name: zone_name + }); + return this.http.post(`${this.url}/import_realm_token`, null, { params: params }); + }); + } + + getRealmTokens() { + return this.rgwDaemonService.request(() => { + return this.http.get(`${this.url}/get_realm_tokens`); + }); + } } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-zone.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-zone.service.ts index 513747c31a2..02877816102 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-zone.service.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-zone.service.ts @@ -3,7 +3,6 @@ import { Injectable } from '@angular/core'; import { Observable } from 'rxjs'; import { RgwRealm, RgwZone, RgwZonegroup } from '~/app/ceph/rgw/models/rgw-multisite'; import { Icons } from '../enum/icons.enum'; -import { RgwDaemonService } from './rgw-daemon.service'; @Injectable({ providedIn: 'root' @@ -11,55 +10,38 @@ import { RgwDaemonService } from './rgw-daemon.service'; export class RgwZoneService { private url = 'api/rgw/zone'; - constructor(private http: HttpClient, private rgwDaemonService: RgwDaemonService) {} + constructor(private http: HttpClient) {} create( zone: RgwZone, zonegroup: RgwZonegroup, defaultZone: boolean, master: boolean, - endpoints: Array<string>, - user: string, - createSystemUser: boolean, - master_zone_of_master_zonegroup: RgwZone + endpoints: string ) { - let master_zone_name = ''; - if (master_zone_of_master_zonegroup !== undefined) { - master_zone_name = master_zone_of_master_zonegroup.name; - } else { - master_zone_name = ''; - } - return this.rgwDaemonService.request((params: HttpParams) => { - params = params.appendAll({ - zone_name: zone.name, - zonegroup_name: zonegroup.name, - default: defaultZone, - master: master, - zone_endpoints: endpoints, - user: user, - createSystemUser: createSystemUser, - master_zone_of_master_zonegroup: master_zone_name - }); - return this.http.post(`${this.url}`, null, { params: params }); + let params = new HttpParams(); + params = params.appendAll({ + zone_name: zone.name, + zonegroup_name: zonegroup.name, + default: defaultZone, + master: master, + zone_endpoints: endpoints, + access_key: zone.system_key.access_key, + secret_key: zone.system_key.secret_key }); + return this.http.post(`${this.url}`, null, { params: params }); } list(): Observable<object> { - return this.rgwDaemonService.request(() => { - return this.http.get<object>(`${this.url}`); - }); + return this.http.get<object>(`${this.url}`); } - get(zone: RgwZone): Observable<RgwZone> { - return this.rgwDaemonService.request(() => { - return this.http.get(`${this.url}/${zone.name}`); - }); + get(zone: RgwZone): Observable<object> { + return this.http.get(`${this.url}/${zone.name}`); } getAllZonesInfo(): Observable<object> { - return this.rgwDaemonService.request(() => { - return this.http.get(`${this.url}/get_all_zones_info`); - }); + return this.http.get(`${this.url}/get_all_zones_info`); } delete( @@ -68,15 +50,14 @@ export class RgwZoneService { pools: Set<string>, zonegroupName: string ): Observable<any> { - return this.rgwDaemonService.request((params: HttpParams) => { - params = params.appendAll({ - zone_name: zoneName, - delete_pools: deletePools, - pools: Array.from(pools.values()), - zonegroup_name: zonegroupName - }); - return this.http.delete(`${this.url}/${zoneName}`, { params: params }); + let params = new HttpParams(); + params = params.appendAll({ + zone_name: zoneName, + delete_pools: deletePools, + pools: Array.from(pools.values()), + zonegroup_name: zonegroupName }); + return this.http.delete(`${this.url}/${zoneName}`, { params: params }); } update( @@ -85,46 +66,42 @@ export class RgwZoneService { newZoneName: string, defaultZone?: boolean, master?: boolean, - endpoints?: Array<string>, - user?: string, + endpoints?: string, placementTarget?: string, dataPool?: string, indexPool?: string, dataExtraPool?: string, storageClass?: string, dataPoolClass?: string, - compression?: string, - master_zone_of_master_zonegroup?: RgwZone + compression?: string ) { - let master_zone_name = ''; - if (master_zone_of_master_zonegroup !== undefined) { - master_zone_name = master_zone_of_master_zonegroup.name; - } else { - master_zone_name = ''; - } - return this.rgwDaemonService.request((requestBody: any) => { - requestBody = { - zone_name: zone.name, - zonegroup_name: zonegroup.name, - new_zone_name: newZoneName, - default: defaultZone, - master: master, - zone_endpoints: endpoints, - user: user, - placement_target: placementTarget, - data_pool: dataPool, - index_pool: indexPool, - data_extra_pool: dataExtraPool, - storage_class: storageClass, - data_pool_class: dataPoolClass, - compression: compression, - master_zone_of_master_zonegroup: master_zone_name - }; - return this.http.put(`${this.url}/${zone.name}`, requestBody); - }); + let requestBody = { + zone_name: zone.name, + zonegroup_name: zonegroup.name, + new_zone_name: newZoneName, + default: defaultZone, + master: master, + zone_endpoints: endpoints, + access_key: zone.system_key.access_key, + secret_key: zone.system_key.secret_key, + placement_target: placementTarget, + data_pool: dataPool, + index_pool: indexPool, + data_extra_pool: dataExtraPool, + storage_class: storageClass, + data_pool_class: dataPoolClass, + compression: compression + }; + return this.http.put(`${this.url}/${zone.name}`, requestBody); } - getZoneTree(zone: RgwZone, defaultZoneId: string, zonegroup?: RgwZonegroup, realm?: RgwRealm) { + getZoneTree( + zone: RgwZone, + defaultZoneId: string, + zones: RgwZone[], + zonegroup?: RgwZonegroup, + realm?: RgwRealm + ) { let nodes = {}; let zoneIds = []; nodes['id'] = zone.id; @@ -141,6 +118,28 @@ export class RgwZoneService { nodes['endpoints'] = zone.endpoints; nodes['is_master'] = zonegroup && zonegroup.master_zone === zone.id ? true : false; nodes['type'] = 'zone'; + const zoneNames = zones.map((zone: RgwZone) => { + return zone['name']; + }); + nodes['secondary_zone'] = !zoneNames.includes(zone.name) ? true : false; + const zoneInfo = zones.filter((zoneInfo) => zoneInfo.name === zone.name); + if (zoneInfo && zoneInfo.length > 0) { + const access_key = zoneInfo[0].system_key['access_key']; + const secret_key = zoneInfo[0].system_key['secret_key']; + nodes['access_key'] = access_key ? access_key : ''; + nodes['secret_key'] = secret_key ? secret_key : ''; + nodes['user'] = access_key && access_key !== '' ? true : false; + } + if (nodes['access_key'] === '' || nodes['access_key'] === 'null') { + nodes['show_warning'] = true; + nodes['warning_message'] = 'Access/Secret keys not found'; + } else { + nodes['show_warning'] = false; + } + if (nodes['endpoints'] && nodes['endpoints'].length === 0) { + nodes['show_warning'] = true; + nodes['warning_message'] = nodes['warning_message'] + '\n' + 'Endpoints not configured'; + } return { nodes: nodes, zoneIds: zoneIds @@ -148,27 +147,22 @@ export class RgwZoneService { } getPoolNames() { - return this.rgwDaemonService.request(() => { - return this.http.get(`${this.url}/get_pool_names`); - }); + return this.http.get(`${this.url}/get_pool_names`); } createSystemUser(userName: string, zone: string) { - return this.rgwDaemonService.request((requestBody: any) => { - requestBody = { - userName: userName, - zoneName: zone - }; - return this.http.put(`${this.url}/create_system_user`, requestBody); - }); + let requestBody = { + userName: userName, + zoneName: zone + }; + return this.http.put(`${this.url}/create_system_user`, requestBody); } getUserList(zoneName: string) { - return this.rgwDaemonService.request((params: HttpParams) => { - params = params.appendAll({ - zoneName: zoneName - }); - return this.http.get(`${this.url}/get_user_list`, { params: params }); + let params = new HttpParams(); + params = params.appendAll({ + zoneName: zoneName }); + return this.http.get(`${this.url}/get_user_list`, { params: params }); } } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-zonegroup.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-zonegroup.service.ts index 28b5cc676a8..7f795c1d1d8 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-zonegroup.service.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-zonegroup.service.ts @@ -3,7 +3,6 @@ import { Injectable } from '@angular/core'; import { Observable } from 'rxjs'; import { RgwRealm, RgwZonegroup } from '~/app/ceph/rgw/models/rgw-multisite'; import { Icons } from '../enum/icons.enum'; -import { RgwDaemonService } from './rgw-daemon.service'; @Injectable({ providedIn: 'root' @@ -11,19 +10,18 @@ import { RgwDaemonService } from './rgw-daemon.service'; export class RgwZonegroupService { private url = 'api/rgw/zonegroup'; - constructor(private http: HttpClient, private rgwDaemonService: RgwDaemonService) {} + constructor(private http: HttpClient) {} create(realm: RgwRealm, zonegroup: RgwZonegroup, defaultZonegroup: boolean, master: boolean) { - return this.rgwDaemonService.request((params: HttpParams) => { - params = params.appendAll({ - realm_name: realm.name, - zonegroup_name: zonegroup.name, - default: defaultZonegroup, - master: master, - zonegroup_endpoints: zonegroup.endpoints - }); - return this.http.post(`${this.url}`, null, { params: params }); + let params = new HttpParams(); + params = params.appendAll({ + realm_name: realm.name, + zonegroup_name: zonegroup.name, + default: defaultZonegroup, + master: master, + zonegroup_endpoints: zonegroup.endpoints }); + return this.http.post(`${this.url}`, null, { params: params }); } update( @@ -35,49 +33,40 @@ export class RgwZonegroupService { removedZones?: string[], addedZones?: string[] ) { - return this.rgwDaemonService.request((requestBody: any) => { - requestBody = { - zonegroup_name: zonegroup.name, - realm_name: realm.name, - new_zonegroup_name: newZonegroupName, - default: defaultZonegroup, - master: master, - zonegroup_endpoints: zonegroup.endpoints, - placement_targets: zonegroup.placement_targets, - remove_zones: removedZones, - add_zones: addedZones - }; - return this.http.put(`${this.url}/${zonegroup.name}`, requestBody); - }); + let requestBody = { + zonegroup_name: zonegroup.name, + realm_name: realm.name, + new_zonegroup_name: newZonegroupName, + default: defaultZonegroup, + master: master, + zonegroup_endpoints: zonegroup.endpoints, + placement_targets: zonegroup.placement_targets, + remove_zones: removedZones, + add_zones: addedZones + }; + return this.http.put(`${this.url}/${zonegroup.name}`, requestBody); } list(): Observable<object> { - return this.rgwDaemonService.request(() => { - return this.http.get<object>(`${this.url}`); - }); + return this.http.get<object>(`${this.url}`); } - get(zonegroup: RgwZonegroup): Observable<RgwZonegroup> { - return this.rgwDaemonService.request(() => { - return this.http.get(`${this.url}/${zonegroup.name}`); - }); + get(zonegroup: RgwZonegroup): Observable<object> { + return this.http.get(`${this.url}/${zonegroup.name}`); } getAllZonegroupsInfo(): Observable<object> { - return this.rgwDaemonService.request(() => { - return this.http.get(`${this.url}/get_all_zonegroups_info`); - }); + return this.http.get(`${this.url}/get_all_zonegroups_info`); } delete(zonegroupName: string, deletePools: boolean, pools: Set<string>): Observable<any> { - return this.rgwDaemonService.request((params: HttpParams) => { - params = params.appendAll({ - zonegroup_name: zonegroupName, - delete_pools: deletePools, - pools: Array.from(pools.values()) - }); - return this.http.delete(`${this.url}/${zonegroupName}`, { params: params }); + let params = new HttpParams(); + params = params.appendAll({ + zonegroup_name: zonegroupName, + delete_pools: deletePools, + pools: Array.from(pools.values()) }); + return this.http.delete(`${this.url}/${zonegroupName}`, { params: params }); } getZonegroupTree(zonegroup: RgwZonegroup, defaultZonegroupId: string, realm?: RgwRealm) { @@ -95,6 +84,10 @@ export class RgwZonegroupService { nodes['zones'] = zonegroup.zones; nodes['placement_targets'] = zonegroup.placement_targets; nodes['default_placement'] = zonegroup.default_placement; + if (nodes['endpoints'].length === 0) { + nodes['show_warning'] = true; + nodes['warning_message'] = 'Endpoints not configured'; + } return nodes; } } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/alert-panel/alert-panel.component.html b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/alert-panel/alert-panel.component.html index be8096427a6..30f8b530a59 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/alert-panel/alert-panel.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/alert-panel/alert-panel.component.html @@ -1,6 +1,7 @@ <ngb-alert type="{{ bootstrapClass }}" [dismissible]="dismissible" - (closed)="onClose()"> + (closed)="onClose()" + [ngClass]="spacingClass"> <table> <ng-container *ngIf="size === 'normal'; else slim"> <tr> diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/alert-panel/alert-panel.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/alert-panel/alert-panel.component.ts index 51088840e33..cc2024baa23 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/alert-panel/alert-panel.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/alert-panel/alert-panel.component.ts @@ -24,6 +24,8 @@ export class AlertPanelComponent implements OnInit { showTitle = true; @Input() dismissible = false; + @Input() + spacingClass = ''; /** * The event that is triggered when the close button (x) has been diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/models/service.interface.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/service.interface.ts index f271c364c26..177382c5350 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/models/service.interface.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/service.interface.ts @@ -37,6 +37,9 @@ export interface CephServiceAdditionalSpec { ssl_key: string; port: number; initial_admin_password: string; + rgw_realm: string; + rgw_zonegroup: string; + rgw_zone: string; } export interface CephServicePlacement { diff --git a/src/pybind/mgr/dashboard/openapi.yaml b/src/pybind/mgr/dashboard/openapi.yaml index 83c58d9c5ca..dff1912142a 100644 --- a/src/pybind/mgr/dashboard/openapi.yaml +++ b/src/pybind/mgr/dashboard/openapi.yaml @@ -8328,6 +8328,47 @@ paths: summary: Display RGW Daemons tags: - RgwDaemon + /api/rgw/daemon/set_multisite_config: + put: + parameters: [] + requestBody: + content: + application/json: + schema: + properties: + daemon_name: + type: string + realm_name: + type: string + zone_name: + type: string + zonegroup_name: + type: string + 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: [] + tags: + - RgwDaemon /api/rgw/daemon/{svc_id}: get: parameters: @@ -8357,12 +8398,7 @@ paths: - RgwDaemon /api/rgw/realm: get: - parameters: - - allowEmptyValue: true - in: query - name: daemon_name - schema: - type: string + parameters: [] responses: '200': content: @@ -8389,8 +8425,6 @@ paths: application/json: schema: properties: - daemon_name: - type: string default: type: string realm_name: @@ -8445,6 +8479,70 @@ paths: - jwt: [] tags: - RgwRealm + /api/rgw/realm/get_realm_tokens: + 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: [] + tags: + - RgwRealm + /api/rgw/realm/import_realm_token: + post: + parameters: [] + requestBody: + content: + application/json: + schema: + properties: + daemon_name: + type: string + realm_token: + type: string + zone_name: + type: string + required: + - realm_token + - zone_name + type: object + responses: + '201': + content: + application/vnd.ceph.api.v1.0+json: + type: object + description: Resource created. + '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: [] + tags: + - RgwRealm /api/rgw/realm/{realm_name}: delete: parameters: @@ -8453,11 +8551,6 @@ paths: required: true schema: type: string - - allowEmptyValue: true - in: query - name: daemon_name - schema: - type: string responses: '202': content: @@ -8489,11 +8582,6 @@ paths: required: true schema: type: string - - allowEmptyValue: true - in: query - name: daemon_name - schema: - type: string responses: '200': content: @@ -8525,8 +8613,6 @@ paths: application/json: schema: properties: - daemon_name: - type: string default: default: '' type: string @@ -9282,12 +9368,7 @@ paths: - RgwUser /api/rgw/zone: get: - parameters: - - allowEmptyValue: true - in: query - name: daemon_name - schema: - type: string + parameters: [] responses: '200': content: @@ -9314,10 +9395,7 @@ paths: application/json: schema: properties: - createSystemUser: - default: false - type: boolean - daemon_name: + access_key: type: string default: default: false @@ -9325,9 +9403,7 @@ paths: master: default: false type: boolean - master_zone_of_master_zonegroup: - type: string - user: + secret_key: type: string zone_endpoints: type: string @@ -9370,8 +9446,6 @@ paths: application/json: schema: properties: - daemon_name: - type: string userName: type: string zoneName: @@ -9453,11 +9527,6 @@ paths: parameters: - allowEmptyValue: true in: query - name: daemon_name - schema: - type: string - - allowEmptyValue: true - in: query name: zoneName schema: type: string @@ -9503,11 +9572,6 @@ paths: name: zonegroup_name schema: type: string - - allowEmptyValue: true - in: query - name: daemon_name - schema: - type: string responses: '202': content: @@ -9539,11 +9603,6 @@ paths: required: true schema: type: string - - allowEmptyValue: true - in: query - name: daemon_name - schema: - type: string responses: '200': content: @@ -9575,10 +9634,11 @@ paths: application/json: schema: properties: - compression: + access_key: default: '' type: string - daemon_name: + compression: + default: '' type: string data_extra_pool: default: '' @@ -9598,21 +9658,19 @@ paths: master: default: '' type: string - master_zone_of_master_zonegroup: - type: string new_zone_name: type: string placement_target: default: '' type: string - storage_class: + secret_key: default: '' type: string - user: + storage_class: default: '' type: string zone_endpoints: - default: [] + default: '' type: string zonegroup_name: type: string @@ -9646,12 +9704,7 @@ paths: - RgwZone /api/rgw/zonegroup: get: - parameters: - - allowEmptyValue: true - in: query - name: daemon_name - schema: - type: string + parameters: [] responses: '200': content: @@ -9678,8 +9731,6 @@ paths: application/json: schema: properties: - daemon_name: - type: string default: type: string master: @@ -9758,11 +9809,6 @@ paths: name: pools schema: type: string - - allowEmptyValue: true - in: query - name: daemon_name - schema: - type: string responses: '202': content: @@ -9794,11 +9840,6 @@ paths: required: true schema: type: string - - allowEmptyValue: true - in: query - name: daemon_name - schema: - type: string responses: '200': content: @@ -9833,8 +9874,6 @@ paths: add_zones: default: [] type: string - daemon_name: - type: string default: default: '' type: string @@ -9852,7 +9891,7 @@ paths: default: [] type: string zonegroup_endpoints: - default: [] + default: '' type: string required: - realm_name diff --git a/src/pybind/mgr/dashboard/services/ceph_service.py b/src/pybind/mgr/dashboard/services/ceph_service.py index f0e21c59898..135f88ca2c9 100644 --- a/src/pybind/mgr/dashboard/services/ceph_service.py +++ b/src/pybind/mgr/dashboard/services/ceph_service.py @@ -294,6 +294,35 @@ class CephService(object): return {} @classmethod + def set_multisite_config(cls, realm_name, zonegroup_name, zone_name, daemon_name): + full_daemon_name = 'rgw.' + daemon_name + + KMS_CONFIG = [ + ['rgw_realm', realm_name], + ['rgw_zonegroup', zonegroup_name], + ['rgw_zone', zone_name] + ] + + for (key, value) in KMS_CONFIG: + if value == 'null': + continue + CephService.send_command('mon', 'config set', + who=name_to_config_section(full_daemon_name), + name=key, value=value) + return {} + + @classmethod + def get_realm_tokens(cls): + tokens_info = mgr.remote('rgw', 'get_realm_tokens') + return tokens_info + + @classmethod + def import_realm_token(cls, realm_token, zone_name): + tokens_info = mgr.remote('rgw', 'import_realm_token', zone_name=zone_name, + realm_token=realm_token, start_radosgw=True) + return tokens_info + + @classmethod def get_pool_pg_status(cls, pool_name): # type: (str) -> dict pool = cls.get_pool_by_attribute('pool_name', pool_name) diff --git a/src/pybind/mgr/dashboard/services/rgw_client.py b/src/pybind/mgr/dashboard/services/rgw_client.py index 648afb13339..707612acf83 100644 --- a/src/pybind/mgr/dashboard/services/rgw_client.py +++ b/src/pybind/mgr/dashboard/services/rgw_client.py @@ -8,7 +8,6 @@ import json import logging import os import re -import subprocess import xml.etree.ElementTree as ET # noqa: N814 from subprocess import SubprocessError @@ -594,6 +593,355 @@ class RgwClient(RestClient): return realm_info['name'] return None + @RestClient.api_get('/{bucket_name}?versioning') + def get_bucket_versioning(self, bucket_name, request=None): + """ + Get bucket versioning. + :param str bucket_name: the name of the bucket. + :return: versioning info + :rtype: Dict + """ + # pylint: disable=unused-argument + result = request() + if 'Status' not in result: + result['Status'] = 'Suspended' + if 'MfaDelete' not in result: + result['MfaDelete'] = 'Disabled' + return result + + @RestClient.api_put('/{bucket_name}?versioning') + def set_bucket_versioning(self, bucket_name, versioning_state, mfa_delete, + mfa_token_serial, mfa_token_pin, request=None): + """ + Set bucket versioning. + :param str bucket_name: the name of the bucket. + :param str versioning_state: + https://docs.aws.amazon.com/AmazonS3/latest/API/RESTBucketPUTVersioningStatus.html + :param str mfa_delete: MFA Delete state. + :param str mfa_token_serial: + https://docs.ceph.com/docs/master/radosgw/mfa/ + :param str mfa_token_pin: value of a TOTP token at a certain time (auth code) + :return: None + """ + # pylint: disable=unused-argument + versioning_configuration = ET.Element('VersioningConfiguration') + status_element = ET.SubElement(versioning_configuration, 'Status') + status_element.text = versioning_state + + headers = {} + if mfa_delete and mfa_token_serial and mfa_token_pin: + headers['x-amz-mfa'] = '{} {}'.format(mfa_token_serial, mfa_token_pin) + mfa_delete_element = ET.SubElement(versioning_configuration, 'MfaDelete') + mfa_delete_element.text = mfa_delete + + data = ET.tostring(versioning_configuration, encoding='unicode') + + try: + request(data=data, headers=headers) + except RequestException as error: + msg = str(error) + if mfa_delete and mfa_token_serial and mfa_token_pin \ + and 'AccessDenied' in error.content.decode(): + msg = 'Bad MFA credentials: {}'.format(msg) + raise DashboardException(msg=msg, + http_status_code=error.status_code, + component='rgw') + + @RestClient.api_get('/{bucket_name}?encryption') + def get_bucket_encryption(self, bucket_name, request=None): + # pylint: disable=unused-argument + try: + result = request() # type: ignore + result['Status'] = 'Enabled' + return result + except RequestException as e: + if e.content: + content = json_str_to_object(e.content) + if content.get( + 'Code') == 'ServerSideEncryptionConfigurationNotFoundError': + return { + 'Status': 'Disabled', + } + raise e + + @RestClient.api_delete('/{bucket_name}?encryption') + def delete_bucket_encryption(self, bucket_name, request=None): + # pylint: disable=unused-argument + result = request() # type: ignore + return result + + @RestClient.api_put('/{bucket_name}?encryption') + def set_bucket_encryption(self, bucket_name, key_id, + sse_algorithm, request: Optional[object] = None): + # pylint: disable=unused-argument + encryption_configuration = ET.Element('ServerSideEncryptionConfiguration') + rule_element = ET.SubElement(encryption_configuration, 'Rule') + default_encryption_element = ET.SubElement(rule_element, + 'ApplyServerSideEncryptionByDefault') + sse_algo_element = ET.SubElement(default_encryption_element, + 'SSEAlgorithm') + sse_algo_element.text = sse_algorithm + if sse_algorithm == 'aws:kms': + kms_master_key_element = ET.SubElement(default_encryption_element, + 'KMSMasterKeyID') + kms_master_key_element.text = key_id + data = ET.tostring(encryption_configuration, encoding='unicode') + try: + _ = request(data=data) # type: ignore + except RequestException as e: + raise DashboardException(msg=str(e), component='rgw') + + @RestClient.api_get('/{bucket_name}?object-lock') + def get_bucket_locking(self, bucket_name, request=None): + # type: (str, Optional[object]) -> dict + """ + Gets the locking configuration for a bucket. The locking + configuration will be applied by default to every new object + placed in the specified bucket. + :param bucket_name: The name of the bucket. + :type bucket_name: str + :return: The locking configuration. + :rtype: Dict + """ + # pylint: disable=unused-argument + + # Try to get the Object Lock configuration. If there is none, + # then return default values. + try: + result = request() # type: ignore + return { + 'lock_enabled': dict_get(result, 'ObjectLockEnabled') == 'Enabled', + 'lock_mode': dict_get(result, 'Rule.DefaultRetention.Mode'), + 'lock_retention_period_days': dict_get(result, 'Rule.DefaultRetention.Days', 0), + 'lock_retention_period_years': dict_get(result, 'Rule.DefaultRetention.Years', 0) + } + except RequestException as e: + if e.content: + content = json_str_to_object(e.content) + if content.get( + 'Code') == 'ObjectLockConfigurationNotFoundError': + return { + 'lock_enabled': False, + 'lock_mode': 'compliance', + 'lock_retention_period_days': None, + 'lock_retention_period_years': None + } + raise e + + @RestClient.api_put('/{bucket_name}?object-lock') + def set_bucket_locking(self, + bucket_name: str, + mode: str, + retention_period_days: Optional[Union[int, str]] = None, + retention_period_years: Optional[Union[int, str]] = None, + request: Optional[object] = None) -> None: + """ + Places the locking configuration on the specified bucket. The + locking configuration will be applied by default to every new + object placed in the specified bucket. + :param bucket_name: The name of the bucket. + :type bucket_name: str + :param mode: The lock mode, e.g. `COMPLIANCE` or `GOVERNANCE`. + :type mode: str + :param retention_period_days: + :type retention_period_days: int + :param retention_period_years: + :type retention_period_years: int + :rtype: None + """ + # pylint: disable=unused-argument + + retention_period_days, retention_period_years = self.perform_validations( + retention_period_days, retention_period_years, mode) + + # Generate the XML data like this: + # <ObjectLockConfiguration> + # <ObjectLockEnabled>string</ObjectLockEnabled> + # <Rule> + # <DefaultRetention> + # <Days>integer</Days> + # <Mode>string</Mode> + # <Years>integer</Years> + # </DefaultRetention> + # </Rule> + # </ObjectLockConfiguration> + locking_configuration = ET.Element('ObjectLockConfiguration') + enabled_element = ET.SubElement(locking_configuration, + 'ObjectLockEnabled') + enabled_element.text = 'Enabled' # Locking can't be disabled. + rule_element = ET.SubElement(locking_configuration, 'Rule') + default_retention_element = ET.SubElement(rule_element, + 'DefaultRetention') + mode_element = ET.SubElement(default_retention_element, 'Mode') + mode_element.text = mode.upper() + if retention_period_days: + days_element = ET.SubElement(default_retention_element, 'Days') + days_element.text = str(retention_period_days) + if retention_period_years: + years_element = ET.SubElement(default_retention_element, 'Years') + years_element.text = str(retention_period_years) + + data = ET.tostring(locking_configuration, encoding='unicode') + + try: + _ = request(data=data) # type: ignore + except RequestException as e: + raise DashboardException(msg=str(e), component='rgw') + + def list_roles(self) -> List[Dict[str, Any]]: + rgw_list_roles_command = ['role', 'list'] + code, roles, err = mgr.send_rgwadmin_command(rgw_list_roles_command) + if code < 0: + logger.warning('Error listing roles with code %d: %s', code, err) + return [] + + return roles + + def create_role(self, role_name: str, role_path: str, role_assume_policy_doc: str) -> None: + try: + json.loads(role_assume_policy_doc) + except: # noqa: E722 + raise DashboardException('Assume role policy document is not a valid json') + + # valid values: + # pylint: disable=C0301 + # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iam-role.html#cfn-iam-role-path # noqa: E501 + if len(role_name) > 64: + raise DashboardException( + f'Role name "{role_name}" is invalid. Should be 64 characters or less') + + role_name_regex = '[0-9a-zA-Z_+=,.@-]+' + if not re.fullmatch(role_name_regex, role_name): + raise DashboardException( + f'Role name "{role_name}" is invalid. Valid characters are "{role_name_regex}"') + + if not os.path.isabs(role_path): + raise DashboardException( + f'Role path "{role_path}" is invalid. It should be an absolute path') + if role_path[-1] != '/': + raise DashboardException( + f'Role path "{role_path}" is invalid. It should start and end with a slash') + path_regex = '(\u002F)|(\u002F[\u0021-\u007E]+\u002F)' + if not re.fullmatch(path_regex, role_path): + raise DashboardException( + (f'Role path "{role_path}" is invalid.' + f'Role path should follow the pattern "{path_regex}"')) + + rgw_create_role_command = ['role', 'create', '--role-name', role_name, '--path', role_path] + if role_assume_policy_doc: + rgw_create_role_command += ['--assume-role-policy-doc', f"{role_assume_policy_doc}"] + + code, _roles, _err = mgr.send_rgwadmin_command(rgw_create_role_command, + stdout_as_json=False) + if code != 0: + # pylint: disable=C0301 + link = 'https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iam-role.html#cfn-iam-role-path' # noqa: E501 + msg = (f'Error creating role with code {code}: ' + 'Looks like the document has a wrong format.' + f' For more information about the format look at {link}') + raise DashboardException(msg=msg, component='rgw') + + def perform_validations(self, retention_period_days, retention_period_years, mode): + try: + retention_period_days = int(retention_period_days) if retention_period_days else 0 + retention_period_years = int(retention_period_years) if retention_period_years else 0 + if retention_period_days < 0 or retention_period_years < 0: + raise ValueError + except (TypeError, ValueError): + msg = "Retention period must be a positive integer." + raise DashboardException(msg=msg, component='rgw') + if retention_period_days and retention_period_years: + # https://docs.aws.amazon.com/AmazonS3/latest/API/archive-RESTBucketPUTObjectLockConfiguration.html + msg = "Retention period requires either Days or Years. "\ + "You can't specify both at the same time." + raise DashboardException(msg=msg, component='rgw') + if not retention_period_days and not retention_period_years: + msg = "Retention period requires either Days or Years. "\ + "You must specify at least one." + raise DashboardException(msg=msg, component='rgw') + if not isinstance(mode, str) or mode.upper() not in ['COMPLIANCE', 'GOVERNANCE']: + msg = "Retention mode must be either COMPLIANCE or GOVERNANCE." + raise DashboardException(msg=msg, component='rgw') + return retention_period_days, retention_period_years + + +class RgwMultisite: + def migrate_to_multisite(self, realm_name: str, zonegroup_name: str, zone_name: str, + zonegroup_endpoints: str, zone_endpoints: str, access_key: str, + secret_key: str): + rgw_realm_create_cmd = ['realm', 'create', '--rgw-realm', realm_name, '--default'] + try: + exit_code, _, err = mgr.send_rgwadmin_command(rgw_realm_create_cmd, False) + if exit_code > 0: + raise DashboardException(e=err, msg='Unable to create realm', + http_status_code=500, component='rgw') + except SubprocessError as error: + raise DashboardException(error, http_status_code=500, component='rgw') + + rgw_zonegroup_edit_cmd = ['zonegroup', 'rename', '--rgw-zonegroup', 'default', + '--zonegroup-new-name', zonegroup_name] + try: + exit_code, _, err = mgr.send_rgwadmin_command(rgw_zonegroup_edit_cmd, False) + if exit_code > 0: + raise DashboardException(e=err, msg='Unable to rename zonegroup to {}'.format(zonegroup_name), # noqa E501 #pylint: disable=line-too-long + http_status_code=500, component='rgw') + except SubprocessError as error: + raise DashboardException(error, http_status_code=500, component='rgw') + + rgw_zone_edit_cmd = ['zone', 'rename', '--rgw-zone', + 'default', '--zone-new-name', zone_name, + '--rgw-zonegroup', zonegroup_name] + try: + exit_code, _, err = mgr.send_rgwadmin_command(rgw_zone_edit_cmd, False) + if exit_code > 0: + raise DashboardException(e=err, msg='Unable to rename zone to {}'.format(zone_name), # noqa E501 #pylint: disable=line-too-long + http_status_code=500, component='rgw') + except SubprocessError as error: + raise DashboardException(error, http_status_code=500, component='rgw') + + rgw_zonegroup_modify_cmd = ['zonegroup', 'modify', + '--rgw-realm', realm_name, + '--rgw-zonegroup', zonegroup_name] + if zonegroup_endpoints: + rgw_zonegroup_modify_cmd.append('--endpoints') + rgw_zonegroup_modify_cmd.append(zonegroup_endpoints) + rgw_zonegroup_modify_cmd.append('--master') + rgw_zonegroup_modify_cmd.append('--default') + try: + exit_code, _, err = mgr.send_rgwadmin_command(rgw_zonegroup_modify_cmd) + if exit_code > 0: + raise DashboardException(e=err, msg='Unable to modify zonegroup {}'.format(zonegroup_name), # noqa E501 #pylint: disable=line-too-long + http_status_code=500, component='rgw') + except SubprocessError as error: + raise DashboardException(error, http_status_code=500, component='rgw') + + rgw_zone_modify_cmd = ['zone', 'modify', '--rgw-realm', realm_name, + '--rgw-zonegroup', zonegroup_name, + '--rgw-zone', zone_name] + if zone_endpoints: + rgw_zone_modify_cmd.append('--endpoints') + rgw_zone_modify_cmd.append(zone_endpoints) + rgw_zone_modify_cmd.append('--master') + rgw_zone_modify_cmd.append('--default') + try: + exit_code, _, err = mgr.send_rgwadmin_command(rgw_zone_modify_cmd) + if exit_code > 0: + raise DashboardException(e=err, msg='Unable to modify zone', + http_status_code=500, component='rgw') + except SubprocessError as error: + raise DashboardException(error, http_status_code=500, component='rgw') + + if access_key and secret_key: + rgw_zone_modify_cmd = ['zone', 'modify', '--rgw-zone', zone_name, + '--access-key', access_key, '--secret', secret_key] + try: + exit_code, _, err = mgr.send_rgwadmin_command(rgw_zone_modify_cmd) + if exit_code > 0: + raise DashboardException(e=err, msg='Unable to modify zone', + http_status_code=500, component='rgw') + except SubprocessError as error: + raise DashboardException(error, http_status_code=500, component='rgw') + def create_realm(self, realm_name: str, default: bool): rgw_realm_create_cmd = ['realm', 'create'] cmd_create_realm_options = ['--rgw-realm', realm_name] @@ -652,26 +1000,6 @@ class RgwClient(RestClient): all_realms_info['default_realm'] = '' # type: ignore return all_realms_info - def delete_realm(self, realm_name: str): - rgw_delete_realm_cmd = ['realm', 'rm', '--rgw-realm', realm_name] - try: - exit_code, _, _ = mgr.send_rgwadmin_command(rgw_delete_realm_cmd) - if exit_code > 0: - raise DashboardException(msg='Unable to delete realm', - http_status_code=500, component='rgw') - except SubprocessError as error: - raise DashboardException(error, http_status_code=500, component='rgw') - - def update_period(self): - rgw_update_period_cmd = ['period', 'update', '--commit'] - try: - exit_code, _, err = mgr.send_rgwadmin_command(rgw_update_period_cmd) - if exit_code > 0: - raise DashboardException(e=err, msg='Unable to update period', - http_status_code=500, component='rgw') - except SubprocessError as error: - raise DashboardException(error, http_status_code=500, component='rgw') - def edit_realm(self, realm_name: str, new_realm_name: str, default: str = ''): rgw_realm_edit_cmd = [] if new_realm_name != realm_name: @@ -694,8 +1022,18 @@ class RgwClient(RestClient): except SubprocessError as error: raise DashboardException(error, http_status_code=500, component='rgw') + def delete_realm(self, realm_name: str): + rgw_delete_realm_cmd = ['realm', 'rm', '--rgw-realm', realm_name] + try: + exit_code, _, _ = mgr.send_rgwadmin_command(rgw_delete_realm_cmd) + if exit_code > 0: + raise DashboardException(msg='Unable to delete realm', + http_status_code=500, component='rgw') + except SubprocessError as error: + raise DashboardException(error, http_status_code=500, component='rgw') + def create_zonegroup(self, realm_name: str, zonegroup_name: str, - default: bool, master: bool, endpoints: List[str]): + default: bool, master: bool, endpoints: str): rgw_zonegroup_create_cmd = ['zonegroup', 'create'] cmd_create_zonegroup_options = ['--rgw-zonegroup', zonegroup_name] if realm_name != 'null': @@ -705,13 +1043,9 @@ class RgwClient(RestClient): cmd_create_zonegroup_options.append('--default') if master != 'false': cmd_create_zonegroup_options.append('--master') - if endpoints != 'null': # type: ignore - if isinstance(endpoints, list) and len(endpoints) > 1: - endpoint = ','.join(endpoints) - else: - endpoint = endpoints # type: ignore + if endpoints: cmd_create_zonegroup_options.append('--endpoints') - cmd_create_zonegroup_options.append(endpoint) + cmd_create_zonegroup_options.append(endpoints) rgw_zonegroup_create_cmd += cmd_create_zonegroup_options try: exit_code, out, err = mgr.send_rgwadmin_command(rgw_zonegroup_create_cmd) @@ -722,19 +1056,79 @@ class RgwClient(RestClient): raise DashboardException(error, http_status_code=500, component='rgw') return out + def list_zonegroups(self): + rgw_zonegroup_list = {} + rgw_zonegroup_list_cmd = ['zonegroup', 'list'] + try: + exit_code, out, _ = mgr.send_rgwadmin_command(rgw_zonegroup_list_cmd) + if exit_code > 0: + raise DashboardException(msg='Unable to fetch zonegroup list', + http_status_code=500, component='rgw') + rgw_zonegroup_list = out + except SubprocessError as error: + raise DashboardException(error, http_status_code=500, component='rgw') + return rgw_zonegroup_list + + def get_zonegroup(self, zonegroup_name: str): + zonegroup_info = {} + if zonegroup_name != 'default': + rgw_zonegroup_info_cmd = ['zonegroup', 'get', '--rgw-zonegroup', zonegroup_name] + else: + rgw_zonegroup_info_cmd = ['zonegroup', 'get', '--rgw-zonegroup', + zonegroup_name, '--rgw-realm', 'default'] + try: + exit_code, out, _ = mgr.send_rgwadmin_command(rgw_zonegroup_info_cmd) + if exit_code > 0: + raise DashboardException('Unable to get zonegroup info', + http_status_code=500, component='rgw') + zonegroup_info = out + except SubprocessError as error: + raise DashboardException(error, http_status_code=500, component='rgw') + return zonegroup_info + + def get_all_zonegroups_info(self): + all_zonegroups_info = {} + zonegroups_info = [] + rgw_zonegroup_list = self.list_zonegroups() + if 'zonegroups' in rgw_zonegroup_list: + if rgw_zonegroup_list['zonegroups'] != []: + for rgw_zonegroup in rgw_zonegroup_list['zonegroups']: + zonegroup_info = self.get_zonegroup(rgw_zonegroup) + zonegroups_info.append(zonegroup_info) + all_zonegroups_info['zonegroups'] = zonegroups_info # type: ignore + else: + all_zonegroups_info['zonegroups'] = [] # type: ignore + if 'default_info' in rgw_zonegroup_list and rgw_zonegroup_list['default_info'] != '': + all_zonegroups_info['default_zonegroup'] = rgw_zonegroup_list['default_info'] + else: + all_zonegroups_info['default_zonegroup'] = '' # type: ignore + return all_zonegroups_info + + def delete_zonegroup(self, zonegroup_name: str, delete_pools: str, pools: List[str]): + if delete_pools == 'true': + zonegroup_info = self.get_zonegroup(zonegroup_name) + rgw_delete_zonegroup_cmd = ['zonegroup', 'delete', '--rgw-zonegroup', zonegroup_name] + try: + exit_code, _, _ = mgr.send_rgwadmin_command(rgw_delete_zonegroup_cmd) + if exit_code > 0: + raise DashboardException(msg='Unable to delete zonegroup', + http_status_code=500, component='rgw') + except SubprocessError as error: + raise DashboardException(error, http_status_code=500, component='rgw') + self.update_period() + if delete_pools == 'true': + for zone in zonegroup_info['zones']: + self.delete_zone(zone['name'], 'true', pools) + def modify_zonegroup(self, realm_name: str, zonegroup_name: str, default: str, master: str, - endpoints: List[str]): - if realm_name: - rgw_zonegroup_modify_cmd = ['zonegroup', 'modify', - '--rgw-realm', realm_name, - '--rgw-zonegroup', zonegroup_name] + endpoints: str): + + rgw_zonegroup_modify_cmd = ['zonegroup', 'modify', + '--rgw-realm', realm_name, + '--rgw-zonegroup', zonegroup_name] if endpoints: - if len(endpoints) > 1: - endpoint = ','.join(str(e) for e in endpoints) - else: - endpoint = endpoints[0] rgw_zonegroup_modify_cmd.append('--endpoints') - rgw_zonegroup_modify_cmd.append(endpoint) + rgw_zonegroup_modify_cmd.append(endpoints) if master and str_to_bool(master): rgw_zonegroup_modify_cmd.append('--master') if default and str_to_bool(default): @@ -852,7 +1246,7 @@ class RgwClient(RestClient): # pylint: disable=W0102 def edit_zonegroup(self, realm_name: str, zonegroup_name: str, new_zonegroup_name: str, - default: str = '', master: str = '', endpoints: List[str] = [], + default: str = '', master: str = '', endpoints: str = '', add_zones: List[str] = [], remove_zones: List[str] = [], placement_targets: List[Dict[str, str]] = []): rgw_zonegroup_edit_cmd = [] @@ -883,73 +1277,18 @@ class RgwClient(RestClient): else: self.add_placement_targets(new_zonegroup_name, placement_targets) - def list_zonegroups(self): - rgw_zonegroup_list = {} - rgw_zonegroup_list_cmd = ['zonegroup', 'list'] - try: - exit_code, out, _ = mgr.send_rgwadmin_command(rgw_zonegroup_list_cmd) - if exit_code > 0: - raise DashboardException(msg='Unable to fetch zonegroup list', - http_status_code=500, component='rgw') - rgw_zonegroup_list = out - except SubprocessError as error: - raise DashboardException(error, http_status_code=500, component='rgw') - return rgw_zonegroup_list - - def get_zonegroup(self, zonegroup_name: str): - zonegroup_info = {} - rgw_zonegroup_info_cmd = ['zonegroup', 'get', '--rgw-zonegroup', zonegroup_name] - try: - exit_code, out, _ = mgr.send_rgwadmin_command(rgw_zonegroup_info_cmd) - if exit_code > 0: - raise DashboardException('Unable to get zonegroup info', - http_status_code=500, component='rgw') - zonegroup_info = out - except SubprocessError as error: - raise DashboardException(error, http_status_code=500, component='rgw') - return zonegroup_info - - def get_all_zonegroups_info(self): - all_zonegroups_info = {} - zonegroups_info = [] - rgw_zonegroup_list = self.list_zonegroups() - if 'zonegroups' in rgw_zonegroup_list: - if rgw_zonegroup_list['zonegroups'] != []: - for rgw_zonegroup in rgw_zonegroup_list['zonegroups']: - zonegroup_info = self.get_zonegroup(rgw_zonegroup) - zonegroups_info.append(zonegroup_info) - all_zonegroups_info['zonegroups'] = zonegroups_info # type: ignore - else: - all_zonegroups_info['zonegroups'] = [] # type: ignore - if 'default_info' in rgw_zonegroup_list and rgw_zonegroup_list['default_info'] != '': - all_zonegroups_info['default_zonegroup'] = rgw_zonegroup_list['default_info'] - else: - all_zonegroups_info['default_zonegroup'] = '' # type: ignore - return all_zonegroups_info - - def delete_zonegroup(self, zonegroup_name: str, delete_pools: str, pools: List[str]): - if delete_pools == 'true': - zonegroup_info = self.get_zonegroup(zonegroup_name) - rgw_delete_zonegroup_cmd = ['zonegroup', 'delete', '--rgw-zonegroup', zonegroup_name] + def update_period(self): + rgw_update_period_cmd = ['period', 'update', '--commit'] try: - exit_code, _, _ = mgr.send_rgwadmin_command(rgw_delete_zonegroup_cmd) + exit_code, _, err = mgr.send_rgwadmin_command(rgw_update_period_cmd) if exit_code > 0: - raise DashboardException(msg='Unable to delete zonegroup', + raise DashboardException(e=err, msg='Unable to update period', http_status_code=500, component='rgw') except SubprocessError as error: raise DashboardException(error, http_status_code=500, component='rgw') - self.update_period() - if delete_pools == 'true': - for zone in zonegroup_info['zones']: - self.delete_zone(zone['name'], 'true', pools) - def create_zone(self, zone_name, zonegroup_name, default, master, endpoints, user, - createSystemUser, master_zone_of_master_zonegroup): - if user != 'null': - access_key, secret_key = self.get_rgw_user_keys(user, master_zone_of_master_zonegroup) - else: - access_key = None # type: ignore - secret_key = None # type: ignore + def create_zone(self, zone_name, zonegroup_name, default, master, endpoints, access_key, + secret_key): rgw_zone_create_cmd = ['zone', 'create'] cmd_create_zone_options = ['--rgw-zone', zone_name] if zonegroup_name != 'null': @@ -978,36 +1317,8 @@ class RgwClient(RestClient): raise DashboardException(error, http_status_code=500, component='rgw') self.update_period() - - if createSystemUser == 'true': - self.create_system_user(user, zone_name) - access_key, secret_key = self.get_rgw_user_keys(user, zone_name) - rgw_zone_modify_cmd = ['zone', 'modify', '--rgw-zone', zone_name, - '--access-key', access_key, '--secret', secret_key] - try: - exit_code, _, err = mgr.send_rgwadmin_command(rgw_zone_modify_cmd) - if exit_code > 0: - raise DashboardException(e=err, msg='Unable to modify zone', - http_status_code=500, component='rgw') - except SubprocessError as error: - raise DashboardException(error, http_status_code=500, component='rgw') - self.update_period() - return out - def get_rgw_user_keys(self, user, zone_name): - access_key = '' - secret_key = '' - rgw_user_info_cmd = ['user', 'info', '--uid', user, '--rgw-zone', zone_name] - try: - _, out, _ = mgr.send_rgwadmin_command(rgw_user_info_cmd) - if out: - access_key, secret_key = self.parse_secrets(user, out) - except SubprocessError as error: - logger.exception(error) - - return access_key, secret_key - def parse_secrets(self, user, data): for key in data.get('keys', []): if key.get('user') == user: @@ -1017,21 +1328,12 @@ class RgwClient(RestClient): return '', '' def modify_zone(self, zone_name: str, zonegroup_name: str, default: str, master: str, - endpoints: List[str], user: str, master_zone_of_master_zonegroup): - if user: - access_key, secret_key = self.get_rgw_user_keys(user, master_zone_of_master_zonegroup) - else: - access_key = None - secret_key = None + endpoints: str, access_key: str, secret_key: str): rgw_zone_modify_cmd = ['zone', 'modify', '--rgw-zonegroup', zonegroup_name, '--rgw-zone', zone_name] if endpoints: - if len(endpoints) > 1: - endpoint = ','.join(str(e) for e in endpoints) - else: - endpoint = endpoints[0] rgw_zone_modify_cmd.append('--endpoints') - rgw_zone_modify_cmd.append(endpoint) + rgw_zone_modify_cmd.append(endpoints) if default and str_to_bool(default): rgw_zone_modify_cmd.append('--default') if master and str_to_bool(master): @@ -1083,10 +1385,10 @@ class RgwClient(RestClient): self.update_period() def edit_zone(self, zone_name: str, new_zone_name: str, zonegroup_name: str, default: str = '', - master: str = '', endpoints: List[str] = [], user: str = '', + master: str = '', endpoints: str = '', access_key: str = '', secret_key: str = '', placement_target: str = '', data_pool: str = '', index_pool: str = '', data_extra_pool: str = '', storage_class: str = '', data_pool_class: str = '', - compression: str = '', master_zone_of_master_zonegroup=None): + compression: str = ''): if new_zone_name != zone_name: rgw_zone_rename_cmd = ['zone', 'rename', '--rgw-zone', zone_name, '--zone-new-name', new_zone_name] @@ -1098,8 +1400,8 @@ class RgwClient(RestClient): except SubprocessError as error: raise DashboardException(error, http_status_code=500, component='rgw') self.update_period() - self.modify_zone(new_zone_name, zonegroup_name, default, master, endpoints, user, - master_zone_of_master_zonegroup) + self.modify_zone(new_zone_name, zonegroup_name, default, master, endpoints, access_key, + secret_key) self.add_placement_targets_zone(new_zone_name, placement_target, data_pool, index_pool, data_extra_pool) self.add_storage_class_zone(new_zone_name, placement_target, storage_class, @@ -1179,29 +1481,6 @@ class RgwClient(RestClient): if mgr.rados.pool_exists(pool): mgr.rados.delete_pool(pool) - def get_multisite_status(self): - is_multisite_configured = True - 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: - is_multisite_configured = False - return is_multisite_configured - - def get_multisite_sync_status(self): - sync_status = '' - rgw_sync_status_cmd = ['sync', 'status'] - try: - exit_code, out, _ = mgr.send_rgwadmin_command(rgw_sync_status_cmd, False) - if exit_code > 0: - raise DashboardException('Unable to get sync status', - http_status_code=500, component='rgw') - sync_status = out - except subprocess.TimeoutExpired: - sync_status = 'Timeout Expired' - return sync_status - def create_system_user(self, userName: str, zoneName: str): rgw_user_create_cmd = ['user', 'create', '--uid', userName, '--display-name', userName, '--rgw-zone', zoneName, '--system'] @@ -1240,358 +1519,12 @@ class RgwClient(RestClient): raise DashboardException(error, http_status_code=500, component='rgw') return all_users_info - def migrate_to_multisite(self, realm_name: str, zonegroup_name: str, zone_name: str, - zonegroup_endpoints: List[str], zone_endpoints: List[str], user: str): - rgw_realm_create_cmd = ['realm', 'create', '--rgw-realm', realm_name, '--default'] - try: - exit_code, _, err = mgr.send_rgwadmin_command(rgw_realm_create_cmd, False) - if exit_code > 0: - raise DashboardException(e=err, msg='Unable to create realm', - http_status_code=500, component='rgw') - except SubprocessError as error: - raise DashboardException(error, http_status_code=500, component='rgw') - - rgw_zonegroup_edit_cmd = ['zonegroup', 'rename', '--rgw-zonegroup', 'default', - '--zonegroup-new-name', zonegroup_name] - try: - exit_code, _, err = mgr.send_rgwadmin_command(rgw_zonegroup_edit_cmd, False) - if exit_code > 0: - raise DashboardException(e=err, msg='Unable to rename zonegroup to {}'.format(zonegroup_name), # noqa E501 #pylint: disable=line-too-long - http_status_code=500, component='rgw') - except SubprocessError as error: - raise DashboardException(error, http_status_code=500, component='rgw') - - rgw_zone_edit_cmd = ['zone', 'rename', '--rgw-zone', - 'default', '--zone-new-name', zone_name, - '--rgw-zonegroup', zonegroup_name] - try: - exit_code, _, err = mgr.send_rgwadmin_command(rgw_zone_edit_cmd, False) - if exit_code > 0: - raise DashboardException(e=err, msg='Unable to rename zone to {}'.format(zone_name), # noqa E501 #pylint: disable=line-too-long - http_status_code=500, component='rgw') - except SubprocessError as error: - raise DashboardException(error, http_status_code=500, component='rgw') - - rgw_zonegroup_modify_cmd = ['zonegroup', 'modify', - '--rgw-realm', realm_name, - '--rgw-zonegroup', zonegroup_name] - if zonegroup_endpoints: - if len(zonegroup_endpoints) > 1: - endpoint = ','.join(str(e) for e in zonegroup_endpoints) - else: - endpoint = zonegroup_endpoints[0] - rgw_zonegroup_modify_cmd.append('--endpoints') - rgw_zonegroup_modify_cmd.append(endpoint) - rgw_zonegroup_modify_cmd.append('--master') - rgw_zonegroup_modify_cmd.append('--default') - try: - exit_code, _, err = mgr.send_rgwadmin_command(rgw_zonegroup_modify_cmd) - if exit_code > 0: - raise DashboardException(e=err, msg='Unable to modify zonegroup {}'.format(zonegroup_name), # noqa E501 #pylint: disable=line-too-long - http_status_code=500, component='rgw') - except SubprocessError as error: - raise DashboardException(error, http_status_code=500, component='rgw') - - rgw_zone_modify_cmd = ['zone', 'modify', '--rgw-realm', realm_name, - '--rgw-zonegroup', zonegroup_name, - '--rgw-zone', zone_name] - if zone_endpoints: - if len(zone_endpoints) > 1: - endpoint = ','.join(str(e) for e in zone_endpoints) - else: - endpoint = zone_endpoints[0] - rgw_zone_modify_cmd.append('--endpoints') - rgw_zone_modify_cmd.append(endpoint) - rgw_zone_modify_cmd.append('--master') - rgw_zone_modify_cmd.append('--default') - try: - exit_code, _, err = mgr.send_rgwadmin_command(rgw_zone_modify_cmd) - if exit_code > 0: - raise DashboardException(e=err, msg='Unable to modify zone', - http_status_code=500, component='rgw') - except SubprocessError as error: - raise DashboardException(error, http_status_code=500, component='rgw') - - if user: - access_key, secret_key = self.get_rgw_user_keys(user, zone_name) - rgw_zone_modify_cmd = ['zone', 'modify', '--rgw-zone', zone_name, - '--access-key', access_key, '--secret', secret_key] - try: - exit_code, _, err = mgr.send_rgwadmin_command(rgw_zone_modify_cmd) - if exit_code > 0: - raise DashboardException(e=err, msg='Unable to modify zone', - http_status_code=500, component='rgw') - except SubprocessError as error: - raise DashboardException(error, http_status_code=500, component='rgw') - self.update_period() - - @RestClient.api_get('/{bucket_name}?versioning') - def get_bucket_versioning(self, bucket_name, request=None): - """ - Get bucket versioning. - :param str bucket_name: the name of the bucket. - :return: versioning info - :rtype: Dict - """ - # pylint: disable=unused-argument - result = request() - if 'Status' not in result: - result['Status'] = 'Suspended' - if 'MfaDelete' not in result: - result['MfaDelete'] = 'Disabled' - return result - - @RestClient.api_put('/{bucket_name}?versioning') - def set_bucket_versioning(self, bucket_name, versioning_state, mfa_delete, - mfa_token_serial, mfa_token_pin, request=None): - """ - Set bucket versioning. - :param str bucket_name: the name of the bucket. - :param str versioning_state: - https://docs.aws.amazon.com/AmazonS3/latest/API/RESTBucketPUTVersioningStatus.html - :param str mfa_delete: MFA Delete state. - :param str mfa_token_serial: - https://docs.ceph.com/docs/master/radosgw/mfa/ - :param str mfa_token_pin: value of a TOTP token at a certain time (auth code) - :return: None - """ - # pylint: disable=unused-argument - versioning_configuration = ET.Element('VersioningConfiguration') - status_element = ET.SubElement(versioning_configuration, 'Status') - status_element.text = versioning_state - - headers = {} - if mfa_delete and mfa_token_serial and mfa_token_pin: - headers['x-amz-mfa'] = '{} {}'.format(mfa_token_serial, mfa_token_pin) - mfa_delete_element = ET.SubElement(versioning_configuration, 'MfaDelete') - mfa_delete_element.text = mfa_delete - - data = ET.tostring(versioning_configuration, encoding='unicode') - - try: - request(data=data, headers=headers) - except RequestException as error: - msg = str(error) - if mfa_delete and mfa_token_serial and mfa_token_pin \ - and 'AccessDenied' in error.content.decode(): - msg = 'Bad MFA credentials: {}'.format(msg) - raise DashboardException(msg=msg, - http_status_code=error.status_code, - component='rgw') - - @RestClient.api_get('/{bucket_name}?encryption') - def get_bucket_encryption(self, bucket_name, request=None): - # pylint: disable=unused-argument - try: - result = request() # type: ignore - result['Status'] = 'Enabled' - return result - except RequestException as e: - if e.content: - content = json_str_to_object(e.content) - if content.get( - 'Code') == 'ServerSideEncryptionConfigurationNotFoundError': - return { - 'Status': 'Disabled', - } - raise e - - @RestClient.api_delete('/{bucket_name}?encryption') - def delete_bucket_encryption(self, bucket_name, request=None): - # pylint: disable=unused-argument - result = request() # type: ignore - return result - - @RestClient.api_put('/{bucket_name}?encryption') - def set_bucket_encryption(self, bucket_name, key_id, - sse_algorithm, request: Optional[object] = None): - # pylint: disable=unused-argument - encryption_configuration = ET.Element('ServerSideEncryptionConfiguration') - rule_element = ET.SubElement(encryption_configuration, 'Rule') - default_encryption_element = ET.SubElement(rule_element, - 'ApplyServerSideEncryptionByDefault') - sse_algo_element = ET.SubElement(default_encryption_element, - 'SSEAlgorithm') - sse_algo_element.text = sse_algorithm - if sse_algorithm == 'aws:kms': - kms_master_key_element = ET.SubElement(default_encryption_element, - 'KMSMasterKeyID') - kms_master_key_element.text = key_id - data = ET.tostring(encryption_configuration, encoding='unicode') - try: - _ = request(data=data) # type: ignore - except RequestException as e: - raise DashboardException(msg=str(e), component='rgw') - - @RestClient.api_get('/{bucket_name}?object-lock') - def get_bucket_locking(self, bucket_name, request=None): - # type: (str, Optional[object]) -> dict - """ - Gets the locking configuration for a bucket. The locking - configuration will be applied by default to every new object - placed in the specified bucket. - :param bucket_name: The name of the bucket. - :type bucket_name: str - :return: The locking configuration. - :rtype: Dict - """ - # pylint: disable=unused-argument - - # Try to get the Object Lock configuration. If there is none, - # then return default values. - try: - result = request() # type: ignore - return { - 'lock_enabled': dict_get(result, 'ObjectLockEnabled') == 'Enabled', - 'lock_mode': dict_get(result, 'Rule.DefaultRetention.Mode'), - 'lock_retention_period_days': dict_get(result, 'Rule.DefaultRetention.Days', 0), - 'lock_retention_period_years': dict_get(result, 'Rule.DefaultRetention.Years', 0) - } - except RequestException as e: - if e.content: - content = json_str_to_object(e.content) - if content.get( - 'Code') == 'ObjectLockConfigurationNotFoundError': - return { - 'lock_enabled': False, - 'lock_mode': 'compliance', - 'lock_retention_period_days': None, - 'lock_retention_period_years': None - } - raise e - - @RestClient.api_put('/{bucket_name}?object-lock') - def set_bucket_locking(self, - bucket_name: str, - mode: str, - retention_period_days: Optional[Union[int, str]] = None, - retention_period_years: Optional[Union[int, str]] = None, - request: Optional[object] = None) -> None: - """ - Places the locking configuration on the specified bucket. The - locking configuration will be applied by default to every new - object placed in the specified bucket. - :param bucket_name: The name of the bucket. - :type bucket_name: str - :param mode: The lock mode, e.g. `COMPLIANCE` or `GOVERNANCE`. - :type mode: str - :param retention_period_days: - :type retention_period_days: int - :param retention_period_years: - :type retention_period_years: int - :rtype: None - """ - # pylint: disable=unused-argument - - retention_period_days, retention_period_years = self.perform_validations( - retention_period_days, retention_period_years, mode) - - # Generate the XML data like this: - # <ObjectLockConfiguration> - # <ObjectLockEnabled>string</ObjectLockEnabled> - # <Rule> - # <DefaultRetention> - # <Days>integer</Days> - # <Mode>string</Mode> - # <Years>integer</Years> - # </DefaultRetention> - # </Rule> - # </ObjectLockConfiguration> - locking_configuration = ET.Element('ObjectLockConfiguration') - enabled_element = ET.SubElement(locking_configuration, - 'ObjectLockEnabled') - enabled_element.text = 'Enabled' # Locking can't be disabled. - rule_element = ET.SubElement(locking_configuration, 'Rule') - default_retention_element = ET.SubElement(rule_element, - 'DefaultRetention') - mode_element = ET.SubElement(default_retention_element, 'Mode') - mode_element.text = mode.upper() - if retention_period_days: - days_element = ET.SubElement(default_retention_element, 'Days') - days_element.text = str(retention_period_days) - if retention_period_years: - years_element = ET.SubElement(default_retention_element, 'Years') - years_element.text = str(retention_period_years) - - data = ET.tostring(locking_configuration, encoding='unicode') - - try: - _ = request(data=data) # type: ignore - except RequestException as e: - raise DashboardException(msg=str(e), component='rgw') - - def list_roles(self) -> List[Dict[str, Any]]: - rgw_list_roles_command = ['role', 'list'] - code, roles, err = mgr.send_rgwadmin_command(rgw_list_roles_command) - if code < 0: - logger.warning('Error listing roles with code %d: %s', code, err) - return [] - - return roles - - def create_role(self, role_name: str, role_path: str, role_assume_policy_doc: str) -> None: - try: - json.loads(role_assume_policy_doc) - except: # noqa: E722 - raise DashboardException('Assume role policy document is not a valid json') - - # valid values: - # pylint: disable=C0301 - # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iam-role.html#cfn-iam-role-path # noqa: E501 - if len(role_name) > 64: - raise DashboardException( - f'Role name "{role_name}" is invalid. Should be 64 characters or less') - - role_name_regex = '[0-9a-zA-Z_+=,.@-]+' - if not re.fullmatch(role_name_regex, role_name): - raise DashboardException( - f'Role name "{role_name}" is invalid. Valid characters are "{role_name_regex}"') - - if not os.path.isabs(role_path): - raise DashboardException( - f'Role path "{role_path}" is invalid. It should be an absolute path') - if role_path[-1] != '/': - raise DashboardException( - f'Role path "{role_path}" is invalid. It should start and end with a slash') - path_regex = '(\u002F)|(\u002F[\u0021-\u007E]+\u002F)' - if not re.fullmatch(path_regex, role_path): - raise DashboardException( - (f'Role path "{role_path}" is invalid.' - f'Role path should follow the pattern "{path_regex}"')) - - rgw_create_role_command = ['role', 'create', '--role-name', role_name, '--path', role_path] - if role_assume_policy_doc: - rgw_create_role_command += ['--assume-role-policy-doc', f"{role_assume_policy_doc}"] - - code, _roles, _err = mgr.send_rgwadmin_command(rgw_create_role_command, - stdout_as_json=False) - if code != 0: - # pylint: disable=C0301 - link = 'https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iam-role.html#cfn-iam-role-path' # noqa: E501 - msg = (f'Error creating role with code {code}: ' - 'Looks like the document has a wrong format.' - f' For more information about the format look at {link}') - raise DashboardException(msg=msg, component='rgw') - - def perform_validations(self, retention_period_days, retention_period_years, mode): - try: - retention_period_days = int(retention_period_days) if retention_period_days else 0 - retention_period_years = int(retention_period_years) if retention_period_years else 0 - if retention_period_days < 0 or retention_period_years < 0: - raise ValueError - except (TypeError, ValueError): - msg = "Retention period must be a positive integer." - raise DashboardException(msg=msg, component='rgw') - if retention_period_days and retention_period_years: - # https://docs.aws.amazon.com/AmazonS3/latest/API/archive-RESTBucketPUTObjectLockConfiguration.html - msg = "Retention period requires either Days or Years. "\ - "You can't specify both at the same time." - raise DashboardException(msg=msg, component='rgw') - if not retention_period_days and not retention_period_years: - msg = "Retention period requires either Days or Years. "\ - "You must specify at least one." - raise DashboardException(msg=msg, component='rgw') - if not isinstance(mode, str) or mode.upper() not in ['COMPLIANCE', 'GOVERNANCE']: - msg = "Retention mode must be either COMPLIANCE or GOVERNANCE." - raise DashboardException(msg=msg, component='rgw') - return retention_period_days, retention_period_years + def get_multisite_status(self): + is_multisite_configured = True + 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: + is_multisite_configured = False + return is_multisite_configured diff --git a/src/pybind/mgr/rgw/module.py b/src/pybind/mgr/rgw/module.py index bb1f9460025..079e7e817ca 100644 --- a/src/pybind/mgr/rgw/module.py +++ b/src/pybind/mgr/rgw/module.py @@ -249,26 +249,30 @@ class Module(orchestrator.OrchestratorClientMixin, MgrModule): @CLICommand('rgw realm tokens', perm='r') def list_realm_tokens(self) -> HandleCommandResult: try: - realms_info = [] - for realm_info in RGWAM(self.env).get_realms_info(): - if not realm_info['master_zone_id']: - realms_info.append({'realm': realm_info['realm_name'], 'token': 'realm has no master zone'}) - elif not realm_info['endpoint']: - realms_info.append({'realm': realm_info['realm_name'], 'token': 'master zone has no endpoint'}) - elif not (realm_info['access_key'] and realm_info['secret']): - realms_info.append({'realm': realm_info['realm_name'], 'token': 'master zone has no access/secret keys'}) - else: - keys = ['realm_name', 'realm_id', 'endpoint', 'access_key', 'secret'] - realm_token = RealmToken(**{k: realm_info[k] for k in keys}) - realm_token_b = realm_token.to_json().encode('utf-8') - realm_token_s = base64.b64encode(realm_token_b).decode('utf-8') - realms_info.append({'realm': realm_info['realm_name'], 'token': realm_token_s}) + realms_info = self.get_realm_tokens() except RGWAMException as e: self.log.error(f'cmd run exception: ({e.retcode}) {e.message}') return HandleCommandResult(retval=e.retcode, stdout=e.stdout, stderr=e.stderr) return HandleCommandResult(retval=0, stdout=json.dumps(realms_info, indent=4), stderr='') + def get_realm_tokens(self) -> List[Dict]: + realms_info = [] + for realm_info in RGWAM(self.env).get_realms_info(): + if not realm_info['master_zone_id']: + realms_info.append({'realm': realm_info['realm_name'], 'token': 'realm has no master zone'}) + elif not realm_info['endpoint']: + realms_info.append({'realm': realm_info['realm_name'], 'token': 'master zone has no endpoint'}) + elif not (realm_info['access_key'] and realm_info['secret']): + realms_info.append({'realm': realm_info['realm_name'], 'token': 'master zone has no access/secret keys'}) + else: + keys = ['realm_name', 'realm_id', 'endpoint', 'access_key', 'secret'] + realm_token = RealmToken(**{k: realm_info[k] for k in keys}) + realm_token_b = realm_token.to_json().encode('utf-8') + realm_token_s = base64.b64encode(realm_token_b).decode('utf-8') + realms_info.append({'realm': realm_info['realm_name'], 'token': realm_token_s}) + return realms_info + @CLICommand('rgw zone modify', perm='rw') def update_zone_info(self, realm_name: str, zonegroup_name: str, zone_name: str, realm_token: str, zone_endpoints: List[str]) -> HandleCommandResult: try: @@ -294,6 +298,19 @@ class Module(orchestrator.OrchestratorClientMixin, MgrModule): inbuf: Optional[str] = None) -> HandleCommandResult: """Bootstrap new rgw zone that syncs with zone on another cluster in the same realm""" + created_zones = self.rgw_zone_create(zone_name, realm_token, port, placement, + start_radosgw, zone_endpoints, inbuf) + + return HandleCommandResult(retval=0, stdout=f"Zones {', '.join(created_zones)} created successfully") + + def rgw_zone_create(self, + zone_name: Optional[str] = None, + realm_token: Optional[str] = None, + port: Optional[int] = None, + placement: Optional[str] = None, + start_radosgw: Optional[bool] = True, + zone_endpoints: Optional[str] = None, + inbuf: Optional[str] = None) -> Any: if inbuf: try: rgw_specs = self._parse_rgw_specs(inbuf) @@ -318,11 +335,11 @@ class Module(orchestrator.OrchestratorClientMixin, MgrModule): RGWAM(self.env).zone_create(rgw_spec, start_radosgw) if rgw_spec.rgw_zone is not None: created_zones.append(rgw_spec.rgw_zone) + return created_zones except RGWAMException as e: self.log.error('cmd run exception: (%d) %s' % (e.retcode, e.message)) return HandleCommandResult(retval=e.retcode, stdout=e.stdout, stderr=e.stderr) - - return HandleCommandResult(retval=0, stdout=f"Zones {', '.join(created_zones)} created successfully") + return created_zones @CLICommand('rgw realm reconcile', perm='rw') def _cmd_rgw_realm_reconcile(self, @@ -349,3 +366,13 @@ class Module(orchestrator.OrchestratorClientMixin, MgrModule): self.log.info('Stopping') self.run = False self.event.set() + + def import_realm_token(self, + zone_name: Optional[str] = None, + realm_token: Optional[str] = None, + port: Optional[int] = None, + placement: Optional[str] = None, + start_radosgw: Optional[bool] = True, + zone_endpoints: Optional[str] = None) -> None: + self.rgw_zone_create(zone_name, realm_token, port, placement, start_radosgw, + zone_endpoints) |