]> git.apps.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard: add port and zone endpoints to import realm token form in 54118/head
authorAashish Sharma <aasharma@li-e74156cc-2f67-11b2-a85c-e98659a63c5c.ibm.com>
Fri, 13 Oct 2023 08:23:23 +0000 (13:53 +0530)
committerAashish Sharma <aasharma@li-e74156cc-2f67-11b2-a85c-e98659a63c5c.ibm.com>
Fri, 20 Oct 2023 09:38:37 +0000 (15:08 +0530)
rgw multisite

Signed-off-by: Aashish Sharma <aasharma@redhat.com>
(cherry picked from commit 84ec19442b2db4f4e389810efbc01674d9824408)

src/pybind/mgr/dashboard/controllers/rgw.py
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-import/rgw-multisite-import.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-import/rgw-multisite-import.component.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-realm.service.ts
src/pybind/mgr/dashboard/openapi.yaml
src/pybind/mgr/dashboard/services/ceph_service.py
src/pybind/mgr/rgw/module.py

index 65c809ebec02d17292547bfeb533836a3cbc3e05..9ccf4b36b2b119785efdd3258c45988fb6ced7a9 100644 (file)
@@ -812,10 +812,10 @@ class RgwRealm(RESTController):
     @UpdatePermission
     @allow_empty_body
     # pylint: disable=W0613
-    def import_realm_token(self, realm_token, zone_name, daemon_name=None):
+    def import_realm_token(self, realm_token, zone_name, port, placement_spec):
         try:
             multisite_instance = RgwMultisite()
-            result = CephService.import_realm_token(realm_token, zone_name)
+            result = CephService.import_realm_token(realm_token, zone_name, port, placement_spec)
             multisite_instance.update_period()
             return result
         except NoRgwDaemonsException as e:
index bf11e04029aad06ec2458856ef9acdfcf275dd5b..a001e4b00c7c988ed951479670b38d0cf09fac10 100644 (file)
@@ -13,9 +13,9 @@
           <li>This feature allows you to configure a connection between your primary and secondary Ceph clusters for data replication. By importing a token, you establish a link between the clusters, enabling data synchronization.</li>
           <li>To obtain the token, generate it from your secondary Ceph cluster. This token includes encoded information about the secondary cluster's endpoint, access key, and secret key.</li>
           <li>The secondary zone represents the destination cluster where your data will be replicated.</li>
-          <li>Please create an RGW service using the secondary zone (created after submitting this form) to start the replication between zones.</li>
         </ul>
       </cd-alert-panel>
+      <legend i18n>Zone Details</legend>
       <div class="form-group row">
         <label class="cd-col-form-label required"
                for="realmToken"
                 i18n>The chosen zone name is already in use.</span>
         </div>
       </div>
