Skip to content

Commit 660fe1e

Browse files
authored
Merge pull request ceph#53994 from rhcs-dashboard/add-port-endpoints-import-multisite
mgr/dashboard: add port and zone endpoints to import realm token form in rgw multisite Reviewed-by: Nizamudeen A <[email protected]>
2 parents 677b522 + 84ec194 commit 660fe1e

File tree

7 files changed

+249
-32
lines changed

7 files changed

+249
-32
lines changed

src/pybind/mgr/dashboard/controllers/rgw.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -812,10 +812,10 @@ def get_realm_tokens(self):
812812
@UpdatePermission
813813
@allow_empty_body
814814
# pylint: disable=W0613
815-
def import_realm_token(self, realm_token, zone_name, daemon_name=None):
815+
def import_realm_token(self, realm_token, zone_name, port, placement_spec):
816816
try:
817817
multisite_instance = RgwMultisite()
818-
result = CephService.import_realm_token(realm_token, zone_name)
818+
result = CephService.import_realm_token(realm_token, zone_name, port, placement_spec)
819819
multisite_instance.update_period()
820820
return result
821821
except NoRgwDaemonsException as e:

src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-import/rgw-multisite-import.component.html

Lines changed: 121 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,9 @@
1313
<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>
1414
<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>
1515
<li>The secondary zone represents the destination cluster where your data will be replicated.</li>
16-
<li>Please create an RGW service using the secondary zone (created after submitting this form) to start the replication between zones.</li>
1716
</ul>
1817
</cd-alert-panel>
18+
<legend i18n>Zone Details</legend>
1919
<div class="form-group row">
2020
<label class="cd-col-form-label required"
2121
for="realmToken"
@@ -51,6 +51,126 @@
5151
i18n>The chosen zone name is already in use.</span>
5252
</div>
5353
</div>
54+
55+
<legend i18n>Service Details</legend>
56+
<div class="form-group row">
57+
<div class="cd-col-form-offset">
58+
<div class="custom-control custom-checkbox">
59+
<input class="custom-control-input"
60+
id="unmanaged"
61+
type="checkbox"
62+
formControlName="unmanaged">
63+
<label class="custom-control-label"
64+
for="unmanaged"
65+
i18n>Unmanaged</label>
66+
<cd-helper i18n>If set to true, the orchestrator will not start nor stop any daemon associated with this service.
67+
Placement and all other properties will be ignored.</cd-helper>
68+
</div>
69+
</div>
70+
</div>
71+
72+
<!-- Placement -->
73+
<div *ngIf="!importTokenForm.controls.unmanaged.value"
74+
class="form-group row">
75+
<label class="cd-col-form-label"
76+
for="placement"
77+
i18n>Placement</label>
78+
<div class="cd-col-form-input">
79+
<select id="placement"
80+
class="form-select"
81+
formControlName="placement">
82+
<option i18n
83+
value="hosts">Hosts</option>
84+
<option i18n
85+
value="label">Label</option>
86+
</select>
87+
</div>
88+
</div>
89+
90+
<!-- Label -->
91+
<div *ngIf="!importTokenForm.controls.unmanaged.value && importTokenForm.controls.placement.value === 'label'"
92+
class="form-group row">
93+
<label i18n
94+
class="cd-col-form-label"
95+
for="label">Label</label>
96+
<div class="cd-col-form-input">
97+
<input id="label"
98+
class="form-control"
99+
type="text"
100+
formControlName="label"
101+
[ngbTypeahead]="searchLabels"
102+
(focus)="labelFocus.next($any($event).target.value)"
103+
(click)="labelClick.next($any($event).target.value)">
104+
<span class="invalid-feedback"
105+
*ngIf="importTokenForm.showError('label', frm, 'required')"
106+
i18n>This field is required.</span>
107+
</div>
108+
</div>
109+
110+
<!-- Hosts -->
111+
<div *ngIf="!importTokenForm.controls.unmanaged.value && importTokenForm.controls.placement.value === 'hosts'"
112+
class="form-group row">
113+
<label class="cd-col-form-label"
114+
for="hosts"
115+
i18n>Hosts</label>
116+
<div class="cd-col-form-input">
117+
<cd-select-badges id="hosts"
118+
[data]="importTokenForm.controls.hosts.value"
119+
[options]="hosts.options"
120+
[messages]="hosts.messages">
121+
</cd-select-badges>
122+
</div>
123+
</div>
124+
125+
<!-- count -->
126+
<div *ngIf="!importTokenForm.controls.unmanaged.value"
127+
class="form-group row">
128+
<label class="cd-col-form-label"
129+
for="count">
130+
<span i18n>Count</span>
131+
<cd-helper i18n>Only that number of daemons will be created.</cd-helper>
132+
</label>
133+
<div class="cd-col-form-input">
134+
<input id="count"
135+
class="form-control"
136+
type="number"
137+
formControlName="count"
138+
min="1">
139+
<span class="invalid-feedback"
140+
*ngIf="importTokenForm.showError('count', frm, 'min')"
141+
i18n>The value must be at least 1.</span>
142+
<span class="invalid-feedback"
143+
*ngIf="importTokenForm.showError('count', frm, 'pattern')"
144+
i18n>The entered value needs to be a number.</span>
145+
</div>
146+
</div>
147+
148+
<!-- RGW -->
149+
<ng-container *ngIf="!importTokenForm.controls.unmanaged.value">
150+
<!-- rgw_frontend_port -->
151+
<div class="form-group row">
152+
<label i18n
153+
class="cd-col-form-label"
154+
for="rgw_frontend_port">Port</label>
155+
<div class="cd-col-form-input">
156+
<input id="rgw_frontend_port"
157+
class="form-control"
158+
type="number"
159+
formControlName="rgw_frontend_port"
160+
min="1"
161+
max="65535">
162+
<span class="invalid-feedback"
163+
*ngIf="importTokenForm.showError('rgw_frontend_port', frm, 'pattern')"
164+
i18n>The entered value needs to be a number.</span>
165+
<span class="invalid-feedback"
166+
*ngIf="importTokenForm.showError('rgw_frontend_port', frm, 'min')"
167+
i18n>The value must be at least 1.</span>
168+
<span class="invalid-feedback"
169+
*ngIf="importTokenForm.showError('rgw_frontend_port', frm, 'max')"
170+
i18n>The value cannot exceed 65535.</span>
171+
</div>
172+
</div>
173+
</ng-container>
54174
</div>
55175
<div class="modal-footer">
56176
<cd-form-button-panel (submitActionEvent)="onSubmit()"
Lines changed: 101 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import { Component, OnInit } from '@angular/core';
1+
import { Component, OnInit, ViewChild } from '@angular/core';
22
import { FormControl, Validators } from '@angular/forms';
3-
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
3+
import { NgbActiveModal, NgbTypeahead } from '@ng-bootstrap/ng-bootstrap';
44
import { RgwRealmService } from '~/app/shared/api/rgw-realm.service';
55
import { ActionLabelsI18n } from '~/app/shared/constants/app.constants';
66
import { NotificationType } from '~/app/shared/enum/notification-type.enum';
@@ -9,6 +9,12 @@ import { CdValidators } from '~/app/shared/forms/cd-validators';
99
import { NotificationService } from '~/app/shared/services/notification.service';
1010
import { RgwZone } from '../models/rgw-multisite';
1111
import _ from 'lodash';
12+
import { SelectMessages } from '~/app/shared/components/select/select-messages.model';
13+
import { HostService } from '~/app/shared/api/host.service';
14+
import { CdTableFetchDataContext } from '~/app/shared/models/cd-table-fetch-data-context';
15+
import { SelectOption } from '~/app/shared/components/select/select-option.model';
16+
import { Observable, Subject, merge } from 'rxjs';
17+
import { debounceTime, distinctUntilChanged, filter, map } from 'rxjs/operators';
1218

