@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:
<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()"
-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';
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',
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 {
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() {
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)
+ )
+ );
+ };
}
};
}
- 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() {
application/json:
schema:
properties:
- daemon_name:
+ placement_spec:
+ type: string
+ port:
type: string
realm_token:
type: string
required:
- realm_token
- zone_name
+ - port
+ - placement_spec
type: object
responses:
'201':
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
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)
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,
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)