# -*- coding: utf-8 -*-
-from typing import Optional
+from typing import List, Optional
from ..exceptions import DashboardException
from ..security import Scope
orch = OrchClient.instance()
res = orch.daemons.action(action=action, daemon_name=daemon_name, image=container_image)
return res
+
+ @raise_if_no_orchestrator([OrchFeature.DAEMON_LIST])
+ @handle_orchestrator_error('daemon')
+ @RESTController.MethodMap(version=APIVersion.DEFAULT)
+ def list(self, daemon_types: Optional[List[str]] = None):
+ """List all daemons in the cluster. Also filter by the daemon types specified
+
+ :param daemon_types: List of daemon types to filter by.
+ :return: Returns list of daemons.
+ :rtype: list
+ """
+ orch = OrchClient.instance()
+ daemons = [d.to_dict() for d in orch.services.list_daemons()]
+ if daemon_types:
+ daemons = [d for d in daemons if d['daemon_type'] in daemon_types]
+ return daemons
},
{
path: 'upgrade',
+ canActivate: [ModuleStatusGuardService],
component: UpgradeComponent,
- data: { breadcrumbs: 'Cluster/Upgrade' }
+ data: {
+ moduleStatusGuardConfig: {
+ uiApiPath: 'orchestrator',
+ redirectTo: 'error',
+ backend: 'cephadm',
+ section: 'orch',
+ section_info: 'Orchestrator',
+ header: 'Orchestrator is not available'
+ },
+ breadcrumbs: 'Cluster/Upgrade'
+ }
},
{
path: 'perf_counters/:type/:id',
class="nav-tabs"
cdStatefulTab="logs"
[cdStatefulTabDefault]="defaultTab"
- [hidden]="hideNavLinks">
+ [hidden]="!showNavLinks">
<ng-container ngbNavItem="cluster-logs">
<a ngbNavLink
i18n>Cluster Logs</a>
*ngIf="clog">
<div class="btn-group"
role="group"
- *ngIf="clog.length && !hideClusterLogs">
+ *ngIf="clog.length && showClusterLogs">
<cd-download-button [objectItem]="clog"
[textItem]="clogText"
fileName="cluster_log"
- *ngIf="!hideDwnldCpyBtn">
+ *ngIf="showDownloadCopyButton">
</cd-download-button>
<cd-copy-2-clipboard-button
[source]="clogText"
[byId]="false"
- *ngIf="!hideDwnldCpyBtn">
+ *ngIf="showDownloadCopyButton">
</cd-copy-2-clipboard-button>
</div>
- <div class="card-body">
+ <div class="card-body"
+ [ngClass]="{'overflow-auto': scrollable}">
<p *ngFor="let line of clog">
<span class="timestamp">{{ line.stamp | cdDate }}</span>
<span class="priority {{ line.priority | logPriority }}">{{ line.priority }}</span>
i18n>Audit Logs</a>
<ng-template ngbNavContent>
<div class="card bg-light mb-3"
- *ngIf="audit_log && !hideAuditLogs">
+ *ngIf="audit_log && showAuditLogs">
<div class="btn-group"
role="group"
*ngIf="audit_log.length">
<cd-download-button [objectItem]="audit_log"
[textItem]="auditLogText"
fileName="audit_log"
- *ngIf="!hideDwnldCpyBtn">
+ *ngIf="showDownloadCopyButton">
</cd-download-button>
<cd-copy-2-clipboard-button
[source]="auditLogText"
[byId]="false"
- *ngIf="!hideDwnldCpyBtn">
+ *ngIf="showDownloadCopyButton">
</cd-copy-2-clipboard-button>
</div>
<div class="card-body">
<a ngbNavLink
i18n>Daemon Logs</a>
<ng-template ngbNavContent>
- <ng-container *ngIf="!hideDaemonLogs && lokiServiceStatus$ | async as lokiServiceStatus ; else daemonLogsTpl ">
+ <ng-container *ngIf="showDaemonLogs && lokiServiceStatus$ | async as lokiServiceStatus ; else daemonLogsTpl ">
<div *ngIf="promtailServiceStatus$ | async as promtailServiceStatus; else daemonLogsTpl">
<cd-grafana i18n-title
title="Daemon logs"
<ng-template #logFiltersTpl>
<div class="row mb-3"
- *ngIf="!hideFilterTools">
+ *ngIf="showFilterTools">
<div class="col-lg-10 d-flex">
<div class="col-sm-1 me-3">
<label for="logs-priority"
::ng-deep cd-logs ngb-timepicker input.ngb-tp-input {
width: 3.5rem !important;
}
+
+.card-body.overflow-auto {
+ height: 50vh;
+}
})
export class LogsComponent implements OnInit, OnDestroy {
@Input()
- hideClusterLogs = false;
+ showClusterLogs = true;
@Input()
- hideAuditLogs = false;
+ showAuditLogs = true;
@Input()
- hideDaemonLogs = false;
+ showDaemonLogs = true;
@Input()
- hideNavLinks = false;
+ showNavLinks = true;
@Input()
- hideFilterTools = false;
+ showFilterTools = true;
@Input()
- hideDwnldCpyBtn = false;
+ showDownloadCopyButton = true;
@Input()
defaultTab = '';
+ @Input()
+ scrollable = false;
contentData: any;
clog: Array<any>;
import { HttpClientTestingModule } from '@angular/common/http/testing';
import { NO_ERRORS_SCHEMA } from '@angular/core';
import { SharedModule } from '~/app/shared/shared.module';
+import { ToastrModule } from 'ngx-toastr';
describe('UpgradeComponent', () => {
let component: UpgradeComponent;
let fixture: ComponentFixture<UpgradeComponent>;
configureTestBed({
- imports: [HttpClientTestingModule, SharedModule],
+ imports: [HttpClientTestingModule, SharedModule, ToastrModule.forRoot()],
schemas: [NO_ERRORS_SCHEMA],
declarations: [UpgradeComponent],
providers: [UpgradeService]
-<ng-container *ngIf="{upgradeInfo: upgradeInfo$ | async, error: upgradeInfoError$ } as upgrade">
- <div class="row h-25 ms-1"
- *ngIf="!upgrade.upgradeInfoError && upgrade.upgradeInfo as upgradeInfo; else checkUpgrade">
- <ng-container *ngIf="healthData$ | async as healthData">
- <div class="col-lg-3 h-50 d-flex flex-column border justify-content-center align-items-center">
- <span class="bold">Current Version</span>
- <span class="mt-1">{{ version }}</span>
+<div class="row h-25"
+ *cdScope="'configOpt'">
+ <ng-container *ngIf="healthData$ | async as healthData">
+ <cd-card class="col-sm-3 px-3 d-flex"
+ cardTitle="New Version"
+ i18n-cardTitle
+ aria-label="New Version"
+ i18n-aria-label
+ id="newVersionAvailable">
+ <div class="d-flex flex-column justify-content-center align-items-center"
+ *ngIf="info$ | async as info; else checkingForUpgradeStatus">
+ <ng-container *ngIf="info.versions.length > 0; else noUpgradesAvailable">
+ <div i18n-ngbTooltip
+ [ngbTooltip]="(healthData.mgr_map | mgrSummary).total <= 1 ? 'To upgrade, you need minimum 2 mgr daemons.' : ''">
+ <button class="btn btn-accent mt-2"
+ id="upgrade"
+ aria-label="Upgrade now"
+ (click)="upgradeNow(info.versions[info.versions.length - 1])"
+ [disabled]="(healthData.mgr_map | mgrSummary).total <= 1"
+ i18n>Upgrade to {{ info.versions[info.versions.length - 1] }}</button>
+ </div>
+ <a class="mt-2 link-primary mb-2"
+ (click)="startUpgradeModal()"
+ i18n>Select another version...</a>
+ </ng-container>
+ </div>
+ </cd-card>
+
+ <cd-card class="col-sm-3 px-3 d-flex"
+ cardTitle="Current Version"
+ i18n-cardTitle
+ aria-label="Current Version"
+ i18n-aria-label
+ id="currentVersion">
+ <div class="d-flex flex-column justify-content-center align-items-center">
+ <h5>{{ version }}</h5>
</div>
- <div class="col-lg-3 h-50 d-flex flex-column border justify-content-center align-items-center">
- <span class="bold">Cluster Status</span>
+ </cd-card>
+
+ <cd-card class="col-sm-3 px-3 d-flex"
+ cardTitle="Cluster Status"
+ i18n-cardTitle
+ aria-label="Cluster Status"
+ i18n-aria-label
+ id="clusterStatus">
+ <div class="d-flex flex-column justify-content-center align-items-center">
<ng-template #healthChecks>
<ul>
<li *ngFor="let check of healthData.health.checks">
[ngStyle]="healthData.health.status | healthColor"
[ngbPopover]="healthChecks"
popoverClass="info-card-popover-cluster-status">
- {{ healthData.health.status | healthLabel | uppercase }}
+ {{ healthData.health.status | healthLabel | uppercase }}
<i *ngIf="healthData.health?.status !== 'HEALTH_OK'"
class="fa fa-exclamation-triangle"></i>
</div>
</div>
- <div class="col-lg-3 h-50 d-flex flex-column border justify-content-center align-items-center">
- <span class="bold">Upgrade Status</span>
- <ng-container *ngIf="upgradeInfo.versions.length > 0; else noUpgradesAvailable">
- <span class="mt-2"
- i18n>
- <i [ngClass]="[icons.up]"
- class="text-info"></i>
- Upgrade available</span>
- <div i18n-ngbTooltip
- [ngbTooltip]="(healthData.mgr_map | mgrSummary).total <= 1 ? 'To upgrade, you need minimum 2 mgr daemons.' : ''">
- <button class="btn btn-accent mt-2"
- id="upgrade"
- aria-label="Upgrade now"
- [disabled]="(healthData.mgr_map | mgrSummary).total <= 1"
- (click)="startUpgradeModal()"
- i18n>Upgrade now</button>
- </div>
- </ng-container>
- </div>
- <div class="col-lg-3 h-50 d-flex flex-column border justify-content-center align-items-center">
- <span class="bold">MGR Count</span>
- <span class="mt-1">
+ </cd-card>
+
+ <cd-card class="col-sm-3 px-3 d-flex"
+ cardTitle="MGR Count"
+ i18n-cardTitle
+ aria-label="MGR Count"
+ i18n-aria-label
+ id="mgrCount">
+ <div class="d-flex flex-column justify-content-center align-items-center">
+ <h5>
<i class="text-success"
[ngClass]="[icons.success]"
*ngIf="(healthData.mgr_map | mgrSummary).total > 1; else warningIcon">
</i>
{{ (healthData.mgr_map | mgrSummary).total }}
- </span>
+ </h5>
</div>
+ </cd-card>
- <div class="d-flex mt-3 p-0">
- <dl class="w-50"
- *ngIf="fsid$ | async as fsid">
- <dt class="bold mt-5"
- i18n>Cluster FSID</dt>
- <dd class="mt-2">{{ fsid }}</dd>
+ <div class="d-flex mt-3">
+ <dl class="w-50"
+ *ngIf="fsid$ | async as fsid">
+ <dt class="bold mt-5"
+ i18n>Cluster FSID</dt>
+ <dd class="mt-2">{{ fsid }}</dd>
+
+ <ng-container *ngIf="info$ | async as info; else loadingDetails">
<dt class="bold mt-5"
i18n>Release Image</dt>
- <dd class="mt-2">{{ upgradeInfo.image }}</dd>
+ <dd class="mt-2">{{ info.image }}</dd>
<dt class="bold mt-5"
i18n>Registry</dt>
- <dd class="mt-2">{{ upgradeInfo.registry }}</dd>
- </dl>
+ <dd class="mt-2">{{ info.registry }}</dd>
+ </ng-container>
+ </dl>
+ <div class="w-50">
+ <ng-container *ngIf="daemons$ | async as daemons">
+ <legend class="cd-header"
+ i18n>Daemon versions</legend>
+ <div>
+ <cd-table #daemonsTable
+ [data]="daemons"
+ selectionType="single"
+ [columns]="columns"
+ columnMode="flex"
+ [limit]="5">
+ </cd-table>
+ </div>
+ </ng-container>
</div>
- </ng-container>
- </div>
-</ng-container>
+ </div>
-<ng-template #checkUpgrade>
- <div class="row h-75 justify-content-center align-items-center">
- <h3 class="mt-1 bold text-center"
- id="checking-for-upgrades"
- i18n>Checking for upgrades
- <i [ngClass]="[icons.spin, icons.spinner]"></i>
- </h3>
- </div>
-</ng-template>
+ <legend class="cd-header"
+ i18n>Cluster logs</legend>
+ <cd-logs [showAuditLogs]="false"
+ [showDaemonLogs]="false"
+ [showNavLinks]="false"
+ [showFilterTools]="false"
+ [showDownloadCopyButton]="false"
+ defaultTab="cluster-logs"
+ [scrollable]="true"></cd-logs>
+ </ng-container>
+</div>
<ng-template #noUpgradesAvailable>
<span class="mt-1"
title="To upgrade, you need minimum 2 mgr daemons.">
</i>
</ng-template>
+
+<ng-template #checkingForUpgradeStatus>
+ <div class="d-flex flex-column justify-content-center align-items-center"
+ *ngIf="!errorMessage; else upgradeStatusError">
+ <button class="btn btn-accent mt-2 mb-4"
+ id="upgrade"
+ aria-label="Upgrade now"
+ [disabled]="true"
+ i18n>Checking for upgrades
+ <i [ngClass]="[icons.spin, icons.spinner]"></i>
+ </button>
+ </div>
+</ng-template>
+
+<ng-template #loadingDetails>
+ <div class="w-50"
+ *ngIf="!errorMessage; else upgradeInfoError">
+ <span class="text-info justify-content-center align-items-center"
+ i18n>Fetching registry informations
+ <i [ngClass]="[icons.spin, icons.spinner]"></i>
+ </span>
+ </div>
+</ng-template>
+
+<ng-template #upgradeStatusError>
+ <div class="d-flex flex-column justify-content-center align-items-center">
+ <span class="text-danger mt-2 mb-4"
+ id="upgrade-status-error"
+ i18n>
+ <i [ngClass]="[icons.danger]"></i>
+ {{ errorMessage }}
+ </span>
+ </div>
+</ng-template>
+
+<ng-template #upgradeInfoError>
+ <span class="text-danger justify-content-center align-items-center"
+ i18n>
+ <i [ngClass]="[icons.danger]"></i>
+ Failed to fetch registry informations
+ </span>
+</ng-template>
import { UpgradeService } from '~/app/shared/api/upgrade.service';
import { HttpClientTestingModule } from '@angular/common/http/testing';
import { UpgradeInfoInterface } from '~/app/shared/models/upgrade.interface';
-import { NO_ERRORS_SCHEMA } from '@angular/core';
import { HealthService } from '~/app/shared/api/health.service';
import { SharedModule } from '~/app/shared/shared.module';
+import { LogsComponent } from '../logs/logs.component';
+import { NgbNavModule } from '@ng-bootstrap/ng-bootstrap';
+import { NO_ERRORS_SCHEMA } from '@angular/core';
+import { ToastrModule } from 'ngx-toastr';
+import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
export class SummaryServiceMock {
summaryDataSource = new BehaviorSubject({
};
configureTestBed({
- imports: [HttpClientTestingModule, SharedModule],
+ imports: [HttpClientTestingModule, SharedModule, NgbNavModule, ToastrModule.forRoot()],
+ declarations: [UpgradeComponent, LogsComponent],
schemas: [NO_ERRORS_SCHEMA],
- declarations: [UpgradeComponent],
providers: [UpgradeService, { provide: SummaryService, useClass: SummaryServiceMock }]
});
upgradeInfoSpy = spyOn(TestBed.inject(UpgradeService), 'list').and.callFake(() => of(null));
getHealthSpy = spyOn(TestBed.inject(HealthService), 'getMinimalHealth');
getHealthSpy.and.returnValue(of(healthPayload));
+ const upgradeInfoPayload = {
+ image: 'quay.io/ceph-test/ceph',
+ registry: 'quay.io',
+ versions: ['18.1.0', '18.1.1', '18.1.2']
+ };
+ upgradeInfoSpy.and.returnValue(of(upgradeInfoPayload));
+ spyOn(TestBed.inject(AuthStorageService), 'getPermissions').and.callFake(() => ({
+ configOpt: { read: true }
+ }));
fixture.detectChanges();
});
});
it('should load the view once check for upgrade is done', () => {
- const upgradeInfoPayload = {
- image: 'quay.io/ceph-test/ceph',
- registry: 'quay.io',
- versions: ['18.1.0', '18.1.1', '18.1.2']
- };
- upgradeInfoSpy.and.returnValue(of(upgradeInfoPayload));
component.ngOnInit();
fixture.detectChanges();
- const firstCellSpan = fixture.debugElement.nativeElement.querySelector('span');
- expect(firstCellSpan.textContent).toBe('Current Version');
+ const firstCellSpan = fixture.debugElement.nativeElement.querySelector(
+ 'cd-card[cardTitle="New Version"] .card-title'
+ );
+ expect(firstCellSpan.textContent).toContain('New Version');
});
it('should show button to Upgrade if a new version is available', () => {
});
it('should show the loading screen while the api call is pending', () => {
- const loading = fixture.debugElement.nativeElement.querySelector('h3');
- expect(loading.textContent).toBe('Checking for upgrades ');
+ upgradeInfoSpy.and.returnValue(of(null));
+ component.ngOnInit();
+ fixture.detectChanges();
+ const loading = fixture.debugElement.nativeElement.querySelector('#newVersionAvailable');
+ expect(loading.textContent).toContain('Checking for upgrade');
});
it('should upgrade only when there are more than 1 mgr', () => {
+ // Only one mgr in payload
const upgradeInfoPayload = {
image: 'quay.io/ceph-test/ceph',
registry: 'quay.io',
fixture.detectChanges();
expect(upgradeBtn.disabled).toBeFalsy();
});
+
+ it('should show the error message when the upgrade fetch fails', () => {
+ upgradeInfoSpy.and.returnValue(of(null));
+ component.errorMessage = 'Failed to retrieve';
+ component.ngOnInit();
+ fixture.detectChanges();
+ const loading = fixture.debugElement.nativeElement.querySelector('#upgrade-status-error');
+ expect(loading.textContent).toContain('Failed to retrieve');
+ });
});
import { Component, OnInit } from '@angular/core';
import { Observable, of } from 'rxjs';
-import { catchError, ignoreElements, tap } from 'rxjs/operators';
+import { catchError, publishReplay, refCount, tap } from 'rxjs/operators';
+import { DaemonService } from '~/app/shared/api/daemon.service';
import { HealthService } from '~/app/shared/api/health.service';
import { UpgradeService } from '~/app/shared/api/upgrade.service';
import { Icons } from '~/app/shared/enum/icons.enum';
+import { NotificationType } from '~/app/shared/enum/notification-type.enum';
+import { CdTableColumn } from '~/app/shared/models/cd-table-column';
+import { Daemon } from '~/app/shared/models/daemon.interface';
import { Permission } from '~/app/shared/models/permissions';
import { UpgradeInfoInterface } from '~/app/shared/models/upgrade.interface';
-import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap';
+import { NotificationService } from '~/app/shared/services/notification.service';
import { SummaryService } from '~/app/shared/services/summary.service';
import { ModalService } from '~/app/shared/services/modal.service';
import { UpgradeStartModalComponent } from './upgrade-form/upgrade-start-modal.component';
})
export class UpgradeComponent implements OnInit {
version: string;
- upgradeInfo$: Observable<UpgradeInfoInterface>;
- upgradeInfoError$: Observable<any>;
+ info$: Observable<UpgradeInfoInterface>;
permission: Permission;
healthData$: Observable<any>;
+ daemons$: Observable<Daemon[]>;
fsid$: Observable<any>;
modalRef: NgbModalRef;
upgradableVersions: string[];
+ errorMessage: string;
+
+ columns: CdTableColumn[] = [];
icons = Icons;
private modalService: ModalService,
private summaryService: SummaryService,
private upgradeService: UpgradeService,
- private authStorageService: AuthStorageService,
- private healthService: HealthService
- ) {
- this.permission = this.authStorageService.getPermissions().configOpt;
- }
+ private healthService: HealthService,
+ private daemonService: DaemonService,
+ private notificationService: NotificationService
+ ) {}
ngOnInit(): void {
+ this.columns = [
+ {
+ name: $localize`Daemon name`,
+ prop: 'daemon_name',
+ flexGrow: 1,
+ filterable: true
+ },
+ {
+ name: $localize`Version`,
+ prop: 'version',
+ flexGrow: 1,
+ filterable: true
+ }
+ ];
+
this.summaryService.subscribe((summary) => {
const version = summary.version.replace('ceph version ', '').split('-');
this.version = version[0];
});
- this.upgradeInfo$ = this.upgradeService
- .list()
- .pipe(
- tap((upgradeInfo: UpgradeInfoInterface) => (this.upgradableVersions = upgradeInfo.versions))
- );
- this.upgradeInfoError$ = this.upgradeInfo$?.pipe(
- ignoreElements(),
- catchError((error) => of(error))
+ this.info$ = this.upgradeService.list().pipe(
+ tap((upgradeInfo: UpgradeInfoInterface) => (this.upgradableVersions = upgradeInfo.versions)),
+ publishReplay(1),
+ refCount(),
+ catchError((err) => {
+ err.preventDefault();
+ this.errorMessage = $localize`Not retrieving upgrades`;
+ this.notificationService.show(
+ NotificationType.error,
+ this.errorMessage,
+ err.error.detail || err.error.message
+ );
+ return of(null);
+ })
);
this.healthData$ = this.healthService.getMinimalHealth();
+ this.daemons$ = this.daemonService.list(this.upgradeService.upgradableServiceTypes);
this.fsid$ = this.healthService.getClusterFsid();
}
startUpgradeModal() {
this.modalRef = this.modalService.show(UpgradeStartModalComponent, {
- versions: this.upgradableVersions.sort()
+ versions: this.upgradableVersions
+ });
+ }
+
+ upgradeNow(version: string) {
+ this.upgradeService.start(version).subscribe({
+ error: (error) => {
+ this.notificationService.show(
+ NotificationType.error,
+ $localize`Failed to start the upgrade`,
+ error
+ );
+ },
+ complete: () => {
+ this.notificationService.show(
+ NotificationType.success,
+ $localize`Started upgrading the cluster`
+ );
+ }
});
}
}
+++ /dev/null
-<div class="d-flex pl-1 pb-2 pt-2">
- <div class="ms-2 me-auto">
- <a [routerLink]="link"
- *ngIf="link && total > 0; else noLinkTitle"
- [ngPlural]="total"
- i18n>
- {{ total }}
- <ng-template ngPluralCase="=0">{{ title }}</ng-template>
- <ng-template ngPluralCase="=1">{{ title }}</ng-template>
- <ng-template ngPluralCase="other">{{ title }}s</ng-template>
- </a>
- </div>
-
- <ng-container [ngSwitch]="summaryType">
- <ng-container *ngSwitchCase="'iscsi'">
- <ng-container *ngTemplateOutlet="iscsiSummary"></ng-container>
- </ng-container>
- <ng-container *ngSwitchCase="'osd'">
- <ng-container *ngTemplateOutlet="osdSummary"></ng-container>
- </ng-container>
- <ng-container *ngSwitchCase="'simplified'">
- <ng-container *ngTemplateOutlet="simplifiedSummary"></ng-container>
- </ng-container>
- <ng-container *ngSwitchDefault>
- <ng-container *ngTemplateOutlet="defaultSummary"></ng-container>
- </ng-container>
- </ng-container>
-</div>
-
-<ng-template #defaultSummary>
- <span *ngIf="data.success || data.categoryPgAmount?.clean || (data.success === 0 && data.total === 0)">
- <span *ngIf="data.success || (data.success === 0 && data.total === 0)">
- {{ data.success }}
- </span>
- <span *ngIf="data.categoryPgAmount?.clean">
- {{ data.categoryPgAmount?.clean }}
- </span>
- <i class="text-success"
- [ngClass]="[icons.success]">
- </i>
- </span>
- <span *ngIf="data.info"
- class="ms-2">
- <span *ngIf="data.info">
- {{ data.info }}
- </span>
- <i class="text-info"
- [ngClass]="[icons.danger]">
- </i>
- </span>
- <span *ngIf="data.warn || data.categoryPgAmount?.warning"
- class="ms-2">
- <span *ngIf="data.warn">
- {{ data.warn }}
- </span>
- <span *ngIf="data.categoryPgAmount?.warning">
- {{ data.categoryPgAmount?.warning }}
- </span>
- <i class="text-warning"
- [ngClass]="[icons.warning]">
- </i>
- </span>
- <span *ngIf="data.error || data.categoryPgAmount?.unknown"
- class="ms-2">
- <span *ngIf="data.error">
- {{ data.error }}
- </span>
- <span *ngIf="data.categoryPgAmount?.unknown">
- {{ data.categoryPgAmount?.unknown }}
- </span>
- <i class="text-danger"
- [ngClass]="[icons.danger]">
- </i>
- </span>
- <span *ngIf="data.categoryPgAmount?.working"
- class="ms-2">
- <span *ngIf="data.categoryPgAmount?.working">
- {{ data.categoryPgAmount?.working }}
- </span>
- <i class="text-warning"
- [ngClass]="[icons.spinner, icons.spin]">
- </i>
- </span>
-</ng-template>
-
-<ng-template #osdSummary>
- <span *ngIf="data.up === data.in">
- {{ data.up }}
- <i class="text-success"
- [ngClass]="[icons.success]">
- </i>
- </span>
- <span *ngIf="data.up !== data.in">
- {{ data.up }}
- <span class="fw-bold text-success">
- up
- </span>
- </span>
- <span *ngIf="data.in !== data.up"
- class="ms-2">
- {{ data.in }}
- <span class="fw-bold text-success">
- in
- </span>
- </span>
- <span *ngIf="data.down"
- class="ms-2">
- {{ data.down }}
- <span class="fw-bold text-danger me-2">
- down
- </span>
- </span>
- <span *ngIf="data.out"
- class="ms-2">
- {{ data.out }}
- <span class="fw-bold text-danger me-2">
- out
- </span>
- </span>
- <span *ngIf="data.nearfull"
- class="ms-2">
- {{ data.nearfull }}
- <span class="fw-bold text-warning me-2">
- nearfull</span></span>
- <span *ngIf="data.full"
- class="ms-2">
- {{ data.full }}
- <span class="fw-bold text-danger">
- full
- </span>
- </span>
-</ng-template>
-
-<ng-template #iscsiSummary>
- <span>
- {{ data.up }}
- <i class="text-success"
- *ngIf="data.up || data.up === 0"
- [ngClass]="[icons.success]">
- </i>
- </span>
- <span *ngIf="data.down"
- class="ms-2">
- {{ data.down }}
- <i class="text-danger"
- [ngClass]="[icons.danger]">
- </i>
- </span>
-</ng-template>
-
-<ng-template #simplifiedSummary>
- <span>
- {{ data }}
- <i class="text-success"
- [ngClass]="[icons.success]"></i>
- </span>
-</ng-template>
-
-<ng-template #noLinkTitle>
- <span *ngIf="total || total === 0"
- [ngPlural]="total">
- {{ total }}
- <ng-template ngPluralCase="=0">{{ title }}</ng-template>
- <ng-template ngPluralCase="=1">{{ title }}</ng-template>
- <ng-template ngPluralCase="other">{{ title }}s</ng-template>
- </span>
-</ng-template>
+++ /dev/null
-import { ComponentFixture, TestBed } from '@angular/core/testing';
-
-import { CardRowComponent } from './card-row.component';
-
-describe('CardRowComponent', () => {
- let component: CardRowComponent;
- let fixture: ComponentFixture<CardRowComponent>;
-
- beforeEach(async () => {
- await TestBed.configureTestingModule({
- declarations: [CardRowComponent]
- }).compileComponents();
- });
-
- beforeEach(() => {
- fixture = TestBed.createComponent(CardRowComponent);
- component = fixture.componentInstance;
- });
-
- it('should create', () => {
- expect(component).toBeTruthy();
- });
-});
+++ /dev/null
-import { Component, Input, OnChanges } from '@angular/core';
-import { Icons } from '~/app/shared/enum/icons.enum';
-
-@Component({
- selector: 'cd-card-row',
- templateUrl: './card-row.component.html',
- styleUrls: ['./card-row.component.scss']
-})
-export class CardRowComponent implements OnChanges {
- @Input()
- title: string;
-
- @Input()
- link: string;
-
- @Input()
- data: any;
-
- @Input()
- summaryType = 'default';
-
- icons = Icons;
- total: number;
-
- ngOnChanges(): void {
- if (this.data.total || this.data.total === 0) {
- this.total = this.data.total;
- } else if (this.summaryType === 'iscsi') {
- this.total = this.data.up + this.data.down || 0;
- } else {
- this.total = this.data;
- }
- }
-}
+++ /dev/null
-<div class="card shadow-sm flex-fill">
- <h4 class="card-title mt-4 ms-4 mb-0">
- {{ cardTitle }}
- </h4>
- <div class="card-body ps-0 pe-0">
- <ng-content></ng-content>
- </div>
-</div>
+++ /dev/null
-.card-body {
- display: flex;
- flex-direction: column;
- justify-content: space-evenly;
-}
+++ /dev/null
-import { ComponentFixture, TestBed } from '@angular/core/testing';
-import { RouterTestingModule } from '@angular/router/testing';
-
-import { configureTestBed } from '~/testing/unit-test-helper';
-import { CardComponent } from './card.component';
-
-describe('CardComponent', () => {
- let component: CardComponent;
- let fixture: ComponentFixture<CardComponent>;
-
- configureTestBed({
- imports: [RouterTestingModule],
- declarations: [CardComponent]
- });
-
- beforeEach(() => {
- fixture = TestBed.createComponent(CardComponent);
- component = fixture.componentInstance;
- });
-
- it('should create', () => {
- expect(component).toBeTruthy();
- });
-
- it('Setting cards title makes title visible', () => {
- const title = 'Card Title';
- component.cardTitle = title;
- fixture.detectChanges();
- const titleDiv = fixture.debugElement.nativeElement.querySelector('.card-title');
-
- expect(titleDiv.textContent).toContain(title);
- });
-});
+++ /dev/null
-import { Component, Input } from '@angular/core';
-
-@Component({
- selector: 'cd-card',
- templateUrl: './card.component.html',
- styleUrls: ['./card.component.scss']
-})
-export class CardComponent {
- @Input()
- cardTitle: string;
-}
import { SharedModule } from '~/app/shared/shared.module';
import { CephSharedModule } from '../shared/ceph-shared.module';
-import { CardComponent } from './card/card.component';
import { DashboardAreaChartComponent } from './dashboard-area-chart/dashboard-area-chart.component';
import { DashboardPieComponent } from './dashboard-pie/dashboard-pie.component';
import { DashboardTimeSelectorComponent } from './dashboard-time-selector/dashboard-time-selector.component';
import { DashboardV3Component } from './dashboard/dashboard-v3.component';
-import { CardRowComponent } from './card-row/card-row.component';
import { PgSummaryPipe } from './pg-summary.pipe';
@NgModule({
declarations: [
DashboardV3Component,
- CardComponent,
DashboardPieComponent,
- CardRowComponent,
PgSummaryPipe,
DashboardAreaChartComponent,
DashboardTimeSelectorComponent
],
- exports: [
- DashboardV3Component,
- CardComponent,
- CardRowComponent,
- DashboardAreaChartComponent,
- DashboardTimeSelectorComponent
- ]
+ exports: [DashboardV3Component, DashboardAreaChartComponent, DashboardTimeSelectorComponent]
})
export class DashboardV3Module {}
import { SharedModule } from '~/app/shared/shared.module';
import { configureTestBed } from '~/testing/unit-test-helper';
import { PgCategoryService } from '../../shared/pg-category.service';
-import { CardRowComponent } from '../card-row/card-row.component';
-import { CardComponent } from '../card/card.component';
import { DashboardPieComponent } from '../dashboard-pie/dashboard-pie.component';
import { PgSummaryPipe } from '../pg-summary.pipe';
import { DashboardV3Component } from './dashboard-v3.component';
configureTestBed({
imports: [RouterTestingModule, HttpClientTestingModule, ToastrModule.forRoot(), SharedModule],
- declarations: [
- DashboardV3Component,
- CardComponent,
- DashboardPieComponent,
- CardRowComponent,
- PgSummaryPipe
- ],
+ declarations: [DashboardV3Component, DashboardPieComponent, PgSummaryPipe],
schemas: [NO_ERRORS_SCHEMA],
providers: [
{ provide: SummaryService, useClass: SummaryServiceMock },
import { RgwDaemonService } from '~/app/shared/api/rgw-daemon.service';
import { RgwDaemon } from '../models/rgw-daemon';
import { HttpClientTestingModule } from '@angular/common/http/testing';
-import { CardComponent } from '../../dashboard-v3/card/card.component';
-import { CardRowComponent } from '../../dashboard-v3/card-row/card-row.component';
import { DimlessBinaryPipe } from '~/app/shared/pipes/dimless-binary.pipe';
import { RgwRealmService } from '~/app/shared/api/rgw-realm.service';
import { RgwZonegroupService } from '~/app/shared/api/rgw-zonegroup.service';
import { RgwBucketService } from '~/app/shared/api/rgw-bucket.service';
import { RgwUserService } from '~/app/shared/api/rgw-user.service';
import { HealthService } from '~/app/shared/api/health.service';
+import { CardRowComponent } from '~/app/shared/components/card-row/card-row.component';
+import { CardComponent } from '~/app/shared/components/card/card.component';
+import { NO_ERRORS_SCHEMA } from '@angular/core';
describe('RgwOverviewDashboardComponent', () => {
let component: RgwOverviewDashboardComponent;
CardRowComponent,
DimlessBinaryPipe
],
+ schemas: [NO_ERRORS_SCHEMA],
imports: [HttpClientTestingModule]
}).compileComponents();
});
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
+import { Observable } from 'rxjs';
import { cdEncode } from '~/app/shared/decorators/cd-encode';
+import { Daemon } from '../models/daemon.interface';
@cdEncode
@Injectable({
}
);
}
+
+ list(daemonTypes: string[]): Observable<Daemon[]> {
+ return this.http.get<Daemon[]>(this.url, {
+ params: { daemon_types: daemonTypes }
+ });
+ }
}
export class UpgradeService extends ApiClient {
baseURL = 'api/cluster/upgrade';
+ upgradableServiceTypes = [
+ 'mgr',
+ 'mon',
+ 'crash',
+ 'osd',
+ 'mds',
+ 'rgw',
+ 'rbd-mirror',
+ 'cephfs-mirror',
+ 'iscsi',
+ 'nfs'
+ ];
+
constructor(private http: HttpClient, private summaryService: SummaryService) {
super();
}
cVersion[0] === tVersion[0] && (cVersion[1] < tVersion[1] || cVersion[2] < tVersion[2])
);
});
- upgradeInfo.versions = upgradableVersions;
+ upgradeInfo.versions = upgradableVersions.sort();
return upgradeInfo;
}
--- /dev/null
+<div class="d-flex pl-1 pb-2 pt-2">
+ <div class="ms-2 me-auto">
+ <a [routerLink]="link"
+ *ngIf="link && total > 0; else noLinkTitle"
+ [ngPlural]="total"
+ i18n>
+ {{ total }}
+ <ng-template ngPluralCase="=0">{{ title }}</ng-template>
+ <ng-template ngPluralCase="=1">{{ title }}</ng-template>
+ <ng-template ngPluralCase="other">{{ title }}s</ng-template>
+ </a>
+ </div>
+
+ <ng-container [ngSwitch]="summaryType">
+ <ng-container *ngSwitchCase="'iscsi'">
+ <ng-container *ngTemplateOutlet="iscsiSummary"></ng-container>
+ </ng-container>
+ <ng-container *ngSwitchCase="'osd'">
+ <ng-container *ngTemplateOutlet="osdSummary"></ng-container>
+ </ng-container>
+ <ng-container *ngSwitchCase="'simplified'">
+ <ng-container *ngTemplateOutlet="simplifiedSummary"></ng-container>
+ </ng-container>
+ <ng-container *ngSwitchDefault>
+ <ng-container *ngTemplateOutlet="defaultSummary"></ng-container>
+ </ng-container>
+ </ng-container>
+</div>
+
+<ng-template #defaultSummary>
+ <span *ngIf="data.success || data.categoryPgAmount?.clean || (data.success === 0 && data.total === 0)">
+ <span *ngIf="data.success || (data.success === 0 && data.total === 0)">
+ {{ data.success }}
+ </span>
+ <span *ngIf="data.categoryPgAmount?.clean">
+ {{ data.categoryPgAmount?.clean }}
+ </span>
+ <i class="text-success"
+ [ngClass]="[icons.success]">
+ </i>
+ </span>
+ <span *ngIf="data.info"
+ class="ms-2">
+ <span *ngIf="data.info">
+ {{ data.info }}
+ </span>
+ <i class="text-info"
+ [ngClass]="[icons.danger]">
+ </i>
+ </span>
+ <span *ngIf="data.warn || data.categoryPgAmount?.warning"
+ class="ms-2">
+ <span *ngIf="data.warn">
+ {{ data.warn }}
+ </span>
+ <span *ngIf="data.categoryPgAmount?.warning">
+ {{ data.categoryPgAmount?.warning }}
+ </span>
+ <i class="text-warning"
+ [ngClass]="[icons.warning]">
+ </i>
+ </span>
+ <span *ngIf="data.error || data.categoryPgAmount?.unknown"
+ class="ms-2">
+ <span *ngIf="data.error">
+ {{ data.error }}
+ </span>
+ <span *ngIf="data.categoryPgAmount?.unknown">
+ {{ data.categoryPgAmount?.unknown }}
+ </span>
+ <i class="text-danger"
+ [ngClass]="[icons.danger]">
+ </i>
+ </span>
+ <span *ngIf="data.categoryPgAmount?.working"
+ class="ms-2">
+ <span *ngIf="data.categoryPgAmount?.working">
+ {{ data.categoryPgAmount?.working }}
+ </span>
+ <i class="text-warning"
+ [ngClass]="[icons.spinner, icons.spin]">
+ </i>
+ </span>
+</ng-template>
+
+<ng-template #osdSummary>
+ <span *ngIf="data.up === data.in">
+ {{ data.up }}
+ <i class="text-success"
+ [ngClass]="[icons.success]">
+ </i>
+ </span>
+ <span *ngIf="data.up !== data.in">
+ {{ data.up }}
+ <span class="fw-bold text-success">
+ up
+ </span>
+ </span>
+ <span *ngIf="data.in !== data.up"
+ class="ms-2">
+ {{ data.in }}
+ <span class="fw-bold text-success">
+ in
+ </span>
+ </span>
+ <span *ngIf="data.down"
+ class="ms-2">
+ {{ data.down }}
+ <span class="fw-bold text-danger me-2">
+ down
+ </span>
+ </span>
+ <span *ngIf="data.out"
+ class="ms-2">
+ {{ data.out }}
+ <span class="fw-bold text-danger me-2">
+ out
+ </span>
+ </span>
+ <span *ngIf="data.nearfull"
+ class="ms-2">
+ {{ data.nearfull }}
+ <span class="fw-bold text-warning me-2">
+ nearfull</span></span>
+ <span *ngIf="data.full"
+ class="ms-2">
+ {{ data.full }}
+ <span class="fw-bold text-danger">
+ full
+ </span>
+ </span>
+</ng-template>
+
+<ng-template #iscsiSummary>
+ <span>
+ {{ data.up }}
+ <i class="text-success"
+ *ngIf="data.up || data.up === 0"
+ [ngClass]="[icons.success]">
+ </i>
+ </span>
+ <span *ngIf="data.down"
+ class="ms-2">
+ {{ data.down }}
+ <i class="text-danger"
+ [ngClass]="[icons.danger]">
+ </i>
+ </span>
+</ng-template>
+
+<ng-template #simplifiedSummary>
+ <span>
+ {{ data }}
+ <i class="text-success"
+ [ngClass]="[icons.success]"></i>
+ </span>
+</ng-template>
+
+<ng-template #noLinkTitle>
+ <span *ngIf="total || total === 0"
+ [ngPlural]="total">
+ {{ total }}
+ <ng-template ngPluralCase="=0">{{ title }}</ng-template>
+ <ng-template ngPluralCase="=1">{{ title }}</ng-template>
+ <ng-template ngPluralCase="other">{{ title }}s</ng-template>
+ </span>
+</ng-template>
--- /dev/null
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { CardRowComponent } from './card-row.component';
+
+describe('CardRowComponent', () => {
+ let component: CardRowComponent;
+ let fixture: ComponentFixture<CardRowComponent>;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ declarations: [CardRowComponent]
+ }).compileComponents();
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(CardRowComponent);
+ component = fixture.componentInstance;
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
--- /dev/null
+import { Component, Input, OnChanges } from '@angular/core';
+import { Icons } from '~/app/shared/enum/icons.enum';
+
+@Component({
+ selector: 'cd-card-row',
+ templateUrl: './card-row.component.html',
+ styleUrls: ['./card-row.component.scss']
+})
+export class CardRowComponent implements OnChanges {
+ @Input()
+ title: string;
+
+ @Input()
+ link: string;
+
+ @Input()
+ data: any;
+
+ @Input()
+ summaryType = 'default';
+
+ icons = Icons;
+ total: number;
+
+ ngOnChanges(): void {
+ if (this.data.total || this.data.total === 0) {
+ this.total = this.data.total;
+ } else if (this.summaryType === 'iscsi') {
+ this.total = this.data.up + this.data.down || 0;
+ } else {
+ this.total = this.data;
+ }
+ }
+}
--- /dev/null
+<div class="card shadow-sm flex-fill">
+ <h4 class="card-title mt-4 ms-4 mb-0">
+ {{ cardTitle }}
+ </h4>
+ <div class="card-body ps-0 pe-0">
+ <ng-content></ng-content>
+ </div>
+</div>
--- /dev/null
+.card-body {
+ display: flex;
+ flex-direction: column;
+ justify-content: space-evenly;
+}
--- /dev/null
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { RouterTestingModule } from '@angular/router/testing';
+
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { CardComponent } from './card.component';
+
+describe('CardComponent', () => {
+ let component: CardComponent;
+ let fixture: ComponentFixture<CardComponent>;
+
+ configureTestBed({
+ imports: [RouterTestingModule],
+ declarations: [CardComponent]
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(CardComponent);
+ component = fixture.componentInstance;
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+
+ it('Setting cards title makes title visible', () => {
+ const title = 'Card Title';
+ component.cardTitle = title;
+ fixture.detectChanges();
+ const titleDiv = fixture.debugElement.nativeElement.querySelector('.card-title');
+
+ expect(titleDiv.textContent).toContain(title);
+ });
+});
--- /dev/null
+import { Component, Input } from '@angular/core';
+
+@Component({
+ selector: 'cd-card',
+ templateUrl: './card.component.html',
+ styleUrls: ['./card.component.scss']
+})
+export class CardComponent {
+ @Input()
+ cardTitle: string;
+}
import { TelemetryNotificationComponent } from './telemetry-notification/telemetry-notification.component';
import { UsageBarComponent } from './usage-bar/usage-bar.component';
import { WizardComponent } from './wizard/wizard.component';
+import { CardComponent } from './card/card.component';
+import { CardRowComponent } from './card-row/card-row.component';
@NgModule({
imports: [
WizardComponent,
CustomLoginBannerComponent,
CdLabelComponent,
- ColorClassFromTextPipe
+ ColorClassFromTextPipe,
+ CardComponent,
+ CardRowComponent
],
providers: [],
exports: [
MotdComponent,
WizardComponent,
CustomLoginBannerComponent,
- CdLabelComponent
+ CdLabelComponent,
+ CardComponent,
+ CardRowComponent
]
})
export class ComponentsModule {}
import { CdUserConfig } from '~/app/shared/models/cd-user-config';
import { TimerService } from '~/app/shared/services/timer.service';
+const TABLE_LIST_LIMIT = 10;
@Component({
selector: 'cd-table',
templateUrl: './table.component.html',
footer? = true;
// Page size to show. Set to 0 to show unlimited number of rows.
@Input()
- limit? = 10;
+ limit? = TABLE_LIST_LIMIT;
@Input()
maxLimit? = 9999;
// Has the row details?
this._loadUserConfig();
this._initUserConfigAutoSave();
}
- if (!this.userConfig.limit) {
+ if (this.limit !== TABLE_LIST_LIMIT || !this.userConfig.limit) {
this.userConfig.limit = this.limit;
}
if (!(this.userConfig.offset >= 0)) {
- jwt: []
tags:
- CrushRule
+ /api/daemon:
+ get:
+ description: "List all daemons in the cluster. Also filter by the daemon types\
+ \ specified\n\n :param daemon_types: List of daemon types to filter\
+ \ by.\n :return: Returns list of daemons.\n :rtype: list\n \
+ \ "
+ parameters:
+ - allowEmptyValue: true
+ in: query
+ name: daemon_types
+ schema:
+ type: string
+ 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:
+ - Daemon
/api/daemon/{daemon_name}:
put:
parameters:
'component': None
})
self.assertStatus(400)
+
+ def test_daemon_list(self):
+ with patch_orch(True):
+ self._get(f'{self.URL_DAEMON}')
+ self.assertStatus(200)