+
+      <legend i18n>Service Details</legend>
+      <div class="form-group row">
+        <div class="cd-col-form-offset">
+          <div class="custom-control custom-checkbox">
+            <input class="custom-control-input"
+                   id="unmanaged"
+                   type="checkbox"
+                   formControlName="unmanaged">
+            <label class="custom-control-label"
+                   for="unmanaged"
+                   i18n>Unmanaged</label>
+            <cd-helper i18n>If set to true, the orchestrator will not start nor stop any daemon associated with this service.
+               Placement and all other properties will be ignored.</cd-helper>
+          </div>
+        </div>
+      </div>
+
+      <!-- Placement -->
+      <div *ngIf="!importTokenForm.controls.unmanaged.value"
+           class="form-group row">
+        <label class="cd-col-form-label"
+               for="placement"
+               i18n>Placement</label>
+        <div class="cd-col-form-input">
+          <select id="placement"
+                  class="form-select"
+                  formControlName="placement">
+            <option i18n
+                    value="hosts">Hosts</option>
+            <option i18n
+                    value="label">Label</option>
+          </select>
+        </div>
+      </div>
+
+      <!-- Label -->
+      <div *ngIf="!importTokenForm.controls.unmanaged.value && importTokenForm.controls.placement.value === 'label'"
+           class="form-group row">
+        <label i18n
+               class="cd-col-form-label"
+               for="label">Label</label>
+        <div class="cd-col-form-input">
+          <input id="label"
+                 class="form-control"
+                 type="text"
+                 formControlName="label"
+                 [ngbTypeahead]="searchLabels"
+                 (focus)="labelFocus.next($any($event).target.value)"
+                 (click)="labelClick.next($any($event).target.value)">
+          <span class="invalid-feedback"
+                *ngIf="importTokenForm.showError('label', frm, 'required')"
+                i18n>This field is required.</span>
+        </div>
+      </div>
+
+      <!-- Hosts -->
+      <div *ngIf="!importTokenForm.controls.unmanaged.value && importTokenForm.controls.placement.value === 'hosts'"
+           class="form-group row">
+        <label class="cd-col-form-label"
+               for="hosts"
+               i18n>Hosts</label>
+        <div class="cd-col-form-input">
+          <cd-select-badges id="hosts"
+                            [data]="importTokenForm.controls.hosts.value"
+                            [options]="hosts.options"
+                            [messages]="hosts.messages">
+          </cd-select-badges>
+        </div>
+      </div>
+
+      <!-- count -->
+      <div *ngIf="!importTokenForm.controls.unmanaged.value"
+           class="form-group row">
+        <label class="cd-col-form-label"
+               for="count">
+          <span i18n>Count</span>
+          <cd-helper i18n>Only that number of daemons will be created.</cd-helper>
+        </label>
+        <div class="cd-col-form-input">
+          <input id="count"
+                 class="form-control"
+                 type="number"
+                 formControlName="count"
+                 min="1">
+          <span class="invalid-feedback"
+                *ngIf="importTokenForm.showError('count', frm, 'min')"
+                i18n>The value must be at least 1.</span>
+          <span class="invalid-feedback"
+                *ngIf="importTokenForm.showError('count', frm, 'pattern')"
+                i18n>The entered value needs to be a number.</span>
+        </div>
+      </div>
+
+      <!-- RGW -->
+      <ng-container *ngIf="!importTokenForm.controls.unmanaged.value">
+        <!-- rgw_frontend_port -->
+        <div class="form-group row">
+          <label i18n
+                 class="cd-col-form-label"
+                 for="rgw_frontend_port">Port</label>
+          <div class="cd-col-form-input">
+            <input id="rgw_frontend_port"
+                   class="form-control"
+                   type="number"
+                   formControlName="rgw_frontend_port"
+                   min="1"
+                   max="65535">
+            <span class="invalid-feedback"
+                  *ngIf="importTokenForm.showError('rgw_frontend_port', frm, 'pattern')"
+                  i18n>The entered value needs to be a number.</span>
+            <span class="invalid-feedback"
+                  *ngIf="importTokenForm.showError('rgw_frontend_port', frm, 'min')"
+                  i18n>The value must be at least 1.</span>
+            <span class="invalid-feedback"
+                  *ngIf="importTokenForm.showError('rgw_frontend_port', frm, 'max')"
+                  i18n>The value cannot exceed 65535.</span>
+          </div>
+        </div>
+      </ng-container>
     </div>
     <div class="modal-footer">
       <cd-form-button-panel (submitActionEvent)="onSubmit()"
index 5581a80bfe1a2ebaf30a40be101c9706cc3d82fb..deda890167077499c6cb4ad269c0e164695d6449 100644 (file)
@@ -1,6 +1,6 @@
-import { Component, OnInit } from '@angular/core';
+import { Component, OnInit, ViewChild } from '@angular/core';
 import { FormControl, Validators } from '@angular/forms';
-import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
+import { NgbActiveModal, NgbTypeahead } from '@ng-bootstrap/ng-bootstrap';
 import { RgwRealmService } from '~/app/shared/api/rgw-realm.service';
 import { ActionLabelsI18n } from '~/app/shared/constants/app.constants';
 import { NotificationType } from '~/app/shared/enum/notification-type.enum';
@@ -9,6 +9,12 @@ import { CdValidators } from '~/app/shared/forms/cd-validators';
 import { NotificationService } from '~/app/shared/services/notification.service';
 import { RgwZone } from '../models/rgw-multisite';
 import _ from 'lodash';
