From b8811c844fa13b533be18015adb25e90dac2bc58 Mon Sep 17 00:00:00 2001 From: Nizamudeen A Date: Tue, 6 Feb 2024 18:09:41 +0530 Subject: [PATCH] mgr/dashboard: introduce multi-cluster overview page https://tracker.ceph.com/issues/64530 Signed-off-by: Nizamudeen A Signed-off-by: Aashish Sharma --- .../dashboards/multi-cluster.libsonnet | 2 +- .../multi-cluster-overview.json | 2 +- .../dashboard/controllers/multi_cluster.py | 104 ++++-- .../mgr/dashboard/controllers/prometheus.py | 5 + .../frontend/cypress/e2e/ui/navigation.po.ts | 19 +- .../frontend/src/app/app-routing.module.ts | 5 +- .../src/app/ceph/cluster/cluster.module.ts | 4 +- .../multi-cluster-form.component.html | 67 +++- .../multi-cluster-form.component.ts | 65 +++- .../multi-cluster-list.component.html | 2 +- .../multi-cluster.component.html | 197 +++++++++++- .../multi-cluster.component.spec.ts | 6 +- .../multi-cluster/multi-cluster.component.ts | 304 +++++++++++++++++- .../dashboard-area-chart.component.html | 8 +- .../dashboard-area-chart.component.ts | 6 +- .../ceph/dashboard-v3/dashboard-v3.module.ts | 7 +- .../workbench-layout.component.html | 2 +- .../app/shared/api/multi-cluster.service.ts | 47 ++- .../src/app/shared/api/prometheus.service.ts | 97 +++++- .../card-group/card-group.component.html | 10 + .../card-group/card-group.component.scss | 4 + .../card-group/card-group.component.spec.ts | 22 ++ .../card-group/card-group.component.ts | 11 + .../shared/components/components.module.ts | 7 +- .../app/shared/enum/dashboard-promqls.enum.ts | 28 ++ .../src/app/shared/models/multi-cluster.ts | 2 + src/pybind/mgr/dashboard/openapi.yaml | 87 +++-- 27 files changed, 976 insertions(+), 144 deletions(-) create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/shared/components/card-group/card-group.component.html create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/shared/components/card-group/card-group.component.scss create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/shared/components/card-group/card-group.component.spec.ts create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/shared/components/card-group/card-group.component.ts diff --git a/monitoring/ceph-mixin/dashboards/multi-cluster.libsonnet b/monitoring/ceph-mixin/dashboards/multi-cluster.libsonnet index ec725f4dfa281..a770565400839 100644 --- a/monitoring/ceph-mixin/dashboards/multi-cluster.libsonnet +++ b/monitoring/ceph-mixin/dashboards/multi-cluster.libsonnet @@ -5,7 +5,7 @@ local g = import 'grafonnet/grafana.libsonnet'; $.dashboardSchema( 'Ceph - Multi-cluster', '', - 'BnxelG7Sz', + 'BnxelG7Sx', 'now-1h', '30s', 22, diff --git a/monitoring/ceph-mixin/dashboards_out/multi-cluster-overview.json b/monitoring/ceph-mixin/dashboards_out/multi-cluster-overview.json index 91b2934f06546..ff8bcdd025443 100644 --- a/monitoring/ceph-mixin/dashboards_out/multi-cluster-overview.json +++ b/monitoring/ceph-mixin/dashboards_out/multi-cluster-overview.json @@ -2118,6 +2118,6 @@ }, "timezone": "", "title": "Ceph - Multi-cluster", - "uid": "BnxelG7Sz", + "uid": "BnxelG7Sx", "version": 0 } diff --git a/src/pybind/mgr/dashboard/controllers/multi_cluster.py b/src/pybind/mgr/dashboard/controllers/multi_cluster.py index c918c2ec3c240..d69a7da26094c 100644 --- a/src/pybind/mgr/dashboard/controllers/multi_cluster.py +++ b/src/pybind/mgr/dashboard/controllers/multi_cluster.py @@ -6,6 +6,7 @@ import time import requests +from .. import mgr from ..exceptions import DashboardException from ..security import Scope from ..settings import Settings @@ -18,9 +19,10 @@ from . import APIDoc, APIRouter, CreatePermission, DeletePermission, Endpoint, \ @APIDoc('Multi-cluster Management API', 'Multi-cluster') class MultiCluster(RESTController): def _proxy(self, method, base_url, path, params=None, payload=None, verify=False, - token=None): + token=None, cert=None): if not base_url.endswith('/'): base_url = base_url + '/' + try: if token: headers = { @@ -33,7 +35,7 @@ class MultiCluster(RESTController): 'Content-Type': 'application/json', } response = requests.request(method, base_url + path, params=params, - json=payload, verify=verify, headers=headers) + json=payload, verify=verify, cert=cert, headers=headers) except Exception as e: raise DashboardException( "Could not reach {}, {}".format(base_url+path, e), @@ -51,10 +53,10 @@ class MultiCluster(RESTController): @Endpoint('POST') @CreatePermission @EndpointDoc("Authenticate to a remote cluster") - def auth(self, url: str, cluster_alias: str, username=None, - password=None, token=None, hub_url=None, cluster_fsid=None): - - if username and password: + def auth(self, url: str, cluster_alias: str, username: str, + password=None, token=None, hub_url=None, cluster_fsid=None, + prometheus_api_url=None, ssl_verify=False, ssl_certificate=None): + if password: payload = { 'username': username, 'password': password @@ -69,16 +71,28 @@ class MultiCluster(RESTController): cluster_token = content['token'] self._proxy('PUT', url, 'ui-api/multi-cluster/set_cors_endpoint', - payload={'url': hub_url}, token=cluster_token) - + payload={'url': hub_url}, token=cluster_token, verify=ssl_verify, + cert=ssl_certificate) fsid = self._proxy('GET', url, 'api/health/get_cluster_fsid', token=cluster_token) - self.set_multi_cluster_config(fsid, username, url, cluster_alias, cluster_token) + # add prometheus targets + prometheus_url = self._proxy('GET', url, 'api/settings/PROMETHEUS_API_HOST', + token=cluster_token) + _set_prometheus_targets(prometheus_url['value']) - if token and cluster_fsid and username: - self.set_multi_cluster_config(cluster_fsid, username, url, cluster_alias, token) + self.set_multi_cluster_config(fsid, username, url, cluster_alias, + cluster_token, prometheus_url['value'], + ssl_verify, ssl_certificate) + return - def set_multi_cluster_config(self, fsid, username, url, cluster_alias, token): + if token and cluster_fsid and prometheus_api_url: + _set_prometheus_targets(prometheus_api_url) + self.set_multi_cluster_config(cluster_fsid, username, url, + cluster_alias, token, prometheus_api_url, + ssl_verify, ssl_certificate) + + def set_multi_cluster_config(self, fsid, username, url, cluster_alias, token, + prometheus_url=None, ssl_verify=False, ssl_certificate=None): multi_cluster_config = self.load_multi_cluster_config() if fsid in multi_cluster_config['config']: existing_entries = multi_cluster_config['config'][fsid] @@ -89,6 +103,9 @@ class MultiCluster(RESTController): "cluster_alias": cluster_alias, "user": username, "token": token, + "prometheus_url": prometheus_url if prometheus_url else '', + "ssl_verify": ssl_verify, + "ssl_certificate": ssl_certificate if ssl_certificate else '' }) else: multi_cluster_config['current_user'] = username @@ -98,6 +115,9 @@ class MultiCluster(RESTController): "cluster_alias": cluster_alias, "user": username, "token": token, + "prometheus_url": prometheus_url if prometheus_url else '', + "ssl_verify": ssl_verify, + "ssl_certificate": ssl_certificate if ssl_certificate else '' }] Settings.MULTICLUSTER_CONFIG = multi_cluster_config @@ -123,16 +143,18 @@ class MultiCluster(RESTController): return Settings.MULTICLUSTER_CONFIG @Endpoint('PUT') - @CreatePermission + @UpdatePermission # pylint: disable=unused-variable - def reconnect_cluster(self, url: str, username=None, password=None, token=None): + def reconnect_cluster(self, url: str, username=None, password=None, token=None, + ssl_verify=False, ssl_certificate=None): multicluster_config = self.load_multi_cluster_config() if username and password: payload = { 'username': username, 'password': password } - content = self._proxy('POST', url, 'api/auth', payload=payload) + content = self._proxy('POST', url, 'api/auth', payload=payload, + verify=ssl_verify, cert=ssl_certificate) if 'token' not in content: raise DashboardException( "Could not authenticate to remote cluster", @@ -143,7 +165,7 @@ class MultiCluster(RESTController): if username and token: if "config" in multicluster_config: - for key, cluster_details in multicluster_config["config"].items(): + for _, cluster_details in multicluster_config["config"].items(): for cluster in cluster_details: if cluster["url"] == url and cluster["user"] == username: cluster['token'] = token @@ -168,31 +190,38 @@ class MultiCluster(RESTController): def delete_cluster(self, cluster_name, cluster_user): multicluster_config = self.load_multi_cluster_config() if "config" in multicluster_config: - keys_to_remove = [] - for key, cluster_details in multicluster_config["config"].items(): - cluster_details_copy = list(cluster_details) - for cluster in cluster_details_copy: - if cluster["name"] == cluster_name and cluster["user"] == cluster_user: - cluster_details.remove(cluster) - if not cluster_details: - keys_to_remove.append(key) - - for key in keys_to_remove: - del multicluster_config["config"][key] + for key, value in list(multicluster_config['config'].items()): + if value[0]['name'] == cluster_name and value[0]['user'] == cluster_user: + + orch_backend = mgr.get_module_option_ex('orchestrator', 'orchestrator') + try: + if orch_backend == 'cephadm': + cmd = { + 'prefix': 'orch prometheus remove-target', + 'url': value[0]['prometheus_url'].replace('http://', '').replace('https://', '') # noqa E501 #pylint: disable=line-too-long + } + mgr.mon_command(cmd) + except KeyError: + pass + + del multicluster_config['config'][key] + break Settings.MULTICLUSTER_CONFIG = multicluster_config return Settings.MULTICLUSTER_CONFIG - @Endpoint() - @ReadPermission + @Endpoint('POST') + @CreatePermission # pylint: disable=R0911 - def verify_connection(self, url=None, username=None, password=None, token=None): + def verify_connection(self, url=None, username=None, password=None, token=None, + ssl_verify=False, ssl_certificate=None): if token: try: payload = { 'token': token } - content = self._proxy('POST', url, 'api/auth/check', payload=payload) + content = self._proxy('POST', url, 'api/auth/check', payload=payload, + verify=ssl_verify, cert=ssl_certificate) if 'permissions' not in content: return content['detail'] user_content = self._proxy('GET', url, f'api/user/{username}', @@ -210,7 +239,8 @@ class MultiCluster(RESTController): 'username': username, 'password': password } - content = self._proxy('POST', url, 'api/auth', payload=payload) + content = self._proxy('POST', url, 'api/auth', payload=payload, + verify=ssl_verify, cert=ssl_certificate) if 'token' not in content: return content['detail'] user_content = self._proxy('GET', url, f'api/user/{username}', @@ -266,3 +296,13 @@ class MultiClusterUi(RESTController): @UpdatePermission def set_cors_endpoint(self, url: str): configure_cors(url) + + +def _set_prometheus_targets(prometheus_url: str): + orch_backend = mgr.get_module_option_ex('orchestrator', 'orchestrator') + if orch_backend == 'cephadm': + cmd = { + 'prefix': 'orch prometheus set-target', + 'url': prometheus_url.replace('http://', '').replace('https://', '') + } + mgr.mon_command(cmd) diff --git a/src/pybind/mgr/dashboard/controllers/prometheus.py b/src/pybind/mgr/dashboard/controllers/prometheus.py index b639d88262739..7f5f193f9ab41 100644 --- a/src/pybind/mgr/dashboard/controllers/prometheus.py +++ b/src/pybind/mgr/dashboard/controllers/prometheus.py @@ -146,6 +146,11 @@ class Prometheus(PrometheusRESTController): def delete_silence(self, s_id): return self.alert_proxy('DELETE', '/silence/' + s_id) if s_id else None + @RESTController.Collection(method='GET', path='/prometheus_query_data') + def get_prometeus_query_data(self, **params): + params['query'] = params.pop('params') + return self.prometheus_proxy('GET', '/query', params) + @APIRouter('/prometheus/notifications', Scope.PROMETHEUS) @APIDoc("Prometheus Notifications Management API", "PrometheusNotifications") diff --git a/src/pybind/mgr/dashboard/frontend/cypress/e2e/ui/navigation.po.ts b/src/pybind/mgr/dashboard/frontend/cypress/e2e/ui/navigation.po.ts index 7d84939b88074..f2eefd826d8a8 100644 --- a/src/pybind/mgr/dashboard/frontend/cypress/e2e/ui/navigation.po.ts +++ b/src/pybind/mgr/dashboard/frontend/cypress/e2e/ui/navigation.po.ts @@ -7,6 +7,13 @@ export class NavigationPageHelper extends PageHelper { navigations = [ { menu: 'Dashboard', component: 'cd-dashboard' }, + { + menu: 'Multi-Cluster', + submenus: [ + { menu: 'Overview', component: 'cd-multi-cluster' }, + { menu: 'Manage Clusters', component: 'cd-multi-cluster-list' } + ] + }, { menu: 'Cluster', submenus: [ @@ -78,7 +85,11 @@ export class NavigationPageHelper extends PageHelper { cy.intercept('/ui-api/block/rbd/status', { fixture: 'block-rbd-status.json' }); navs.forEach((nav: any) => { - cy.contains('.simplebar-content li.nav-item a', nav.menu).click(); + cy.get('.simplebar-content li.nav-item a').each(($link) => { + if ($link.text().trim() === nav.menu.trim()) { + cy.wrap($link).click(); + } + }); if (nav.submenus) { this.checkNavSubMenu(nav.menu, nav.submenus); } else { @@ -89,8 +100,10 @@ export class NavigationPageHelper extends PageHelper { checkNavSubMenu(menu: any, submenu: any) { submenu.forEach((nav: any) => { - cy.contains('.simplebar-content li.nav-item', menu).within(() => { - cy.contains(`ul.list-unstyled li a`, nav.menu).click(); + cy.get('.simplebar-content li.nav-item a').each(($link) => { + if ($link.text().trim() === menu.trim()) { + cy.contains(`ul.list-unstyled li a`, nav.menu).click(); + } }); }); } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/app-routing.module.ts b/src/pybind/mgr/dashboard/frontend/src/app/app-routing.module.ts index c54681b065f27..6744e9cf23b35 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/app-routing.module.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/app-routing.module.ts @@ -191,10 +191,7 @@ const routes: Routes = [ children: [ { path: 'overview', - component: MultiClusterComponent, - data: { - breadcrumbs: 'Multi-Cluster/Overview' - } + component: MultiClusterComponent }, { path: 'manage-clusters', diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/cluster.module.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/cluster.module.ts index 2f0734885d857..b76189612b8b3 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/cluster.module.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/cluster.module.ts @@ -64,6 +64,7 @@ import { UpgradeProgressComponent } from './upgrade/upgrade-progress/upgrade-pro import { MultiClusterComponent } from './multi-cluster/multi-cluster.component'; import { MultiClusterFormComponent } from './multi-cluster/multi-cluster-form/multi-cluster-form.component'; import { MultiClusterListComponent } from './multi-cluster/multi-cluster-list/multi-cluster-list.component'; +import { DashboardV3Module } from '../dashboard-v3/dashboard-v3.module'; @NgModule({ imports: [ @@ -84,7 +85,8 @@ import { MultiClusterListComponent } from './multi-cluster/multi-cluster-list/mu NgbPopoverModule, NgbDropdownModule, NgxPipeFunctionModule, - NgbProgressbarModule + NgbProgressbarModule, + DashboardV3Module ], declarations: [ HostsComponent, diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/multi-cluster/multi-cluster-form/multi-cluster-form.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/multi-cluster/multi-cluster-form/multi-cluster-form.component.html index c875557306a85..a2d36e4232aa4 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/multi-cluster/multi-cluster-form/multi-cluster-form.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/multi-cluster/multi-cluster-form/multi-cluster-form.component.html @@ -117,15 +117,25 @@ i18n>Password
- - This field is required. - +
+ + + + + + + This field is required. + +
+ +
+
+
+ + +
+
+
+ + +
+ +
+ + + This field is required. + Invalid SSL certificate. +
+
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/multi-cluster/multi-cluster-form/multi-cluster-form.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/multi-cluster/multi-cluster-form/multi-cluster-form.component.ts index 83eb9fb5d51ee..ee39a51d47001 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/multi-cluster/multi-cluster-form/multi-cluster-form.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/multi-cluster/multi-cluster-form/multi-cluster-form.component.ts @@ -1,5 +1,5 @@ import { Component, EventEmitter, OnDestroy, OnInit, Output } from '@angular/core'; -import { FormControl, Validators } from '@angular/forms'; +import { AbstractControl, FormControl, Validators } from '@angular/forms'; import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; import _ from 'lodash'; import { Subscription } from 'rxjs'; @@ -50,6 +50,8 @@ export class MultiClusterFormComponent implements OnInit, OnDestroy { this.remoteClusterForm.get('remoteClusterUrl').setValue(this.cluster.url); this.remoteClusterForm.get('remoteClusterUrl').disable(); this.remoteClusterForm.get('clusterAlias').setValue(this.cluster.cluster_alias); + this.remoteClusterForm.get('ssl').setValue(this.cluster.ssl_verify); + this.remoteClusterForm.get('ssl_cert').setValue(this.cluster.ssl_certificate); } if (this.action === 'reconnect') { this.remoteClusterForm.get('remoteClusterUrl').setValue(this.cluster.url); @@ -60,6 +62,8 @@ export class MultiClusterFormComponent implements OnInit, OnDestroy { this.remoteClusterForm.get('username').disable(); this.remoteClusterForm.get('clusterFsid').setValue(this.cluster.name); this.remoteClusterForm.get('clusterFsid').disable(); + this.remoteClusterForm.get('ssl').setValue(this.cluster.ssl_verify); + this.remoteClusterForm.get('ssl_cert').setValue(this.cluster.ssl_certificate); } [this.clusterAliasNames, this.clusterUrls, this.clusterUsers] = [ 'cluster_alias', @@ -128,6 +132,14 @@ export class MultiClusterFormComponent implements OnInit, OnDestroy { ); }) ] + }), + ssl: new FormControl(false), + ssl_cert: new FormControl('', { + validators: [ + CdValidators.requiredIf({ + ssl: true + }) + ] }) }); } @@ -144,6 +156,8 @@ export class MultiClusterFormComponent implements OnInit, OnDestroy { const password = this.remoteClusterForm.getValue('password'); const token = this.remoteClusterForm.getValue('apiToken'); const clusterFsid = this.remoteClusterForm.getValue('clusterFsid'); + const ssl = this.remoteClusterForm.getValue('ssl'); + const ssl_certificate = this.remoteClusterForm.getValue('ssl_cert')?.trim(); if (this.action === 'edit') { this.subs.add( @@ -167,19 +181,21 @@ export class MultiClusterFormComponent implements OnInit, OnDestroy { if (this.action === 'reconnect') { this.subs.add( - this.multiClusterService.reConnectCluster(updatedUrl, username, password, token).subscribe({ - error: () => { - this.remoteClusterForm.setErrors({ cdSubmitButton: true }); - }, - complete: () => { - this.notificationService.show( - NotificationType.success, - $localize`Cluster reconnected successfully` - ); - this.submitAction.emit(); - this.activeModal.close(); - } - }) + this.multiClusterService + .reConnectCluster(updatedUrl, username, password, token, ssl, ssl_certificate) + .subscribe({ + error: () => { + this.remoteClusterForm.setErrors({ cdSubmitButton: true }); + }, + complete: () => { + this.notificationService.show( + NotificationType.success, + $localize`Cluster reconnected successfully` + ); + this.submitAction.emit(); + this.activeModal.close(); + } + }) ); } @@ -193,7 +209,9 @@ export class MultiClusterFormComponent implements OnInit, OnDestroy { password, token, window.location.origin, - clusterFsid + clusterFsid, + ssl, + ssl_certificate ) .subscribe({ error: () => { @@ -217,10 +235,12 @@ export class MultiClusterFormComponent implements OnInit, OnDestroy { const username = this.remoteClusterForm.getValue('username'); const password = this.remoteClusterForm.getValue('password'); const token = this.remoteClusterForm.getValue('apiToken'); + const ssl = this.remoteClusterForm.getValue('ssl'); + const ssl_certificate = this.remoteClusterForm.getValue('ssl_cert')?.trim(); this.subs.add( this.multiClusterService - .verifyConnection(url, username, password, token) + .verifyConnection(url, username, password, token, ssl, ssl_certificate) .subscribe((resp: string) => { switch (resp) { case 'Connection successful': @@ -259,4 +279,17 @@ export class MultiClusterFormComponent implements OnInit, OnDestroy { toggleToken() { this.showToken = !this.showToken; } + + fileUpload(files: FileList, controlName: string) { + const file: File = files[0]; + const reader = new FileReader(); + reader.addEventListener('load', (event: ProgressEvent) => { + const control: AbstractControl = this.remoteClusterForm.get(controlName); + control.setValue(event.target.result); + control.markAsDirty(); + control.markAsTouched(); + control.updateValueAndValidity(); + }); + reader.readAsText(file, 'utf8'); + } } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/multi-cluster/multi-cluster-list/multi-cluster-list.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/multi-cluster/multi-cluster-list/multi-cluster-list.component.html index 7aea2f4707610..74cfc78ab8af4 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/multi-cluster/multi-cluster-list/multi-cluster-list.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/multi-cluster/multi-cluster-list/multi-cluster-list.component.html @@ -29,7 +29,7 @@ let-row="row"> - {{ row.url.endsWith('/') ? row.url.slice(0, -1) : row.url }} + {{ row?.url?.endsWith('/') ? row?.url?.slice(0, -1) : row.url }} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/multi-cluster/multi-cluster.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/multi-cluster/multi-cluster.component.html index c826f155c40bf..0542b1868bae3 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/multi-cluster/multi-cluster.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/multi-cluster/multi-cluster.component.html @@ -6,7 +6,7 @@ [ngClass]="icons.wrench">
-

Connect Cluster

+

Connect Cluster

Upgrade your current cluster to a multi-cluster setup effortlessly. Click on the "Connect Cluster" button to begin the process.

@@ -35,17 +35,190 @@
-
-
- -
- - - - -
-
+ +
+
+
+ + +
+
+ +
+ + +
+
+ + +

{{ queriesResults.CLUSTER_COUNT[0][1] }}

+

+ + {{ queriesResults.HEALTH_ERROR_COUNT[0][1] }} +

+

+ + {{ queriesResults.HEALTH_WARNING_COUNT[0][1] }} +

+
+
+ + +

+ {{ queriesResults['ALERTS_COUNT'][0][1] }} +

+

+ + {{ queriesResults['CRITICAL_ALERTS_COUNT'][0][1] }} +

+

+ + {{ queriesResults['WARNING_ALERTS_COUNT'][0][1] }} +

+
+
+
+
+ + +

+ + {{ connectionErrorsCount }} +

+
+
+ + +

{{ queriesResults['TOTAL_HOSTS'][0][1] }}

+
+
+
+ +
+ + + + + + +
+
+ +
+ +
+ + + + + + + + +
+
+
+
+ + +
+ + +
+
+ + +
+
+ +
+ + + + + + + + +
+
+
+
+
+
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/multi-cluster/multi-cluster.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/multi-cluster/multi-cluster.component.spec.ts index 8db81cd790fc6..ad210968aa5be 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/multi-cluster/multi-cluster.component.spec.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/multi-cluster/multi-cluster.component.spec.ts @@ -2,6 +2,8 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { HttpClientTestingModule } from '@angular/common/http/testing'; import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; import { MultiClusterComponent } from './multi-cluster.component'; +import { SharedModule } from '~/app/shared/shared.module'; +import { DimlessBinaryPipe } from '~/app/shared/pipes/dimless-binary.pipe'; describe('MultiClusterComponent', () => { let component: MultiClusterComponent; @@ -9,9 +11,9 @@ describe('MultiClusterComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [HttpClientTestingModule], + imports: [HttpClientTestingModule, SharedModule], declarations: [MultiClusterComponent], - providers: [NgbActiveModal] + providers: [NgbActiveModal, DimlessBinaryPipe] }).compileComponents(); fixture = TestBed.createComponent(MultiClusterComponent); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/multi-cluster/multi-cluster.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/multi-cluster/multi-cluster.component.ts index dbbf10e74848b..ab8b413e73623 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/multi-cluster/multi-cluster.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/multi-cluster/multi-cluster.component.ts @@ -5,6 +5,11 @@ import { MultiClusterService } from '~/app/shared/api/multi-cluster.service'; import { Icons } from '~/app/shared/enum/icons.enum'; import { ModalService } from '~/app/shared/services/modal.service'; import { MultiClusterFormComponent } from './multi-cluster-form/multi-cluster-form.component'; +import { PrometheusService } from '~/app/shared/api/prometheus.service'; +import { MultiClusterPromqls as queries } from '~/app/shared/enum/dashboard-promqls.enum'; +import { CdTableColumn } from '~/app/shared/models/cd-table-column'; +import { CellTemplate } from '~/app/shared/enum/cell-template.enum'; +import { DimlessBinaryPipe } from '~/app/shared/pipes/dimless-binary.pipe'; @Component({ selector: 'cd-multi-cluster', @@ -12,41 +17,150 @@ import { MultiClusterFormComponent } from './multi-cluster-form/multi-cluster-fo styleUrls: ['./multi-cluster.component.scss'] }) export class MultiClusterComponent implements OnInit { + COUNT_OF_UTILIZATION_CHARTS = 5; + @ViewChild('nameTpl', { static: true }) nameTpl: any; + columns: Array = []; + + queriesResults: any = { + ALERTS_COUNT: 0, + CLUSTER_COUNT: 0, + HEALTH_OK_COUNT: 0, + HEALTH_WARNING_COUNT: 0, + HEALTH_ERROR_COUNT: 0, + TOTAL_CLUSTERS_CAPACITY: 0, + TOTAL_USED_CAPACITY: 0, + CLUSTER_CAPACITY_UTILIZATION: 0, + CLUSTER_IOPS_UTILIZATION: 0, + CLUSTER_THROUGHPUT_UTILIZATION: 0, + POOL_CAPACITY_UTILIZATION: 0, + POOL_IOPS_UTILIZATION: 0, + POOL_THROUGHPUT_UTILIZATION: 0, + TOTAL_CAPACITY: 0, + USED_CAPACITY: 0, + HOSTS: 0, + POOLS: 0, + OSDS: 0, + CLUSTER_ALERTS: 0, + version: '' + }; + alerts: any; + private subs = new Subscription(); dashboardClustersMap: Map = new Map(); icons = Icons; loading = true; bsModalRef: NgbModalRef; + isMultiCluster = true; + clusterTokenStatus: object = {}; + localClusterName: string; + clusters: any; + connectionErrorsCount = 0; + + capacityLabels: string[] = []; + iopsLabels: string[] = []; + throughputLabels: string[] = []; + poolIOPSLabels: string[] = []; + poolCapacityLabels: string[] = []; + poolThroughputLabels: string[] = []; + + capacityValues: string[] = []; + iopsValues: string[] = []; + throughputValues: string[] = []; + poolIOPSValues: string[] = []; + poolCapacityValues: string[] = []; + poolThroughputValues: string[] = []; constructor( private multiClusterService: MultiClusterService, - private modalService: ModalService + private modalService: ModalService, + private prometheusService: PrometheusService, + private dimlessBinaryPipe: DimlessBinaryPipe ) {} ngOnInit(): void { + this.columns = [ + { + prop: 'cluster', + name: $localize`Cluster Name`, + flexGrow: 2, + cellTemplate: this.nameTpl + }, + { + prop: 'cluster_connection_status', + name: $localize`Connection`, + flexGrow: 2, + cellTransformation: CellTemplate.badge, + customTemplateConfig: { + map: { + 1: { value: 'DISCONNECTED', class: 'badge-danger' }, + 0: { value: 'CONNECTED', class: 'badge-success' }, + 2: { value: 'CHECKING..', class: 'badge-info' } + } + } + }, + { + prop: 'status', + name: $localize`Status`, + flexGrow: 1, + cellTransformation: CellTemplate.badge, + customTemplateConfig: { + map: { + 1: { value: 'WARN', class: 'badge-warning' }, + 0: { value: 'OK', class: 'badge-success' }, + 2: { value: 'ERROR', class: 'badge-danger' } + } + } + }, + { prop: 'alert', name: $localize`Alerts`, flexGrow: 1 }, + { prop: 'version', name: $localize`Version`, flexGrow: 2 }, + { + prop: 'total_capacity', + name: $localize`Total Capacity`, + pipe: this.dimlessBinaryPipe, + flexGrow: 1 + }, + { + prop: 'used_capacity', + name: $localize`Used Capacity`, + pipe: this.dimlessBinaryPipe, + flexGrow: 1 + }, + { + prop: 'available_capacity', + name: $localize`Available Capacity`, + pipe: this.dimlessBinaryPipe, + flexGrow: 1 + }, + { prop: 'pools', name: $localize`Pools`, flexGrow: 1 }, + { prop: 'hosts', name: $localize`Hosts`, flexGrow: 1 }, + { prop: 'osds', name: $localize`OSDs`, flexGrow: 1 } + ]; + this.subs.add( this.multiClusterService.subscribe((resp: any) => { - const clustersConfig = resp['config']; - if (clustersConfig) { - Object.keys(clustersConfig).forEach((clusterKey: string) => { - const clusterDetailsList = clustersConfig[clusterKey]; - - clusterDetailsList.forEach((clusterDetails: any) => { - const clusterUrl = clusterDetails['url']; - const clusterName = clusterDetails['name']; - this.dashboardClustersMap.set(clusterUrl, clusterName); - }); - }); - - if (this.dashboardClustersMap.size >= 1) { - this.loading = false; + this.isMultiCluster = Object.keys(resp['config']).length > 1; + const hubUrl = resp['hub_url']; + for (const key in resp['config']) { + if (resp['config'].hasOwnProperty(key)) { + const cluster = resp['config'][key][0]; + if (hubUrl === cluster.url) { + this.localClusterName = cluster.name; + break; + } } } }) ); + + this.subs.add( + this.multiClusterService.subscribeClusterTokenStatus((resp: object) => { + this.clusterTokenStatus = resp; + }) + ); + this.getPrometheusData(this.prometheusService.lastHourDateObject); } openRemoteClusterInfoModal() { @@ -54,7 +168,165 @@ export class MultiClusterComponent implements OnInit { action: 'connect' }; this.bsModalRef = this.modalService.show(MultiClusterFormComponent, initialState, { - size: 'xl' + size: 'lg' + }); + } + + getPrometheusData(selectedTime: any) { + this.prometheusService + .getMultiClusterQueriesData(selectedTime, queries, this.queriesResults) + .subscribe((data: any) => { + this.queriesResults = data; + this.loading = false; + this.alerts = this.queriesResults.ALERTS; + this.getAlertsInfo(); + this.getClustersInfo(); + }); + } + + getAlertsInfo() { + interface Alert { + alertName: string; + alertState: string; + severity: string; + cluster: string; + } + + const alerts: Alert[] = []; + + this.alerts?.forEach((item: any) => { + const metric = item.metric; + const alert: Alert = { + alertName: metric.alertname, + cluster: metric.cluster, + alertState: metric.alertstate, + severity: metric.severity + }; + alerts.push(alert); + }); + + this.alerts = alerts; + } + + getClustersInfo() { + interface ClusterInfo { + cluster: string; + status: number; + alert: number; + total_capacity: number; + used_capacity: number; + available_capacity: number; + pools: number; + osds: number; + hosts: number; + version: string; + cluster_connection_status: number; + } + + const clusters: ClusterInfo[] = []; + + this.queriesResults.TOTAL_CAPACITY?.forEach((totalCapacityMetric: any) => { + const clusterName = totalCapacityMetric.metric.cluster; + const totalCapacity = parseInt(totalCapacityMetric.value[1]); + const getMgrMetadata = this.findCluster(this.queriesResults?.MGR_METADATA, clusterName); + const version = this.getVersion(getMgrMetadata.metric.ceph_version); + + const usedCapacity = this.findClusterData(this.queriesResults?.USED_CAPACITY, clusterName); + const pools = this.findClusterData(this.queriesResults?.POOLS, clusterName); + const hosts = this.findClusterData(this.queriesResults?.HOSTS, clusterName); + const alert = this.findClusterData(this.queriesResults?.CLUSTER_ALERTS, clusterName); + const osds = this.findClusterData(this.queriesResults?.OSDS, clusterName); + const status = this.findClusterData(this.queriesResults?.HEALTH_STATUS, clusterName); + const available_capacity = totalCapacity - usedCapacity; + + clusters.push({ + cluster: clusterName, + status, + alert, + total_capacity: totalCapacity, + used_capacity: usedCapacity, + available_capacity: available_capacity, + pools, + osds, + hosts, + version, + cluster_connection_status: 2 + }); }); + + if (this.clusterTokenStatus) { + clusters.forEach((cluster: any) => { + cluster.cluster_connection_status = this.clusterTokenStatus[cluster.cluster]?.status; + if (cluster.cluster === this.localClusterName) { + cluster.cluster_connection_status = 0; + } + }); + this.connectionErrorsCount = clusters.filter( + (cluster) => cluster.cluster_connection_status === 1 + ).length; + } + + this.clusters = clusters; + + // Generate labels and metrics for utilization charts + this.capacityLabels = this.generateQueryLabel(this.queriesResults.CLUSTER_CAPACITY_UTILIZATION); + this.iopsLabels = this.generateQueryLabel(this.queriesResults.CLUSTER_IOPS_UTILIZATION); + this.throughputLabels = this.generateQueryLabel( + this.queriesResults.CLUSTER_THROUGHPUT_UTILIZATION + ); + this.poolCapacityLabels = this.generateQueryLabel( + this.queriesResults.POOL_CAPACITY_UTILIZATION, + true + ); + this.poolIOPSLabels = this.generateQueryLabel(this.queriesResults.POOL_IOPS_UTILIZATION, true); + this.poolThroughputLabels = this.generateQueryLabel( + this.queriesResults.POOL_THROUGHPUT_UTILIZATION, + true + ); + + this.capacityValues = this.getQueryValues(this.queriesResults.CLUSTER_CAPACITY_UTILIZATION); + this.iopsValues = this.getQueryValues(this.queriesResults.CLUSTER_IOPS_UTILIZATION); + this.throughputValues = this.getQueryValues(this.queriesResults.CLUSTER_THROUGHPUT_UTILIZATION); + this.poolCapacityValues = this.getQueryValues(this.queriesResults.POOL_CAPACITY_UTILIZATION); + this.poolIOPSValues = this.getQueryValues(this.queriesResults.POOL_IOPS_UTILIZATION); + this.poolThroughputValues = this.getQueryValues( + this.queriesResults.POOL_THROUGHPUT_UTILIZATION + ); + } + + findClusterData(metrics: any, clusterName: string) { + const clusterMetrics = this.findCluster(metrics, clusterName); + return parseInt(clusterMetrics?.value[1] || 0); + } + + findCluster(metrics: any, clusterName: string) { + return metrics.find((metric: any) => metric?.metric?.cluster === clusterName); + } + + getVersion(fullVersion: string) { + const version = fullVersion.replace('ceph version ', '').split(' '); + return version[0] + ' ' + version.slice(2, version.length).join(' '); + } + + generateQueryLabel(query: any, name = false, count = this.COUNT_OF_UTILIZATION_CHARTS) { + let labels = []; + for (let i = 0; i < count; i++) { + let label = ''; + if (query[i]) { + label = query[i]?.metric?.cluster; + if (name) label = query[i]?.metric?.name + ' - ' + label; + } + labels.push(label); + } + // console.log(labels) + return labels; + } + + getQueryValues(query: any, count = this.COUNT_OF_UTILIZATION_CHARTS) { + let values = []; + for (let i = 0; i < count; i++) { + if (query[i]) values.push(query[i]?.values); + } + return values; } } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/dashboard-area-chart/dashboard-area-chart.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/dashboard-area-chart/dashboard-area-chart.component.html index 6151843e4e01d..2b4878e995d2a 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/dashboard-area-chart/dashboard-area-chart.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/dashboard-area-chart/dashboard-area-chart.component.html @@ -10,8 +10,12 @@
-
{{ data.label }}:
- {{ data?.currentData || 'N/A' }} {{ data?.currentDataUnits }} + + {{ data.label }}: + + {{ data?.currentData || 'N/A' }} {{ data?.currentDataUnits }}
used of {{ maxConvertedValue }} {{ maxConvertedValueUnits }}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/dashboard-area-chart/dashboard-area-chart.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/dashboard-area-chart/dashboard-area-chart.component.ts index ac0b9ac2ff431..607a3b7d51ad7 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/dashboard-area-chart/dashboard-area-chart.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/dashboard-area-chart/dashboard-area-chart.component.ts @@ -29,6 +29,8 @@ export class DashboardAreaChartComponent implements OnChanges { labelsArray?: string[] = []; // Array of chart labels @Input() decimals?: number = 1; + @Input() + truncateLabel = false; currentDataUnits: string; currentData: number; @@ -201,8 +203,8 @@ export class DashboardAreaChartComponent implements OnChanges { this.currentChartData = this.chartData; for (let index = 0; index < this.dataArray.length; index++) { this.chartData.dataset[index].data = this.formatData(this.dataArray[index]); - let currentDataValue = this.dataArray[index][this.dataArray[index].length - 1] - ? this.dataArray[index][this.dataArray[index].length - 1][1] + let currentDataValue = this.dataArray?.[index]?.[this.dataArray[index]?.length - 1] + ? this.dataArray[index][this.dataArray[index]?.length - 1][1] : 0; if (currentDataValue) { [ diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/dashboard-v3.module.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/dashboard-v3.module.ts index 73b4f9fa840fb..82843289b3834 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/dashboard-v3.module.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/dashboard-v3.module.ts @@ -38,6 +38,11 @@ import { PgSummaryPipe } from './pg-summary.pipe'; DashboardTimeSelectorComponent ], - exports: [DashboardV3Component, DashboardAreaChartComponent, DashboardTimeSelectorComponent] + exports: [ + DashboardV3Component, + DashboardAreaChartComponent, + DashboardTimeSelectorComponent, + DashboardPieComponent + ] }) export class DashboardV3Module {} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/layouts/workbench-layout/workbench-layout.component.html b/src/pybind/mgr/dashboard/frontend/src/app/core/layouts/workbench-layout/workbench-layout.component.html index 1c1846dae15ea..2b3c82bfe20be 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/core/layouts/workbench-layout/workbench-layout.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/core/layouts/workbench-layout/workbench-layout.component.html @@ -1,7 +1,7 @@
+ [ngClass]="{'dashboard': (router.url == '/dashboard' || router.url == '/dashboard_3' || router.url == '/multi-cluster/overview'), 'rgw-dashboard': (router.url == '/rgw/overview')}"> diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/multi-cluster.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/multi-cluster.service.ts index 7252e969e60ae..ffb312de4d9d7 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/multi-cluster.service.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/multi-cluster.service.ts @@ -94,7 +94,9 @@ export class MultiClusterService { password: string, token = '', hub_url = '', - clusterFsid = '' + clusterFsid = '', + ssl = false, + cert = '' ) { return this.http.post('api/multi-cluster/auth', { url, @@ -103,27 +105,46 @@ export class MultiClusterService { password, token, hub_url, - cluster_fsid: clusterFsid + cluster_fsid: clusterFsid, + ssl_verify: ssl, + ssl_certificate: cert }); } - reConnectCluster(url: any, username: string, password: string, token = '') { - return this.http.put('api/multi-cluster/reconnect_cluster', { + reConnectCluster( + url: any, + username: string, + password: string, + token = '', + ssl = false, + cert = '' + ) { + return this.http.post('api/multi-cluster/reconnect_cluster', { url, username, password, - token + token, + ssl_verify: ssl, + ssl_certificate: cert }); } - verifyConnection(url: string, username: string, password: string, token = ''): Observable { - let params = new HttpParams() - .set('url', url) - .set('username', username) - .set('password', password) - .set('token', token); - - return this.http.get('api/multi-cluster/verify_connection', { params }); + verifyConnection( + url: string, + username: string, + password: string, + token = '', + ssl = false, + cert = '' + ): Observable { + return this.http.post('api/multi-cluster/verify_connection', { + url: url, + username: username, + password: password, + token: token, + ssl_verify: ssl, + ssl_certificate: cert + }); } private getClusterObserver() { diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/prometheus.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/prometheus.service.ts index e1aa7a07cafc2..b7db0bc2f3cca 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/prometheus.service.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/prometheus.service.ts @@ -1,7 +1,7 @@ import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; -import { Observable, Subscription, timer } from 'rxjs'; +import { Observable, Subscription, forkJoin, timer } from 'rxjs'; import { map } from 'rxjs/operators'; import { AlertmanagerSilence } from '../models/alertmanager-silence'; @@ -191,4 +191,99 @@ export class PrometheusService { }; return formattedDate; } + + getMultiClusterData(params: any): any { + return this.http.get(`${this.baseURL}/prometheus_query_data`, { params }); + } + + getMultiClusterQueryRangeData(params: any): any { + return this.http.get(`${this.baseURL}/data`, { params }); + } + + getMultiClusterQueriesData(selectedTime: any, queries: any, queriesResults: any) { + return new Observable((observer) => { + this.ifPrometheusConfigured(() => { + if (this.timerGetPrometheusDataSub) { + this.timerGetPrometheusDataSub.unsubscribe(); + } + + this.timerGetPrometheusDataSub = timer(0, this.timerTime).subscribe(() => { + selectedTime = this.updateTimeStamp(selectedTime); + + const requests = []; + for (const queryName in queries) { + if (queries.hasOwnProperty(queryName)) { + const validRangeQueries1 = [ + 'CLUSTER_CAPACITY_UTILIZATION', + 'CLUSTER_IOPS_UTILIZATION', + 'CLUSTER_THROUGHPUT_UTILIZATION', + 'POOL_CAPACITY_UTILIZATION', + 'POOL_IOPS_UTILIZATION', + 'POOL_THROUGHPUT_UTILIZATION' + ]; + if (validRangeQueries1.includes(queryName)) { + const query = queries[queryName]; + const request = this.getMultiClusterQueryRangeData({ + params: encodeURIComponent(query), + start: selectedTime['start'], + end: selectedTime['end'], + step: selectedTime['step'] + }); + requests.push(request); + } else { + const query = queries[queryName]; + const request = this.getMultiClusterData({ + params: encodeURIComponent(query), + start: selectedTime['start'], + end: selectedTime['end'], + step: selectedTime['step'] + }); + requests.push(request); + } + } + } + + forkJoin(requests).subscribe( + (responses: any[]) => { + for (let i = 0; i < responses.length; i++) { + const data = responses[i]; + const queryName = Object.keys(queries)[i]; + const validQueries = [ + 'ALERTS', + 'MGR_METADATA', + 'HEALTH_STATUS', + 'TOTAL_CAPACITY', + 'USED_CAPACITY', + 'POOLS', + 'OSDS', + 'CLUSTER_CAPACITY_UTILIZATION', + 'CLUSTER_IOPS_UTILIZATION', + 'CLUSTER_THROUGHPUT_UTILIZATION', + 'POOL_CAPACITY_UTILIZATION', + 'POOL_IOPS_UTILIZATION', + 'POOL_THROUGHPUT_UTILIZATION', + 'HOSTS', + 'CLUSTER_ALERTS' + ]; + if (data.result.length) { + if (validQueries.includes(queryName)) { + queriesResults[queryName] = data.result; + } else { + queriesResults[queryName] = data.result.map( + (result: { value: any }) => result.value + ); + } + } + } + observer.next(queriesResults); + observer.complete(); + }, + (error: Error) => { + observer.error(error); + } + ); + }); + }); + }); + } } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/card-group/card-group.component.html b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/card-group/card-group.component.html new file mode 100644 index 0000000000000..04ecabfc81304 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/card-group/card-group.component.html @@ -0,0 +1,10 @@ +
+
+ {{ groupTitle }} +
+
+ +
+ +
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/card-group/card-group.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/card-group/card-group.component.scss new file mode 100644 index 0000000000000..b30e1a71c47b1 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/card-group/card-group.component.scss @@ -0,0 +1,4 @@ +.info-group-title { + font-size: 1.75rem; + margin: 0 0 0.5vw; +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/card-group/card-group.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/card-group/card-group.component.spec.ts new file mode 100644 index 0000000000000..35c7955d49403 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/card-group/card-group.component.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { CardGroupComponent } from './card-group.component'; + +describe('CardGroupComponent', () => { + let component: CardGroupComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [CardGroupComponent] + }).compileComponents(); + + fixture = TestBed.createComponent(CardGroupComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/card-group/card-group.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/card-group/card-group.component.ts new file mode 100644 index 0000000000000..c7de8caa5da83 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/card-group/card-group.component.ts @@ -0,0 +1,11 @@ +import { Component, Input } from '@angular/core'; + +@Component({ + selector: 'cd-card-group', + templateUrl: './card-group.component.html', + styleUrls: ['./card-group.component.scss'] +}) +export class CardGroupComponent { + @Input() + groupTitle = ''; +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/components.module.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/components.module.ts index 5b533f1cddb30..867ef3b16d87b 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/components.module.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/components.module.ts @@ -53,6 +53,7 @@ import { CardComponent } from './card/card.component'; import { CardRowComponent } from './card-row/card-row.component'; import { CodeBlockComponent } from './code-block/code-block.component'; import { VerticalNavigationComponent } from './vertical-navigation/vertical-navigation.component'; +import { CardGroupComponent } from './card-group/card-group.component'; @NgModule({ imports: [ @@ -109,7 +110,8 @@ import { VerticalNavigationComponent } from './vertical-navigation/vertical-navi CardComponent, CardRowComponent, CodeBlockComponent, - VerticalNavigationComponent + VerticalNavigationComponent, + CardGroupComponent ], providers: [], exports: [ @@ -143,7 +145,8 @@ import { VerticalNavigationComponent } from './vertical-navigation/vertical-navi CardComponent, CardRowComponent, CodeBlockComponent, - VerticalNavigationComponent + VerticalNavigationComponent, + CardGroupComponent ] }) export class ComponentsModule {} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/enum/dashboard-promqls.enum.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/enum/dashboard-promqls.enum.ts index 2d8aa22819dc4..9a85d108a6b56 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/enum/dashboard-promqls.enum.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/enum/dashboard-promqls.enum.ts @@ -16,3 +16,31 @@ export enum RgwPromqls { GET_BANDWIDTH = 'sum(rate(ceph_rgw_op_get_obj_bytes[1m]))', PUT_BANDWIDTH = 'sum(rate(ceph_rgw_op_put_obj_bytes[1m]))' } + +export enum MultiClusterPromqls { + ALERTS_COUNT = 'count(ALERTS{alertstate="firing"}) or vector(0)', + CLUSTER_COUNT = 'count(ceph_health_status) or vector(0)', + HEALTH_OK_COUNT = 'count(ceph_health_status==0) or vector(0)', + HEALTH_WARNING_COUNT = 'count(ceph_health_status==1) or vector(0)', + HEALTH_ERROR_COUNT = 'count(ceph_health_status==2) or vector(0)', + TOTAL_CLUSTERS_CAPACITY = 'sum(ceph_cluster_total_bytes) or vector(0)', + TOTAL_USED_CAPACITY = 'sum(ceph_cluster_by_class_total_used_bytes) or vector(0)', + HEALTH_STATUS = 'ceph_health_status', + MGR_METADATA = 'ceph_mgr_metadata', + TOTAL_CAPACITY = 'ceph_cluster_total_bytes', + USED_CAPACITY = 'ceph_cluster_total_used_bytes', + POOLS = 'count by (cluster) (ceph_pool_metadata) or vector(0)', + OSDS = 'count by (cluster) (ceph_osd_metadata) or vector(0)', + CRITICAL_ALERTS_COUNT = 'count(ALERTS{alertstate="firing",severity="critical"}) or vector(0)', + WARNING_ALERTS_COUNT = 'count(ALERTS{alertstate="firing",severity="warning"}) or vector(0)', + ALERTS = 'ALERTS{alertstate="firing"}', + HOSTS = 'sum by (hostname, cluster) (group by (hostname, cluster) (ceph_osd_metadata)) or vector(0)', + TOTAL_HOSTS = 'count by (cluster) (ceph_osd_metadata) or vector(0)', + CLUSTER_ALERTS = 'count by (cluster) (ALERTS{alertstate="firing"}) or vector(0)', + CLUSTER_CAPACITY_UTILIZATION = 'topk(2, ceph_cluster_total_used_bytes)', + CLUSTER_IOPS_UTILIZATION = 'topk(2, sum by (cluster) (rate(ceph_pool_wr[1m])) + sum by (cluster) (rate(ceph_pool_rd[1m])) )', + CLUSTER_THROUGHPUT_UTILIZATION = 'topk(2, sum by (cluster) (rate(ceph_pool_wr_bytes[1m])) + sum by (cluster) (rate(ceph_pool_rd_bytes[1m])) )', + POOL_CAPACITY_UTILIZATION = 'topk(2, ceph_pool_bytes_used/ceph_pool_max_avail * on(pool_id, cluster) group_left(instance, name) ceph_pool_metadata)', + POOL_IOPS_UTILIZATION = 'topk(2, (rate(ceph_pool_rd[1m]) + rate(ceph_pool_wr[1m])) * on(pool_id, cluster) group_left(instance, name) ceph_pool_metadata )', + POOL_THROUGHPUT_UTILIZATION = 'topk(2, (irate(ceph_pool_rd_bytes[1m]) + irate(ceph_pool_wr_bytes[1m])) * on(pool_id, cluster) group_left(instance, name) ceph_pool_metadata )' +} 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 ce4e02603f833..329aefb592eb5 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 @@ -5,4 +5,6 @@ export interface MultiCluster { token: string; cluster_alias: string; cluster_connection_status: number; + ssl_verify: boolean; + ssl_certificate: string; } diff --git a/src/pybind/mgr/dashboard/openapi.yaml b/src/pybind/mgr/dashboard/openapi.yaml index 4e58517a9636f..21f5ef86000aa 100644 --- a/src/pybind/mgr/dashboard/openapi.yaml +++ b/src/pybind/mgr/dashboard/openapi.yaml @@ -6974,6 +6974,13 @@ paths: type: string password: type: string + prometheus_api_url: + type: string + ssl_certificate: + type: string + ssl_verify: + default: false + type: boolean token: type: string url: @@ -6983,6 +6990,7 @@ paths: required: - url - cluster_alias + - username type: object responses: '201': @@ -7148,6 +7156,11 @@ paths: properties: password: type: string + ssl_certificate: + type: string + ssl_verify: + default: false + type: boolean token: type: string url: @@ -7219,34 +7232,38 @@ paths: tags: - Multi-cluster /api/multi-cluster/verify_connection: - get: - parameters: - - allowEmptyValue: true - in: query - name: url - schema: - type: string - - allowEmptyValue: true - in: query - name: username - schema: - type: string - - allowEmptyValue: true - in: query - name: password - schema: - type: string - - allowEmptyValue: true - in: query - name: token - schema: - type: string + post: + parameters: [] + requestBody: + content: + application/json: + schema: + properties: + password: + type: string + ssl_certificate: + type: string + ssl_verify: + default: false + type: boolean + token: + type: string + url: + type: string + username: + type: string + type: object responses: - '200': + '201': content: application/vnd.ceph.api.v1.0+json: type: object - description: OK + description: Resource created. + '202': + content: + application/vnd.ceph.api.v1.0+json: + type: object + description: Operation is still executing. Please check the task queue. '400': description: Operation exception. Please check the response body for details. '401': @@ -10017,6 +10034,28 @@ paths: - jwt: [] tags: - PrometheusNotifications + /api/prometheus/prometheus_query_data: + get: + parameters: [] + responses: + '200': + content: + application/vnd.ceph.api.v1.0+json: + type: object + description: OK + '400': + description: Operation exception. Please check the response body for details. + '401': + description: Unauthenticated access. Please login first. + '403': + description: Unauthorized access. Please check your permissions. + '500': + description: Unexpected error. Please check the response body for the stack + trace. + security: + - jwt: [] + tags: + - Prometheus /api/prometheus/rules: get: parameters: [] -- 2.39.5