From d2f5c3b7edbbd95ddcaab518cf65db0e785e12ab Mon Sep 17 00:00:00 2001 From: Aashish Sharma Date: Wed, 16 Apr 2025 16:49:23 +0530 Subject: [PATCH] mgr/dashboard: Allow the user to re-use existing r ealm/zg/zone and setup replication 1. Currently, we just allow the user to create a new realm/zg/zone and setup replication using the multi-site replication wizard. The ask is to allow the user to select the pre-existing realm/zg/zone and setup replication via automatic export and import of token as well. 2. Enable rgw module automatically in the selected cluster if its not enabled Fixes: https://tracker.ceph.com/issues/70276 Signed-off-by: Aashish Sharma --- src/pybind/mgr/dashboard/controllers/rgw.py | 5 +- .../multisite-wizard-steps.enum.ts | 6 + .../rgw-multisite-wizard.component.html | 416 ++++++++++-------- .../rgw-multisite-wizard.component.ts | 182 +++++--- .../app/shared/api/rgw-multisite.service.ts | 7 +- .../src/app/shared/models/multi-cluster.ts | 9 + .../mgr/dashboard/services/rgw_client.py | 337 +++++++------- src/pybind/mgr/dashboard/services/service.py | 45 ++ 8 files changed, 622 insertions(+), 385 deletions(-) diff --git a/src/pybind/mgr/dashboard/controllers/rgw.py b/src/pybind/mgr/dashboard/controllers/rgw.py index 10e6d0dbbc678..757a8b89fcbaa 100755 --- a/src/pybind/mgr/dashboard/controllers/rgw.py +++ b/src/pybind/mgr/dashboard/controllers/rgw.py @@ -122,14 +122,15 @@ class RgwMultisiteStatus(RESTController): def setup_multisite_replication(self, daemon_name=None, realm_name=None, zonegroup_name=None, zonegroup_endpoints=None, zone_name=None, zone_endpoints=None, username=None, cluster_fsid=None, replication_zone_name=None, - cluster_details=None): + cluster_details=None, selectedRealmName=None): multisite_instance = RgwMultisiteAutomation() result = multisite_instance.setup_multisite_replication(realm_name, zonegroup_name, zonegroup_endpoints, zone_name, zone_endpoints, username, cluster_fsid, replication_zone_name, - cluster_details) + cluster_details, + selectedRealmName) return result @RESTController.Collection(method='PUT', path='/setup-rgw-credentials') diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-wizard/multisite-wizard-steps.enum.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-wizard/multisite-wizard-steps.enum.ts index 299ffee5b7318..d6437a3fa313f 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-wizard/multisite-wizard-steps.enum.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-wizard/multisite-wizard-steps.enum.ts @@ -17,3 +17,9 @@ export const STEP_TITLES_SINGLE_CLUSTER = [ StepTitles.CreateZone, StepTitles.Review ]; + +export const STEP_TITLES_EXISTING_REALM = [ + StepTitles.CreateRealmAndZonegroup, + StepTitles.SelectCluster, + StepTitles.Review +]; diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-wizard/rgw-multisite-wizard.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-wizard/rgw-multisite-wizard.component.html index d431cd3aa6caf..3658ebf5451ae 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-wizard/rgw-multisite-wizard.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-wizard/rgw-multisite-wizard.component.html @@ -36,183 +36,241 @@ later to manually import into a desired cluster to establish replication between the clusters. -
+
-
- - - Enter a unique name for the Realm. The Realm is a logical grouping of all your Zonegroups. - - This field is required. - The chosen realm name is already in use. + for="configType" + i18n>Realm configuration mode +
+ +
-
-
- -
- - - Enter a name for the Zonegroup. Zonegroup will help you identify and manage the group of zones. - - This field is required. - The chosen zone group name is already in use. +
+ +
-
- +
+
- - - - Select the endpoints for the Zonegroup. Endpoints are the URLs or IP addresses from which the rgw gateways in that zonegroup can be accessed. You can select multiple endpoints in case you have multiple rgw gateways in a zonegroup - +
-
-
-
- -
- - - Enter a unique name for the Zone. A Zone represents a distinct data center or geographical location within a Zonegroup. - - This field is required. - The chosen zone name is already in use. +
+
+ +
+ + + Enter a unique name for the Realm. The Realm is a logical grouping of all your Zonegroups. + + This field is required. + This realm name is already in use. Choose a unique name. +
-
-
- -
- - - - Select the endpoints for the Zone. Endpoints are the URLs or IP addresses from which the rgw gateways in that zone can be accessed. You can select multiple endpoints in case you have multiple rgw gateways in a zone - +
+ +
+ + + Enter a name for the Zonegroup. Zonegroup will help you identify and manage the group of zones. + + This field is required. + This zonegroup name is already in use. Choose a unique name. +
-
-
- -
- - - Specify the username for the system user. - - - This user will be created automatically as part of the process, and it will have the necessary permissions to manage and synchronize resources across zones. - - This field is required. - The username already exists. +
+ +
+ + + + Select the endpoints for the Zonegroup. Endpoints are the URLs or IP addresses from which the rgw gateways in that zonegroup can be accessed. You can select multiple endpoints in case you have multiple rgw gateways in a zonegroup + +
-
-
+ +
+ for="zonegroupName" + i18n>Zone Name
- + - Choose the cluster where you want to apply this multisite configuration. The selected cluster will integrate the defined Realm, Zonegroup, and Zones, enabling data synchronization and management across the multisite setup. + Enter a unique name for the Zone. A Zone represents a distinct data center or geographical location within a Zonegroup. - - Before submitting this form, please verify that the selected cluster has an active RGW (Rados Gateway) service running. - + This field is required. + This zone name is already in use. Choose a unique name.
+ for="zone_endpoints" + i18n>Zone Endpoints +
+ + + Select the endpoints for the Zone. Endpoints are the URLs or IP addresses from which the rgw gateways in that zone can be accessed. You can select multiple endpoints in case you have multiple rgw gateways in a zone + +
+
+
+
+ id="username" + formControlName="username" + ngbTooltip="White spaces at the beginning and end will be trimmed" + i18n-ngbTooltip + cdTrim> - Replication zone represents the zone to be created in the replication cluster where your data will be replicated. + Specify the username for the system user. + + This user will be created automatically as part of the process, and it will have the necessary permissions to manage and synchronize resources across zones. + This field is required. + This username is already in use. Choose a unique name.
-
+
+ +
+ + +
+ + + +
+
+ + + + + +
+
+ + + + +
+ +
+ + + Choose the cluster where you want to apply this multisite configuration. The selected cluster will integrate the defined Realm, Zonegroup, and Zones, enabling data synchronization and management across the multisite setup. + + + Before submitting this form, please verify that the selected cluster has an active RGW (Rados Gateway) service running. + +
+
+
+ +
+ + + Replication zone represents the zone to be created in the replication cluster where your data will be replicated. + + This field is required. +
+
+
@@ -233,12 +291,11 @@ name="skip-cluster-selection" aria-label="Skip" (click)="onSkip()" - *ngIf="stepTitles[currentStep.stepIndex]['label'] === 'Select Cluster'" + *ngIf="stepTitles[currentStep.stepIndex]['label'] === 'Select Cluster' && multisiteSetupForm.get('configType').value === 'newRealm'" i18n>Skip + @@ -321,65 +380,70 @@ + + + + + + + + + + + + +
-
- {{ multisiteSetupForm.get('realmName').value }} -
+
{{ multisiteSetupForm.get('realmName').value }}
-
- {{ multisiteSetupForm.get('zonegroupName').value }} -
+
{{ multisiteSetupForm.get('zonegroupName').value }}
-
- {{ rgwEndpoints.value.join(', ') }} -
+
{{ rgwEndpoints.value.join(', ') }}
-
- {{ multisiteSetupForm.get('zoneName').value }} -
+
{{ multisiteSetupForm.get('zoneName').value }}
-
- {{ rgwEndpoints.value.join(', ') }} -
+
{{ rgwEndpoints.value.join(', ') }}
-
- {{ multisiteSetupForm.get('username').value }} -
+
{{ multisiteSetupForm.get('username').value }}
+
+ + +
+ +
{{ multisiteSetupForm.get('selectedRealm').value }}
+
+
+ +
-
- {{ selectedCluster }} -
+
{{ selectedCluster }}
-
- {{ multisiteSetupForm.get('replicationZoneName').value }} -
+
{{ multisiteSetupForm.get('replicationZoneName').value }}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-wizard/rgw-multisite-wizard.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-wizard/rgw-multisite-wizard.component.ts index 2fbe1163ef841..4872ffb3dbda1 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-wizard/rgw-multisite-wizard.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-wizard/rgw-multisite-wizard.component.ts @@ -1,8 +1,8 @@ -import { Component, OnInit } from '@angular/core'; +import { ChangeDetectorRef, Component, OnInit } from '@angular/core'; import { Location } from '@angular/common'; import { UntypedFormControl, Validators } from '@angular/forms'; import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { Subscription, forkJoin } from 'rxjs'; +import { Observable, Subscription, forkJoin } from 'rxjs'; import { ActionLabelsI18n } from '~/app/shared/constants/app.constants'; import { CdFormGroup } from '~/app/shared/forms/cd-form-group'; import { WizardStepModel } from '~/app/shared/models/wizard-steps'; @@ -23,9 +23,34 @@ import { BaseModal, Step } from 'carbon-components-angular'; import { SummaryService } from '~/app/shared/services/summary.service'; import { ExecutingTask } from '~/app/shared/models/executing-task'; import { + STEP_TITLES_EXISTING_REALM, STEP_TITLES_MULTI_CLUSTER_CONFIGURED, STEP_TITLES_SINGLE_CLUSTER } from './multisite-wizard-steps.enum'; +import { RgwRealmService } from '~/app/shared/api/rgw-realm.service'; +import { MultiCluster, MultiClusterConfig } from '~/app/shared/models/multi-cluster'; + +interface DaemonStats { + rgw_metadata?: { + [key: string]: string; + }; +} + +interface EndpointInfo { + hostname: string; + port: number; + frontendConfig: string; +} + +enum Protocol { + HTTP = 'http', + HTTPS = 'https' +} + +enum ConfigType { + NewRealm = 'newRealm', + ExistingRealm = 'existingRealm' +} @Component({ selector: 'cd-rgw-multisite-wizard', @@ -43,7 +68,7 @@ export class RgwMultisiteWizardComponent extends BaseModal implements OnInit { stepsToSkip: { [steps: string]: boolean } = {}; daemons: RgwDaemon[] = []; selectedCluster = ''; - clusterDetailsArray: any; + clusterDetailsArray: MultiCluster[] = []; isMultiClusterConfigured = false; exportTokenForm: CdFormGroup; realms: any; @@ -53,6 +78,9 @@ export class RgwMultisiteWizardComponent extends BaseModal implements OnInit { rgwEndpoints: { value: any[]; options: any[]; messages: any }; executingTask: ExecutingTask; setupCompleted = false; + showConfigType = false; + realmList: string[] = []; + realmsInfo: { realm: string; token: string }[]; constructor( private wizardStepsService: WizardStepsService, @@ -61,10 +89,12 @@ export class RgwMultisiteWizardComponent extends BaseModal implements OnInit { private rgwDaemonService: RgwDaemonService, private multiClusterService: MultiClusterService, private rgwMultisiteService: RgwMultisiteService, + private rgwRealmService: RgwRealmService, public notificationService: NotificationService, private route: ActivatedRoute, private summaryService: SummaryService, - private location: Location + private location: Location, + private cdr: ChangeDetectorRef ) { super(); this.pageURL = 'rgw/multisite/configuration'; @@ -87,63 +117,85 @@ export class RgwMultisiteWizardComponent extends BaseModal implements OnInit { ngOnInit(): void { this.open = this.route.outlet === 'modal'; - this.rgwDaemonService - .list() - .pipe( - switchMap((daemons) => { - this.daemons = daemons; - const daemonStatsObservables = daemons.map((daemon) => - this.rgwDaemonService.get(daemon.id).pipe( - map((daemonStats) => ({ - hostname: daemon.server_hostname, - port: daemon.port, - frontendConfig: daemonStats['rgw_metadata']['frontend_config#0'] - })) - ) - ); - return forkJoin(daemonStatsObservables); - }) - ) - .subscribe((daemonStatsArray) => { - this.rgwEndpoints.value = daemonStatsArray.map((daemonStats) => { - const protocol = daemonStats.frontendConfig.includes('ssl_port') ? 'https' : 'http'; - return `${protocol}://${daemonStats.hostname}:${daemonStats.port}`; - }); - const options: SelectOption[] = this.rgwEndpoints.value.map( - (endpoint: string) => new SelectOption(false, endpoint, '') - ); - this.rgwEndpoints.options = [...options]; - }); - - this.multiClusterService.getCluster().subscribe((clusters) => { + this.loadRGWEndpoints(); + this.multiClusterService.getCluster().subscribe((clusters: MultiClusterConfig) => { + const currentUrl = clusters['current_url']; this.clusterDetailsArray = Object.values(clusters['config']) .flat() - .filter((cluster) => cluster['url'] !== clusters['current_url']); + .filter((cluster) => cluster['url'] !== currentUrl); this.isMultiClusterConfigured = this.clusterDetailsArray.length > 0; - if (!this.isMultiClusterConfigured) { - this.stepTitles = STEP_TITLES_SINGLE_CLUSTER.map((title) => ({ - label: title - })); - this.stepTitles.forEach((steps, index) => { - steps.onClick = () => (this.currentStep.stepIndex = index); - }); - } else { - this.selectedCluster = this.clusterDetailsArray[0]['name']; - } + this.stepTitles = (this.isMultiClusterConfigured + ? STEP_TITLES_MULTI_CLUSTER_CONFIGURED + : STEP_TITLES_SINGLE_CLUSTER + ).map((label, index) => ({ + label, + onClick: () => (this.currentStep.stepIndex = index) + })); this.wizardStepsService.setTotalSteps(this.stepTitles.length); + this.selectedCluster = this.isMultiClusterConfigured + ? this.clusterDetailsArray[0]['name'] + : null; }); this.summaryService.subscribe((summary) => { - this.executingTask = summary.executing_tasks.filter((tasks) => - tasks.name.includes('progress/Multisite-Setup') - )[0]; + this.executingTask = summary.executing_tasks.find((task) => + task.name.includes('progress/Multisite-Setup') + ); + }); + + this.stepTitles.forEach((step) => { + this.stepsToSkip[step.label] = false; }); - this.stepTitles.forEach((stepTitle) => { - this.stepsToSkip[stepTitle.label] = false; + this.rgwRealmService.getRealmTokens().subscribe((data: { realm: string; token: string }[]) => { + const base64Matcher = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=|[A-Za-z0-9+/]{4})$/; + this.realmsInfo = data.filter((realmInfo) => base64Matcher.test(realmInfo.token)); + this.showConfigType = this.realmsInfo.length > 0; + if (this.showConfigType) { + this.multisiteSetupForm.get('selectedRealm')?.setValue(this.realmsInfo[0].realm); + this.cdr.detectChanges(); + } }); } + private loadRGWEndpoints(): void { + this.rgwDaemonService + .list() + .pipe( + switchMap((daemons: RgwDaemon[]) => { + this.daemons = daemons; + return this.fetchDaemonStats(daemons); + }) + ) + .subscribe((daemonStatsArray: EndpointInfo[]) => { + this.populateRGWEndpoints(daemonStatsArray); + }); + } + + private fetchDaemonStats(daemons: RgwDaemon[]): Observable { + const observables = daemons.map((daemon) => + this.rgwDaemonService.get(daemon.id).pipe( + map((daemonStats: DaemonStats) => ({ + hostname: daemon.server_hostname, + port: daemon.port, + frontendConfig: daemonStats?.rgw_metadata?.['frontend_config#0'] || '' + })) + ) + ); + return forkJoin(observables); + } + + private populateRGWEndpoints(statsArray: EndpointInfo[]): void { + this.rgwEndpoints.value = statsArray.map((stats: EndpointInfo) => { + const protocol = stats.frontendConfig.includes('ssl_port') ? Protocol.HTTPS : Protocol.HTTP; + return `${protocol}://${stats.hostname}:${stats.port}`; + }); + this.rgwEndpoints.options = this.rgwEndpoints.value.map( + (endpoint) => new SelectOption(false, endpoint, '') + ); + this.cdr.detectChanges(); + } + createForm() { this.multisiteSetupForm = new CdFormGroup({ realmName: new UntypedFormControl('default_realm', { @@ -167,7 +219,9 @@ export class RgwMultisiteWizardComponent extends BaseModal implements OnInit { }), replicationZoneName: new UntypedFormControl('new_replicated_zone', { validators: [Validators.required] - }) + }), + configType: new UntypedFormControl(ConfigType.NewRealm, {}), + selectedRealm: new UntypedFormControl(null, {}) }); if (!this.isMultiClusterConfigured) { @@ -244,6 +298,10 @@ export class RgwMultisiteWizardComponent extends BaseModal implements OnInit { } else { const cluster = values['cluster']; const replicationZoneName = values['replicationZoneName']; + let selectedRealmName = ''; + if (this.multisiteSetupForm.get('configType').value === ConfigType.ExistingRealm) { + selectedRealmName = this.multisiteSetupForm.get('selectedRealm').value; + } this.rgwMultisiteService .setUpMultisiteReplication( realmName, @@ -254,7 +312,8 @@ export class RgwMultisiteWizardComponent extends BaseModal implements OnInit { username, cluster, replicationZoneName, - this.clusterDetailsArray + this.clusterDetailsArray, + selectedRealmName ) .subscribe( () => { @@ -294,4 +353,25 @@ export class RgwMultisiteWizardComponent extends BaseModal implements OnInit { closeModal(): void { this.location.back(); } + + onConfigTypeChange() { + const configType = this.multisiteSetupForm.get('configType')?.value; + if (configType === ConfigType.ExistingRealm) { + this.stepTitles = STEP_TITLES_EXISTING_REALM.map((title) => ({ + label: title + })); + this.stepTitles.forEach((steps, index) => { + steps.onClick = () => (this.currentStep.stepIndex = index); + }); + } else if (this.isMultiClusterConfigured) { + this.stepTitles = STEP_TITLES_MULTI_CLUSTER_CONFIGURED.map((title) => ({ + label: title + })); + } else { + this.stepTitles = STEP_TITLES_SINGLE_CLUSTER.map((title) => ({ + label: title + })); + } + this.wizardStepsService.setTotalSteps(this.stepTitles.length); + } } 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 8a39dc8a284fa..4063c619e8822 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 @@ -86,7 +86,8 @@ export class RgwMultisiteService { username: string, cluster?: string, replicationZoneName?: string, - clusterDetailsArray?: any + clusterDetailsArray?: any, + selectedRealmName?: string ) { let params = new HttpParams() .set('realm_name', realmName) @@ -108,6 +109,10 @@ export class RgwMultisiteService { params = params.set('replication_zone_name', replicationZoneName); } + if (selectedRealmName) { + params = params.set('selectedRealmName', selectedRealmName); + } + return this.http.post(`${this.uiUrl}/multisite-replications`, null, { params: params }); } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/models/multi-cluster.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/multi-cluster.ts index e41bb12e16d4c..5661c0d3ebee7 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/models/multi-cluster.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/multi-cluster.ts @@ -9,3 +9,12 @@ export interface MultiCluster { ssl_certificate: string; ttl: number; } + +export interface MultiClusterConfig { + current_url: string; + current_user: string; + hub_url: string; + config: { + [clusterId: string]: MultiCluster[]; + }; +} diff --git a/src/pybind/mgr/dashboard/services/rgw_client.py b/src/pybind/mgr/dashboard/services/rgw_client.py index 13c03ec9c95a2..cefad2b045bb7 100755 --- a/src/pybind/mgr/dashboard/services/rgw_client.py +++ b/src/pybind/mgr/dashboard/services/rgw_client.py @@ -1217,196 +1217,221 @@ class RgwMultisiteAutomation: zone_endpoints: str, username: str, cluster_fsid: Optional[str] = None, replication_zone_name: Optional[str] = None, - cluster_details: Optional[str] = None): + cluster_details: Optional[str] = None, + selectedRealmName: Optional[str] = None): - # Set up multisite replication for Ceph RGW. logger.info("Starting multisite replication setup") - if cluster_details: - cluster_details_dict = json.loads(cluster_details) + + cluster_details_dict = json.loads(cluster_details) if cluster_details else {} orch = OrchClient.instance() - rgw_multisite_instance = RgwMultisite() if cluster_fsid: self.progress_total = 4 - def get_updated_endpoints(endpoints): - # Update endpoint URLs by replacing hostnames with IP addresses. - try: - hostname_to_ip = {host['hostname']: host['addr'] for host in (h.to_json() for h in orch.hosts.list())} # noqa E501 # pylint: disable=line-too-long - updated_endpoints = [self.replace_hostname(endpoint, hostname_to_ip) for endpoint in endpoints.split(',')] # noqa E501 # pylint: disable=line-too-long - logger.debug("Updated endpoints: %s", updated_endpoints) - return updated_endpoints - except Exception as e: - logger.error("Failed to update endpoints: %s", e) - raise - - zonegroup_ip_url = ','.join(get_updated_endpoints(zonegroup_endpoints)) - zone_ip_url = ','.join(get_updated_endpoints(zone_endpoints)) - try: - # Create the realm and zonegroup - self.update_progress( - f"Creating realm: {realm_name}, zonegroup: {zonegroup_name} and zone: {zone_name}") - logger.info("Creating realm: %s", realm_name) - rgw_multisite_instance.create_realm(realm_name=realm_name, default=True) - logger.info("Creating zonegroup: %s", zonegroup_name) - rgw_multisite_instance.create_zonegroup(realm_name=realm_name, - zonegroup_name=zonegroup_name, - default=True, master=True, - endpoints=zonegroup_ip_url) + if selectedRealmName: + self.progress_total = 2 + + zonegroup_ip_url = ','.join(self.get_updated_endpoints(zonegroup_endpoints, orch)) + zone_ip_url = ','.join(self.get_updated_endpoints(zone_endpoints, orch)) + + if not selectedRealmName: + self.create_realm_and_zonegroup( + realm_name, zonegroup_name, zone_name, zonegroup_ip_url) + self.create_zone_and_user(zone_name, zonegroup_name, username, zone_ip_url) + self.restart_daemons() + + return self.export_and_import_realm( + realm_name, zonegroup_name, cluster_fsid, replication_zone_name, + cluster_details_dict, selectedRealmName, username + ) + + def get_updated_endpoints(self, endpoints: str, orch: OrchClient) -> list[str]: + try: + hostname_to_ip = {host['hostname']: host['addr'] for host in + (h.to_json() for h in orch.hosts.list())} + return [self.replace_hostname(endpoint, hostname_to_ip) for endpoint + in endpoints.split(',')] + except Exception as e: + logger.error("Failed to update endpoints: %s", e) + raise + + def create_realm_and_zonegroup(self, realm: str, zg: str, zone: str, zg_url: str): + try: + rgw_multisite_instance = RgwMultisite() + self.update_progress(f"Creating realm: {realm}, zonegroup: {zg} and zone: {zone}") + rgw_multisite_instance.create_realm(realm_name=realm, default=True) + rgw_multisite_instance.create_zonegroup(realm_name=realm, zonegroup_name=zg, + default=True, master=True, endpoints=zg_url) except Exception as e: logger.error("Failed to create realm or zonegroup: %s", e) self.update_progress("Failed to create realm or zonegroup", 'fail', str(e)) raise + + def create_zone_and_user(self, zone: str, zg: str, username: str, zone_url: str): try: - # Create the zone and system user, then modify the zone with user credentials - logger.info("Creating zone: %s", zone_name) - if rgw_multisite_instance.create_zone(zone_name=zone_name, - zonegroup_name=zonegroup_name, - default=True, master=True, - endpoints=zone_ip_url, - access_key=None, - secret_key=None): + rgw_multisite_instance = RgwMultisite() + if rgw_multisite_instance.create_zone(zone_name=zone, zonegroup_name=zg, default=True, + master=True, endpoints=zone_url, + access_key=None, secret_key=None): self.progress_done += 1 - logger.info("Creating system user: %s", username) - user_details = rgw_multisite_instance.create_system_user(username, zone_name) - if user_details: - keys = user_details['keys'][0] - access_key = keys['access_key'] - secret_key = keys['secret_key'] - if access_key and secret_key: - rgw_multisite_instance.modify_zone(zone_name=zone_name, - zonegroup_name=zonegroup_name, - default='true', master='true', - endpoints=zone_ip_url, - access_key=keys['access_key'], - secret_key=keys['secret_key']) - else: - raise ValueError("Access key or secret key is missing") + user_details = rgw_multisite_instance.create_system_user(username, zone) + keys = user_details.get('keys', [{}])[0] + access_key = keys.get('access_key') + secret_key = keys.get('secret_key') + if access_key and secret_key: + rgw_multisite_instance.modify_zone( + zone_name=zone, zonegroup_name=zg, default='true', + master='true', endpoints=zone_url, access_key=access_key, + secret_key=secret_key) + else: + raise DashboardException("Access key or secret key is missing", + component='rgw', http_status_code=500) except Exception as e: logger.error("Failed to create zone or system user: %s", e) - self.update_progress("Failed to create zone or system user:", 'fail', str(e)) + self.update_progress("Failed to create zone or system user", 'fail', str(e)) raise + + def restart_daemons(self): try: - logger.info("Restarting RGW daemons and setting credentials") self.update_progress("Restarting RGW daemons and setting credentials") - rgw_service_manager = RgwServiceManager() - rgw_service_manager.restart_rgw_daemons_and_set_credentials() + RgwServiceManager().restart_rgw_daemons_and_set_credentials() self.progress_done += 1 except Exception as e: logger.error("Failed to restart RGW daemon: %s", e) - self.update_progress("Failed to restart RGW daemons:", 'fail', str(e)) + self.update_progress("Failed to restart RGW daemons", 'fail', str(e)) raise + + def export_and_import_realm(self, realm: str, zg: str, + fsid: Optional[str], rep_zone: Optional[str], + details_dict: dict, selectedRealm: Optional[str], + username: str): try: - # Get realm tokens and import to another cluster if specified - logger.info("Getting realm tokens") realm_token_info = CephService.get_realm_tokens() - logger.info("Realm tokens: %s", realm_token_info) - - if cluster_fsid and realm_token_info and replication_zone_name and cluster_details_dict: - logger.info("Importing realm token to cluster: %s", cluster_fsid) - self.update_progress(f"Importing realm token to cluster: {cluster_fsid}") - self.import_realm_token_to_cluster(cluster_fsid, realm_name, - zonegroup_name, realm_token_info, - username, replication_zone_name, - cluster_details_dict) + if fsid and realm_token_info and rep_zone and details_dict: + self.update_progress(f"Importing realm token to cluster: {fsid}") + self.import_realm_token_to_cluster(fsid, realm, zg, realm_token_info, username, + rep_zone, details_dict, selectedRealm) else: self.update_progress("Realm Export Token fetched successfully", 'complete') + logger.info("Multisite replication setup completed") + return realm_token_info except Exception as e: logger.error("Failed to get realm tokens or import to cluster: %s", e) - self.update_progress("Failed to get realm tokens or import to cluster:", 'fail', str(e)) + self.update_progress("Failed to get realm tokens or import to cluster", + 'fail', str(e)) raise - logger.info("Multisite replication setup completed") - return realm_token_info def import_realm_token_to_cluster(self, cluster_fsid, realm_name, zonegroup_name, realm_token_info, username, replication_zone_name, - cluster_details): + cluster_details, selectedRealmName): try: - for realm_token in realm_token_info: - if realm_token['realm'] == realm_name: - realm_export_token = realm_token['token'] - break - else: - raise ValueError(f"Realm {realm_name} not found in realm tokens") - for cluster in cluster_details: - if cluster['name'] == cluster_fsid: - cluster_token = cluster['token'] - cluster_url = cluster['url'] - break - if cluster_token: - if not cluster_url.endswith('/'): - cluster_url += '/' + if selectedRealmName: + rgw_service_manager = RgwServiceManager() + username = rgw_service_manager.get_username_from_realm_name(selectedRealmName) + realm_name = selectedRealmName + + realm_export_token = self._get_realm_export_token(realm_token_info, realm_name) + cluster_url, cluster_token = self._get_cluster_details(cluster_fsid, cluster_details) + + self._configure_selected_cluster(cluster_url, cluster_token, realm_name, + zonegroup_name, replication_zone_name) + + token_import_response = self._import_realm_token( + cluster_url, cluster_token, realm_export_token, + replication_zone_name) + + self.progress_done += 1 + self.update_progress(f"Checking for user {username} in the selected cluster \ + and setting credentials") + + self._verify_user_and_daemons(cluster_url, cluster_token, realm_name, + replication_zone_name, username) + + return token_import_response - path = 'api/rgw/realm/import_realm_token' - try: - multi_cluster_instance = MultiCluster() - daemon_name = f"{realm_name}.{replication_zone_name}" - # pylint: disable=protected-access - config_payload = { - 'realm_name': realm_name, - 'zonegroup_name': zonegroup_name, - 'zone_name': replication_zone_name, - 'daemon_name': daemon_name, - } - config_info = multi_cluster_instance._proxy(method='PUT', base_url=cluster_url, - path='api/rgw/daemon/set_multisite_config', # noqa E501 # pylint: disable=line-too-long - payload=config_payload, - token=cluster_token) - logger.info("setting config response: %s", config_info) - available_port = multi_cluster_instance._proxy(method='GET', - base_url=cluster_url, - path='ui-api/rgw/multisite/available-ports', # noqa E501 # pylint: disable=line-too-long - token=cluster_token) - placement_spec: Dict[str, Dict] = {"placement": {}} - payload = { - 'realm_token': realm_export_token, - 'zone_name': replication_zone_name, - 'port': available_port, - 'placement_spec': placement_spec, - } - token_import_response = multi_cluster_instance._proxy(method='POST', - base_url=cluster_url, - path=path, - payload=payload, - token=cluster_token) - logger.info("Import realm token response: %s", token_import_response) - self.progress_done += 1 - self.update_progress(f"Checking for user {username} in the selected cluster and setting credentials") # noqa E501 # pylint: disable=line-too-long - service_name = f"rgw.{daemon_name}" - daemons_status = multi_cluster_instance._proxy(method='GET', - base_url=cluster_url, - path=f'ui-api/rgw/multisite/check-daemons-status?service_name={service_name}', # noqa E501 # pylint: disable=line-too-long - token=cluster_token) - logger.info("Daemons status: %s", daemons_status) - realms_list = multi_cluster_instance._proxy( - method='GET', - base_url=cluster_url, - path='api/rgw/realm', - token=cluster_token - ) - logger.debug("Realms info in the selected cluster: %s", realms_list) - system_user_param = "realmName" if realms_list.get('default_info') \ - else "zoneName" - if daemons_status is True: - self.check_user_in_second_cluster(cluster_url, cluster_token, - username, replication_zone_name, - system_user_param, realm_name) - else: - self.update_progress("Failed to set credentials in selected cluster", 'fail', "RGW daemons failed to start") # noqa E501 # pylint: disable=line-too-long - return token_import_response - except requests.RequestException as e: - logger.error("Could not reach %s: %s", cluster_url, e) - raise DashboardException(f"Could not reach {cluster_url}: {e}", - http_status_code=404, component='dashboard') - except json.JSONDecodeError as e: - logger.error("Error parsing Dashboard API response: %s", e.msg) - raise DashboardException(f"Error parsing Dashboard API response: {e.msg}", - component='dashboard') except Exception as e: logger.error("Failed to import realm token to cluster: %s", e) self.update_progress("Failed to import realm token to cluster:", 'fail', str(e)) raise + def _get_realm_export_token(self, realm_token_info, realm_name): + for realm_token in realm_token_info: + if realm_token['realm'] == realm_name: + return realm_token['token'] + raise DashboardException('Realm token not found', + http_status_code=500, component='rgw') + + def _get_cluster_details(self, cluster_fsid, cluster_details): + for cluster in cluster_details: + if cluster['name'] == cluster_fsid: + cluster_url = cluster['url'].rstrip('/') + '/' + return cluster_url, cluster['token'] + raise DashboardException("Cluster details not found", + http_status_code=500, component='rgw') + + def _configure_selected_cluster(self, cluster_url, cluster_token, realm_name, + zonegroup_name, replication_zone_name): + daemon_name = f"{realm_name}.{replication_zone_name}" + config_payload = { + 'realm_name': realm_name, + 'zonegroup_name': zonegroup_name, + 'zone_name': replication_zone_name, + 'daemon_name': daemon_name, + } + multi_cluster_instance = MultiCluster() + # pylint: disable=protected-access + config_info = multi_cluster_instance._proxy(method='PUT', base_url=cluster_url, + path='api/rgw/daemon/set_multisite_config', + payload=config_payload, + token=cluster_token) + logger.info("setting config response: %s", config_info) + + def _import_realm_token(self, cluster_url, cluster_token, realm_token, zone_name): + multi_cluster_instance = MultiCluster() + # pylint: disable=protected-access + available_port = multi_cluster_instance._proxy( + method='GET', base_url=cluster_url, path='ui-api/rgw/multisite/available-ports', + token=cluster_token) + payload = { + 'realm_token': realm_token, + 'zone_name': zone_name, + 'port': available_port, + 'placement_spec': {"placement": {}} + } + # pylint: disable=protected-access + token_import_response = multi_cluster_instance._proxy( + method='POST', base_url=cluster_url, path='api/rgw/realm/import_realm_token', + payload=payload, token=cluster_token) + logger.info("Import realm token response: %s", token_import_response) + return token_import_response + + def _verify_user_and_daemons(self, cluster_url, cluster_token, realm_name, zone_name, + username): + multi_cluster_instance = MultiCluster() + daemon_name = f"{realm_name}.{zone_name}" + service_name = f"rgw.{daemon_name}" + # pylint: disable=protected-access + daemons_status = multi_cluster_instance._proxy( + method='GET', base_url=cluster_url, + path=f'ui-api/rgw/multisite/check-daemons-status?service_name={service_name}', + token=cluster_token) + logger.debug("Daemons status: %s", daemons_status) + # pylint: disable=protected-access + realms_list = multi_cluster_instance._proxy(method='GET', + base_url=cluster_url, + path='api/rgw/realm', + token=cluster_token) + logger.debug("Realms info in the selected cluster: %s", realms_list) + + system_user_param = "realmName" if realms_list.get('default_info') else "zoneName" + + if daemons_status is True: + self.check_user_in_second_cluster(cluster_url, cluster_token, username, zone_name, + system_user_param, realm_name) + else: + self.update_progress("Failed to set credentials in selected cluster", 'fail', + "RGW daemons failed to start") + def check_user_in_second_cluster(self, cluster_url, cluster_token, username, replication_zone_name, system_user_param, realm_name): @@ -1438,10 +1463,12 @@ class RgwMultisiteAutomation: user_found = True logger.info("User %s found in the second cluster", username) # pylint: disable=protected-access - set_creds_cont = multi_cluster_instance._proxy(method='PUT', base_url=cluster_url, # noqa E501 # pylint: disable=line-too-long - path='ui-api/rgw/multisite/setup-rgw-credentials', # noqa E501 # pylint: disable=line-too-long - token=cluster_token) # noqa E501 # pylint: disable=line-too-long - logger.info("set credentials in selected cluster response: %s", set_creds_cont) # noqa E501 # pylint: disable=line-too-long # noqa E501 # pylint: disable=line-too-long + set_creds_cont = multi_cluster_instance._proxy( + method='PUT', base_url=cluster_url, + path='ui-api/rgw/multisite/setup-rgw-credentials', + token=cluster_token) + logger.info("set credentials in selected cluster response: %s", + set_creds_cont) self.progress_done += 1 self.update_progress("Multisite replication setup completed", 'complete') diff --git a/src/pybind/mgr/dashboard/services/service.py b/src/pybind/mgr/dashboard/services/service.py index 057b6f03ef7c6..12285b061c0b9 100644 --- a/src/pybind/mgr/dashboard/services/service.py +++ b/src/pybind/mgr/dashboard/services/service.py @@ -221,3 +221,48 @@ class RgwServiceManager: rgw_hostname_setting = Settings.RGW_HOSTNAME_PER_DAEMON rgw_hostname_setting.pop(daemon_name, None) Settings.RGW_HOSTNAME_PER_DAEMON = rgw_hostname_setting + + def get_username_from_realm_name(self, realm_name: str) -> str: + realm_period_info = {} + master_zone_info = {} + rgw_realm_period_cmd = ['period', 'get', '--rgw-realm', realm_name] + try: + exit_code, out, _ = mgr.send_rgwadmin_command(rgw_realm_period_cmd) + if exit_code > 0: + raise DashboardException('Unable to get realm period info', + http_status_code=500, component='rgw') + realm_period_info = out + except SubprocessError as error: + raise DashboardException(error, http_status_code=500, component='rgw') + + if realm_period_info: + master_zone_id = realm_period_info.get('master_zone') + rgw_zone_info_cmd = ['zone', 'get', '--zone-id', master_zone_id] + try: + exit_code, out, _ = mgr.send_rgwadmin_command(rgw_zone_info_cmd) + if exit_code > 0: + raise DashboardException('Unable to get master zone info', + http_status_code=500, component='rgw') + master_zone_info = out + except SubprocessError as error: + raise DashboardException(error, http_status_code=500, component='rgw') + + if master_zone_info: + access_key = master_zone_info['system_key']['access_key'] + user_info = {} + rgw_user_info_cmd = ['user', 'info', '--access-key', access_key] + 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') + user_info = out + except SubprocessError as error: + raise DashboardException(error, http_status_code=500, component='rgw') + if user_info: + user_id = user_info.get('user_id') + if user_id: + return user_id + + raise DashboardException('Failed to retrieve user_id for realm', + http_status_code=500, component='rgw') -- 2.39.5