+import { SelectMessages } from '~/app/shared/components/select/select-messages.model';
+import { HostService } from '~/app/shared/api/host.service';
+import { CdTableFetchDataContext } from '~/app/shared/models/cd-table-fetch-data-context';
+import { SelectOption } from '~/app/shared/components/select/select-option.model';
+import { Observable, Subject, merge } from 'rxjs';
+import { debounceTime, distinctUntilChanged, filter, map } from 'rxjs/operators';
 
 @Component({
   selector: 'cd-rgw-multisite-import',
@@ -19,18 +25,33 @@ export class RgwMultisiteImportComponent implements OnInit {
   readonly endpoints = /^((https?:\/\/)|(www.))(?:([a-zA-Z]+)|(\d+\.\d+.\d+.\d+)):\d{2,4}$/;
   readonly ipv4Rgx = /^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/i;
   readonly ipv6Rgx = /^(?:[a-f0-9]{1,4}:){7}[a-f0-9]{1,4}$/i;
+  @ViewChild(NgbTypeahead, { static: false })
+  typeahead: NgbTypeahead;
 
   importTokenForm: CdFormGroup;
   multisiteInfo: object[] = [];
   zoneList: RgwZone[] = [];
   zoneNames: string[];
+  hosts: any;
+  labels: string[];
+  labelClick = new Subject<string>();
+  labelFocus = new Subject<string>();
 
   constructor(
     public activeModal: NgbActiveModal,
+    public hostService: HostService,
+
     public rgwRealmService: RgwRealmService,
     public actionLabels: ActionLabelsI18n,
     public notificationService: NotificationService
   ) {
+    this.hosts = {
+      options: [],
+      messages: new SelectMessages({
+        empty: $localize`There are no hosts.`,
+        filter: $localize`Filter hosts`
+      })
+    };
     this.createForm();
   }
   ngOnInit(): void {
@@ -41,6 +62,20 @@ export class RgwMultisiteImportComponent implements OnInit {
     this.zoneNames = this.zoneList.map((zone) => {
       return zone['name'];
     });
+    const hostContext = new CdTableFetchDataContext(() => undefined);
+    this.hostService.list(hostContext.toParams(), 'false').subscribe((resp: object[]) => {
+      const options: SelectOption[] = [];
+      _.forEach(resp, (host: object) => {
+        if (_.get(host, 'sources.orchestrator', false)) {
+          const option = new SelectOption(false, _.get(host, 'hostname'), '');
+          options.push(option);
+        }
+      });
+      this.hosts.options = [...options];
+    });
+    this.hostService.getLabels().subscribe((resp: string[]) => {
+      this.labels = resp;
+    });
   }
 
   createForm() {
@@ -55,23 +90,75 @@ export class RgwMultisiteImportComponent implements OnInit {
             return this.zoneNames && this.zoneNames.indexOf(zoneName) !== -1;
           })
         ]
-      })
+      }),
+      rgw_frontend_port: new FormControl(null, {
+        validators: [Validators.required, Validators.pattern('^[0-9]*$')]
+      }),
+      placement: new FormControl('hosts'),
+      label: new FormControl(null, [
+        CdValidators.requiredIf({
+          placement: 'label',
+          unmanaged: false
+        })
+      ]),
+      hosts: new FormControl([]),
+      count: new FormControl(null, [CdValidators.number(false)]),
+      unmanaged: new FormControl(false)
     });
   }
 
   onSubmit() {
     const values = this.importTokenForm.value;
-    this.rgwRealmService.importRealmToken(values['realmToken'], values['zoneName']).subscribe(
-      () => {
-        this.notificationService.show(
-          NotificationType.success,
-          $localize`Realm token import successfull`
-        );
-        this.activeModal.close();
-      },
-      () => {
-        this.importTokenForm.setErrors({ cdSubmitButton: true });
+    const placementSpec: object = {
+      placement: {}
+    };
+    if (!values['unmanaged']) {
+      switch (values['placement']) {
+        case 'hosts':
+          if (values['hosts'].length > 0) {
+            placementSpec['placement']['hosts'] = values['hosts'];
+          }
+          break;
+        case 'label':
+          placementSpec['placement']['label'] = values['label'];
+          break;
       }
-    );
+      if (_.isNumber(values['count']) && values['count'] > 0) {
+        placementSpec['placement']['count'] = values['count'];
+      }
+    }
+    this.rgwRealmService
+      .importRealmToken(
+        values['realmToken'],
+        values['zoneName'],
+        values['rgw_frontend_port'],
+        placementSpec
+      )
+      .subscribe(
+        () => {
+          this.notificationService.show(
+            NotificationType.success,
+            $localize`Realm token import successfull`
+          );
+          this.activeModal.close();
+        },
+        () => {
+          this.importTokenForm.setErrors({ cdSubmitButton: true });
+        }
+      );
   }
+
+  searchLabels = (text$: Observable<string>) => {
+    return merge(
+      text$.pipe(debounceTime(200), distinctUntilChanged()),
+      this.labelFocus,
+      this.labelClick.pipe(filter(() => !this.typeahead.isPopupOpen()))
+    ).pipe(
+      map((value) =>
+        this.labels
+          .filter((label: string) => label.toLowerCase().indexOf(value.toLowerCase()) > -1)
+          .slice(0, 10)
+      )
+    );
+  };
 }
