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):
+ try:
+ hub_fsid = mgr.get('config')['fsid']
+ except KeyError:
+ hub_fsid = ''
+
if password:
payload = {
'username': username,
'password': password
}
- content = self._proxy('POST', url, 'api/auth', payload=payload)
- if 'token' not in content:
- raise DashboardException(
- "Could not authenticate to remote cluster",
- http_status_code=400,
- component='dashboard')
-
- cluster_token = content['token']
+ cluster_token = self.check_cluster_connection(url, payload, username,
+ ssl_verify, ssl_certificate)
self._proxy('PUT', url, 'ui-api/multi-cluster/set_cors_endpoint',
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)
+ managed_by_clusters_content = self._proxy('GET', url,
+ 'api/settings/MANAGED_BY_CLUSTERS',
+ token=cluster_token)
+
+ managed_by_clusters_config = managed_by_clusters_content['value']
+
+ if managed_by_clusters_config is not None:
+ managed_by_clusters_config.append({'url': hub_url, 'fsid': hub_fsid})
+
+ self._proxy('PUT', url, 'api/settings/MANAGED_BY_CLUSTERS',
+ payload={'value': managed_by_clusters_config}, token=cluster_token,
+ verify=ssl_verify, cert=ssl_certificate)
+
# add prometheus targets
prometheus_url = self._proxy('GET', url, 'api/settings/PROMETHEUS_API_HOST',
token=cluster_token)
+
_set_prometheus_targets(prometheus_url['value'])
self.set_multi_cluster_config(fsid, username, url, cluster_alias,
cluster_token, prometheus_url['value'],
ssl_verify, ssl_certificate)
- return
+ return True
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)
+ return True
+
+ def check_cluster_connection(self, url, payload, username, ssl_verify, ssl_certificate):
+ try:
+ content = self._proxy('POST', url, 'api/auth', payload=payload,
+ verify=ssl_verify, cert=ssl_certificate)
+ if 'token' not in content:
+ raise DashboardException(msg=content['detail'], code='invalid_credentials',
+ component='multi-cluster')
+
+ user_content = self._proxy('GET', url, f'api/user/{username}',
+ token=content['token'])
+
+ if 'status' in user_content and user_content['status'] == '403 Forbidden':
+ raise DashboardException(msg='User is not an administrator',
+ code='invalid_permission', component='multi-cluster')
+ if 'roles' in user_content and 'administrator' not in user_content['roles']:
+ raise DashboardException(msg='User is not an administrator',
+ code='invalid_permission', component='multi-cluster')
+
+ except Exception as e:
+ if '[Errno 111] Connection refused' in str(e):
+ raise DashboardException(msg='Connection refused',
+ code='connection_refused', component='multi-cluster')
+ raise DashboardException(msg=str(e), code='connection_failed',
+ component='multi-cluster')
+
+ cluster_token = content['token']
+
+ managed_by_clusters_content = self._proxy('GET', url, 'api/settings/MANAGED_BY_CLUSTERS',
+ token=cluster_token)
+
+ managed_by_clusters_config = managed_by_clusters_content['value']
+
+ if len(managed_by_clusters_config) > 1:
+ raise DashboardException(msg='Cluster is already managed by another cluster',
+ code='cluster_managed_by_another_cluster',
+ component='multi-cluster')
+ return cluster_token
def set_multi_cluster_config(self, fsid, username, url, cluster_alias, token,
prometheus_url=None, ssl_verify=False, ssl_certificate=None):
@Endpoint('PUT')
@UpdatePermission
- # pylint: disable=unused-variable
+ # pylint: disable=W0613
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()
'username': username,
'password': password
}
- 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",
- http_status_code=400,
- component='dashboard')
- token = content['token']
+ cluster_token = self.check_cluster_connection(url, payload, username,
+ ssl_verify, ssl_certificate)
- if username and token:
+ if username and cluster_token:
if "config" in multicluster_config:
for _, cluster_details in multicluster_config["config"].items():
for cluster in cluster_details:
if cluster["url"] == url and cluster["user"] == username:
- cluster['token'] = token
+ cluster['token'] = cluster_token
Settings.MULTICLUSTER_CONFIG = multicluster_config
- return Settings.MULTICLUSTER_CONFIG
+ return True
@Endpoint('PUT')
@UpdatePermission
@DeletePermission
def delete_cluster(self, cluster_name, cluster_user):
multicluster_config = self.load_multi_cluster_config()
+ try:
+ hub_fsid = mgr.get('config')['fsid']
+ except KeyError:
+ hub_fsid = ''
if "config" in multicluster_config:
for key, value in list(multicluster_config['config'].items()):
if value[0]['name'] == cluster_name and value[0]['user'] == cluster_user:
-
+ cluster_url = value[0]['url']
+ cluster_token = value[0]['token']
+ cluster_ssl_certificate = value[0]['ssl_certificate']
+ cluster_ssl_verify = value[0]['ssl_verify']
orch_backend = mgr.get_module_option_ex('orchestrator', 'orchestrator')
try:
if orch_backend == 'cephadm':
except KeyError:
pass
+ managed_by_clusters_content = self._proxy('GET', cluster_url,
+ 'api/settings/MANAGED_BY_CLUSTERS',
+ token=cluster_token)
+
+ managed_by_clusters_config = managed_by_clusters_content['value']
+ for cluster in managed_by_clusters_config:
+ if cluster['fsid'] == hub_fsid:
+ managed_by_clusters_config.remove(cluster)
+
+ self._proxy('PUT', cluster_url, 'api/settings/MANAGED_BY_CLUSTERS',
+ payload={'value': managed_by_clusters_config}, token=cluster_token,
+ verify=cluster_ssl_verify, cert=cluster_ssl_certificate)
+
del multicluster_config['config'][key]
break
Settings.MULTICLUSTER_CONFIG = multicluster_config
return Settings.MULTICLUSTER_CONFIG
- @Endpoint('POST')
- @CreatePermission
- # pylint: disable=R0911
- 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,
- verify=ssl_verify, cert=ssl_certificate)
- if 'permissions' not in content:
- return content['detail']
- user_content = self._proxy('GET', url, f'api/user/{username}',
- token=content['token'])
- if 'status' in user_content and user_content['status'] == '403 Forbidden':
- return 'User is not an administrator'
- except Exception as e: # pylint: disable=broad-except
- if '[Errno 111] Connection refused' in str(e):
- return 'Connection refused'
- return 'Connection failed'
-
- if username and password:
- try:
- payload = {
- 'username': username,
- 'password': password
- }
- 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}',
- token=content['token'])
- if 'status' in user_content and user_content['status'] == '403 Forbidden':
- return 'User is not an administrator'
- except Exception as e: # pylint: disable=broad-except
- if '[Errno 111] Connection refused' in str(e):
- return 'Connection refused'
- return 'Connection failed'
- return 'Connection successful'
-
@Endpoint()
@ReadPermission
def get_config(self):
</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') && !connectionVerified">
- <div class="cd-col-form-offset">
- <div class="custom-control">
- <button class="btn btn-primary"
- type="button"
- [disabled]="(remoteClusterForm.getValue('showToken') && remoteClusterForm.getValue('apiToken') === '') || (!remoteClusterForm.getValue('showToken') && (remoteClusterForm.getValue('username') === '' || remoteClusterForm.getValue('password') === ''))"
- (click)="verifyConnection()">
- Verify Connection
- </button>
- </div>
+ <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>
this.subs.unsubscribe();
}
+ handleError(error: any): void {
+ if (error.error.code === 'connection_refused') {
+ this.connectionVerified = false;
+ this.showCrossOriginError = true;
+ this.connectionMessage = error.error.detail;
+ this.crossOriginCmd = `ceph config set mgr mgr/dashboard/cross_origin_url ${window.location.origin} `;
+ } else {
+ this.connectionVerified = false;
+ this.connectionMessage = error.error.detail;
+ }
+ this.remoteClusterForm.setErrors({ cdSubmitButton: true });
+ this.notificationService.show(
+ NotificationType.error,
+ $localize`Connection to the cluster failed`
+ );
+ }
+
+ handleSuccess(message?: string): void {
+ this.notificationService.show(NotificationType.success, message);
+ this.submitAction.emit();
+ this.activeModal.close();
+ }
+
onSubmit() {
const url = this.remoteClusterForm.getValue('remoteClusterUrl');
const updatedUrl = url.endsWith('/') ? url.slice(0, -1) : url;
const clusterAlias = this.remoteClusterForm.getValue('clusterAlias');
- const prometheusApiUrl = this.remoteClusterForm.getValue('prometheusApiUrl');
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();
- if (this.action === 'edit') {
- this.subs.add(
- this.multiClusterService
- .editCluster(this.cluster.url, clusterAlias, this.cluster.user)
- .subscribe({
- error: () => {
- this.remoteClusterForm.setErrors({ cdSubmitButton: true });
- },
- complete: () => {
- this.notificationService.show(
- NotificationType.success,
- $localize`Cluster updated successfully`
- );
- this.submitAction.emit();
- this.activeModal.close();
- }
- })
- );
- }
+ const commonSubscribtion = {
+ error: (error: any) => this.handleError(error),
+ next: (response: any) => {
+ if (response === true) {
+ this.handleSuccess($localize`Cluster connected successfully`);
+ }
+ }
+ };
- if (this.action === 'reconnect') {
- this.subs.add(
- 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();
- }
- })
- );
+ switch (this.action) {
+ case 'edit':
+ this.subs.add(
+ this.multiClusterService
+ .editCluster(this.cluster.url, clusterAlias, this.cluster.user)
+ .subscribe({
+ ...commonSubscribtion,
+ complete: () => this.handleSuccess($localize`Cluster updated successfully`)
+ })
+ );
+ break;
+ case 'reconnect':
+ this.subs.add(
+ this.multiClusterService
+ .reConnectCluster(updatedUrl, username, password, token, ssl, ssl_certificate)
+ .subscribe(commonSubscribtion)
+ );
+ break;
+ case 'connect':
+ this.subs.add(
+ this.multiClusterService
+ .addCluster(
+ updatedUrl,
+ clusterAlias,
+ username,
+ password,
+ token,
+ window.location.origin,
+ clusterFsid,
+ ssl,
+ ssl_certificate
+ )
+ .subscribe(commonSubscribtion)
+ );
+ break;
+ default:
+ break;
}
-
- if (this.action === 'connect') {
- this.subs.add(
- this.multiClusterService
- .addCluster(
- updatedUrl,
- clusterAlias,
- username,
- password,
- token,
- window.location.origin,
- clusterFsid,
- prometheusApiUrl,
- ssl,
- ssl_certificate
- )
- .subscribe({
- error: () => {
- this.remoteClusterForm.setErrors({ cdSubmitButton: true });
- },
- complete: () => {
- this.notificationService.show(
- NotificationType.success,
- $localize`Cluster connected successfully`
- );
- this.submitAction.emit();
- this.activeModal.close();
- }
- })
- );
- }
- }
-
- verifyConnection() {
- const url = this.remoteClusterForm.getValue('remoteClusterUrl');
- 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, ssl, ssl_certificate)
- .subscribe((resp: string) => {
- switch (resp) {
- case 'Connection successful':
- this.connectionVerified = true;
- this.connectionMessage = 'Connection Verified Successfully';
- this.notificationService.show(
- NotificationType.success,
- $localize`Connection Verified Successfully`
- );
- break;
-
- case 'Connection refused':
- this.connectionVerified = false;
- this.showCrossOriginError = true;
- this.connectionMessage = resp;
- this.notificationService.show(
- NotificationType.error,
- $localize`Connection to the cluster failed`
- );
- break;
-
- default:
- this.connectionVerified = false;
- this.connectionMessage = resp;
- this.notificationService.show(
- NotificationType.error,
- $localize`Connection to the cluster failed`
- );
- break;
- }
- })
- );
}
toggleToken() {
-<nav ngbNav
- #nav="ngbNav"
- class="nav-tabs">
- <ng-container ngbNavItem>
- <a ngbNavLink
- i18n>Clusters List</a>
- <ng-template ngbNavContent>
- <cd-table #table
- [data]="data"
- [columns]="columns"
- columnMode="flex"
- selectionType="single"
- [maxLimit]="25"
- (updateSelection)="updateSelection($event)">
- <div class="table-actions btn-toolbar">
- <cd-table-actions [permission]="permissions.user"
- [selection]="selection"
- class="btn-group"
- id="cluster-actions"
- [tableActions]="tableActions">
- </cd-table-actions>
- </div>
- </cd-table>
- </ng-template>
+<ng-template #emptyCluster>
+ <ng-container class="container h-75"
+ *ngIf="managedByConfig$ | async as managedByConfig">
+ <div class="row h-100 justify-content-center align-items-center">
+ <div class="blank-page">
+ <i class="mx-auto d-block"
+ [ngClass]="[icons.large, icons.wrench]">
+ </i>
+ <div class="mt-4 text-center">
+ <h4 class="mt-3">This cluster is already managed by cluster -
+ <a target="_blank"
+ [href]="managedByConfig['MANAGED_BY_CLUSTERS'][0]['url']">
+ {{ managedByConfig['MANAGED_BY_CLUSTERS'][0]['fsid'] }}
+ <i class="fa fa-external-link"></i>
+ </a>
+ </h4>
+ </div>
+ </div>
+ </div>
</ng-container>
-</nav>
+</ng-template>
+
+<ng-container *ngIf="managedByConfig$ | async as managedByConfig">
+ <div *ngIf="managedByConfig['MANAGED_BY_CLUSTERS'].length === 0; else emptyCluster">
+ <nav ngbNav
+ #nav="ngbNav"
+ class="nav-tabs">
+ <ng-container ngbNavItem>
+ <a ngbNavLink
+ i18n>Clusters List</a>
+ <ng-template ngbNavContent>
+ <cd-table #table
+ [data]="data"
+ [columns]="columns"
+ columnMode="flex"
+ selectionType="single"
+ [maxLimit]="25"
+ (updateSelection)="updateSelection($event)">
+ <div class="table-actions btn-toolbar">
+ <cd-table-actions [permission]="permissions.user"
+ [selection]="selection"
+ class="btn-group"
+ id="cluster-actions"
+ [tableActions]="tableActions">
+ </cd-table-actions>
+ </div>
+ </cd-table>
+ </ng-template>
+ </ng-container>
+ </nav>
+ <div [ngbNavOutlet]="nav"></div>
+ </div>
+</ng-container>
<ng-template #urlTpl
let-row="row">
</a>
</ng-template>
-<div [ngbNavOutlet]="nav"></div>
+@use '../../../../../styles/vendor/variables' as vv;
+
+.fa-wrench {
+ color: vv.$info;
+ font-size: 6em;
+}
-import { Component, TemplateRef, ViewChild } from '@angular/core';
+import { Component, OnDestroy, OnInit, TemplateRef, ViewChild } from '@angular/core';
import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap';
import { MultiClusterService } from '~/app/shared/api/multi-cluster.service';
import { ActionLabelsI18n } from '~/app/shared/constants/app.constants';
import { MultiCluster } from '~/app/shared/models/multi-cluster';
import { Router } from '@angular/router';
import { CookiesService } from '~/app/shared/services/cookie.service';
+import { Observable, Subscription } from 'rxjs';
+import { SettingsService } from '~/app/shared/api/settings.service';
@Component({
selector: 'cd-multi-cluster-list',
templateUrl: './multi-cluster-list.component.html',
styleUrls: ['./multi-cluster-list.component.scss']
})
-export class MultiClusterListComponent {
+export class MultiClusterListComponent implements OnInit, OnDestroy {
@ViewChild(TableComponent)
table: TableComponent;
@ViewChild('urlTpl', { static: true })
public urlTpl: TemplateRef<any>;
+ private subs = new Subscription();
permissions: Permissions;
tableActions: CdTableAction[];
clusterTokenStatus: object = {};
modalRef: NgbModalRef;
hubUrl: string;
currentUrl: string;
+ icons = Icons;
+ managedByConfig$: Observable<any>;
constructor(
private multiClusterService: MultiClusterService,
+ private settingsService: SettingsService,
private router: Router,
public actionLabels: ActionLabelsI18n,
private notificationService: NotificationService,
}
ngOnInit(): void {
- this.multiClusterService.subscribe((resp: object) => {
- if (resp && resp['config']) {
- this.hubUrl = resp['hub_url'];
- this.currentUrl = resp['current_url'];
- const clusterDetailsArray = Object.values(resp['config']).flat();
- this.data = clusterDetailsArray;
- this.checkClusterConnectionStatus();
- }
- });
+ this.subs.add(
+ this.multiClusterService.subscribe((resp: object) => {
+ if (resp && resp['config']) {
+ const clusterDetailsArray = Object.values(resp['config']).flat();
+ this.data = clusterDetailsArray;
+ this.checkClusterConnectionStatus();
+ }
+ })
+ );
+
+ this.managedByConfig$ = this.settingsService.getValues('MANAGED_BY_CLUSTERS');
this.columns = [
{
}
];
- this.multiClusterService.subscribeClusterTokenStatus((resp: object) => {
- this.clusterTokenStatus = resp;
- this.checkClusterConnectionStatus();
- });
+ this.subs.add(
+ this.multiClusterService.subscribeClusterTokenStatus((resp: object) => {
+ this.clusterTokenStatus = resp;
+ this.checkClusterConnectionStatus();
+ })
+ );
+ }
+
+ ngOnDestroy() {
+ this.subs.unsubscribe();
}
checkClusterConnectionStatus() {
<ng-template #emptyCluster>
- <div class="container h-75">
+ <ng-container class="container h-75"
+ *ngIf="managedByConfig$ | async as managedByConfig">
<div class="row h-100 justify-content-center align-items-center">
<div class="blank-page">
<i class="mx-auto d-block"
- [ngClass]="icons.wrench">
+ [ngClass]="[icons.large, icons.wrench]">
</i>
- <div class="mt-4 text-center">
+ <div class="mt-4 text-center"
+ *ngIf="managedByConfig['MANAGED_BY_CLUSTERS'].length === 0">
<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 class="mt-4">
+ <div class="mt-4 text-center"
+ *ngIf="managedByConfig['MANAGED_BY_CLUSTERS'].length > 0">
+ <h4 class="mt-3">This cluster is already managed by cluster -
+ <a target="_blank"
+ [href]="managedByConfig['MANAGED_BY_CLUSTERS'][0]['url']">
+ {{ managedByConfig['MANAGED_BY_CLUSTERS'][0]['fsid'] }}
+ <i class="fa fa-external-link"></i>
+ </a>
+ </h4>
+ </div>
+ <div class="mt-4"
+ *ngIf="managedByConfig['MANAGED_BY_CLUSTERS'].length === 0">
<div class="text-center">
<button class="btn btn-primary"
(click)="openRemoteClusterInfoModal()">
</div>
</div>
</div>
- </div>
+ </ng-container>
</ng-template>
<ng-template #nametpl>
</div>
</ng-template>
-<div class="container-fluid h-100 p-4"
- *ngIf="isMultiCluster; else emptyCluster">
- <ng-container *ngIf="!loading; else loadingTpl">
- <cd-alert-panel type="info"
- spacingClass="mb-3"
- [showTitle]="false"
- size="slim"
- *ngIf="showDeletionMessage"
- (dismissed)="onDismissed()"
- [dismissible]="true"
- i18n>
- <p>Please note that the data for the disconnected cluster will be visible for a duration of ~ 5 minutes. After this period, it will be automatically removed.</p>
- </cd-alert-panel>
- <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'] && queriesResults['TOTAL_HOSTS'][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, 'clusterUtilization')">
- </cd-dashboard-time-selector>
- <cd-dashboard-area-chart chartTitle="Capacity"
- [labelsArray]="capacityLabels"
- isMultiCluster="true"
- dataUnits="B"
- [dataArray]="capacityValues"
- [truncateLabel]="true"
- *ngIf="capacityLabels && capacityValues">
- </cd-dashboard-area-chart>
- <cd-dashboard-area-chart chartTitle="IOPS"
- [labelsArray]="iopsLabels"
- dataUnits=""
- decimals="0"
- isMultiCluster="true"
- [dataArray]="iopsValues"
- [truncateLabel]="true"
- *ngIf="iopsLabels && iopsValues">
- </cd-dashboard-area-chart>
- <cd-dashboard-area-chart chartTitle="Throughput"
- [labelsArray]="throughputLabels"
- dataUnits="B/s"
- decimals="2"
- isMultiCluster="true"
- [dataArray]="throughputValues"
- [truncateLabel]="true"
- *ngIf="throughputLabels && throughputLabels">
- </cd-dashboard-area-chart>
+<ng-container *ngIf="managedByConfig$ | async as managedByConfig">
+ <div class="container-fluid h-100 p-4"
+ *ngIf="isMultiCluster && managedByConfig['MANAGED_BY_CLUSTERS'].length === 0; else emptyCluster">
+ <ng-container *ngIf="!loading; else loadingTpl">
+ <cd-alert-panel type="info"
+ spacingClass="mb-3"
+ [showTitle]="false"
+ size="slim"
+ *ngIf="showDeletionMessage"
+ (dismissed)="onDismissed()"
+ [dismissible]="true"
+ i18n>
+ <p>Please note that the data for the disconnected cluster will be visible for a duration of ~ 5 minutes. After this period, it will be automatically removed.</p>
+ </cd-alert-panel>
+ <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'] && queriesResults['TOTAL_HOSTS'][0]">
+ <span class="text-center">
+ <h3>{{ queriesResults['TOTAL_HOSTS'][0][1] }}</h3>
+ </span>
+ </cd-card>
</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>
+ <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>
- <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'"
+ <div class="col-sm-8 ps-2">
+ <cd-card [cardTitle]="'Top ' + COUNT_OF_UTILIZATION_CHARTS + ' Cluster Utilization'"
i18n-title
- aria-label="Pools Utilization card"
+ [fullHeight]="true"
+ aria-label="Cluster Utilization card"
*ngIf="clusters">
<div class="ms-4 me-4 mt-0">
- <cd-dashboard-time-selector (selectedTime)="getPrometheusData($event, 'poolUtilization')">
+ <cd-dashboard-time-selector (selectedTime)="getPrometheusData($event, 'clusterUtilization')">
</cd-dashboard-time-selector>
<cd-dashboard-area-chart chartTitle="Capacity"
- [labelsArray]="poolCapacityLabels"
- dataUnits="B"
+ [labelsArray]="capacityLabels"
isMultiCluster="true"
- [dataArray]="poolCapacityValues"
- *ngIf="poolCapacityLabels && poolCapacityValues"
- [truncateLabel]="true">
+ dataUnits="B"
+ [dataArray]="capacityValues"
+ [truncateLabel]="true"
+ *ngIf="capacityLabels && capacityValues">
</cd-dashboard-area-chart>
<cd-dashboard-area-chart chartTitle="IOPS"
- [labelsArray]="poolIOPSLabels"
+ [labelsArray]="iopsLabels"
+ isMultiCluster="true"
dataUnits=""
decimals="0"
- isMultiCluster="true"
- [dataArray]="poolIOPSValues"
- *ngIf="poolIOPSLabels && poolIOPSValues"
- [truncateLabel]="true">
+ [dataArray]="iopsValues"
+ [truncateLabel]="true"
+ *ngIf="iopsLabels && iopsValues">
</cd-dashboard-area-chart>
- <cd-dashboard-area-chart chartTitle="Client Throughput"
- [labelsArray]="poolThroughputLabels"
+ <cd-dashboard-area-chart chartTitle="Throughput"
+ [labelsArray]="throughputLabels"
+ isMultiCluster="true"
dataUnits="B/s"
decimals="2"
- isMultiCluster="true"
- [dataArray]="poolThroughputValues"
- *ngIf="poolThroughputLabels && poolThroughputValues"
- [truncateLabel]="true">
+ [dataArray]="throughputValues"
+ [truncateLabel]="true"
+ *ngIf="throughputLabels && throughputLabels">
</cd-dashboard-area-chart>
</div>
</cd-card>
</div>
- </div>
- </cd-card-group>
- </ng-container>
-</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, 'poolUtilization')">
+ </cd-dashboard-time-selector>
+ <cd-dashboard-area-chart chartTitle="Capacity"
+ [labelsArray]="poolCapacityLabels"
+ dataUnits="B"
+ isMultiCluster="true"
+ [dataArray]="poolCapacityValues"
+ *ngIf="poolCapacityLabels && poolCapacityValues"
+ [truncateLabel]="true">
+ </cd-dashboard-area-chart>
+ <cd-dashboard-area-chart chartTitle="IOPS"
+ [labelsArray]="poolIOPSLabels"
+ dataUnits=""
+ decimals="0"
+ isMultiCluster="true"
+ [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"
+ isMultiCluster="true"
+ [dataArray]="poolThroughputValues"
+ *ngIf="poolThroughputLabels && poolThroughputValues"
+ [truncateLabel]="true">
+ </cd-dashboard-area-chart>
+ </div>
+ </cd-card>
+ </div>
+ </div>
+ </cd-card-group>
+ </ng-container>
+ </div>
+</ng-container>
.fa-wrench {
color: vv.$info;
font-size: 6em;
- margin-top: 200px;
}
import { Component, OnDestroy, OnInit, ViewChild } from '@angular/core';
import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap';
-import { Subscription } from 'rxjs';
+import { Observable, Subscription } from 'rxjs';
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';
MultiClusterPromqlsForClusterUtilization as ClusterUltilizationQueries,
MultiClusterPromqlsForPoolUtilization as PoolUltilizationQueries
} from '~/app/shared/enum/dashboard-promqls.enum';
+import { SettingsService } from '~/app/shared/api/settings.service';
@Component({
selector: 'cd-multi-cluster',
interval: NodeJS.Timer;
selectedTime: any;
multiClusterQueries: any = {};
+ managedByConfig$: Observable<any>;
constructor(
private multiClusterService: MultiClusterService,
+ private settingsService: SettingsService,
private modalService: ModalService,
private router: Router,
private prometheusService: PrometheusService,
}
})
);
-
+ this.managedByConfig$ = this.settingsService.getValues('MANAGED_BY_CLUSTERS');
this.subs.add(
this.multiClusterService.subscribeClusterTokenStatus((resp: object) => {
this.clusterTokenStatus = resp;
}
getPrometheusData(selectedTime: any, selectedQueries?: string) {
- const validRangeQueries = [
- 'CLUSTER_CAPACITY_UTILIZATION',
- 'CLUSTER_IOPS_UTILIZATION',
- 'CLUSTER_THROUGHPUT_UTILIZATION',
- 'POOL_CAPACITY_UTILIZATION',
- 'POOL_IOPS_UTILIZATION',
- 'POOL_THROUGHPUT_UTILIZATION'
- ];
+ const validRangeQueries = Object.keys(ClusterUltilizationQueries).concat(
+ Object.keys(PoolUltilizationQueries)
+ );
+
+ const allMultiClusterQueries = Object.keys(allQueries).concat(
+ Object.keys(ClusterUltilizationQueries).concat(Object.keys(PoolUltilizationQueries))
+ );
+
const validQueries = [
'ALERTS',
'MGR_METADATA',
'CLUSTER_ALERTS'
];
+ let validSelectedQueries = allMultiClusterQueries;
+
if (selectedQueries) {
if (selectedQueries === 'poolUtilization') {
this.multiClusterQueries.pool['selectedTime'] = selectedTime;
+ validSelectedQueries = Object.keys(PoolUltilizationQueries);
}
if (selectedQueries === 'clusterUtilization') {
this.multiClusterQueries.cluster.selectedTime = selectedTime;
+ validSelectedQueries = Object.keys(ClusterUltilizationQueries);
}
}
this.queriesResults,
validQueries,
validRangeQueries,
- this.multiClusterQueries
+ this.multiClusterQueries,
+ validSelectedQueries,
+ allMultiClusterQueries
)
.subscribe((data: any) => {
this.queriesResults = data;
}
ngOnDestroy(): void {
+ this.subs.unsubscribe();
clearInterval(this.interval);
}
}
</a>
</dd>
</ng-container>
+ <ng-container *ngIf="managedByConfig$ | async as managedByConfig">
+ <span *ngIf="managedByConfig['MANAGED_BY_CLUSTERS'].length > 0">
+ <dt>Managed By</dt>
+ <dd>
+ <a target="_blank"
+ [href]="managedByConfig['MANAGED_BY_CLUSTERS'][0]['url']">
+ {{ managedByConfig['MANAGED_BY_CLUSTERS'][0]['fsid'] }}
+ <i class="fa fa-external-link"></i>
+ </a>
+ </dd>
+ </span>
+ </ng-container>
</dl>
</cd-card>
import { MgrModuleService } from '~/app/shared/api/mgr-module.service';
import { AlertClass } from '~/app/shared/enum/health-icon.enum';
import { HardwareService } from '~/app/shared/api/hardware.service';
+import { SettingsService } from '~/app/shared/api/settings.service';
@Component({
selector: 'cd-dashboard-v3',
isHardwareEnabled$: Observable<boolean>;
hardwareSummary$: Observable<any>;
hardwareSubject = new BehaviorSubject<any>([]);
+ managedByConfig$: Observable<any>;
constructor(
private summaryService: SummaryService,
private authStorageService: AuthStorageService,
private featureToggles: FeatureTogglesService,
private healthService: HealthService,
+ private settingsService: SettingsService,
public prometheusService: PrometheusService,
private mgrModuleService: MgrModuleService,
private refreshIntervalService: RefreshIntervalService,
this.getPrometheusData(this.prometheusService.lastHourDateObject);
this.getDetailsCardData();
this.getTelemetryReport();
+ this.managedByConfig$ = this.settingsService.getValues('MANAGED_BY_CLUSTERS');
}
getTelemetryText(): string {
});
}
- 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() {
return (data: any) => {
this.msSource.next(data);
queriesResults: any,
validQueries: string[],
validRangeQueries: string[],
- multiClusterQueries: any
+ multiClusterQueries: any,
+ validSelectedQueries: string[],
+ allMultiClusterQueries: string[]
) {
return new Observable((observer) => {
this.ifPrometheusConfigured(() => {
Object.entries(multiClusterQueries).forEach(([key, _value]) => {
for (const queryName in multiClusterQueries[key].queries) {
- if (multiClusterQueries[key].queries.hasOwnProperty(queryName)) {
+ if (
+ multiClusterQueries[key].queries.hasOwnProperty(queryName) &&
+ validSelectedQueries.includes(queryName)
+ ) {
const query = multiClusterQueries[key].queries[queryName];
const start = this.updateTimeStamp(multiClusterQueries[key].selectedTime)['start'];
const end = this.updateTimeStamp(multiClusterQueries[key].selectedTime)['end'];
}
});
+ validSelectedQueries = allMultiClusterQueries;
+
forkJoin(requests).subscribe(
(responses: any[]) => {
for (let i = 0; i < responses.length; i++) {
- jwt: []
tags:
- Multi-cluster
- /api/multi-cluster/verify_connection:
- 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:
- '201':
- content:
- application/vnd.ceph.api.v1.0+json:
- type: object
- 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':
- 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:
- - Multi-cluster
/api/nfs-ganesha/cluster:
get:
parameters: []
[str])
MULTICLUSTER_CONFIG = Setting({}, [dict, str])
-
+ MANAGED_BY_CLUSTERS = Setting([], [dict, list])
UNSAFE_TLS_v1_2 = Setting(False, [bool])
@staticmethod