Skip to content

Commit b8811c8

Browse files
nizamial09Aashish Sharma
authored andcommitted
mgr/dashboard: introduce multi-cluster overview page
https://tracker.ceph.com/issues/64530 Signed-off-by: Nizamudeen A <[email protected]> Signed-off-by: Aashish Sharma <[email protected]>
1 parent 495f669 commit b8811c8

27 files changed

+976
-144
lines changed

monitoring/ceph-mixin/dashboards/multi-cluster.libsonnet

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ local g = import 'grafonnet/grafana.libsonnet';
55
$.dashboardSchema(
66
'Ceph - Multi-cluster',
77
'',
8-
'BnxelG7Sz',
8+
'BnxelG7Sx',
99
'now-1h',
1010
'30s',
1111
22,

monitoring/ceph-mixin/dashboards_out/multi-cluster-overview.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2118,6 +2118,6 @@
21182118
},
21192119
"timezone": "",
21202120
"title": "Ceph - Multi-cluster",
2121-
"uid": "BnxelG7Sz",
2121+
"uid": "BnxelG7Sx",
21222122
"version": 0
21232123
}

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

Lines changed: 72 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
import requests
88

9+
from .. import mgr
910
from ..exceptions import DashboardException
1011
from ..security import Scope
1112
from ..settings import Settings
@@ -18,9 +19,10 @@
1819
@APIDoc('Multi-cluster Management API', 'Multi-cluster')
1920
class MultiCluster(RESTController):
2021
def _proxy(self, method, base_url, path, params=None, payload=None, verify=False,
21-
token=None):
22+
token=None, cert=None):
2223
if not base_url.endswith('/'):
2324
base_url = base_url + '/'
25+
2426
try:
2527
if token:
2628
headers = {
@@ -33,7 +35,7 @@ def _proxy(self, method, base_url, path, params=None, payload=None, verify=False
3335
'Content-Type': 'application/json',
3436
}
3537
response = requests.request(method, base_url + path, params=params,
36-
json=payload, verify=verify, headers=headers)
38+
json=payload, verify=verify, cert=cert, headers=headers)
3739
except Exception as e:
3840
raise DashboardException(
3941
"Could not reach {}, {}".format(base_url+path, e),
@@ -51,10 +53,10 @@ def _proxy(self, method, base_url, path, params=None, payload=None, verify=False
5153
@Endpoint('POST')
5254
@CreatePermission
5355
@EndpointDoc("Authenticate to a remote cluster")
54-
def auth(self, url: str, cluster_alias: str, username=None,
55-
password=None, token=None, hub_url=None, cluster_fsid=None):
56-
57-
if username and password:
56+
def auth(self, url: str, cluster_alias: str, username: str,
57+
password=None, token=None, hub_url=None, cluster_fsid=None,
58+
prometheus_api_url=None, ssl_verify=False, ssl_certificate=None):
59+
if password:
5860
payload = {
5961
'username': username,
6062
'password': password
@@ -69,16 +71,28 @@ def auth(self, url: str, cluster_alias: str, username=None,
6971
cluster_token = content['token']
7072

7173
self._proxy('PUT', url, 'ui-api/multi-cluster/set_cors_endpoint',
72-
payload={'url': hub_url}, token=cluster_token)
73-
74+
payload={'url': hub_url}, token=cluster_token, verify=ssl_verify,
75+
cert=ssl_certificate)
7476
fsid = self._proxy('GET', url, 'api/health/get_cluster_fsid', token=cluster_token)
7577

76-
self.set_multi_cluster_config(fsid, username, url, cluster_alias, cluster_token)
78+
# add prometheus targets
79+
prometheus_url = self._proxy('GET', url, 'api/settings/PROMETHEUS_API_HOST',
80+
token=cluster_token)
81+
_set_prometheus_targets(prometheus_url['value'])
7782

78-
if token and cluster_fsid and username:
79-
self.set_multi_cluster_config(cluster_fsid, username, url, cluster_alias, token)
83+
self.set_multi_cluster_config(fsid, username, url, cluster_alias,
84+
cluster_token, prometheus_url['value'],
85+
ssl_verify, ssl_certificate)
86+
return
8087

81-
def set_multi_cluster_config(self, fsid, username, url, cluster_alias, token):
88+
if token and cluster_fsid and prometheus_api_url:
89+
_set_prometheus_targets(prometheus_api_url)
90+
self.set_multi_cluster_config(cluster_fsid, username, url,
91+
cluster_alias, token, prometheus_api_url,
92+
ssl_verify, ssl_certificate)
93+
94+
def set_multi_cluster_config(self, fsid, username, url, cluster_alias, token,
95+
prometheus_url=None, ssl_verify=False, ssl_certificate=None):
8296
multi_cluster_config = self.load_multi_cluster_config()
8397
if fsid in multi_cluster_config['config']:
8498
existing_entries = multi_cluster_config['config'][fsid]
@@ -89,6 +103,9 @@ def set_multi_cluster_config(self, fsid, username, url, cluster_alias, token):
89103
"cluster_alias": cluster_alias,
90104
"user": username,
91105
"token": token,
106+
"prometheus_url": prometheus_url if prometheus_url else '',
107+
"ssl_verify": ssl_verify,
108+
"ssl_certificate": ssl_certificate if ssl_certificate else ''
92109
})
93110
else:
94111
multi_cluster_config['current_user'] = username
@@ -98,6 +115,9 @@ def set_multi_cluster_config(self, fsid, username, url, cluster_alias, token):
98115
"cluster_alias": cluster_alias,
99116
"user": username,
100117
"token": token,
118+
"prometheus_url": prometheus_url if prometheus_url else '',
119+
"ssl_verify": ssl_verify,
120+
"ssl_certificate": ssl_certificate if ssl_certificate else ''
101121
}]
102122
Settings.MULTICLUSTER_CONFIG = multi_cluster_config
103123

