$.dashboardSchema(
'Ceph - Multi-cluster',
'',
- 'BnxelG7Sz',
+ 'BnxelG7Sx',
'now-1h',
'30s',
22,
},
"timezone": "",
"title": "Ceph - Multi-cluster",
- "uid": "BnxelG7Sz",
+ "uid": "BnxelG7Sx",
"version": 0
}
import requests
+from .. import mgr
from ..exceptions import DashboardException
from ..security import Scope
from ..settings import Settings
@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 = {
'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),
@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
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]
"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
"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
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",
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
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}',
'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}',
@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)
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")
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: [
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 {
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();
+ }
});
});
}
children: [
{
path: 'overview',
- component: MultiClusterComponent,
- data: {
- breadcrumbs: 'Multi-Cluster/Overview'
- }
+ component: MultiClusterComponent
},
{
path: 'manage-clusters',
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: [
NgbPopoverModule,
NgbDropdownModule,
NgxPipeFunctionModule,
- NgbProgressbarModule
+ NgbProgressbarModule,
+ DashboardV3Module
],
declarations: [
HostsComponent,
i18n>Password
</label>
<div class="cd-col-form-input">
- <input id="password"
- name="password"
- class="form-control"
- type="password"
- formControlName="password">
- <span class="invalid-feedback"
- *ngIf="remoteClusterForm.showError('password', frm, 'required')"
- i18n>This field is required.
- </span>
+ <div class="input-group">
+ <input id="password"
+ name="password"
+ class="form-control"
+ type="password"
+ formControlName="password">
+ <span class="input-group-button">
+ <button type="button"
+ class="btn btn-light"
+ cdPasswordButton="password">
+ </button>
+ <cd-copy-2-clipboard-button source="password">
+ </cd-copy-2-clipboard-button>
+ </span>
+ <span class="invalid-feedback"
+ *ngIf="remoteClusterForm.showError('password', frm, 'required')"
+ i18n>This field is required.
+ </span>
+ </div>
</div>
</div>
<div class="form-group row"
</div>
</div>
</div>
+ <!-- ssl -->
+ <div class="form-group row">
+ <div class="cd-col-form-offset">
+ <div class="custom-control custom-checkbox">
+ <input class="custom-control-input"
+ id="ssl"
+ type="checkbox"
+ formControlName="ssl">
+ <label class="custom-control-label"
+ for="ssl"
+ i18n>SSL</label>
+ </div>
+ </div>
+ </div>
+
+ <!-- ssl_cert -->
+ <div *ngIf="remoteClusterForm.controls.ssl.value"
+ class="form-group row">
+ <label class="cd-col-form-label"
+ for="ssl_cert">
+ <span i18n>Certificate</span>
+ <cd-helper i18n>The SSL certificate in PEM format.</cd-helper>
+ </label>
+ <div class="cd-col-form-input">
+ <textarea id="ssl_cert"
+ class="form-control resize-vertical text-monospace text-pre"
+ formControlName="ssl_cert"
+ rows="5">
+ </textarea>
+ <input type="file"
+ (change)="fileUpload($event.target.files, 'ssl_cert')">
+ <span class="invalid-feedback"
+ *ngIf="remoteClusterForm.showError('ssl_cert', frm, 'required')"
+ i18n>This field is required.</span>
+ <span class="invalid-feedback"
+ *ngIf="remoteClusterForm.showError('ssl_cert', frm, 'pattern')"
+ i18n>Invalid SSL certificate.</span>
+ </div>
+ </div>
<div class="form-group row"
*ngIf="!showCrossOriginError && action !== 'edit' && !remoteClusterForm.getValue('showToken')">
<div class="cd-col-form-offset">
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';
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);
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',
);
})
]
+ }),
+ ssl: new FormControl(false),
+ ssl_cert: new FormControl('', {
+ validators: [
+ CdValidators.requiredIf({
+ ssl: true
+ })
+ ]
})
});
}
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(
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();
+ }
+ })
);
}
password,
token,
window.location.origin,
- clusterFsid
+ clusterFsid,
+ ssl,
+ ssl_certificate
)
.subscribe({
error: () => {
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':
toggleToken() {
this.showToken = !this.showToken;
}
+
+ fileUpload(files: FileList, controlName: string) {
+ const file: File = files[0];
+ const reader = new FileReader();
+ reader.addEventListener('load', (event: ProgressEvent<FileReader>) => {
+ const control: AbstractControl = this.remoteClusterForm.get(controlName);
+ control.setValue(event.target.result);
+ control.markAsDirty();
+ control.markAsTouched();
+ control.updateValueAndValidity();
+ });
+ reader.readAsText(file, 'utf8');
+ }
}
let-row="row">
<a target="_blank"
[href]="row.url">
- {{ row.url.endsWith('/') ? row.url.slice(0, -1) : row.url }}
+ {{ row?.url?.endsWith('/') ? row?.url?.slice(0, -1) : row.url }}
<i class="fa fa-external-link"></i>
</a>
</ng-template>
[ngClass]="icons.wrench">
</i>
<div class="mt-4 text-center">
- <h3><b>Connect Cluster </b></h3>
+ <h3 class="fw-bold">Connect Cluster</h3>
<h4 class="mt-3">Upgrade your current cluster to a multi-cluster setup effortlessly.
Click on the "Connect Cluster" button to begin the process.</h4>
</div>
</div>
</ng-template>
-<div class="container-fluid h-100 p-4">
- <div *ngIf="dashboardClustersMap?.size === 1">
- <ng-container *ngTemplateOutlet="emptyCluster"></ng-container>
- </div>
-
- <span *ngIf="loading"
- class="d-flex justify-content-center">
- <i [ngClass]="[icons.large3x, icons.spinner, icons.spin]"></i>
- </span>
- <div *ngIf="dashboardClustersMap?.size > 1">
- <div *ngIf="!loading">
+<ng-template #loadingTpl>
+ <div class="container h-75">
+ <div class="row h-100 justify-content-center align-items-center">
+ <div class="blank-page">
+ <i class="mx-auto d-block"
+ [ngClass]="[icons.large3x, icons.spinner, icons.spin]">
+ </i>
+ </div>
</div>
</div>
+</ng-template>
+
+<div class="container-fluid h-100 p-4"
+ *ngIf="isMultiCluster; else emptyCluster">
+ <ng-container *ngIf="!loading; else loadingTpl">
+ <cd-card-group>
+ <div class="col-lg-4">
+ <div class="row">
+ <cd-card cardTitle="Clusters"
+ i18n-title
+ class="col-sm-6 m-0 p-0 ps-4 pe-2"
+ aria-label="Clusters"
+ [fullHeight]="true"
+ *ngIf="queriesResults.CLUSTER_COUNT && queriesResults.CLUSTER_COUNT[0]">
+ <span class="text-center">
+ <h3 *ngIf="queriesResults['HEALTH_ERROR_COUNT'][0][1] === '0' && queriesResults['HEALTH_WARNING_COUNT'][0][1] === '0'">{{ queriesResults.CLUSTER_COUNT[0][1] }}</h3>
+ <h3 class="text-danger"
+ *ngIf="queriesResults.HEALTH_ERROR_COUNT[0][1] !== '0'">
+ <i [ngClass]="icons.danger"></i>
+ {{ queriesResults.HEALTH_ERROR_COUNT[0][1] }}
+ </h3>
+ <h3 class="text-warning"
+ *ngIf="queriesResults.HEALTH_WARNING_COUNT[0][1] !== '0'">
+ <i [ngClass]="icons.warning"></i>
+ {{ queriesResults.HEALTH_WARNING_COUNT[0][1] }}
+ </h3>
+ </span>
+ </cd-card>
+ <cd-card cardTitle="Alerts"
+ i18n-title
+ class="col-sm-6 m-0 p-0 ps-2 pe-2"
+ aria-label="Alerts"
+ *ngIf="queriesResults['ALERTS_COUNT'] && queriesResults['ALERTS_COUNT'][0]">
+ <span class="text-center">
+ <h3 *ngIf="queriesResults['CRITICAL_ALERTS_COUNT'][0][1] === '0' && queriesResults['WARNING_ALERTS_COUNT'][0][1] === '0'">
+ {{ queriesResults['ALERTS_COUNT'][0][1] }}
+ </h3>
+ <h3 class="text-danger"
+ *ngIf="queriesResults['CRITICAL_ALERTS_COUNT'][0][1] !== '0'">
+ <i [ngClass]="icons.danger"></i>
+ {{ queriesResults['CRITICAL_ALERTS_COUNT'][0][1] }}
+ </h3>
+ <h3 class="text-warning"
+ *ngIf="queriesResults['WARNING_ALERTS_COUNT'][0][1] !== '0'">
+ <i [ngClass]="icons.warning"></i>
+ {{ queriesResults['WARNING_ALERTS_COUNT'][0][1] }}
+ </h3>
+ </span>
+ </cd-card>
+ </div>
+ <div class="row pt-3">
+ <cd-card cardTitle="Connection Errors"
+ i18n-title
+ class="col-sm-6 m-0 p-0 ps-4 pe-2"
+ aria-label="Connection Errors">
+ <span class="text-center">
+ <h3 [ngClass]="{'text-danger': connectionErrorsCount > 0}">
+ <i [ngClass]="icons.danger"
+ *ngIf="connectionErrorsCount > 0"></i>
+ {{ connectionErrorsCount }}
+ </h3>
+ </span>
+ </cd-card>
+ <cd-card cardTitle="Hosts"
+ i18n-title
+ class="col-sm-6 m-0 p-0 ps-2 pe-2"
+ aria-label="Total number of hosts"
+ *ngIf="queriesResults['TOTAL_HOSTS'][0][1] !== '0'">
+ <span class="text-center">
+ <h3>{{ queriesResults['TOTAL_HOSTS'][0][1] }}</h3>
+ </span>
+ </cd-card>
+ </div>
+
+ <div class="row pt-3">
+ <cd-card cardTitle="Capacity"
+ i18n-title
+ class="col-sm-12 m-0 p-0 ps-4 pe-2"
+ aria-label="Capacity card"
+ *ngIf="queriesResults['TOTAL_CLUSTERS_CAPACITY'] && queriesResults['TOTAL_CLUSTERS_CAPACITY'][0] && queriesResults['TOTAL_USED_CAPACITY'] && queriesResults['TOTAL_USED_CAPACITY'][0]">
+ <ng-container class="ms-4 me-4">
+ <cd-dashboard-pie [data]="{max: queriesResults['TOTAL_CLUSTERS_CAPACITY'][0][1], current: queriesResults['TOTAL_USED_CAPACITY'][0][1]}"
+ lowThreshold=".95"
+ highThreshold=".99">
+ </cd-dashboard-pie>
+ </ng-container>
+ </cd-card>
+ </div>
+ </div>
+
+ <div class="col-sm-8 ps-2">
+ <cd-card [cardTitle]="'Top ' + COUNT_OF_UTILIZATION_CHARTS + ' Cluster Utilization'"
+ i18n-title
+ [fullHeight]="true"
+ aria-label="Cluster Utilization card"
+ *ngIf="clusters">
+ <div class="ms-4 me-4 mt-0">
+ <cd-dashboard-time-selector (selectedTime)="getPrometheusData($event)">
+ </cd-dashboard-time-selector>
+ <cd-dashboard-area-chart chartTitle="Capacity"
+ [labelsArray]="capacityLabels"
+ dataUnits="B"
+ [dataArray]="capacityValues"
+ [truncateLabel]="true"
+ *ngIf="capacityLabels && capacityValues">
+ </cd-dashboard-area-chart>
+ <cd-dashboard-area-chart chartTitle="IOPS"
+ [labelsArray]="iopsLabels"
+ dataUnits=""
+ decimals="0"
+ [dataArray]="iopsValues"
+ [truncateLabel]="true"
+ *ngIf="iopsLabels && iopsValues">
+ </cd-dashboard-area-chart>
+ <cd-dashboard-area-chart chartTitle="Throughput"
+ [labelsArray]="throughputLabels"
+ dataUnits="B/s"
+ decimals="2"
+ [dataArray]="throughputValues"
+ [truncateLabel]="true"
+ *ngIf="throughputLabels && throughputLabels">
+ </cd-dashboard-area-chart>
+ </div>
+ </cd-card>
+ </div>
+ </cd-card-group>
+
+ <cd-card-group>
+ <div class="col-lg-12 mt-3 m-0 p-0 ps-4 pe-4">
+ <cd-table [data]="clusters"
+ [columns]="columns"
+ [limit]="5"
+ *ngIf="clusters">
+ </cd-table>
+ </div>
+ </cd-card-group>
+
+ <cd-card-group>
+ <div class="col-lg-12 mb-4 m-0 p-0 ps-4 pe-4">
+ <div class="row">
+ <cd-card [cardTitle]="'Top ' + COUNT_OF_UTILIZATION_CHARTS + ' Pools Utilization'"
+ i18n-title
+ aria-label="Pools Utilization card"
+ *ngIf="clusters">
+ <div class="ms-4 me-4 mt-0">
+ <cd-dashboard-time-selector (selectedTime)="getPrometheusData($event)">
+ </cd-dashboard-time-selector>
+ <cd-dashboard-area-chart chartTitle="Capacity"
+ [labelsArray]="poolCapacityLabels"
+ dataUnits="B"
+ [dataArray]="poolCapacityValues"
+ *ngIf="poolCapacityLabels && poolCapacityValues"
+ [truncateLabel]="true">
+ </cd-dashboard-area-chart>
+ <cd-dashboard-area-chart chartTitle="IOPS"
+ [labelsArray]="poolIOPSLabels"
+ dataUnits=""
+ decimals="0"
+ [dataArray]="poolIOPSValues"
+ *ngIf="poolIOPSLabels && poolIOPSValues"
+ [truncateLabel]="true">
+ </cd-dashboard-area-chart>
+ <cd-dashboard-area-chart chartTitle="Client Throughput"
+ [labelsArray]="poolThroughputLabels"
+ dataUnits="B/s"
+ decimals="2"
+ [dataArray]="poolThroughputValues"
+ *ngIf="poolThroughputLabels && poolThroughputValues"
+ [truncateLabel]="true">
+ </cd-dashboard-area-chart>
+ </div>
+ </cd-card>
+ </div>
+ </div>
+ </cd-card-group>
+ </ng-container>
</div>
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;
beforeEach(async () => {
await TestBed.configureTestingModule({
- imports: [HttpClientTestingModule],
+ imports: [HttpClientTestingModule, SharedModule],
declarations: [MultiClusterComponent],
- providers: [NgbActiveModal]
+ providers: [NgbActiveModal, DimlessBinaryPipe]
}).compileComponents();
fixture = TestBed.createComponent(MultiClusterComponent);
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',
styleUrls: ['./multi-cluster.component.scss']
})
export class MultiClusterComponent implements OnInit {
+ COUNT_OF_UTILIZATION_CHARTS = 5;
+
@ViewChild('nameTpl', { static: true })
nameTpl: any;
+ columns: Array<CdTableColumn> = [];
+
+ 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<string, string> = new Map<string, string>();
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() {
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;
}
}
<div class="box"
[style.background-color]="data.pointBackgroundColor">
</div>
- <div *ngIf="!chartTitle.includes(data.label)">{{ data.label }}:</div>
- {{ data?.currentData || 'N/A' }} {{ data?.currentDataUnits }}
+ <ng-container *ngIf="!chartTitle.includes(data.label)">
+ <span [ngClass]="{'d-inline-block text-truncate': truncateLabel}"
+ [ngStyle]="{'width': truncateLabel ? '10rem' : 'auto'}"
+ [title]="data.label">{{ data.label }}</span>:
+ </ng-container>
+ <span>{{ data?.currentData || 'N/A' }} {{ data?.currentDataUnits }}</span>
<div *ngIf="maxValue && data.currentData">
used of {{ maxConvertedValue }} {{ maxConvertedValueUnits }}
</div>
labelsArray?: string[] = []; // Array of chart labels
@Input()
decimals?: number = 1;
+ @Input()
+ truncateLabel = false;
currentDataUnits: string;
currentData: number;
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) {
[
DashboardTimeSelectorComponent
],
- exports: [DashboardV3Component, DashboardAreaChartComponent, DashboardTimeSelectorComponent]
+ exports: [
+ DashboardV3Component,
+ DashboardAreaChartComponent,
+ DashboardTimeSelectorComponent,
+ DashboardPieComponent
+ ]
})
export class DashboardV3Module {}
<block-ui>
<cd-navigation>
<div class="container-fluid h-100"
- [ngClass]="{'dashboard': (router.url == '/dashboard' || router.url == '/dashboard_3' || router.url == '/multi-cluster'), 'rgw-dashboard': (router.url == '/rgw/overview')}">
+ [ngClass]="{'dashboard': (router.url == '/dashboard' || router.url == '/dashboard_3' || router.url == '/multi-cluster/overview'), 'rgw-dashboard': (router.url == '/rgw/overview')}">
<cd-context></cd-context>
<cd-breadcrumbs></cd-breadcrumbs>
<router-outlet></router-outlet>
password: string,
token = '',
hub_url = '',
- clusterFsid = ''
+ clusterFsid = '',
+ ssl = false,
+ cert = ''
) {
return this.http.post('api/multi-cluster/auth', {
url,
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<any> {
- 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<any> {
+ 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() {
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';
};
return formattedDate;
}
+
+ getMultiClusterData(params: any): any {
+ return this.http.get<any>(`${this.baseURL}/prometheus_query_data`, { params });
+ }
+
+ getMultiClusterQueryRangeData(params: any): any {
+ return this.http.get<any>(`${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);
+ }
+ );
+ });
+ });
+ });
+ }
}
--- /dev/null
+<div class="row"
+ *ngIf="groupTitle">
+ <div class="info-group-title">
+ <span i18n>{{ groupTitle }}</span>
+ </div>
+</div>
+
+<div class="row">
+ <ng-content></ng-content>
+</div>
--- /dev/null
+.info-group-title {
+ font-size: 1.75rem;
+ margin: 0 0 0.5vw;
+}
--- /dev/null
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { CardGroupComponent } from './card-group.component';
+
+describe('CardGroupComponent', () => {
+ let component: CardGroupComponent;
+ let fixture: ComponentFixture<CardGroupComponent>;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ declarations: [CardGroupComponent]
+ }).compileComponents();
+
+ fixture = TestBed.createComponent(CardGroupComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
--- /dev/null
+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 = '';
+}
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: [
CardComponent,
CardRowComponent,
CodeBlockComponent,
- VerticalNavigationComponent
+ VerticalNavigationComponent,
+ CardGroupComponent
],
providers: [],
exports: [
CardComponent,
CardRowComponent,
CodeBlockComponent,
- VerticalNavigationComponent
+ VerticalNavigationComponent,
+ CardGroupComponent
]
})
export class ComponentsModule {}
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 )'
+}
token: string;
cluster_alias: string;
cluster_connection_status: number;
+ ssl_verify: boolean;
+ ssl_certificate: string;
}
type: string
password:
type: string
+ prometheus_api_url:
+ type: string
+ ssl_certificate:
+ type: string
+ ssl_verify:
+ default: false
+ type: boolean
token:
type: string
url:
required:
- url
- cluster_alias
+ - username
type: object
responses:
'201':
properties:
password:
type: string
+ ssl_certificate:
+ type: string
+ ssl_verify:
+ default: false
+ type: boolean
token:
type: string
url:
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':
- 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: []