From a93e4afc74db257eb45adb5a32d239379b30b2b6 Mon Sep 17 00:00:00 2001 From: Nizamudeen A Date: Fri, 7 Jul 2023 11:20:21 +0530 Subject: [PATCH] mgr/dashboard: cluster upgrade base UI component - Base layout for the Upgrade Component - Shows the Upgrade available button when an upgrade is available Fixes: https://tracker.ceph.com/issues/61927 Signed-off-by: Nizamudeen A (cherry picked from commit d95b68ab10d6d94a0fac910db7b7f4f8f21f4d66) --- .../frontend/src/app/app-routing.module.ts | 6 + .../src/app/ceph/cluster/cluster.module.ts | 4 +- .../cluster/upgrade/upgrade.component.html | 101 ++++++++++++ .../cluster/upgrade/upgrade.component.scss | 0 .../cluster/upgrade/upgrade.component.spec.ts | 149 ++++++++++++++++++ .../ceph/cluster/upgrade/upgrade.component.ts | 49 ++++++ .../navigation/navigation.component.html | 6 + .../navigation/navigation.component.spec.ts | 7 +- .../app/shared/api/upgrade.service.spec.ts | 60 +++++++ .../src/app/shared/api/upgrade.service.ts | 44 ++++++ .../app/shared/models/upgrade.interface.ts | 5 + 11 files changed, 429 insertions(+), 2 deletions(-) create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/upgrade/upgrade.component.html create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/upgrade/upgrade.component.scss create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/upgrade/upgrade.component.spec.ts create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/upgrade/upgrade.component.ts create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/shared/api/upgrade.service.spec.ts create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/shared/api/upgrade.service.ts create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/shared/models/upgrade.interface.ts diff --git a/src/pybind/mgr/dashboard/frontend/src/app/app-routing.module.ts b/src/pybind/mgr/dashboard/frontend/src/app/app-routing.module.ts index 6aa96ee49e176..595759e5d4924 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/app-routing.module.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/app-routing.module.ts @@ -46,6 +46,7 @@ import { FeatureTogglesGuardService } from './shared/services/feature-toggles-gu import { ModuleStatusGuardService } from './shared/services/module-status-guard.service'; import { NoSsoGuardService } from './shared/services/no-sso-guard.service'; import { CephfsVolumeFormComponent } from './ceph/cephfs/cephfs-form/cephfs-form.component'; +import { UpgradeComponent } from './ceph/cluster/upgrade/upgrade.component'; @Injectable() export class PerformanceCounterBreadcrumbsResolver extends BreadcrumbsResolver { @@ -283,6 +284,11 @@ const routes: Routes = [ } ] }, + { + path: 'upgrade', + component: UpgradeComponent, + data: { breadcrumbs: 'Cluster/Upgrade' } + }, { path: 'perf_counters/:type/:id', component: PerformanceCounterComponent, diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/cluster.module.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/cluster.module.ts index 610bb79baebc7..d8bfde368b975 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/cluster.module.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/cluster.module.ts @@ -57,6 +57,7 @@ import { ServiceDetailsComponent } from './services/service-details/service-deta import { ServiceFormComponent } from './services/service-form/service-form.component'; import { ServicesComponent } from './services/services.component'; import { TelemetryComponent } from './telemetry/telemetry.component'; +import { UpgradeComponent } from './upgrade/upgrade.component'; @NgModule({ imports: [ @@ -116,7 +117,8 @@ import { TelemetryComponent } from './telemetry/telemetry.component'; OsdFlagsIndivModalComponent, PlacementPipe, CreateClusterComponent, - CreateClusterReviewComponent + CreateClusterReviewComponent, + UpgradeComponent ], providers: [NgbActiveModal] }) diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/upgrade/upgrade.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/upgrade/upgrade.component.html new file mode 100644 index 0000000000000..49dab51cc28c5 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/upgrade/upgrade.component.html @@ -0,0 +1,101 @@ + +
+ +
+ Current Version + {{ version }} +
+
+ Cluster Status + +
    +
  • + + {{ check.type }}: {{ check.summary.message }} +
  • +