@@ -123,16 +143,18 @@ def set_config(self, config: object):
123143
return Settings.MULTICLUSTER_CONFIG
124144

125145
@Endpoint('PUT')
126-
@CreatePermission
146+
@UpdatePermission
127147
# pylint: disable=unused-variable
128-
def reconnect_cluster(self, url: str, username=None, password=None, token=None):
148+
def reconnect_cluster(self, url: str, username=None, password=None, token=None,
149+
ssl_verify=False, ssl_certificate=None):
129150
multicluster_config = self.load_multi_cluster_config()
130151
if username and password:
131152
payload = {
132153
'username': username,
133154
'password': password
134155
}
135-
content = self._proxy('POST', url, 'api/auth', payload=payload)
156+
content = self._proxy('POST', url, 'api/auth', payload=payload,
157+
verify=ssl_verify, cert=ssl_certificate)
136158
if 'token' not in content:
137159
raise DashboardException(
138160
"Could not authenticate to remote cluster",
@@ -143,7 +165,7 @@ def reconnect_cluster(self, url: str, username=None, password=None, token=None):
143165

144166
if username and token:
145167
if "config" in multicluster_config:
146-
for key, cluster_details in multicluster_config["config"].items():
168+
for _, cluster_details in multicluster_config["config"].items():
147169
for cluster in cluster_details:
148170
if cluster["url"] == url and cluster["user"] == username:
149171
cluster['token'] = token
@@ -168,31 +190,38 @@ def edit_cluster(self, url, cluster_alias, username):
168190
def delete_cluster(self, cluster_name, cluster_user):
169191
multicluster_config = self.load_multi_cluster_config()
170192
if "config" in multicluster_config:
171-
keys_to_remove = []
172-
for key, cluster_details in multicluster_config["config"].items():
173-
cluster_details_copy = list(cluster_details)
174-
for cluster in cluster_details_copy:
175-
if cluster["name"] == cluster_name and cluster["user"] == cluster_user:
176-
cluster_details.remove(cluster)
177-
if not cluster_details:
178-
keys_to_remove.append(key)
179-
180-
for key in keys_to_remove:
181-
del multicluster_config["config"][key]
193+
for key, value in list(multicluster_config['config'].items()):
194+
if value[0]['name'] == cluster_name and value[0]['user'] == cluster_user:
195+
196+
orch_backend = mgr.get_module_option_ex('orchestrator', 'orchestrator')
197+
try:
198+
if orch_backend == 'cephadm':
199+
cmd = {
200+
'prefix': 'orch prometheus remove-target',
201+
'url': value[0]['prometheus_url'].replace('http://', '').replace('https://', '') # noqa E501 #pylint: disable=line-too-long
202+
}
203+
mgr.mon_command(cmd)
204+
except KeyError:
205+
pass
206+
207+
del multicluster_config['config'][key]
208+
break
182209

183210
Settings.MULTICLUSTER_CONFIG = multicluster_config
184211
return Settings.MULTICLUSTER_CONFIG
185212

186-
@Endpoint()
187-
@ReadPermission
213+
@Endpoint('POST')
214+
@CreatePermission
188215
# pylint: disable=R0911
189-
def verify_connection(self, url=None, username=None, password=None, token=None):
216+
def verify_connection(self, url=None, username=None, password=None, token=None,
217+
ssl_verify=False, ssl_certificate=None):
190218
if token:
191219
try:
192220
payload = {
193221
'token': token
194222
}
195-
content = self._proxy('POST', url, 'api/auth/check', payload=payload)
223+
content = self._proxy('POST', url, 'api/auth/check', payload=payload,
224+
verify=ssl_verify, cert=ssl_certificate)
196225
if 'permissions' not in content:
197226
return content['detail']
198227
user_content = self._proxy('GET', url, f'api/user/{username}',
@@ -210,7 +239,8 @@ def verify_connection(self, url=None, username=None, password=None, token=None):
210239
'username': username,
211240
'password': password
212241
}
213-
content = self._proxy('POST', url, 'api/auth', payload=payload)
242+
content = self._proxy('POST', url, 'api/auth', payload=payload,
243+
verify=ssl_verify, cert=ssl_certificate)
214244
if 'token' not in content:
215245
return content['detail']
216246
user_content = self._proxy('GET', url, f'api/user/{username}',
@@ -266,3 +296,13 @@ class MultiClusterUi(RESTController):
266296
@UpdatePermission
267297
def set_cors_endpoint(self, url: str):
268298
configure_cors(url)
299+
300+
301+
def _set_prometheus_targets(prometheus_url: str):
302+
orch_backend = mgr.get_module_option_ex('orchestrator', 'orchestrator')
303+
if orch_backend == 'cephadm':
304+
cmd = {
305+
'prefix': 'orch prometheus set-target',
306+
'url': prometheus_url.replace('http://', '').replace('https://', '')
307+
}
308+
mgr.mon_command(cmd)

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,11 @@ def create_silence(self, **params):
146146
def delete_silence(self, s_id):
147147
return self.alert_proxy('DELETE', '/silence/' + s_id) if s_id else None
148148

149+
@RESTController.Collection(method='GET', path='/prometheus_query_data')
150+
def get_prometeus_query_data(self, **params):
151+
params['query'] = params.pop('params')
152+
return self.prometheus_proxy('GET', '/query', params)
153+
149154

150155
@APIRouter('/prometheus/notifications', Scope.PROMETHEUS)
151156
@APIDoc("Prometheus Notifications Management API", "PrometheusNotifications")

src/pybind/mgr/dashboard/frontend/cypress/e2e/ui/navigation.po.ts

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,13 @@ export class NavigationPageHelper extends PageHelper {
77

88
navigations = [
99
{ menu: 'Dashboard', component: 'cd-dashboard' },
10+
{
11+
menu: 'Multi-Cluster',
12+
submenus: [
13+
{ menu: 'Overview', component: 'cd-multi-cluster' },
14+
{ menu: 'Manage Clusters', component: 'cd-multi-cluster-list' }
15+
]
16+
},
1017
{
1118
menu: 'Cluster',
1219
submenus: [
@@ -78,7 +85,11 @@ export class NavigationPageHelper extends PageHelper {
7885
cy.intercept('/ui-api/block/rbd/status', { fixture: 'block-rbd-status.json' });
7986

8087
navs.forEach((nav: any) => {
81-
cy.contains('.simplebar-content li.nav-item a', nav.menu).click();
88+
cy.get('.simplebar-content li.nav-item a').each(($link) => {
89+
if ($link.text().trim() === nav.menu.trim()) {
90+
cy.wrap($link).click();
91+
}
92+
});
8293
if (nav.submenus) {
8394
this.checkNavSubMenu(nav.menu, nav.submenus);
8495
} else {
@@ -89,8 +100,10 @@ export class NavigationPageHelper extends PageHelper {
89100

90101
checkNavSubMenu(menu: any, submenu: any) {
91102
submenu.forEach((nav: any) => {
92-
cy.contains('.simplebar-content li.nav-item', menu).within(() => {
93-
cy.contains(`ul.list-unstyled li a`, nav.menu).click();
103+
cy.get('.simplebar-content li.nav-item a').each(($link) => {
104+
if ($link.text().trim() === menu.trim()) {
105+
cy.contains(`ul.list-unstyled li a`, nav.menu).click();
106+
}
94107
});
95108
});
96109
}

src/pybind/mgr/dashboard/frontend/src/app/app-routing.module.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -191,10 +191,7 @@ const routes: Routes = [
191191
children: [
192192
{
193193
path: 'overview',
194-
component: MultiClusterComponent,
195-
data: {
196-
breadcrumbs: 'Multi-Cluster/Overview'
197-
}
194+
component: MultiClusterComponent
198195
},
199196
{
200197
path: 'manage-clusters',

src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/cluster.module.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ import { UpgradeProgressComponent } from './upgrade/upgrade-progress/upgrade-pro
6464
import { MultiClusterComponent } from './multi-cluster/multi-cluster.component';
6565
import { MultiClusterFormComponent } from './multi-cluster/multi-cluster-form/multi-cluster-form.component';
6666
import { MultiClusterListComponent } from './multi-cluster/multi-cluster-list/multi-cluster-list.component';
67+
import { DashboardV3Module } from '../dashboard-v3/dashboard-v3.module';
6768

6869
@NgModule({
6970
imports: [
@@ -84,7 +85,8 @@ import { MultiClusterListComponent } from './multi-cluster/multi-cluster-list/mu
8485
NgbPopoverModule,
8586
NgbDropdownModule,
8687
NgxPipeFunctionModule,
87-
NgbProgressbarModule
88+
NgbProgressbarModule,
89+
DashboardV3Module
8890
],
8991
declarations: [
9092
HostsComponent,

src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/multi-cluster/multi-cluster-form/multi-cluster-form.component.html

Lines changed: 58 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -117,15 +117,25 @@
117117
i18n>Password
118118
</label>
119119
<div class="cd-col-form-input">
120-
<input id="password"
121-
name="password"
122-
class="form-control"
123-
type="password"
124-
formControlName="password">
125-
<span class="invalid-feedback"
126-
*ngIf="remoteClusterForm.showError('password', frm, 'required')"
127-
i18n>This field is required.
128-
</span>
120+
<div class="input-group">
121+
<input id="password"
122+
name="password"
123+
class="form-control"
124+
type="password"
125+
formControlName="password">
126+
<span class="input-group-button">
127+
<button type="button"
128+
class="btn btn-light"
129+
cdPasswordButton="password">
130+
</button>
131+
<cd-copy-2-clipboard-button source="password">
132+
</cd-copy-2-clipboard-button>
133+
</span>
134+
<span class="invalid-feedback"
135+
*ngIf="remoteClusterForm.showError('password', frm, 'required')"
136+
i18n>This field is required.
137+
</span>
138+
</div>
129139
</div>
130140
</div>
131141
<div class="form-group row"
@@ -161,6 +171,45 @@
161171
</div>
162172
</div>
163173
</div>
174+
<!-- ssl -->
175+
<div class="form-group row">
176+
<div class="cd-col-form-offset">
177+
<div class="custom-control custom-checkbox">
178+
<input class="custom-control-input"
179+
id="ssl"
180+
type="checkbox"
181+
formControlName="ssl">
182+
<label class="custom-control-label"
183+
for="ssl"
184+
i18n>SSL</label>
185+
</div>
186+
</div>
187+
</div>
188+
189+
<!-- ssl_cert -->
190+
<div *ngIf="remoteClusterForm.controls.ssl.value"
191+
class="form-group row">
192+
<label class="cd-col-form-label"
193+
for="ssl_cert">
194+
<span i18n>Certificate</span>
195+
<cd-helper i18n>The SSL certificate in PEM format.</cd-helper>
196+
</label>
197+
<div class="cd-col-form-input">
198+
<textarea id="ssl_cert"
199+
class="form-control resize-vertical text-monospace text-pre"
200+
formControlName="ssl_cert"
201+
rows="5">
202+
</textarea>
203+
<input type="file"
204+
(change)="fileUpload($event.target.files, 'ssl_cert')">
205+
<span class="invalid-feedback"
206+
*ngIf="remoteClusterForm.showError('ssl_cert', frm, 'required')"
207+
i18n>This field is required.</span>
208+
<span class="invalid-feedback"
209+
*ngIf="remoteClusterForm.showError('ssl_cert', frm, 'pattern')"
210+
i18n>Invalid SSL certificate.</span>
211+
</div>
212+
</div>
164213
<div class="form-group row"
165214
*ngIf="!showCrossOriginError && action !== 'edit' && !remoteClusterForm.getValue('showToken')">
166215
<div class="cd-col-form-offset">

0 commit comments

Comments
 (0)