1319
@Component({
1420
selector: 'cd-rgw-multisite-import',
@@ -19,18 +25,33 @@ export class RgwMultisiteImportComponent implements OnInit {
1925
readonly endpoints = /^((https?:\/\/)|(www.))(?:([a-zA-Z]+)|(\d+\.\d+.\d+.\d+)):\d{2,4}$/;
2026
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;
2127
readonly ipv6Rgx = /^(?:[a-f0-9]{1,4}:){7}[a-f0-9]{1,4}$/i;
28+
@ViewChild(NgbTypeahead, { static: false })
29+
typeahead: NgbTypeahead;
2230

2331
importTokenForm: CdFormGroup;
2432
multisiteInfo: object[] = [];
2533
zoneList: RgwZone[] = [];
2634
zoneNames: string[];
35+
hosts: any;
36+
labels: string[];
37+
labelClick = new Subject<string>();
38+
labelFocus = new Subject<string>();
2739

2840
constructor(
2941
public activeModal: NgbActiveModal,
42+
public hostService: HostService,
43+
3044
public rgwRealmService: RgwRealmService,
3145
public actionLabels: ActionLabelsI18n,
3246
public notificationService: NotificationService
3347
) {
48+
this.hosts = {
49+
options: [],
50+
messages: new SelectMessages({
51+
empty: $localize`There are no hosts.`,
52+
filter: $localize`Filter hosts`
53+
})
54+
};
3455
this.createForm();
3556
}
3657
ngOnInit(): void {
@@ -41,6 +62,20 @@ export class RgwMultisiteImportComponent implements OnInit {
4162
this.zoneNames = this.zoneList.map((zone) => {
4263
return zone['name'];
4364
});
65+
const hostContext = new CdTableFetchDataContext(() => undefined);
66+
this.hostService.list(hostContext.toParams(), 'false').subscribe((resp: object[]) => {
67+
const options: SelectOption[] = [];
68+
_.forEach(resp, (host: object) => {
69+
if (_.get(host, 'sources.orchestrator', false)) {
70+
const option = new SelectOption(false, _.get(host, 'hostname'), '');
71+
options.push(option);
72+
}
73+
});
74+
this.hosts.options = [...options];
75+
});
76+
this.hostService.getLabels().subscribe((resp: string[]) => {
77+
this.labels = resp;
78+
});
4479
}
4580

4681
createForm() {
@@ -55,23 +90,75 @@ export class RgwMultisiteImportComponent implements OnInit {
5590
return this.zoneNames && this.zoneNames.indexOf(zoneName) !== -1;
5691
})
5792
]
58-
})
93+
}),
94+
rgw_frontend_port: new FormControl(null, {
95+
validators: [Validators.required, Validators.pattern('^[0-9]*$')]
96+
}),
97+
placement: new FormControl('hosts'),
98+
label: new FormControl(null, [
99+
CdValidators.requiredIf({
100+
placement: 'label',
101+
unmanaged: false
102+
})
103+
]),
104+
hosts: new FormControl([]),
105+
count: new FormControl(null, [CdValidators.number(false)]),
106+
unmanaged: new FormControl(false)
59107
});
60108
}
61109

