From b80eb1c8499298ccf9067681ad10af717b6f3cd0 Mon Sep 17 00:00:00 2001 From: avanthakkar Date: Thu, 30 Mar 2023 22:48:52 +0530 Subject: [PATCH] mgr/dashboard: add support for editing RGW zone Fixes: https://tracker.ceph.com/issues/59328 Signed-off-by: Avan Thakkar Co-authored-by: Aashish Sharma (cherry picked from commit bcc92adb96d1ab8155d30cb51933b0d07b398cdc) --- src/pybind/mgr/dashboard/controllers/rgw.py | 78 ++++- .../src/app/ceph/rgw/models/rgw-multisite.ts | 1 + .../rgw-multisite-details.component.html | 15 +- .../rgw-multisite-details.component.spec.ts | 2 +- .../rgw-multisite-realm-form.component.html | 11 +- .../rgw-multisite-realm-form.component.ts | 25 +- .../rgw-multisite-zone-form.component.html | 169 +++++++++- .../rgw-multisite-zone-form.component.ts | 308 +++++++++++++++--- ...gw-multisite-zonegroup-form.component.html | 28 +- .../rgw-multisite-zonegroup-form.component.ts | 11 + .../rgw-system-user.component.html | 37 +++ .../rgw-system-user.component.scss | 0 .../rgw-system-user.component.spec.ts | 38 +++ .../rgw-system-user.component.ts | 50 +++ .../frontend/src/app/ceph/rgw/rgw.module.ts | 4 +- .../app/shared/api/rgw-multisite.service.ts | 18 + .../src/app/shared/api/rgw-zone.service.ts | 88 ++++- .../app/shared/api/rgw-zonegroup.service.ts | 9 +- .../src/app/shared/services/doc.service.ts | 1 + src/pybind/mgr/dashboard/openapi.yaml | 182 +++++++++++ .../mgr/dashboard/requirements-test.txt | 2 +- .../mgr/dashboard/services/rgw_client.py | 201 +++++++++++- 22 files changed, 1186 insertions(+), 92 deletions(-) create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-system-user/rgw-system-user.component.html create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-system-user/rgw-system-user.component.scss create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-system-user/rgw-system-user.component.spec.ts create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-system-user/rgw-system-user.component.ts create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-multisite.service.ts diff --git a/src/pybind/mgr/dashboard/controllers/rgw.py b/src/pybind/mgr/dashboard/controllers/rgw.py index 4e01fb9ea3a..80f6b6ad575 100644 --- a/src/pybind/mgr/dashboard/controllers/rgw.py +++ b/src/pybind/mgr/dashboard/controllers/rgw.py @@ -6,6 +6,7 @@ from typing import Any, Dict, List, NamedTuple, Optional, Union import cherrypy +from .. import mgr from ..exceptions import DashboardException from ..rest_client import RequestException from ..security import Permission, Scope @@ -13,11 +14,11 @@ from ..services.auth import AuthManager, JwtManager from ..services.ceph_service import CephService from ..services.rgw_client import NoRgwDaemonsException, RgwClient from ..tools import json_str_to_object, str_to_bool -from . import APIDoc, APIRouter, BaseController, CRUDCollectionMethod, \ - CRUDEndpoint, Endpoint, EndpointDoc, ReadPermission, RESTController, \ - UIRouter, allow_empty_body -from ._crud import CRUDMeta, Form, FormField, FormTaskInfo, Icon, MethodType, \ - TableAction, Validator, VerticalContainer +from . import APIDoc, APIRouter, BaseController, CreatePermission, \ + CRUDCollectionMethod, CRUDEndpoint, Endpoint, EndpointDoc, ReadPermission, \ + RESTController, UIRouter, allow_empty_body +from ._crud import CRUDMeta, Form, FormField, FormTaskInfo, Icon, MethodType, TableAction, \ + Validator, VerticalContainer from ._version import APIVersion logger = logging.getLogger("controllers.rgw") @@ -97,6 +98,17 @@ class RgwStatus(BaseController): raise DashboardException(e, http_status_code=404, component='rgw') 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 + @APIRouter('/rgw/daemon', Scope.RGW) @APIDoc("RGW Daemon Management API", "RgwDaemon") @@ -838,11 +850,13 @@ 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, daemon_name=None): + 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) + 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') @@ -884,3 +898,53 @@ class RgwZone(RESTController): 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, zone_name: str, new_zone_name: str, zonegroup_name: str, default: str = '', + master: str = '', zone_endpoints: List[str] = [], user: 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') + + @Endpoint() + @ReadPermission + def get_pool_names(self): + pool_names = [] + ret, out, _ = mgr.check_mon_command({ + 'prefix': 'osd lspools', + 'format': 'json', + }) + if ret == 0 and out is not None: + pool_names = json.loads(out) + return pool_names + + @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') + + @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') 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 9c324f48727..fb0ce154900 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 @@ -43,4 +43,5 @@ export class RgwZone { placement_pools: any[]; realm_id: string; notif_pool: string; + endpoints: 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 12ae56a2d07..3c65390a177 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 @@ -9,7 +9,7 @@
Multi-site Topology viewer
+ i18n>Topology Viewer
@@ -26,10 +26,6 @@ {{ node.data.name }} - - {{ node.data.type }} - default @@ -45,8 +41,7 @@
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 173bdb4bd82..7f1c0b19769 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 @@ -32,6 +32,6 @@ describe('RgwMultisiteDetailsComponent', () => { it('should display right title', () => { const span = debugElement.nativeElement.querySelector('.card-header'); - expect(span.textContent).toBe('Multi-site Topology viewer'); + expect(span.textContent).toBe('Topology Viewer'); }); }); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-realm-form/rgw-multisite-realm-form.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-realm-form/rgw-multisite-realm-form.component.html index 46e732659c1..0bcf88b8cd2 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-realm-form/rgw-multisite-realm-form.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-realm-form/rgw-multisite-realm-form.component.html @@ -30,12 +30,19 @@ id="default_realm" name="default_realm" formControlName="default_realm" + [attr.disabled]="action === 'edit' ? true: null" type="checkbox"> - - You cannot unset the default flag. Please create another realm and set it as default. + + You cannot unset the default flag. + + + Please consult the documentation to follow the failover mechanism + + + Default realm already exists. diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-realm-form/rgw-multisite-realm-form.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-realm-form/rgw-multisite-realm-form.component.ts index b3d000c94fb..afbb29e8efe 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-realm-form/rgw-multisite-realm-form.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-realm-form/rgw-multisite-realm-form.component.ts @@ -8,6 +8,7 @@ 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 } from '../models/rgw-multisite'; +import { DocService } from '~/app/shared/services/doc.service'; @Component({ selector: 'cd-rgw-multisite-realm-form', @@ -23,14 +24,20 @@ export class RgwMultisiteRealmFormComponent implements OnInit { multisiteInfo: object[] = []; realm: RgwRealm; realmList: RgwRealm[] = []; + zonegroupList: RgwRealm[] = []; realmNames: string[]; newRealmName: string; + isMaster: boolean; + defaultsInfo: string[]; + defaultRealmDisabled = false; + docUrl: string; constructor( public activeModal: NgbActiveModal, public actionLabels: ActionLabelsI18n, public rgwRealmService: RgwRealmService, - public notificationService: NotificationService + public notificationService: NotificationService, + public docService: DocService ) { this.action = this.editing ? this.actionLabels.EDIT + this.resource @@ -65,16 +72,30 @@ export class RgwMultisiteRealmFormComponent implements OnInit { return realm['name']; }); if (this.action === 'edit') { + this.zonegroupList = + this.multisiteInfo[1] !== undefined && this.multisiteInfo[1].hasOwnProperty('zonegroups') + ? this.multisiteInfo[1]['zonegroups'] + : []; this.multisiteRealmForm.get('realmName').setValue(this.info.data.name); this.multisiteRealmForm.get('default_realm').setValue(this.info.data.is_default); if (this.info.data.is_default) { this.multisiteRealmForm.get('default_realm').disable(); } } + this.zonegroupList.forEach((zgp: any) => { + if (zgp.is_master === true && zgp.realm_id === this.info.data.id) { + this.isMaster = true; + } + }); + if (this.defaultsInfo && this.defaultsInfo['defaultRealmName'] !== null) { + this.multisiteRealmForm.get('default_realm').disable(); + this.defaultRealmDisabled = true; + } + this.docUrl = this.docService.urlGenerator('rgw-multisite'); } submit() { - const values = this.multisiteRealmForm.value; + const values = this.multisiteRealmForm.getRawValue(); this.realm = new RgwRealm(); if (this.action === 'create') { this.realm.name = values['realmName']; 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 715cb46b13f..0c4ec560d38 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 @@ -15,6 +15,7 @@

+ i18n>Default + + Default zone can only exist in a default zonegroup. + + + + You cannot unset the default flag. + + + + Please consult the documentation to follow the failover mechanism +
+
+
+ + Master zone already exists for the selected zonegroup. + + + + You cannot unset the master flag. + + + + Please consult the documentation to follow the failover mechanism +
@@ -82,7 +109,8 @@ i18n>Please enter a valid IP address. -
+
@@ -99,7 +127,140 @@ [ngValue]="null">-- Select a user -- - +

+
+ +
+
+
+ Placement Targets +
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ Storage Classes +
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
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 567c5f096dd..7a0c7ecb983 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 @@ -1,7 +1,8 @@ import { Component, OnInit } from '@angular/core'; import { FormControl, Validators } from '@angular/forms'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; +import { NgbActiveModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; import _ from 'lodash'; +import { RgwMultisiteService } from '~/app/shared/api/rgw-multisite.service'; import { RgwUserService } from '~/app/shared/api/rgw-user.service'; import { RgwZoneService } from '~/app/shared/api/rgw-zone.service'; import { RgwZonegroupService } from '~/app/shared/api/rgw-zonegroup.service'; @@ -11,6 +12,8 @@ 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 { ModalService } from '~/app/shared/services/modal.service'; +import { RgwSystemUserComponent } from '../rgw-system-user/rgw-system-user.component'; @Component({ selector: 'cd-rgw-multisite-zone-form', @@ -22,6 +25,7 @@ export class RgwMultisiteZoneFormComponent implements OnInit { 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; action: string; + info: any; multisiteZoneForm: CdFormGroup; editing = false; resource: string; @@ -33,15 +37,34 @@ export class RgwMultisiteZoneFormComponent implements OnInit { zonegroupList: RgwZonegroup[] = []; zoneList: RgwZone[] = []; zoneNames: string[]; - users: string[]; + users: any; + placementTargets: any; + zoneInfo: RgwZone; + poolList: object[] = []; + storageClassList: object[] = []; + disableDefault: boolean = false; + disableMaster: boolean = false; + isMetadataSync: boolean = false; + isMasterZone: boolean; + isDefaultZone: boolean; + syncStatusTimedOut: boolean = false; + bsModalRef: NgbModalRef; + createSystemUser: boolean = false; + master_zone_of_master_zonegroup: RgwZone; + masterZoneUser: any; + access_key: any; + master_zonegroup_of_realm: RgwZonegroup; + compressionTypes = ['lz4', 'zlib', 'snappy']; constructor( public activeModal: NgbActiveModal, public actionLabels: ActionLabelsI18n, + public rgwMultisiteService: RgwMultisiteService, public rgwZoneService: RgwZoneService, public rgwZoneGroupService: RgwZonegroupService, public notificationService: NotificationService, - public rgwUserService: RgwUserService + public rgwUserService: RgwUserService, + public modalService: ModalService ) { this.action = this.editing ? this.actionLabels.EDIT + this.resource @@ -55,14 +78,16 @@ export class RgwMultisiteZoneFormComponent implements OnInit { validators: [ Validators.required, CdValidators.custom('uniqueName', (zoneName: string) => { - return this.zoneNames && this.zoneNames.indexOf(zoneName) !== -1; + return ( + this.action === 'create' && this.zoneNames && this.zoneNames.indexOf(zoneName) !== -1 + ); }) ] }), default_zone: new FormControl(false), master_zone: new FormControl(false), selectedZonegroup: new FormControl(null), - zone_endpoints: new FormControl(null, { + zone_endpoints: new FormControl([], { validators: [ CdValidators.custom('endpoint', (value: string) => { if (_.isEmpty(value)) { @@ -83,10 +108,18 @@ export class RgwMultisiteZoneFormComponent implements OnInit { } return false; } - }) + }), + Validators.required ] }), - users: new FormControl(null) + users: new FormControl(null), + placementTarget: new FormControl(null), + placementDataPool: new FormControl(''), + placementIndexPool: new FormControl(null), + placementDataExtraPool: new FormControl(null), + storageClass: new FormControl(null), + storageDataPool: new FormControl(null), + storageCompression: new FormControl(null) }); } @@ -97,8 +130,38 @@ export class RgwMultisiteZoneFormComponent implements OnInit { if (_.isEmpty(zonegroup.master_zone)) { this.multisiteZoneForm.get('master_zone').setValue(true); this.multisiteZoneForm.get('master_zone').disable(); + this.disableMaster = false; + } else if (!_.isEmpty(zonegroup.master_zone) && this.action === 'create') { + this.multisiteZoneForm.get('master_zone').setValue(false); + 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') !== + this.defaultsInfo['defaultZonegroupName'] + ) { + this.disableDefault = true; + this.multisiteZoneForm.get('default_zone').disable(); + } } ngOnInit(): void { @@ -114,42 +177,211 @@ export class RgwMultisiteZoneFormComponent implements OnInit { return zone['name']; }); if (this.action === 'create') { + if (this.defaultsInfo['defaultZonegroupName'] !== undefined) { + this.multisiteZoneForm + .get('selectedZonegroup') + .setValue(this.defaultsInfo['defaultZonegroupName']); + this.onZoneGroupChange(this.defaultsInfo['defaultZonegroupName']); + } + } + if (this.action === 'edit') { + this.placementTargets = this.info.parent ? this.info.parent.data.placement_targets : []; + this.rgwZoneService.getPoolNames().subscribe((pools: object[]) => { + this.poolList = pools; + }); + this.multisiteZoneForm.get('zoneName').setValue(this.info.data.name); + 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('selectedZonegroup') - .setValue(this.defaultsInfo['defaultZonegroupName']); - this.onZoneGroupChange(this.defaultsInfo['defaultZonegroupName']); + .get('placementTarget') + .setValue(this.info.parent.data.default_placement); + this.getZonePlacementData(this.multisiteZoneForm.getValue('placementTarget')); + if (this.info.data.is_default) { + this.isDefaultZone = true; + this.multisiteZoneForm.get('default_zone').disable(); + } + if (this.info.data.is_master) { + this.isMasterZone = true; + this.multisiteZoneForm.get('master_zone').disable(); + } + const zone = new RgwZone(); + zone.name = this.info.data.name; + this.onZoneGroupChange(this.info.data.parent); + setTimeout(() => { + this.getUserInfo(zone); + }, 1500); + } + if ( + this.multisiteZoneForm.getValue('selectedZonegroup') !== + this.defaultsInfo['defaultZonegroupName'] + ) { + this.disableDefault = true; + this.multisiteZoneForm.get('default_zone').disable(); } - this.rgwUserService.list().subscribe((users: any) => { - this.users = users.filter((user: any) => user.keys.length !== 0); - }); } - submit() { - const values = this.multisiteZoneForm.value; - this.zonegroup = new RgwZonegroup(); - this.zonegroup.name = values['selectedZonegroup']; - this.zone = new RgwZone(); - this.zone.name = values['zoneName']; + getUserInfo(zone: RgwZone) { this.rgwZoneService - .create( - this.zone, - this.zonegroup, - values['default_zone'], - values['master_zone'], - values['zone_endpoints'], - values['users'] - ) - .subscribe( - () => { - this.notificationService.show( - NotificationType.success, - $localize`Zone: '${values['zoneName']}' created successfully` - ); - this.activeModal.close(); - }, - () => { - this.multisiteZoneForm.setErrors({ cdSubmitButton: true }); + .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; + if (this.placementTargets) { + this.placementTargets.forEach((placement: any) => { + if (placement.name === placementTarget) { + let storageClasses = placement.storage_classes; + this.storageClassList = Object.entries(storageClasses).map(([key, value]) => ({ + key, + value + })); } - ); + }); + } + this.rgwZoneService.get(this.zone).subscribe((zoneInfo: RgwZone) => { + this.zoneInfo = zoneInfo; + if (this.zoneInfo && this.zoneInfo['placement_pools']) { + this.zoneInfo['placement_pools'].forEach((plc_pool) => { + if (plc_pool.key === placementTarget) { + let storageClasses = plc_pool.val.storage_classes; + let placementDataPool = storageClasses['STANDARD'] + ? storageClasses['STANDARD']['data_pool'] + : ''; + let placementIndexPool = plc_pool.val.index_pool; + let placementDataExtraPool = plc_pool.val.data_extra_pool; + this.poolList.push({ poolname: placementDataPool }); + this.poolList.push({ poolname: placementIndexPool }); + this.poolList.push({ poolname: placementDataExtraPool }); + this.multisiteZoneForm.get('storageClass').setValue(this.storageClassList[0]['value']); + this.multisiteZoneForm.get('storageDataPool').setValue(placementDataPool); + this.multisiteZoneForm.get('storageCompression').setValue(this.compressionTypes[0]); + this.multisiteZoneForm.get('placementDataPool').setValue(placementDataPool); + this.multisiteZoneForm.get('placementIndexPool').setValue(placementIndexPool); + this.multisiteZoneForm.get('placementDataExtraPool').setValue(placementDataExtraPool); + } + }); + } + }); + } + + getStorageClassData(storageClass: string) { + let storageClassSelected = this.storageClassList.find((x) => x['value'] == storageClass)[ + 'value' + ]; + this.poolList.push({ poolname: storageClassSelected.data_pool }); + this.multisiteZoneForm.get('storageDataPool').setValue(storageClassSelected.data_pool); + this.multisiteZoneForm + .get('storageCompression') + .setValue(storageClassSelected.compression_type); + } + + submit() { + const values = this.multisiteZoneForm.getRawValue(); + if (this.action === 'create') { + this.zonegroup = new RgwZonegroup(); + 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.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 + ) + .subscribe( + () => { + this.notificationService.show( + NotificationType.success, + $localize`Zone: '${values['zoneName']}' created successfully` + ); + this.activeModal.close(); + }, + () => { + this.multisiteZoneForm.setErrors({ cdSubmitButton: true }); + } + ); + } else if (this.action === 'edit') { + this.zonegroup = new RgwZonegroup(); + 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['zonegroup_endpoints'] + : this.checkUrlArray(values['zone_endpoints']); + this.rgwZoneService + .update( + this.zone, + this.zonegroup, + values['zoneName'], + 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 + ) + .subscribe( + () => { + this.notificationService.show( + NotificationType.success, + $localize`Zone: '${values['zoneName']}' updated successfully` + ); + this.activeModal.close(); + }, + () => { + this.multisiteZoneForm.setErrors({ cdSubmitButton: true }); + } + ); + } + } + + checkUrlArray(endpoints: string) { + let endpointsArray = []; + if (endpoints.includes(',')) { + endpointsArray = endpoints.split(','); + } else { + endpointsArray.push(endpoints); + } + 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.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-zonegroup-form/rgw-multisite-zonegroup-form.component.html index b3848052b5b..e028d513ffe 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-zonegroup-form/rgw-multisite-zonegroup-form.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-zonegroup-form/rgw-multisite-zonegroup-form.component.html @@ -49,30 +49,38 @@ id="default_zonegroup" name="default_zonegroup" formControlName="default_zonegroup" + [attr.disabled]="action === 'edit' ? true : null" type="checkbox"> - - Zonegroup doesn't belong to the default realm. -
+ + Zonegroup doesn't belong to the default realm. + + + Please consult the documentation to follow the failover mechanism + + + You cannot unset the default flag. +
- - RGW multi-site configuration must have a master zonegroup. Setting - the first zonegroup created as master, to avoid any errors on udating the period. - Can be modified later by editing a zonegroup. - - - + Multiple master zonegroups can't be configured. If you want to create a new zonegroup and make it the master zonegroup, you must delete the default zonegroup. + + Please consult the documentation to follow the failover mechanism + + + You cannot unset the master flag. + 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 2172a301e2c..be9e3f64859 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 @@ -137,9 +137,20 @@ export class RgwMultisiteZonegroupFormComponent implements OnInit { this.zonegroupNames = this.zonegroupList.map((zonegroup) => { return zonegroup['name']; }); + let allZonegroupZonesList = this.zonegroupList.map((zonegroup: RgwZonegroup) => { + return zonegroup['zones']; + }); + const allZonegroupZonesInfo = allZonegroupZonesList.reduce( + (accumulator, value) => accumulator.concat(value), + [] + ); + const allZonegroupZonesNames = allZonegroupZonesInfo.map((zone) => { + return zone['name']; + }); this.allZoneNames = this.zoneList.map((zone: RgwZone) => { return zone['name']; }); + this.allZoneNames = _.difference(this.allZoneNames, allZonegroupZonesNames); if (this.action === 'create' && this.defaultsInfo['defaultRealmName'] !== null) { this.multisiteZonegroupForm .get('selectedRealm') diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-system-user/rgw-system-user.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-system-user/rgw-system-user.component.html new file mode 100644 index 00000000000..86aa3d25568 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-system-user/rgw-system-user.component.html @@ -0,0 +1,37 @@ + + Create System User + + +
+ + +
+
+
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-system-user/rgw-system-user.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-system-user/rgw-system-user.component.scss new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-system-user/rgw-system-user.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-system-user/rgw-system-user.component.spec.ts new file mode 100644 index 00000000000..b3d576ae60b --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-system-user/rgw-system-user.component.spec.ts @@ -0,0 +1,38 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { RgwSystemUserComponent } from './rgw-system-user.component'; +import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { ReactiveFormsModule } from '@angular/forms'; +import { RouterTestingModule } from '@angular/router/testing'; +import { ToastrModule } from 'ngx-toastr'; +import { SharedModule } from '~/app/shared/shared.module'; + +describe('RgwSystemUserComponent', () => { + let component: RgwSystemUserComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ + SharedModule, + ReactiveFormsModule, + RouterTestingModule, + HttpClientTestingModule, + ToastrModule.forRoot() + ], + declarations: [RgwSystemUserComponent], + providers: [NgbActiveModal] + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(RgwSystemUserComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-system-user/rgw-system-user.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-system-user/rgw-system-user.component.ts new file mode 100644 index 00000000000..6ace671c7a6 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-system-user/rgw-system-user.component.ts @@ -0,0 +1,50 @@ +import { Component, EventEmitter, Output } from '@angular/core'; +import { FormControl, Validators } from '@angular/forms'; +import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; +import { RgwZoneService } from '~/app/shared/api/rgw-zone.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 { NotificationService } from '~/app/shared/services/notification.service'; + +@Component({ + selector: 'cd-rgw-system-user', + templateUrl: './rgw-system-user.component.html', + styleUrls: ['./rgw-system-user.component.scss'] +}) +export class RgwSystemUserComponent { + multisiteSystemUserForm: CdFormGroup; + zoneName: string; + + @Output() + submitAction = new EventEmitter(); + + constructor( + public activeModal: NgbActiveModal, + public actionLabels: ActionLabelsI18n, + public rgwZoneService: RgwZoneService, + public notificationService: NotificationService + ) { + this.createForm(); + } + + createForm() { + this.multisiteSystemUserForm = new CdFormGroup({ + userName: new FormControl(null, { + validators: [Validators.required] + }) + }); + } + + submit() { + const userName = this.multisiteSystemUserForm.getValue('userName'); + this.rgwZoneService.createSystemUser(userName, this.zoneName).subscribe(() => { + this.submitAction.emit(); + this.notificationService.show( + NotificationType.success, + $localize`User: '${this.multisiteSystemUserForm.getValue('userName')}' created successfully` + ); + this.activeModal.close(); + }); + } +} 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 d3a48ad6b22..671178b6d74 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 @@ -36,6 +36,7 @@ import { RgwMultisiteZonegroupFormComponent } from './rgw-multisite-zonegroup-fo import { RgwMultisiteZoneFormComponent } from './rgw-multisite-zone-form/rgw-multisite-zone-form.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 { RgwSystemUserComponent } from './rgw-system-user/rgw-system-user.component'; @NgModule({ imports: [ @@ -81,7 +82,8 @@ import { RgwMultisiteZonegroupDeletionFormComponent } from './models/rgw-multisi RgwMultisiteZonegroupFormComponent, RgwMultisiteZoneFormComponent, RgwMultisiteZoneDeletionFormComponent, - RgwMultisiteZonegroupDeletionFormComponent + RgwMultisiteZonegroupDeletionFormComponent, + RgwSystemUserComponent ] }) export class RgwModule {} 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 new file mode 100644 index 00000000000..400a273cb03 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-multisite.service.ts @@ -0,0 +1,18 @@ +import { HttpClient } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { RgwDaemonService } from './rgw-daemon.service'; + +@Injectable({ + providedIn: 'root' +}) +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`); + }); + } +} 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 a8530c9558e..ffff267ceb2 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 @@ -19,8 +19,16 @@ export class RgwZoneService { defaultZone: boolean, master: boolean, endpoints: Array, - user: string + user: string, + createSystemUser: boolean, + master_zone_of_master_zonegroup: RgwZone ) { + 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, @@ -28,7 +36,9 @@ export class RgwZoneService { default: defaultZone, master: master, zone_endpoints: endpoints, - user: user + user: user, + createSystemUser: createSystemUser, + master_zone_of_master_zonegroup: master_zone_name }); return this.http.post(`${this.url}`, null, { params: params }); }); @@ -63,17 +73,66 @@ export class RgwZoneService { }); } + update( + zone: RgwZone, + zonegroup: RgwZonegroup, + newZoneName: string, + defaultZone?: boolean, + master?: boolean, + endpoints?: Array, + user?: string, + placementTarget?: string, + dataPool?: string, + indexPool?: string, + dataExtraPool?: string, + storageClass?: string, + dataPoolClass?: string, + compression?: string, + master_zone_of_master_zonegroup?: RgwZone + ) { + 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); + }); + } + getZoneTree(zone: RgwZone, defaultZoneId: string, zonegroup?: RgwZonegroup, realm?: RgwRealm) { let nodes = {}; let zoneIds = []; nodes['id'] = zone.id; zoneIds.push(zone.id); nodes['name'] = zone.name; + nodes['type'] = 'zone'; + nodes['name'] = zone.name; nodes['info'] = zone; nodes['icon'] = Icons.deploy; + nodes['zone_zonegroup'] = zonegroup; nodes['parent'] = zonegroup ? zonegroup.name : ''; nodes['second_parent'] = realm ? realm.name : ''; nodes['is_default'] = zone.id === defaultZoneId ? true : false; + nodes['endpoints'] = zone.endpoints; nodes['is_master'] = zonegroup && zonegroup.master_zone === zone.id ? true : false; nodes['type'] = 'zone'; return { @@ -81,4 +140,29 @@ export class RgwZoneService { zoneIds: zoneIds }; } + + getPoolNames() { + return this.rgwDaemonService.request(() => { + 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); + }); + } + + 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 }); + }); + } } 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 bb54606c2fe..7c9b6b870ca 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 @@ -30,10 +30,10 @@ export class RgwZonegroupService { realm: RgwRealm, zonegroup: RgwZonegroup, newZonegroupName: string, - defaultZonegroup: boolean, - master: boolean, - removedZones: string[], - addedZones: string[] + defaultZonegroup?: boolean, + master?: boolean, + removedZones?: string[], + addedZones?: string[] ) { return this.rgwDaemonService.request((requestBody: any) => { requestBody = { @@ -93,6 +93,7 @@ export class RgwZonegroupService { nodes['master_zone'] = zonegroup.master_zone; nodes['zones'] = zonegroup.zones; nodes['placement_targets'] = zonegroup.placement_targets; + nodes['default_placement'] = zonegroup.default_placement; return nodes; } } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/doc.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/doc.service.ts index 22f2230cbd4..00c91abd092 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/doc.service.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/doc.service.ts @@ -35,6 +35,7 @@ export class DocService { 'nfs-ganesha': `${domain}mgr/dashboard/#configuring-nfs-ganesha-in-the-dashboard`, 'rgw-nfs': `${domain}radosgw/nfs`, rgw: `${domain}mgr/dashboard/#enabling-the-object-gateway-management-frontend`, + 'rgw-multisite': `${domain}/radosgw/multisite/#failover-and-disaster-recovery`, dashboard: `${domain}mgr/dashboard`, grafana: `${domain}mgr/dashboard/#enabling-the-embedding-of-grafana-dashboards`, orch: `${domain}mgr/orchestrator`, diff --git a/src/pybind/mgr/dashboard/openapi.yaml b/src/pybind/mgr/dashboard/openapi.yaml index 255fd0dedff..723acc09268 100644 --- a/src/pybind/mgr/dashboard/openapi.yaml +++ b/src/pybind/mgr/dashboard/openapi.yaml @@ -9310,6 +9310,9 @@ paths: application/json: schema: properties: + createSystemUser: + default: false + type: boolean daemon_name: type: string default: @@ -9318,6 +9321,8 @@ paths: master: default: false type: boolean + master_zone_of_master_zonegroup: + type: string user: type: string zone_endpoints: @@ -9353,6 +9358,48 @@ paths: - jwt: [] tags: - RgwZone + /api/rgw/zone/create_system_user: + put: + parameters: [] + requestBody: + content: + application/json: + schema: + properties: + daemon_name: + type: string + userName: + type: string + zoneName: + type: string + required: + - userName + - zoneName + 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: + - RgwZone /api/rgw/zone/get_all_zones_info: get: parameters: [] @@ -9375,6 +9422,60 @@ paths: - jwt: [] tags: - RgwZone + /api/rgw/zone/get_pool_names: + 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: + - RgwZone + /api/rgw/zone/get_user_list: + get: + parameters: + - allowEmptyValue: true + in: query + name: daemon_name + schema: + type: string + - allowEmptyValue: true + in: query + name: zoneName + schema: + type: string + responses: + '200': + content: + application/vnd.ceph.api.v1.0+json: + type: object + description: OK + '400': + description: Operation exception. Please check the response body for details. + '401': + description: Unauthenticated access. Please login first. + '403': + description: Unauthorized access. Please check your permissions. + '500': + description: Unexpected error. Please check the response body for the stack + trace. + security: + - jwt: [] + tags: + - RgwZone /api/rgw/zone/{zone_name}: delete: parameters: @@ -9453,6 +9554,87 @@ paths: - jwt: [] tags: - RgwZone + put: + parameters: + - in: path + name: zone_name + required: true + schema: + type: string + requestBody: + content: + application/json: + schema: + properties: + compression: + default: '' + type: string + daemon_name: + type: string + data_extra_pool: + default: '' + type: string + data_pool: + default: '' + type: string + data_pool_class: + default: '' + type: string + default: + default: '' + type: string + index_pool: + default: '' + type: string + master: + default: '' + type: string + master_zone_of_master_zonegroup: + type: string + new_zone_name: + type: string + placement_target: + default: '' + type: string + storage_class: + default: '' + type: string + user: + default: '' + type: string + zone_endpoints: + default: [] + type: string + zonegroup_name: + type: string + required: + - new_zone_name + - zonegroup_name + 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: + - RgwZone /api/rgw/zonegroup: get: parameters: diff --git a/src/pybind/mgr/dashboard/requirements-test.txt b/src/pybind/mgr/dashboard/requirements-test.txt index 4e925e8616f..d2566bab59f 100644 --- a/src/pybind/mgr/dashboard/requirements-test.txt +++ b/src/pybind/mgr/dashboard/requirements-test.txt @@ -1,4 +1,4 @@ pytest-cov pytest-instafail pyfakefs==4.5.0 -jsonschema==4.16.0 +jsonschema diff --git a/src/pybind/mgr/dashboard/services/rgw_client.py b/src/pybind/mgr/dashboard/services/rgw_client.py index fe432f079c3..ee765218709 100644 --- a/src/pybind/mgr/dashboard/services/rgw_client.py +++ b/src/pybind/mgr/dashboard/services/rgw_client.py @@ -8,6 +8,7 @@ import json import logging import os import re +import subprocess import xml.etree.ElementTree as ET # noqa: N814 from subprocess import SubprocessError @@ -791,6 +792,15 @@ class RgwClient(RestClient): if placement_target['tags']: cmd_add_placement_options += ['--tags', placement_target['tags']] rgw_add_placement_cmd += cmd_add_placement_options + try: + exit_code, _, err = mgr.send_rgwadmin_command(rgw_add_placement_cmd) + if exit_code > 0: + raise DashboardException(e=err, + msg='Unable to add placement target {} to zonegroup {}'.format(placement_target['placement_id'], 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') + self.update_period() storage_classes = placement_target['storage_class'].split(",") if placement_target['storage_class'] else [] # noqa E501 #pylint: disable=line-too-long if storage_classes: for sc in storage_classes: @@ -805,16 +815,6 @@ class RgwClient(RestClient): except SubprocessError as error: raise DashboardException(error, http_status_code=500, component='rgw') self.update_period() - return - try: - exit_code, _, err = mgr.send_rgwadmin_command(rgw_add_placement_cmd) - if exit_code > 0: - raise DashboardException(e=err, - msg='Unable to add placement target {} to zonegroup {}'.format(placement_target['placement_id'], 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') - self.update_period() def modify_placement_targets(self, zonegroup_name: str, placement_targets: List[Dict]): rgw_add_placement_cmd = ['zonegroup', 'placement', 'modify'] @@ -944,9 +944,10 @@ class RgwClient(RestClient): zone_info = self.get_zone(zone['name']) self.delete_zone_pools(zone['name'], zone_info) - def create_zone(self, zone_name, zonegroup_name, default, master, endpoints, user): + 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 = _get_user_keys(user) + 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 @@ -978,8 +979,133 @@ 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: + access_key = key.get('access_key') + secret_key = key.get('secret_key') + return access_key, secret_key + 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 + 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) + if default and str_to_bool(default): + rgw_zone_modify_cmd.append('--default') + if master and str_to_bool(master): + rgw_zone_modify_cmd.append('--master') + if access_key is not None: + rgw_zone_modify_cmd.append('--access-key') + rgw_zone_modify_cmd.append(access_key) + if secret_key is not None: + rgw_zone_modify_cmd.append('--secret') + rgw_zone_modify_cmd.append(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() + + def add_placement_targets_zone(self, zone_name: str, placement_target: str, data_pool: str, + index_pool: str, data_extra_pool: str): + rgw_zone_add_placement_cmd = ['zone', 'placement', 'add', '--rgw-zone', zone_name, + '--placement-id', placement_target, '--data-pool', data_pool, + '--index-pool', index_pool, + '--data-extra-pool', data_extra_pool] + try: + exit_code, _, err = mgr.send_rgwadmin_command(rgw_zone_add_placement_cmd) + if exit_code > 0: + raise DashboardException(e=err, msg='Unable to add placement target {} to zone {}'.format(placement_target, 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') + self.update_period() + + def add_storage_class_zone(self, zone_name: str, placement_target: str, storage_class: str, + data_pool: str, compression: str): + rgw_zone_add_storage_class_cmd = ['zone', 'placement', 'add', '--rgw-zone', zone_name, + '--placement-id', placement_target, + '--storage-class', storage_class, + '--data-pool', data_pool, + '--compression', compression] + try: + exit_code, _, err = mgr.send_rgwadmin_command(rgw_zone_add_storage_class_cmd) + if exit_code > 0: + raise DashboardException(e=err, msg='Unable to add storage class {} to zone {}'.format(storage_class, 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') + 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 = '', + 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): + if new_zone_name != zone_name: + rgw_zone_rename_cmd = ['zone', 'rename', '--rgw-zone', + zone_name, '--zone-new-name', new_zone_name] + try: + exit_code, _, err = mgr.send_rgwadmin_command(rgw_zone_rename_cmd, False) + if exit_code > 0: + raise DashboardException(e=err, msg='Unable to rename zone to {}'.format(new_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') + self.update_period() + self.modify_zone(new_zone_name, zonegroup_name, default, master, endpoints, user, + master_zone_of_master_zonegroup) + 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, + data_pool_class, compression) + def list_zones(self): rgw_zone_list = {} rgw_zone_list_cmd = ['zone', 'list'] @@ -1083,6 +1209,57 @@ class RgwClient(RestClient): 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'] + try: + exit_code, out, _ = mgr.send_rgwadmin_command(rgw_user_create_cmd) + if exit_code > 0: + raise DashboardException(msg='Unable to create system user', + http_status_code=500, component='rgw') + return out + except SubprocessError as error: + raise DashboardException(error, http_status_code=500, component='rgw') + + def get_user_list(self, zoneName: str): + all_users_info = [] + user_list = [] + rgw_user_list_cmd = ['user', 'list', '--rgw-zone', zoneName] + try: + exit_code, out, _ = mgr.send_rgwadmin_command(rgw_user_list_cmd) + if exit_code > 0: + raise DashboardException('Unable to get user list', + http_status_code=500, component='rgw') + user_list = out + except SubprocessError as error: + raise DashboardException(error, http_status_code=500, component='rgw') + + if len(user_list) > 0: + for user_name in user_list: + rgw_user_info_cmd = ['user', 'info', '--uid', user_name, '--rgw-zone', zoneName] + try: + exit_code, out, _ = mgr.send_rgwadmin_command(rgw_user_info_cmd) + if exit_code > 0: + raise DashboardException('Unable to get user info', + http_status_code=500, component='rgw') + all_users_info.append(out) + except SubprocessError as error: + raise DashboardException(error, http_status_code=500, component='rgw') + return all_users_info + @RestClient.api_get('/{bucket_name}?versioning') def get_bucket_versioning(self, bucket_name, request=None): """ -- 2.39.5