index efa882c8b34cc4a0aa17e9bbd55f8dbd830e1b09..e81731cd5203016c1dbe33d131406e350f6de65a 100644 (file)
@@ -66,14 +66,14 @@ export class RgwRealmService {
     };
   }
 
-  importRealmToken(realm_token: string, zone_name: string) {
-    return this.rgwDaemonService.request((params: HttpParams) => {
-      params = params.appendAll({
-        realm_token: realm_token,
-        zone_name: zone_name
-      });
-      return this.http.post(`${this.url}/import_realm_token`, null, { params: params });
-    });
+  importRealmToken(realm_token: string, zone_name: string, port: number, placementSpec: object) {
+    let requestBody = {
+      realm_token: realm_token,
+      zone_name: zone_name,
+      port: port,
+      placement_spec: placementSpec
+    };
+    return this.http.post(`${this.url}/import_realm_token`, requestBody);
   }
 
   getRealmTokens() {
index 3cb746673041df87e824bf428fba6abe57be2436..aeb5d94643903f3e4154946dfc0998d5a7b5e9d5 100644 (file)
@@ -9228,7 +9228,9 @@ paths:
           application/json:
             schema:
               properties:
-                daemon_name:
+                placement_spec:
+                  type: string
+                port:
                   type: string
                 realm_token:
                   type: string
@@ -9237,6 +9239,8 @@ paths:
               required:
               - realm_token
               - zone_name
+              - port
+              - placement_spec
               type: object
       responses:
         '201':
index 135f88ca2c974222ef6566e5a86e62fdedace22e..53cd0e7ad936a124a73239a0e915e1fcf7568570 100644 (file)
@@ -317,9 +317,10 @@ class CephService(object):
         return tokens_info
 
     @classmethod
-    def import_realm_token(cls, realm_token, zone_name):
+    def import_realm_token(cls, realm_token, zone_name, port, placement_spec):
         tokens_info = mgr.remote('rgw', 'import_realm_token', zone_name=zone_name,
-                                 realm_token=realm_token, start_radosgw=True)
+                                 realm_token=realm_token, port=port, placement=placement_spec,
+                                 start_radosgw=True)
         return tokens_info
 
     @classmethod
index 079e7e817ca55a80810c3baa13cd0d6891bc9bf8..f48e2e09fc32350592cc9c7bb4c335ea1ce744ae 100644 (file)
@@ -307,10 +307,11 @@ class Module(orchestrator.OrchestratorClientMixin, MgrModule):
                         zone_name: Optional[str] = None,
                         realm_token: Optional[str] = None,
                         port: Optional[int] = None,
-                        placement: Optional[str] = None,
+                        placement: Optional[Union[str, Dict[str, Any]]] = None,
                         start_radosgw: Optional[bool] = True,
                         zone_endpoints: Optional[str] = None,
                         inbuf: Optional[str] = None) -> Any:
+
         if inbuf:
             try:
                 rgw_specs = self._parse_rgw_specs(inbuf)
@@ -318,7 +319,10 @@ class Module(orchestrator.OrchestratorClientMixin, MgrModule):
                 return HandleCommandResult(retval=-errno.EINVAL, stderr=f'{e}')
         elif (zone_name and realm_token):
             token = RealmToken.from_base64_str(realm_token)
-            placement_spec = PlacementSpec.from_string(placement) if placement else None
+            if isinstance(placement, dict):
+                placement_spec = PlacementSpec.from_json(placement) if placement else None
+            elif isinstance(placement, str):
+                placement_spec = PlacementSpec.from_string(placement) if placement else None
             rgw_specs = [RGWSpec(rgw_realm=token.realm_name,
                                  rgw_zone=zone_name,
                                  rgw_realm_token=realm_token,
@@ -371,8 +375,9 @@ class Module(orchestrator.OrchestratorClientMixin, MgrModule):
                            zone_name: Optional[str] = None,
                            realm_token: Optional[str] = None,
                            port: Optional[int] = None,
-                           placement: Optional[str] = None,
+                           placement: Optional[dict] = None,
                            start_radosgw: Optional[bool] = True,
                            zone_endpoints: Optional[str] = None) -> None:
-        self.rgw_zone_create(zone_name, realm_token, port, placement, start_radosgw,
+        placement_spec = placement.get('placement') if placement else None
+        self.rgw_zone_create(zone_name, realm_token, port, placement_spec, start_radosgw,
                              zone_endpoints)