+
+
+ {{ healthData.health.status | healthLabel | uppercase }} + +
+
+
+ Upgrade Status + + + + Upgrade available +
+ +
+
+
+
+ MGR Count + + + + {{ (healthData.mgr_map | mgrSummary).total }} + +
+ +
+
+
Cluster FSID
+
{{ fsid }}
+
Release Image
+
{{ upgradeInfo.image }}
+
Registry
+
{{ upgradeInfo.registry }}
+
+
+
+
+
+ + +
+

Checking for upgrades + +

+
+
+ + + + + Cluster is up-to-date + + + + + + + diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/upgrade/upgrade.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/upgrade/upgrade.component.scss new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/upgrade/upgrade.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/upgrade/upgrade.component.spec.ts new file mode 100644 index 0000000000000..d0d3997d6d582 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/upgrade/upgrade.component.spec.ts @@ -0,0 +1,149 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { UpgradeComponent } from './upgrade.component'; +import { configureTestBed } from '~/testing/unit-test-helper'; +import { SummaryService } from '~/app/shared/services/summary.service'; +import { BehaviorSubject, of } from 'rxjs'; +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'; + +export class SummaryServiceMock { + summaryDataSource = new BehaviorSubject({ + version: + 'ceph version 17.0.0-12222-gcd0cd7cb ' + + '(b8193bb4cda16ccc5b028c3e1df62bc72350a15d) quincy (dev)' + }); + summaryData$ = this.summaryDataSource.asObservable(); + + subscribe(call: any) { + return this.summaryData$.subscribe(call); + } +} + +describe('UpgradeComponent', () => { + let component: UpgradeComponent; + let fixture: ComponentFixture; + let upgradeInfoSpy: jasmine.Spy; + let getHealthSpy: jasmine.Spy; + + const healthPayload: Record = { + health: { status: 'HEALTH_OK' }, + mon_status: { monmap: { mons: [] }, quorum: [] }, + osd_map: { osds: [] }, + mgr_map: { active_name: 'test_mgr', standbys: [] }, + hosts: 0, + rgw: 0, + fs_map: { filesystems: [], standbys: [] }, + iscsi_daemons: 1, + client_perf: {}, + scrub_status: 'Inactive', + pools: [], + df: { stats: {} }, + pg_info: { object_stats: { num_objects: 1 } } + }; + + configureTestBed({ + imports: [HttpClientTestingModule, SharedModule], + schemas: [NO_ERRORS_SCHEMA], + declarations: [UpgradeComponent], + providers: [UpgradeService, { provide: SummaryService, useClass: SummaryServiceMock }] + }); + + beforeEach(() => { + fixture = TestBed.createComponent(UpgradeComponent); + component = fixture.componentInstance; + upgradeInfoSpy = spyOn(TestBed.inject(UpgradeService), 'list'); + getHealthSpy = spyOn(TestBed.inject(HealthService), 'getMinimalHealth'); + getHealthSpy.and.returnValue(of(healthPayload)); + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + 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'); + }); + + it('should show button to Upgrade if a new version is available', () => { + 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 upgradeNowBtn = fixture.debugElement.nativeElement.querySelector('#upgrade'); + expect(upgradeNowBtn).not.toBeNull(); + }); + + it('should not show the upgrade button if there are no new version available', () => { + const upgradeInfoPayload: UpgradeInfoInterface = { + image: 'quay.io/ceph-test/ceph', + registry: 'quay.io', + versions: [] + }; + upgradeInfoSpy.and.returnValue(of(upgradeInfoPayload)); + component.ngOnInit(); + fixture.detectChanges(); + const noUpgradesSpan = fixture.debugElement.nativeElement.querySelector( + '#no-upgrades-available' + ); + expect(noUpgradesSpan.textContent).toBe(' Cluster is up-to-date '); + }); + + 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 '); + }); + + it('should upgrade only when there are more than 1 mgr', () => { + 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 upgradeBtn = fixture.debugElement.nativeElement.querySelector('#upgrade'); + expect(upgradeBtn.disabled).toBeTruthy(); + + // Add a standby mgr to the payload + const healthPayload2: Record = { + health: { status: 'HEALTH_OK' }, + mon_status: { monmap: { mons: [] }, quorum: [] }, + osd_map: { osds: [] }, + mgr_map: { active_name: 'test_mgr', standbys: ['mgr1'] }, + hosts: 0, + rgw: 0, + fs_map: { filesystems: [], standbys: [] }, + iscsi_daemons: 1, + client_perf: {}, + scrub_status: 'Inactive', + pools: [], + df: { stats: {} }, + pg_info: { object_stats: { num_objects: 1 } } + }; + + getHealthSpy.and.returnValue(of(healthPayload2)); + component.ngOnInit(); + fixture.detectChanges(); + expect(upgradeBtn.disabled).toBeFalsy(); + }); +}); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/upgrade/upgrade.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/upgrade/upgrade.component.ts new file mode 100644 index 0000000000000..e45914afd1a11 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/upgrade/upgrade.component.ts @@ -0,0 +1,49 @@ +import { Component, OnInit } from '@angular/core'; +import { Observable, of } from 'rxjs'; +import { catchError, ignoreElements } from 'rxjs/operators'; +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 { Permission } from '~/app/shared/models/permissions'; +import { UpgradeInfoInterface } from '~/app/shared/models/upgrade.interface'; +import { AuthStorageService } from '~/app/shared/services/auth-storage.service'; +import { SummaryService } from '~/app/shared/services/summary.service'; + +@Component({ + selector: 'cd-upgrade', + templateUrl: './upgrade.component.html', + styleUrls: ['./upgrade.component.scss'] +}) +export class UpgradeComponent implements OnInit { + version: string; + upgradeInfo$: Observable; + upgradeInfoError$: Observable; + permission: Permission; + healthData$: Observable; + fsid$: Observable; + + icons = Icons; + + constructor( + private summaryService: SummaryService, + private upgradeService: UpgradeService, + private authStorageService: AuthStorageService, + private healthService: HealthService + ) { + this.permission = this.authStorageService.getPermissions().configOpt; + } + + ngOnInit(): void { + this.summaryService.subscribe((summary) => { + const version = summary.version.replace('ceph version ', '').split('-'); + this.version = version[0]; + }); + this.upgradeInfo$ = this.upgradeService.list(); + this.upgradeInfoError$ = this.upgradeInfo$?.pipe( + ignoreElements(), + catchError((error) => of(error)) + ); + this.healthData$ = this.healthService.getMinimalHealth(); + this.fsid$ = this.healthService.getClusterFsid(); + } +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/navigation/navigation.component.html b/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/navigation/navigation.component.html index 4a8b6d11ce59a..6a05aea981791 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/navigation/navigation.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/navigation/navigation.component.html @@ -174,6 +174,12 @@ class="badge badge-warning ms-1">{{ prometheusAlertService.activeWarningAlerts }} +
  • + Upgrade +
  • diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/navigation/navigation.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/navigation/navigation.component.spec.ts index 64aaca65b5a4e..c8873186eb84f 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/navigation/navigation.component.spec.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/core/navigation/navigation/navigation.component.spec.ts @@ -111,7 +111,12 @@ describe('NavigationComponent', () => { [['osd'], ['.tc_submenuitem_osds', '.tc_submenuitem_crush']], [ ['configOpt'], - ['.tc_submenuitem_configuration', '.tc_submenuitem_modules', '.tc_submenuitem_users'] + [ + '.tc_submenuitem_configuration', + '.tc_submenuitem_modules', + '.tc_submenuitem_users', + '.tc_submenuitem_upgrade' + ] ], [['log'], ['.tc_submenuitem_log']], [['prometheus'], ['.tc_submenuitem_monitoring']], diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/upgrade.service.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/upgrade.service.spec.ts new file mode 100644 index 0000000000000..3bfc2bef3829b --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/upgrade.service.spec.ts @@ -0,0 +1,60 @@ +import { UpgradeService } from './upgrade.service'; +import { configureTestBed } from '~/testing/unit-test-helper'; +import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; +import { TestBed } from '@angular/core/testing'; +import { SummaryService } from '../services/summary.service'; +import { BehaviorSubject } from 'rxjs'; + +export class SummaryServiceMock { + summaryDataSource = new BehaviorSubject({ + version: + 'ceph version 18.1.3-12222-gcd0cd7cb ' + + '(b8193bb4cda16ccc5b028c3e1df62bc72350a15d) reef (dev)' + }); + summaryData$ = this.summaryDataSource.asObservable(); + + subscribe(call: any) { + return this.summaryData$.subscribe(call); + } +} + +describe('UpgradeService', () => { + let service: UpgradeService; + let httpTesting: HttpTestingController; + + configureTestBed({ + imports: [HttpClientTestingModule], + providers: [UpgradeService, { provide: SummaryService, useClass: SummaryServiceMock }] + }); + + beforeEach(() => { + service = TestBed.inject(UpgradeService); + httpTesting = TestBed.inject(HttpTestingController); + }); + + afterEach(() => { + httpTesting.verify(); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); + + it('should call upgrade list', () => { + service.list().subscribe(); + const req = httpTesting.expectOne('api/cluster/upgrade'); + expect(req.request.method).toBe('GET'); + }); + + it('should not show any version if the registry versions are older than the cluster version', () => { + const upgradeInfoPayload = { + image: 'quay.io/ceph-test/ceph', + registry: 'quay.io', + versions: ['18.1.0', '18.1.1', '18.1.2'] + }; + const expectedVersions: string[] = []; + expect(service.versionAvailableForUpgrades(upgradeInfoPayload).versions).toEqual( + expectedVersions + ); + }); +}); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/upgrade.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/upgrade.service.ts new file mode 100644 index 0000000000000..c510164148c72 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/upgrade.service.ts @@ -0,0 +1,44 @@ +import { HttpClient } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { ApiClient } from './api-client'; +import { map } from 'rxjs/operators'; +import { SummaryService } from '../services/summary.service'; +import { UpgradeInfoInterface } from '../models/upgrade.interface'; + +@Injectable({ + providedIn: 'root' +}) +export class UpgradeService extends ApiClient { + baseURL = 'api/cluster/upgrade'; + + constructor(private http: HttpClient, private summaryService: SummaryService) { + super(); + } + + list() { + return this.http.get(this.baseURL).pipe( + map((resp: UpgradeInfoInterface) => { + return this.versionAvailableForUpgrades(resp); + }) + ); + } + + // Filter out versions that are older than the current cluster version + // Only allow upgrades to the same major version + versionAvailableForUpgrades(upgradeInfo: UpgradeInfoInterface): UpgradeInfoInterface { + let version = ''; + this.summaryService.subscribe((summary) => { + version = summary.version.replace('ceph version ', '').split('-')[0]; + }); + + const upgradableVersions = upgradeInfo.versions.filter((targetVersion) => { + const cVersion = version.split('.'); + const tVersion = targetVersion.split('.'); + return ( + cVersion[0] === tVersion[0] && (cVersion[1] < tVersion[1] || cVersion[2] < tVersion[2]) + ); + }); + upgradeInfo.versions = upgradableVersions; + return upgradeInfo; + } +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/models/upgrade.interface.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/upgrade.interface.ts new file mode 100644 index 0000000000000..ada46bcd6b8d1 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/upgrade.interface.ts @@ -0,0 +1,5 @@ +export interface UpgradeInfoInterface { + image: string; + registry: string; + versions: string[]; +} -- 2.39.5