62110
onSubmit() {
63111
const values = this.importTokenForm.value;
64-
this.rgwRealmService.importRealmToken(values['realmToken'], values['zoneName']).subscribe(
65-
() => {
66-
this.notificationService.show(
67-
NotificationType.success,
68-
$localize`Realm token import successfull`
69-
);
70-
this.activeModal.close();
71-
},
72-
() => {
73-
this.importTokenForm.setErrors({ cdSubmitButton: true });
112+
const placementSpec: object = {
113+
placement: {}
114+
};
115+
if (!values['unmanaged']) {
116+
switch (values['placement']) {
117+
case 'hosts':
118+
if (values['hosts'].length > 0) {
119+
placementSpec['placement']['hosts'] = values['hosts'];
120+
}
121+
break;
122+
case 'label':
123+
placementSpec['placement']['label'] = values['label'];
124+
break;
74125
}
75-
);
126+
if (_.isNumber(values['count']) && values['count'] > 0) {
127+
placementSpec['placement']['count'] = values['count'];
128+
}
129+
}
130+
this.rgwRealmService
131+
.importRealmToken(
132+
values['realmToken'],
133+
values['zoneName'],
134+
values['rgw_frontend_port'],
135+
placementSpec
136+
)
137+
.subscribe(
138+
() => {
139+
this.notificationService.show(
140+
NotificationType.success,
141+
$localize`Realm token import successfull`
142+
);
143+
this.activeModal.close();
144+
},
145+
() => {
146+
this.importTokenForm.setErrors({ cdSubmitButton: true });
147+
}
148+
);
76149
}
150+
151+
searchLabels = (text$: Observable<string>) => {
152+
return merge(
153+
text$.pipe(debounceTime(200), distinctUntilChanged()),
154+
this.labelFocus,
155+
this.labelClick.pipe(filter(() => !this.typeahead.isPopupOpen()))
156+
).pipe(
157+
map((value) =>
158+
this.labels
159+
.filter((label: string) => label.toLowerCase().indexOf(value.toLowerCase()) > -1)
160+
.slice(0, 10)
161+
)
162+
);
163+
};
77164
}

src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-realm.service.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -66,14 +66,14 @@ export class RgwRealmService {
6666
};
6767
}
6868

69-
importRealmToken(realm_token: string, zone_name: string) {
70-
return this.rgwDaemonService.request((params: HttpParams) => {
71-
params = params.appendAll({
72-
realm_token: realm_token,
73-
zone_name: zone_name
74-
});
75-
return this.http.post(`${this.url}/import_realm_token`, null, { params: params });
76-
});
69+
importRealmToken(realm_token: string, zone_name: string, port: number, placementSpec: object) {
70+
let requestBody = {
71+
realm_token: realm_token,
72+
zone_name: zone_name,
73+
port: port,
74+
placement_spec: placementSpec
75+
};
76+
return this.http.post(`${this.url}/import_realm_token`, requestBody);
7777
}
7878

7979
getRealmTokens() {

src/pybind/mgr/dashboard/openapi.yaml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9394,7 +9394,9 @@ paths:
93949394
application/json:
93959395
schema:
93969396
properties:
9397-
daemon_name:
9397+
placement_spec:
9398+
type: string
9399+
port:
93989400
type: string
93999401
realm_token:
94009402
type: string
@@ -9403,6 +9405,8 @@ paths:
94039405
required:
94049406
- realm_token
94059407
- zone_name
9408+
- port
9409+
- placement_spec
94069410
type: object
94079411
responses:
94089412
'201':

src/pybind/mgr/dashboard/services/ceph_service.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -317,9 +317,10 @@ def get_realm_tokens(cls):
317317
return tokens_info
318318

319319
@classmethod
320-
def import_realm_token(cls, realm_token, zone_name):
320+
def import_realm_token(cls, realm_token, zone_name, port, placement_spec):
321321
tokens_info = mgr.remote('rgw', 'import_realm_token', zone_name=zone_name,
322-
realm_token=realm_token, start_radosgw=True)
322+
realm_token=realm_token, port=port, placement=placement_spec,
323+
start_radosgw=True)
323324
return tokens_info
324325

325326
@classmethod

0 commit comments

Comments
 (0)