pwd_expiration_date = user_data.get('pwdExpirationDate', None)
pwd_update_required = user_data.get('pwdUpdateRequired', False)
- if isinstance(Settings.MULTICLUSTER_CONFIG, str):
- try:
- item_to_dict = json.loads(Settings.MULTICLUSTER_CONFIG)
- except json.JSONDecodeError:
- item_to_dict = {}
- multicluster_config = item_to_dict.copy()
- else:
- multicluster_config = Settings.MULTICLUSTER_CONFIG.copy()
- try:
- if fsid in multicluster_config['config']:
- existing_entries = multicluster_config['config'][fsid]
- if not any(entry['user'] == username for entry in existing_entries):
- existing_entries.append({
- "name": fsid,
- "url": origin,
- "cluster_alias": "local-cluster",
- "user": username
- })
- else:
- multicluster_config['config'][fsid] = [{
- "name": fsid,
- "url": origin,
- "cluster_alias": "local-cluster",
- "user": username
- }]
-
- except KeyError:
- multicluster_config = {
- 'current_url': origin,
- 'current_user': username,
- 'hub_url': origin,
- 'config': {
- fsid: [
- {
- "name": fsid,
- "url": origin,
- "cluster_alias": "local-cluster",
- "user": username
- }
- ]
- }
- }
- Settings.MULTICLUSTER_CONFIG = multicluster_config
-
if user_perms is not None:
url_prefix = 'https' if mgr.get_localized_module_option('ssl') else 'http'
token = token.decode('utf-8') if isinstance(token, bytes) else token
self._set_token_cookie(url_prefix, token)
+ if isinstance(Settings.MULTICLUSTER_CONFIG, str):
+ try:
+ item_to_dict = json.loads(Settings.MULTICLUSTER_CONFIG)
+ except json.JSONDecodeError:
+ item_to_dict = {}
+ multicluster_config = item_to_dict.copy()
+ else:
+ multicluster_config = Settings.MULTICLUSTER_CONFIG.copy()
+ try:
+ if fsid in multicluster_config['config']:
+ existing_entries = multicluster_config['config'][fsid]
+ if not any((entry['user'] == username or entry['cluster_alias'] == 'local-cluster') for entry in existing_entries): # noqa E501 #pylint: disable=line-too-long
+ existing_entries.append({
+ "name": fsid,
+ "url": origin,
+ "cluster_alias": "local-cluster",
+ "user": username
+ })
+ else:
+ multicluster_config['config'][fsid] = [{
+ "name": fsid,
+ "url": origin,
+ "cluster_alias": "local-cluster",
+ "user": username
+ }]
+
+ except KeyError:
+ multicluster_config = {
+ 'current_url': origin,
+ 'current_user': username,
+ 'hub_url': origin,
+ 'config': {
+ fsid: [
+ {
+ "name": fsid,
+ "url": origin,
+ "cluster_alias": "local-cluster",
+ "user": username
+ }
+ ]
+ }
+ }
+ Settings.MULTICLUSTER_CONFIG = multicluster_config
return {
'token': token,
'username': username,
<cd-alert-panel type="info"
spacingClass="mb-3"
i18n
- *ngIf="connectionVerified !== undefined && !connectionVerified && connectionMessage === 'Connection refused'">
+ *ngIf="connectionVerified !== undefined && !connectionVerified && connectionMessage === 'Connection refused' || remoteClusterForm.getValue('showToken')">
<p>You need to set this cluster's url as the cross origin url in the remote cluster you are trying to connect.
You can do it by running this CLI command in your remote cluster and proceed with authentication via token.</p>
<cd-code-block [codes]="[crossOriginCmd]"></cd-code-block>
<label class="cd-col-form-label required"
for="remoteClusterUrl"
i18n>Cluster API URL
- <cd-helper>Enter the Dashboard API URL. You can retrieve it from the CLI with: <b>ceph mgr services</b></cd-helper>
+ <cd-helper>
+ <span>
+ <p>Enter the Dashboard API URL. You can retrieve it from the CLI with: <b>{{ clusterApiUrlCmd }} </b>
+ <cd-copy-2-clipboard-button [source]="clusterApiUrlCmd"
+ [byId]="false"></cd-copy-2-clipboard-button>
+ </p>
+ </span>
+ </cd-helper>
</label>
<div class="cd-col-form-input">
<input class="form-control"
</span>
</div>
</div>
+ <div class="form-group row"
+ *ngIf="remoteClusterForm.getValue('showToken') && action !== 'edit'">
+ <label class="cd-col-form-label required"
+ for="prometheusApiUrl"
+ i18n>Prometheus API URL
+ <cd-helper>
+ <span>
+ <p>Enter the Prometheus API URL. You can retrieve it from the CLI with: <b>{{ prometheusApiUrlCmd }} </b>
+ <cd-copy-2-clipboard-button [source]="prometheusApiUrlCmd"
+ [byId]="false"></cd-copy-2-clipboard-button>
+ </p>
+ </span>
+ </cd-helper>
+ </label>
+ <div class="cd-col-form-input">
+ <input id="prometheusApiUrl"
+ name="prometheusApiUrl"
+ class="form-control"
+ type="text"
+ formControlName="prometheusApiUrl">
+ <span class="invalid-feedback"
+ *ngIf="remoteClusterForm.showError('prometheusApiUrl', frm, 'required')"
+ i18n>This field is required.
+ </span>
+ </div>
+ </div>
<div class="form-group row"
*ngIf="!remoteClusterForm.getValue('showToken') && !showCrossOriginError && action !== 'edit'">
<label class="cd-col-form-label required"
</div>
</div>
<div class="form-group row"
- *ngIf="!showCrossOriginError && action !== 'edit' && !remoteClusterForm.getValue('showToken')">
+ *ngIf="!showCrossOriginError && action !== 'edit' && !remoteClusterForm.getValue('showToken') && !connectionVerified">
<div class="cd-col-form-offset">
<div class="custom-control">
<button class="btn btn-primary"
readonly endpoints = /^((https?:\/\/)|(www.))(?:([a-zA-Z]+)|(\d+\.\d+.\d+.\d+)):\d{2,5}\/?$/;
readonly ipv4Rgx = /^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/i;
readonly ipv6Rgx = /^(?:[a-f0-9]{1,4}:){7}[a-f0-9]{1,4}$/i;
+ clusterApiUrlCmd = 'ceph mgr services';
+ prometheusApiUrlCmd = 'ceph config get mgr mgr/dashboard/PROMETHEUS_API_HOST';
+ crossOriginCmd = `ceph dashboard set-cross-origin-url ${window.location.origin}`;
remoteClusterForm: CdFormGroup;
showToken = false;
connectionVerified: boolean;
connectionMessage = '';
private subs = new Subscription();
showCrossOriginError = false;
- crossOriginCmd: string;
action: string;
cluster: MultiCluster;
clustersData: MultiCluster[];
showToken: true
})
]),
+ prometheusApiUrl: new FormControl('', [
+ CdValidators.requiredIf({
+ showToken: true
+ })
+ ]),
password: new FormControl('', []),
remoteClusterUrl: new FormControl(null, {
validators: [
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');
token,
window.location.origin,
clusterFsid,
+ prometheusApiUrl,
ssl,
ssl_certificate
)
this.connectionVerified = false;
this.showCrossOriginError = true;
this.connectionMessage = resp;
- this.crossOriginCmd = `ceph config set mgr mgr/dashboard/cross_origin_url ${window.location.origin} `;
this.notificationService.show(
NotificationType.error,
$localize`Connection to the cluster failed`
import { NotificationType } from '~/app/shared/enum/notification-type.enum';
import { CellTemplate } from '~/app/shared/enum/cell-template.enum';
import { MultiCluster } from '~/app/shared/models/multi-cluster';
-import { SummaryService } from '~/app/shared/services/summary.service';
import { Router } from '@angular/router';
import { CookiesService } from '~/app/shared/services/cookie.service';
clustersTokenMap: Map<string, string> = new Map<string, string>();
newData: any;
modalRef: NgbModalRef;
+ hubUrl: string;
+ currentUrl: string;
constructor(
private multiClusterService: MultiClusterService,
private router: Router,
- private summaryService: SummaryService,
public actionLabels: ActionLabelsI18n,
private notificationService: NotificationService,
private authStorageService: AuthStorageService,
permission: 'create',
icon: Icons.add,
name: this.actionLabels.CONNECT,
+ disable: (selection: CdTableSelection) => this.getDisable('connect', selection),
click: () => this.openRemoteClusterInfoModal('connect')
},
{
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();
checkClusterConnectionStatus() {
if (this.clusterTokenStatus && this.data) {
this.data.forEach((cluster: MultiCluster) => {
- const clusterStatus = this.clusterTokenStatus[cluster.name];
+ const clusterStatus = this.clusterTokenStatus[cluster.name.trim()];
if (clusterStatus !== undefined) {
cluster.cluster_connection_status = clusterStatus.status;
size: 'xl'
});
this.bsModalRef.componentInstance.submitAction.subscribe(() => {
- this.multiClusterService.refresh();
- this.summaryService.refresh();
const currentRoute = this.router.url.split('?')[0];
- if (currentRoute.includes('dashboard')) {
- this.router.navigateByUrl('/pool', { skipLocationChange: true }).then(() => {
- this.router.navigate([currentRoute]);
- });
- } else {
- this.router.navigateByUrl('/', { skipLocationChange: true }).then(() => {
- this.router.navigate([currentRoute]);
- });
- }
+ this.multiClusterService.refreshMultiCluster(currentRoute);
+ this.checkClusterConnectionStatus();
+ this.multiClusterService.isClusterAdded(true);
});
}
openDeleteClusterModal() {
const cluster = this.selection.first();
this.modalRef = this.modalService.show(CriticalConfirmationModalComponent, {
+ infoMessage: $localize`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.`,
actionDescription: $localize`Disconnect`,
itemDescription: $localize`Cluster`,
itemNames: [cluster['cluster_alias'] + ' - ' + cluster['user']],
submitAction: () =>
this.multiClusterService.deleteCluster(cluster['name'], cluster['user']).subscribe(() => {
this.cookieService.deleteToken(`${cluster['name']}-${cluster['user']}`);
+ this.multiClusterService.showPrometheusDelayMessage(true);
this.modalRef.close();
this.notificationService.show(
NotificationType.success,
$localize`Disconnected cluster '${cluster['cluster_alias']}'`
);
+ const currentRoute = this.router.url.split('?')[0];
+ this.multiClusterService.refreshMultiCluster(currentRoute);
})
});
}
getDisable(action: string, selection: CdTableSelection): string | boolean {
- if (!selection.hasSelection) {
+ if (this.hubUrl !== this.currentUrl) {
+ return $localize`Please switch to the local-cluster to ${action} a remote cluster`;
+ }
+ if (!selection.hasSelection && action !== 'connect') {
return $localize`Please select one or more clusters to ${action}`;
}
if (selection.hasSingleSelection) {
const cluster = selection.first();
- if (cluster['cluster_alias'] === 'local-cluster') {
+ if (cluster['cluster_alias'] === 'local-cluster' && action !== 'connect') {
return $localize`Cannot ${action} local cluster`;
}
}
<i class="mx-auto d-block"
[ngClass]="[icons.large3x, icons.spinner, icons.spin]">
</i>
+ <p class="text-center mt-3"
+ i18n>Loading data, Please wait...</p>
</div>
</div>
</div>
<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">
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'">
+ *ngIf="queriesResults['TOTAL_HOSTS'] && queriesResults['TOTAL_HOSTS'][0]">
<span class="text-center">
<h3>{{ queriesResults['TOTAL_HOSTS'][0][1] }}</h3>
</span>
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 (selectedTime)="getPrometheusData($event, 'clusterUtilization')">
</cd-dashboard-time-selector>
<cd-dashboard-area-chart chartTitle="Capacity"
[labelsArray]="capacityLabels"
+ isMultiCluster="true"
dataUnits="B"
[dataArray]="capacityValues"
[truncateLabel]="true"
[labelsArray]="iopsLabels"
dataUnits=""
decimals="0"
+ isMultiCluster="true"
[dataArray]="iopsValues"
[truncateLabel]="true"
*ngIf="iopsLabels && iopsValues">
[labelsArray]="throughputLabels"
dataUnits="B/s"
decimals="2"
+ isMultiCluster="true"
[dataArray]="throughputValues"
[truncateLabel]="true"
*ngIf="throughputLabels && throughputLabels">
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 (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">
[labelsArray]="poolIOPSLabels"
dataUnits=""
decimals="0"
+ isMultiCluster="true"
[dataArray]="poolIOPSValues"
*ngIf="poolIOPSLabels && poolIOPSValues"
[truncateLabel]="true">
[labelsArray]="poolThroughputLabels"
dataUnits="B/s"
decimals="2"
+ isMultiCluster="true"
[dataArray]="poolThroughputValues"
*ngIf="poolThroughputLabels && poolThroughputValues"
[truncateLabel]="true">
-import { Component, OnInit, ViewChild } from '@angular/core';
+import { Component, OnDestroy, OnInit, ViewChild } from '@angular/core';
import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap';
import { Subscription } from 'rxjs';
import { MultiClusterService } from '~/app/shared/api/multi-cluster.service';
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';
+import { Router } from '@angular/router';
+
+import {
+ MultiClusterPromqls as allQueries,
+ MultiClusterPromqlsForClusterUtilization as ClusterUltilizationQueries,
+ MultiClusterPromqlsForPoolUtilization as PoolUltilizationQueries
+} from '~/app/shared/enum/dashboard-promqls.enum';
@Component({
selector: 'cd-multi-cluster',
templateUrl: './multi-cluster.component.html',
styleUrls: ['./multi-cluster.component.scss']
})
-export class MultiClusterComponent implements OnInit {
+export class MultiClusterComponent implements OnInit, OnDestroy {
COUNT_OF_UTILIZATION_CHARTS = 5;
@ViewChild('nameTpl', { static: true })
isMultiCluster = true;
clusterTokenStatus: object = {};
localClusterName: string;
- clusters: any;
+ clusters: any = [];
connectionErrorsCount = 0;
capacityLabels: string[] = [];
poolIOPSValues: string[] = [];
poolCapacityValues: string[] = [];
poolThroughputValues: string[] = [];
+ showDeletionMessage = false;
+ isClusterAdded = false;
+ selectedQueries: any;
+ PROMETHEUS_DELAY = 20000;
+ LOAD_DELAY = 5000;
+ CLUSTERS_REFRESH_INTERVAL = 30000;
+ interval: NodeJS.Timer;
+ selectedTime: any;
+ multiClusterQueries: any = {};
constructor(
private multiClusterService: MultiClusterService,
private modalService: ModalService,
+ private router: Router,
private prometheusService: PrometheusService,
private dimlessBinaryPipe: DimlessBinaryPipe
- ) {}
+ ) {
+ this.multiClusterQueries = {
+ cluster: {
+ queries: ClusterUltilizationQueries,
+ selectedTime: this.prometheusService.lastHourDateObject
+ },
+ pool: {
+ queries: PoolUltilizationQueries,
+ selectedTime: this.prometheusService.lastHourDateObject
+ },
+ all: {
+ queries: allQueries,
+ selectedTime: this.prometheusService.lastHourDateObject
+ }
+ };
+ }
ngOnInit(): void {
this.columns = [
this.clusterTokenStatus = resp;
})
);
- this.getPrometheusData(this.prometheusService.lastHourDateObject);
+
+ this.isClusterAdded = this.multiClusterService.isClusterAdded();
+
+ if (this.isClusterAdded) {
+ setTimeout(() => {
+ this.getPrometheusData(this.prometheusService.lastHourDateObject);
+ this.multiClusterService.isClusterAdded(false);
+ }, this.PROMETHEUS_DELAY);
+ } else {
+ this.showDeletionMessage = this.multiClusterService.showPrometheusDelayMessage();
+ if (this.showDeletionMessage) {
+ setTimeout(() => {
+ this.getPrometheusData(this.prometheusService.lastHourDateObject);
+ }, this.LOAD_DELAY);
+ } else {
+ this.getPrometheusData(this.prometheusService.lastHourDateObject);
+ }
+ }
}
openRemoteClusterInfoModal() {
this.bsModalRef = this.modalService.show(MultiClusterFormComponent, initialState, {
size: 'lg'
});
+ this.bsModalRef.componentInstance.submitAction.subscribe(() => {
+ this.loading = true;
+ setTimeout(() => {
+ const currentRoute = this.router.url.split('?')[0];
+ this.multiClusterService.refreshMultiCluster(currentRoute);
+ this.getPrometheusData(this.prometheusService.lastHourDateObject);
+ }, this.PROMETHEUS_DELAY);
+ });
}
- getPrometheusData(selectedTime: any) {
+ 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 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 (selectedQueries) {
+ if (selectedQueries === 'poolUtilization') {
+ this.multiClusterQueries.pool['selectedTime'] = selectedTime;
+ }
+
+ if (selectedQueries === 'clusterUtilization') {
+ this.multiClusterQueries.cluster.selectedTime = selectedTime;
+ }
+ }
+
this.prometheusService
- .getMultiClusterQueriesData(selectedTime, queries, this.queriesResults)
+ .getMultiClusterQueriesData(
+ this.queriesResults,
+ validQueries,
+ validRangeQueries,
+ this.multiClusterQueries
+ )
.subscribe((data: any) => {
this.queriesResults = data;
this.loading = false;
this.alerts = this.queriesResults.ALERTS;
this.getAlertsInfo();
this.getClustersInfo();
+ this.interval = setInterval(() => {
+ this.getClustersInfo();
+ }, this.CLUSTERS_REFRESH_INTERVAL);
});
}
}
const clusters: ClusterInfo[] = [];
-
this.queriesResults.TOTAL_CAPACITY?.forEach((totalCapacityMetric: any) => {
const clusterName = totalCapacityMetric.metric.cluster;
const totalCapacity = parseInt(totalCapacityMetric.value[1]);
const available_capacity = totalCapacity - usedCapacity;
clusters.push({
- cluster: clusterName,
+ cluster: clusterName.trim(),
status,
alert,
total_capacity: totalCapacity,
}
labels.push(label);
}
- // console.log(labels)
return labels;
}
}
return values;
}
+
+ onDismissed() {
+ this.showDeletionMessage = false;
+ this.multiClusterService.showPrometheusDelayMessage(false);
+ }
+
+ ngOnDestroy(): void {
+ clearInterval(this.interval);
+ }
}
</div>
<div class="col-9 d-flex flex-column">
- <div class="chart mt-3">
+ <div [ngClass]="{'chart mt-3': !isMultiCluster, 'mt-3': isMultiCluster}">
<canvas baseChart
[datasets]="chartData.dataset"
[options]="options"
it('should set label', () => {
component.ngOnChanges({ dataArray: new SimpleChange(null, component.dataArray, false) });
- expect(component.chartData.dataset[0].label).toEqual('Read');
+ expect(component.chartData.dataset[0].label).toEqual('Total');
expect(component.chartData.dataset[1].label).toEqual('Write');
- expect(component.chartData.dataset[2].label).toEqual('Total');
+ expect(component.chartData.dataset[2].label).toEqual('Read');
});
it('should transform and update data', () => {
component.ngOnChanges({ dataArray: new SimpleChange(null, component.dataArray, false) });
expect(component.chartData.dataset[0].data).toEqual([
- { x: 1000, y: 110 },
- { x: 3000, y: 130 }
+ { x: 5000, y: 150 },
+ { x: 6000, y: 160 }
]);
});
it('should set currentData to last value', () => {
component.ngOnChanges({ dataArray: new SimpleChange(null, component.dataArray, false) });
- expect(component.currentChartData.dataset[0].currentData).toBe('130');
+ expect(component.currentChartData.dataset[0].currentData).toBe('160');
});
it('should keep data units consistency', () => {
decimals?: number = 1;
@Input()
truncateLabel = false;
+ @Input()
+ isMultiCluster?: boolean = false;
currentDataUnits: string;
currentData: number;
[this.maxConvertedValue, this.maxConvertedValueUnits] = this.convertUnits(
this.maxValue
).split(' ');
+ this.currentChartData.dataset[index]['currentDataValue'] = currentDataValue;
}
}
+ this.currentChartData.dataset.sort(
+ (a: { currentDataValue: string }, b: { currentDataValue: string }) =>
+ parseFloat(b['currentDataValue']) - parseFloat(a['currentDataValue'])
+ );
}
if (this.chart) {
private prepareRawUsage(chart: Record<string, any>, data: Record<string, any>) {
const nearFullRatioPercent = this.lowThreshold * 100;
const fullRatioPercent = this.highThreshold * 100;
- const percentAvailable = this.calcPercentage(data.max - data.current, data.max);
- const percentUsed = this.calcPercentage(data.current, data.max);
+ const max = typeof data.max === 'string' ? parseFloat(data.max) : data.max;
+ const current = typeof data.current === 'string' ? parseFloat(data.current) : data.current;
+ const percentAvailable = this.calcPercentage(max - current, max);
+ const percentUsed = this.calcPercentage(current, max);
if (fullRatioPercent >= 0 && percentUsed >= fullRatioPercent) {
this.color = 'chart-color-red';
this.multiClusterService.subscribe((resp: object) => {
const clustersConfig = resp['config'];
if (clustersConfig) {
+ this.clustersMap.clear();
Object.keys(clustersConfig).forEach((clusterKey: string) => {
const clusterDetailsList = clustersConfig[clusterKey];
clusterDetailsList.forEach((clusterDetails: MultiCluster) => {
},
() => {},
() => {
- this.multiClusterService.refresh();
- this.summaryService.refresh();
-
// force refresh grafana api url to get the correct url for the selected cluster
this.settingsService.ifSettingConfigured(
'api/grafana/url',
true
);
const currentRoute = this.router.url.split('?')[0];
- if (currentRoute.includes('dashboard')) {
- this.router.navigateByUrl('/pool', { skipLocationChange: true }).then(() => {
- this.router.navigate([currentRoute]);
- });
- } else {
- this.router.navigateByUrl('/', { skipLocationChange: true }).then(() => {
- this.router.navigate([currentRoute]);
- });
- }
+ this.multiClusterService.refreshMultiCluster(currentRoute);
}
);
}
import { BehaviorSubject, Observable, Subscription } from 'rxjs';
import { TimerService } from '../services/timer.service';
import { filter } from 'rxjs/operators';
+import { SummaryService } from '../services/summary.service';
+import { Router } from '@angular/router';
@Injectable({
providedIn: 'root'
msData$ = this.msSource.asObservable();
private tokenStatusSource = new BehaviorSubject<any>(null);
tokenStatusSource$ = this.tokenStatusSource.asObservable();
- constructor(private http: HttpClient, private timerService: TimerService) {}
+ showDeletionMessage = false;
+ isClusterAddedFlag = false;
+ constructor(
+ private http: HttpClient,
+ private timerService: TimerService,
+ private summaryService: SummaryService,
+ private router: Router
+ ) {}
startPolling(): Subscription {
return this.timerService
if (tempMap.size > 0) {
clustersTokenMap = tempMap;
- dataSubscription.unsubscribe();
+ if (dataSubscription) {
+ dataSubscription.unsubscribe();
+ }
this.checkAndStartTimer(clustersTokenMap);
}
}
token = '',
hub_url = '',
clusterFsid = '',
+ prometheusApiUrl = '',
ssl = false,
cert = ''
) {
token,
hub_url,
cluster_fsid: clusterFsid,
+ prometheus_api_url: prometheusApiUrl,
ssl_verify: ssl,
ssl_certificate: cert
});
ssl = false,
cert = ''
) {
- return this.http.post('api/multi-cluster/reconnect_cluster', {
+ return this.http.put('api/multi-cluster/reconnect_cluster', {
url,
username,
password,
return this.http.get<object>('api/multi-cluster/check_token_status', { params });
}
+
+ showPrometheusDelayMessage(showDeletionMessage?: boolean) {
+ if (showDeletionMessage !== undefined) {
+ this.showDeletionMessage = showDeletionMessage;
+ }
+ return this.showDeletionMessage;
+ }
+
+ isClusterAdded(isClusterAddedFlag?: boolean) {
+ if (isClusterAddedFlag !== undefined) {
+ this.isClusterAddedFlag = isClusterAddedFlag;
+ }
+ return this.isClusterAddedFlag;
+ }
+
+ refreshMultiCluster(currentRoute: string) {
+ this.refresh();
+ this.summaryService.refresh();
+ if (currentRoute.includes('dashboard')) {
+ this.router.navigateByUrl('/pool', { skipLocationChange: true }).then(() => {
+ this.router.navigate([currentRoute]);
+ });
+ } else {
+ this.router.navigateByUrl('/', { skipLocationChange: true }).then(() => {
+ this.router.navigate([currentRoute]);
+ });
+ }
+ }
}
return this.http.get<any>(`${this.baseURL}/data`, { params });
}
- getMultiClusterQueriesData(selectedTime: any, queries: any, queriesResults: any) {
+ getMultiClusterQueriesData(
+ queriesResults: any,
+ validQueries: string[],
+ validRangeQueries: string[],
+ multiClusterQueries: any
+ ) {
return new Observable((observer) => {
this.ifPrometheusConfigured(() => {
if (this.timerGetPrometheusDataSub) {
}
this.timerGetPrometheusDataSub = timer(0, this.timerTime).subscribe(() => {
- selectedTime = this.updateTimeStamp(selectedTime);
+ let requests: any[] = [];
+ let queryNames: string[] = [];
- 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);
+ Object.entries(multiClusterQueries).forEach(([key, _value]) => {
+ for (const queryName in multiClusterQueries[key].queries) {
+ if (multiClusterQueries[key].queries.hasOwnProperty(queryName)) {
+ const query = multiClusterQueries[key].queries[queryName];
+ const start = this.updateTimeStamp(multiClusterQueries[key].selectedTime)['start'];
+ const end = this.updateTimeStamp(multiClusterQueries[key].selectedTime)['end'];
+ const step = this.updateTimeStamp(multiClusterQueries[key].selectedTime)['step'];
+
+ if (validRangeQueries.includes(queryName)) {
+ const request = this.getMultiClusterQueryRangeData({
+ params: encodeURIComponent(query),
+ start,
+ end,
+ step
+ });
+ requests.push(request);
+ queryNames.push(queryName);
+ } else {
+ const request = this.getMultiClusterData({
+ params: encodeURIComponent(query),
+ start,
+ end,
+ step
+ });
+ requests.push(request);
+ queryNames.push(queryName);
+ }
}
}
- }
+ });
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'
- ];
+ const queryName = queryNames[i];
if (data.result.length) {
if (validQueries.includes(queryName)) {
queriesResults[queryName] = data.result;
[formGroup]="deletionForm"
novalidate>
<div class="modal-body">
+ <cd-alert-panel *ngIf="infoMessage"
+ type="info"
+ spacingClass="mb-3"
+ i18n>
+ <p>{{ infoMessage }}</p>
+ </cd-alert-panel>
<ng-container *ngTemplateOutlet="bodyTemplate; context: bodyContext"></ng-container>
<div class="question">
<span *ngIf="itemNames; else noNames">
itemDescription: 'entry';
itemNames: string[];
actionDescription = 'delete';
+ infoMessage: string;
childFormGroup: CdFormGroup;
childFormGroupTemplate: TemplateRef<any>;
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 )'
+ CLUSTER_ALERTS = 'count by (cluster) (ALERTS{alertstate="firing"}) or vector(0)'
+}
+
+export enum MultiClusterPromqlsForClusterUtilization {
+ CLUSTER_CAPACITY_UTILIZATION = 'topk(5, ceph_cluster_total_used_bytes)',
+ CLUSTER_IOPS_UTILIZATION = 'topk(5, sum by (cluster) (rate(ceph_pool_wr[1m])) + sum by (cluster) (rate(ceph_pool_rd[1m])) )',
+ CLUSTER_THROUGHPUT_UTILIZATION = 'topk(5, sum by (cluster) (rate(ceph_pool_wr_bytes[1m])) + sum by (cluster) (rate(ceph_pool_rd_bytes[1m])) )'
+}
+
+export enum MultiClusterPromqlsForPoolUtilization {
+ POOL_CAPACITY_UTILIZATION = 'topk(5, ceph_pool_bytes_used/ceph_pool_max_avail * on(pool_id, cluster) group_left(instance, name) ceph_pool_metadata)',
+ POOL_IOPS_UTILIZATION = 'topk(5, (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(5, (irate(ceph_pool_rd_bytes[1m]) + irate(ceph_pool_wr_bytes[1m])) * on(pool_id, cluster) group_left(instance, name) ceph_pool_metadata )'
}
import { CdNotificationConfig } from '../models/cd-notification';
import { FinishedTask } from '../models/finished-task';
import { NotificationService } from './notification.service';
-import { MultiClusterService } from '../api/multi-cluster.service';
-import { SummaryService } from './summary.service';
import { AuthStorageService } from './auth-storage.service';
import { CookiesService } from './cookie.service';
providedIn: 'root'
})
export class ApiInterceptorService implements HttpInterceptor {
- localClusterDetails: object;
- dashboardClustersMap: Map<string, string> = new Map<string, string>();
constructor(
private router: Router,
public notificationService: NotificationService,
- private summaryService: SummaryService,
private authStorageService: AuthStorageService,
- private multiClusterService: MultiClusterService,
private cookieService: CookiesService
- ) {
- this.multiClusterService.subscribe((resp: any) => {
- const clustersConfig = resp['config'];
- const hub_url = resp['hub_url'];
- 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 (clusterDetails['url'] === hub_url) {
- this.localClusterDetails = clusterDetails;
- }
- });
- });
- }
- });
- }
+ ) {}
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
const acceptHeader = request.headers.get('Accept');
const ALWAYS_TO_HUB_APIs = [
'api/auth/login',
'api/auth/logout',
- 'api/multi-cluster/get_config',
'api/multi-cluster/set_config',
+ 'api/multi-cluster/get_config',
'api/multi-cluster/auth'
];
timeoutId = this.notificationService.notifyTask(finishedTask);
break;
case 401:
- if (this.dashboardClustersMap.size > 1) {
- this.multiClusterService.setCluster(this.localClusterDetails).subscribe(() => {
- localStorage.setItem('cluster_api_url', this.localClusterDetails['url']);
- });
- this.multiClusterService.refresh();
- this.summaryService.refresh();
- const currentRoute = this.router.url.split('?')[0];
- if (currentRoute.includes('dashboard')) {
- this.router.navigateByUrl('/pool', { skipLocationChange: true }).then(() => {
- this.router.navigate([currentRoute]);
- });
- } else {
- this.router.navigateByUrl('/', { skipLocationChange: true }).then(() => {
- this.router.navigate([currentRoute]);
- });
- }
- } else {
- this.authStorageService.remove();
- this.router.navigate(['/login']);
- }
+ this.authStorageService.remove();
+ this.router.navigate(['/login']);
break;
case 403:
this.router.navigate(['error'], {