mgr/dashboard: Create Cluster Workflow welcome screen and e2e tests

A module option called CLUSTER_STATUS has two option. INSTALLED
AND POST_INSTALLED. When CLUSTER_STATUS is INSTALLED it will allow to show the
create-cluster-wizard after login the initial time.  After the cluster
creation is succesfull this option is set to POST_INSTALLED
Also has the e2e codes for the Review Section

Fixes: https://tracker.ceph.com/issues/50336
Signed-off-by: Avan Thakkar <athakkar@redhat.com>
Signed-off-by: Nizamudeen A <nia@redhat.com>
This commit is contained in:
Avan Thakkar 2021-06-01 18:25:15 +05:30 committed by Nizamudeen A
parent f41eae16af
commit b9f38cadc4
23 changed files with 520 additions and 9 deletions

View File

@ -335,7 +335,8 @@ class AuthTest(DashboardTestCase):
self.assertStatus(200) self.assertStatus(200)
data = self.jsonBody() data = self.jsonBody()
self.assertSchema(data, JObj(sub_elems={ self.assertSchema(data, JObj(sub_elems={
"login_url": JLeaf(str) "login_url": JLeaf(str),
"cluster_status": JLeaf(str)
}, allow_unknown=False)) }, allow_unknown=False))
self.logout() self.logout()
@ -345,6 +346,7 @@ class AuthTest(DashboardTestCase):
self.assertStatus(200) self.assertStatus(200)
data = self.jsonBody() data = self.jsonBody()
self.assertSchema(data, JObj(sub_elems={ self.assertSchema(data, JObj(sub_elems={
"login_url": JLeaf(str) "login_url": JLeaf(str),
"cluster_status": JLeaf(str)
}, allow_unknown=False)) }, allow_unknown=False))
self.logout(set_cookies=True) self.logout(set_cookies=True)

View File

@ -0,0 +1,23 @@
from .helper import DashboardTestCase, JLeaf, JObj
class ClusterTest(DashboardTestCase):
def setUp(self):
super().setUp()
self.reset_session()
def test_get_status(self):
data = self._get('/api/cluster', version='0.1')
self.assertStatus(200)
self.assertSchema(data, JObj(sub_elems={
"status": JLeaf(str)
}, allow_unknown=False))
def test_update_status(self):
req = {'status': 'POST_INSTALLED'}
self._put('/api/cluster', req, version='0.1')
self.assertStatus(200)
data = self._get('/api/cluster', version='0.1')
self.assertStatus(200)
self.assertEqual(data, req)

View File

@ -7,6 +7,7 @@ import sys
from .. import mgr from .. import mgr
from ..exceptions import InvalidCredentialsError, UserDoesNotExist from ..exceptions import InvalidCredentialsError, UserDoesNotExist
from ..services.auth import AuthManager, JwtManager from ..services.auth import AuthManager, JwtManager
from ..services.cluster import ClusterModel
from ..settings import Settings from ..settings import Settings
from . import APIDoc, APIRouter, ControllerAuthMixin, EndpointDoc, RESTController, allow_empty_body from . import APIDoc, APIRouter, ControllerAuthMixin, EndpointDoc, RESTController, allow_empty_body
@ -117,4 +118,5 @@ class Auth(RESTController, ControllerAuthMixin):
} }
return { return {
'login_url': self._get_login_url(), 'login_url': self._get_login_url(),
'cluster_status': ClusterModel.from_db().dict()['status']
} }

View File

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
from ..security import Scope
from ..services.cluster import ClusterModel
from . import ApiController, ControllerDoc, EndpointDoc, RESTController
@ApiController('/cluster', Scope.CONFIG_OPT)
@ControllerDoc("Get Cluster Details", "Cluster")
class Cluster(RESTController):
@RESTController.MethodMap(version='0.1')
@EndpointDoc("Get the cluster status")
def list(self):
return ClusterModel.from_db().dict()
@RESTController.MethodMap(version='0.1')
@EndpointDoc("Update the cluster status",
parameters={'status': (str, 'Cluster Status')})
def singleton_set(self, status: str):
ClusterModel(status).to_db()

View File

@ -0,0 +1,22 @@
import { PageHelper } from '../page-helper.po';
import { NotificationSidebarPageHelper } from '../ui/notification.po';
export class CreateClusterWelcomePageHelper extends PageHelper {
pages = {
index: { url: '#/create-cluster', id: 'cd-create-cluster' }
};
createCluster() {
cy.get('cd-create-cluster').should('contain.text', 'Welcome to Ceph');
cy.get('[name=create-cluster]').click();
}
doSkip() {
cy.get('[name=skip-cluster-creation]').click();
cy.get('cd-dashboard').should('exist');
const notification = new NotificationSidebarPageHelper();
notification.open();
notification.getNotifications().should('contain', 'Cluster creation skipped by user');
}
}

View File

@ -0,0 +1,11 @@
import { PageHelper } from '../page-helper.po';
export class CreateClusterReviewPageHelper extends PageHelper {
pages = {
index: { url: '#/create-cluster', id: 'cd-create-cluster-review' }
};
checkDefaultHostName() {
this.getTableCell(1, 'ceph-node-00.cephlab.com').should('exist');
}
}

View File

@ -0,0 +1,19 @@
import { CreateClusterWelcomePageHelper } from '../cluster/cluster-welcome-page.po';
describe('Create cluster page', () => {
const createCluster = new CreateClusterWelcomePageHelper();
beforeEach(() => {
cy.login();
Cypress.Cookies.preserveOnce('token');
createCluster.navigateTo();
});
it('should fail to create cluster', () => {
createCluster.createCluster();
});
it('should skip to dashboard landing page', () => {
createCluster.doSkip();
});
});

View File

@ -0,0 +1,61 @@
import { CreateClusterWelcomePageHelper } from 'cypress/integration/cluster/cluster-welcome-page.po';
import { CreateClusterReviewPageHelper } from 'cypress/integration/cluster/create-cluster-review.po';
describe('Create Cluster Review page', () => {
const reviewPage = new CreateClusterReviewPageHelper();
const createCluster = new CreateClusterWelcomePageHelper();
beforeEach(() => {
cy.login();
Cypress.Cookies.preserveOnce('token');
createCluster.navigateTo();
createCluster.createCluster();
cy.get('button[aria-label="Next"]').click();
});
describe('navigation link and title test', () => {
it('should check if nav-link and title contains Review', () => {
cy.get('.nav-link').should('contain.text', 'Review');
cy.get('.title').should('contain.text', 'Review');
});
});
describe('fields check', () => {
it('should check cluster resources table is present', () => {
// check for table header 'Status'
reviewPage.getLegends().its(0).should('have.text', 'Cluster Resources');
// check for fields in table
reviewPage.getStatusTables().should('contain.text', 'Hosts');
});
it('should check Hosts Per Label and Host Details tables are present', () => {
// check for there to be two tables
reviewPage.getDataTables().should('have.length', 2);
// check for table header 'Hosts Per Label'
reviewPage.getLegends().its(1).should('have.text', 'Hosts Per Label');
// check for table header 'Host Details'
reviewPage.getLegends().its(2).should('have.text', 'Host Details');
// verify correct columns on Hosts Per Label table
reviewPage.getDataTableHeaders(0).contains('Label');
reviewPage.getDataTableHeaders(0).contains('Number of Hosts');
// verify correct columns on Host Details table
reviewPage.getDataTableHeaders(1).contains('Host Name');
reviewPage.getDataTableHeaders(1).contains('Labels');
});
it('should check hosts count and default host name are present', () => {
reviewPage.getStatusTables().should('contain.text', '1');
reviewPage.checkDefaultHostName();
});
});
});

View File

@ -6,6 +6,7 @@ import _ from 'lodash';
import { CephfsListComponent } from './ceph/cephfs/cephfs-list/cephfs-list.component'; import { CephfsListComponent } from './ceph/cephfs/cephfs-list/cephfs-list.component';
import { ConfigurationFormComponent } from './ceph/cluster/configuration/configuration-form/configuration-form.component'; import { ConfigurationFormComponent } from './ceph/cluster/configuration/configuration-form/configuration-form.component';
import { ConfigurationComponent } from './ceph/cluster/configuration/configuration.component'; import { ConfigurationComponent } from './ceph/cluster/configuration/configuration.component';
import { CreateClusterComponent } from './ceph/cluster/create-cluster/create-cluster.component';
import { CrushmapComponent } from './ceph/cluster/crushmap/crushmap.component'; import { CrushmapComponent } from './ceph/cluster/crushmap/crushmap.component';
import { HostFormComponent } from './ceph/cluster/hosts/host-form/host-form.component'; import { HostFormComponent } from './ceph/cluster/hosts/host-form/host-form.component';
import { HostsComponent } from './ceph/cluster/hosts/hosts.component'; import { HostsComponent } from './ceph/cluster/hosts/hosts.component';
@ -89,6 +90,19 @@ const routes: Routes = [
{ path: 'error', component: ErrorComponent }, { path: 'error', component: ErrorComponent },
// Cluster // Cluster
{
path: 'create-cluster',
component: CreateClusterComponent,
canActivate: [ModuleStatusGuardService],
data: {
moduleStatusGuardConfig: {
apiPath: 'orchestrator',
redirectTo: 'dashboard',
backend: 'cephadm'
},
breadcrumbs: 'Create Cluster'
}
},
{ {
path: 'hosts', path: 'hosts',
data: { breadcrumbs: 'Cluster/Hosts' }, data: { breadcrumbs: 'Cluster/Hosts' },

View File

@ -21,6 +21,7 @@ import { CephSharedModule } from '../shared/ceph-shared.module';
import { ConfigurationDetailsComponent } from './configuration/configuration-details/configuration-details.component'; import { ConfigurationDetailsComponent } from './configuration/configuration-details/configuration-details.component';
import { ConfigurationFormComponent } from './configuration/configuration-form/configuration-form.component'; import { ConfigurationFormComponent } from './configuration/configuration-form/configuration-form.component';
import { ConfigurationComponent } from './configuration/configuration.component'; import { ConfigurationComponent } from './configuration/configuration.component';
import { CreateClusterComponent } from './create-cluster/create-cluster.component';
import { CrushmapComponent } from './crushmap/crushmap.component'; import { CrushmapComponent } from './crushmap/crushmap.component';
import { HostDetailsComponent } from './hosts/host-details/host-details.component'; import { HostDetailsComponent } from './hosts/host-details/host-details.component';
import { HostFormComponent } from './hosts/host-form/host-form.component'; import { HostFormComponent } from './hosts/host-form/host-form.component';
@ -112,7 +113,8 @@ import { TelemetryComponent } from './telemetry/telemetry.component';
PrometheusTabsComponent, PrometheusTabsComponent,
ServiceFormComponent, ServiceFormComponent,
OsdFlagsIndivModalComponent, OsdFlagsIndivModalComponent,
PlacementPipe PlacementPipe,
CreateClusterComponent
] ]
}) })
export class ClusterModule {} export class ClusterModule {}

View File

@ -0,0 +1,29 @@
<div class="container h-75">
<div class="row h-100 justify-content-center align-items-center">
<div class="blank-page">
<!-- htmllint img-req-src="false" -->
<img [src]="projectConstants.cephLogo"
alt="Ceph"
class="img-fluid mx-auto d-block">
<h3 class="text-center m-2"
i18n>Welcome to {{ projectConstants.projectName }}</h3>
<div class="m-4">
<h4 class="text-center"
i18n>Please proceed to complete the cluster creation</h4>
<div class="offset-md-3">
<button class="btn btn-accent m-3"
name="create-cluster"
[routerLink]="'/dashboard'"
(click)="createCluster()"
i18n>Create Cluster</button>
<button class="btn btn-light"
name="skip-cluster-creation"
[routerLink]="'/dashboard'"
(click)="skipClusterCreation()"
i18n>Skip</button>
</div>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,45 @@
import { HttpClientTestingModule } from '@angular/common/http/testing';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { RouterTestingModule } from '@angular/router/testing';
import { ToastrModule } from 'ngx-toastr';
import { ClusterService } from '~/app/shared/api/cluster.service';
import { SharedModule } from '~/app/shared/shared.module';
import { configureTestBed } from '~/testing/unit-test-helper';
import { CreateClusterComponent } from './create-cluster.component';
describe('CreateClusterComponent', () => {
let component: CreateClusterComponent;
let fixture: ComponentFixture<CreateClusterComponent>;
let clusterService: ClusterService;
configureTestBed({
declarations: [CreateClusterComponent],
imports: [HttpClientTestingModule, RouterTestingModule, ToastrModule.forRoot(), SharedModule]
});
beforeEach(() => {
fixture = TestBed.createComponent(CreateClusterComponent);
component = fixture.componentInstance;
clusterService = TestBed.inject(ClusterService);
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should have the heading "Welcome to Ceph Dashboard"', () => {
const heading = fixture.debugElement.query(By.css('h3')).nativeElement;
expect(heading.innerHTML).toBe('Welcome to Ceph Dashboard');
});
it('should call updateStatus when cluster creation is skipped', () => {
const clusterServiceSpy = spyOn(clusterService, 'updateStatus').and.callThrough();
expect(clusterServiceSpy).not.toHaveBeenCalled();
component.skipClusterCreation();
expect(clusterServiceSpy).toHaveBeenCalledTimes(1);
});
});

View File

@ -0,0 +1,44 @@
import { Component } from '@angular/core';
import { ClusterService } from '~/app/shared/api/cluster.service';
import { AppConstants } from '~/app/shared/constants/app.constants';
import { NotificationType } from '~/app/shared/enum/notification-type.enum';
import { Permission } from '~/app/shared/models/permissions';
import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
import { NotificationService } from '~/app/shared/services/notification.service';
@Component({
selector: 'cd-create-cluster',
templateUrl: './create-cluster.component.html',
styleUrls: ['./create-cluster.component.scss']
})
export class CreateClusterComponent {
permission: Permission;
orchStatus = false;
featureAvailable = false;
projectConstants: typeof AppConstants = AppConstants;
constructor(
private authStorageService: AuthStorageService,
private clusterService: ClusterService,
private notificationService: NotificationService
) {
this.permission = this.authStorageService.getPermissions().configOpt;
}
createCluster() {
this.notificationService.show(
NotificationType.error,
$localize`Cluster creation feature not implemented`
);
}
skipClusterCreation() {
this.clusterService.updateStatus('POST_INSTALLED').subscribe(() => {
this.notificationService.show(
NotificationType.info,
$localize`Cluster creation skipped by user`
);
});
}
}

View File

@ -1,7 +1,11 @@
import { HttpClientTestingModule } from '@angular/common/http/testing'; import { HttpClientTestingModule } from '@angular/common/http/testing';
import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ComponentFixture, TestBed } from '@angular/core/testing';
import { Router } from '@angular/router';
import { RouterTestingModule } from '@angular/router/testing'; import { RouterTestingModule } from '@angular/router/testing';
import { of } from 'rxjs';
import { AuthService } from '~/app/shared/api/auth.service';
import { configureTestBed } from '~/testing/unit-test-helper'; import { configureTestBed } from '~/testing/unit-test-helper';
import { AuthModule } from '../auth.module'; import { AuthModule } from '../auth.module';
import { LoginComponent } from './login.component'; import { LoginComponent } from './login.component';
@ -9,6 +13,8 @@ import { LoginComponent } from './login.component';
describe('LoginComponent', () => { describe('LoginComponent', () => {
let component: LoginComponent; let component: LoginComponent;
let fixture: ComponentFixture<LoginComponent>; let fixture: ComponentFixture<LoginComponent>;
let routerNavigateSpy: jasmine.Spy;
let authServiceLoginSpy: jasmine.Spy;
configureTestBed({ configureTestBed({
imports: [RouterTestingModule, HttpClientTestingModule, AuthModule] imports: [RouterTestingModule, HttpClientTestingModule, AuthModule]
@ -17,6 +23,10 @@ describe('LoginComponent', () => {
beforeEach(() => { beforeEach(() => {
fixture = TestBed.createComponent(LoginComponent); fixture = TestBed.createComponent(LoginComponent);
component = fixture.componentInstance; component = fixture.componentInstance;
routerNavigateSpy = spyOn(TestBed.inject(Router), 'navigate');
routerNavigateSpy.and.returnValue(true);
authServiceLoginSpy = spyOn(TestBed.inject(AuthService), 'login');
authServiceLoginSpy.and.returnValue(of(null));
fixture.detectChanges(); fixture.detectChanges();
}); });
@ -29,4 +39,20 @@ describe('LoginComponent', () => {
component.ngOnInit(); component.ngOnInit();
expect(component['modalService'].hasOpenModals()).toBeFalsy(); expect(component['modalService'].hasOpenModals()).toBeFalsy();
}); });
it('should not show create cluster wizard if cluster creation was successful', () => {
component.postInstalled = true;
component.login();
expect(routerNavigateSpy).toHaveBeenCalledTimes(1);
expect(routerNavigateSpy).toHaveBeenCalledWith(['/']);
});
it('should show create cluster wizard if cluster creation was failed', () => {
component.postInstalled = false;
component.login();
expect(routerNavigateSpy).toHaveBeenCalledTimes(1);
expect(routerNavigateSpy).toHaveBeenCalledWith(['/create-cluster']);
});
}); });

View File

@ -17,6 +17,7 @@ export class LoginComponent implements OnInit {
model = new Credentials(); model = new Credentials();
isLoginActive = false; isLoginActive = false;
returnUrl: string; returnUrl: string;
postInstalled = false;
constructor( constructor(
private authService: AuthService, private authService: AuthService,
@ -43,6 +44,7 @@ export class LoginComponent implements OnInit {
} }
this.authService.check(token).subscribe((login: any) => { this.authService.check(token).subscribe((login: any) => {
if (login.login_url) { if (login.login_url) {
this.postInstalled = login.cluster_status === 'POST_INSTALLED';
if (login.login_url === '#/login') { if (login.login_url === '#/login') {
this.isLoginActive = true; this.isLoginActive = true;
} else { } else {
@ -63,7 +65,11 @@ export class LoginComponent implements OnInit {
login() { login() {
this.authService.login(this.model).subscribe(() => { this.authService.login(this.model).subscribe(() => {
const url = _.get(this.route.snapshot.queryParams, 'returnUrl', '/'); const urlPath = this.postInstalled ? '/' : '/create-cluster';
let url = _.get(this.route.snapshot.queryParams, 'returnUrl', urlPath);
if (!this.postInstalled && this.route.snapshot.queryParams['returnUrl'] === '/dashboard') {
url = '/create-cluster';
}
this.router.navigate([url]); this.router.navigate([url]);
}); });
} }

View File

@ -0,0 +1,42 @@
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
import { fakeAsync, TestBed } from '@angular/core/testing';
import { configureTestBed } from '~/testing/unit-test-helper';
import { ClusterService } from './cluster.service';
describe('ClusterService', () => {
let service: ClusterService;
let httpTesting: HttpTestingController;
configureTestBed({
imports: [HttpClientTestingModule],
providers: [ClusterService]
});
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(ClusterService);
httpTesting = TestBed.inject(HttpTestingController);
});
afterEach(() => {
httpTesting.verify();
});
it('should be created', () => {
expect(service).toBeTruthy();
});
it('should call getStatus', () => {
service.getStatus().subscribe();
const req = httpTesting.expectOne('api/cluster');
expect(req.request.method).toBe('GET');
});
it('should update cluster status', fakeAsync(() => {
service.updateStatus('fakeStatus').subscribe();
const req = httpTesting.expectOne('api/cluster');
expect(req.request.method).toBe('PUT');
expect(req.request.body).toEqual({ status: 'fakeStatus' });
}));
});

View File

@ -0,0 +1,27 @@
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
@Injectable({
providedIn: 'root'
})
export class ClusterService {
baseURL = 'api/cluster';
constructor(private http: HttpClient) {}
getStatus(): Observable<string> {
return this.http.get<string>(`${this.baseURL}`, {
headers: { Accept: 'application/vnd.ceph.api.v0.1+json' }
});
}
updateStatus(status: string) {
return this.http.put(
`${this.baseURL}`,
{ status: status },
{ headers: { Accept: 'application/vnd.ceph.api.v0.1+json' } }
);
}
}

View File

@ -7,6 +7,7 @@ export class AppConstants {
public static readonly projectName = 'Ceph Dashboard'; public static readonly projectName = 'Ceph Dashboard';
public static readonly license = 'Free software (LGPL 2.1).'; public static readonly license = 'Free software (LGPL 2.1).';
public static readonly copyright = 'Copyright(c) ' + environment.year + ' Ceph contributors.'; public static readonly copyright = 'Copyright(c) ' + environment.year + ' Ceph contributors.';
public static readonly cephLogo = 'assets/Ceph_Logo.svg';
} }
export enum URLVerbs { export enum URLVerbs {

View File

@ -7,6 +7,7 @@ import { RouterTestingModule } from '@angular/router/testing';
import { of as observableOf } from 'rxjs'; import { of as observableOf } from 'rxjs';
import { configureTestBed } from '~/testing/unit-test-helper'; import { configureTestBed } from '~/testing/unit-test-helper';
import { MgrModuleService } from '../api/mgr-module.service';
import { ModuleStatusGuardService } from './module-status-guard.service'; import { ModuleStatusGuardService } from './module-status-guard.service';
describe('ModuleStatusGuardService', () => { describe('ModuleStatusGuardService', () => {
@ -15,6 +16,7 @@ describe('ModuleStatusGuardService', () => {
let router: Router; let router: Router;
let route: ActivatedRouteSnapshot; let route: ActivatedRouteSnapshot;
let ngZone: NgZone; let ngZone: NgZone;
let mgrModuleService: MgrModuleService;
@Component({ selector: 'cd-foo', template: '' }) @Component({ selector: 'cd-foo', template: '' })
class FooComponent {} class FooComponent {}
@ -25,9 +27,16 @@ describe('ModuleStatusGuardService', () => {
const routes: Routes = [{ path: '**', component: FooComponent }]; const routes: Routes = [{ path: '**', component: FooComponent }];
const testCanActivate = (getResult: {}, activateResult: boolean, urlResult: string) => { const testCanActivate = (
getResult: {},
activateResult: boolean,
urlResult: string,
backend = 'cephadm'
) => {
let result: boolean; let result: boolean;
spyOn(httpClient, 'get').and.returnValue(observableOf(getResult)); spyOn(httpClient, 'get').and.returnValue(observableOf(getResult));
const test = { orchestrator: backend };
spyOn(mgrModuleService, 'getConfig').and.returnValue(observableOf(test));
ngZone.run(() => { ngZone.run(() => {
service.canActivateChild(route).subscribe((resp) => { service.canActivateChild(route).subscribe((resp) => {
result = resp; result = resp;
@ -48,13 +57,15 @@ describe('ModuleStatusGuardService', () => {
beforeEach(() => { beforeEach(() => {
service = TestBed.inject(ModuleStatusGuardService); service = TestBed.inject(ModuleStatusGuardService);
httpClient = TestBed.inject(HttpClient); httpClient = TestBed.inject(HttpClient);
mgrModuleService = TestBed.inject(MgrModuleService);
router = TestBed.inject(Router); router = TestBed.inject(Router);
route = new ActivatedRouteSnapshot(); route = new ActivatedRouteSnapshot();
route.url = []; route.url = [];
route.data = { route.data = {
moduleStatusGuardConfig: { moduleStatusGuardConfig: {
apiPath: 'bar', apiPath: 'bar',
redirectTo: '/foo' redirectTo: '/foo',
backend: 'rook'
} }
}; };
ngZone = TestBed.inject(NgZone); ngZone = TestBed.inject(NgZone);
@ -76,4 +87,8 @@ describe('ModuleStatusGuardService', () => {
it('should test canActivateChild with status unavailable', fakeAsync(() => { it('should test canActivateChild with status unavailable', fakeAsync(() => {
testCanActivate(null, false, '/foo'); testCanActivate(null, false, '/foo');
})); }));
it('should redirect normally if the backend provided matches the current backend', fakeAsync(() => {
testCanActivate({ available: true, message: 'foo' }, true, '/', 'rook');
}));
}); });

View File

@ -5,7 +5,8 @@ import { ActivatedRouteSnapshot, CanActivate, CanActivateChild, Router } from '@
import { of as observableOf } from 'rxjs'; import { of as observableOf } from 'rxjs';
import { catchError, map } from 'rxjs/operators'; import { catchError, map } from 'rxjs/operators';
import { Icons } from '../enum/icons.enum'; import { MgrModuleService } from '~/app/shared/api/mgr-module.service';
import { Icons } from '~/app/shared/enum/icons.enum';
/** /**
* This service checks if a route can be activated by executing a * This service checks if a route can be activated by executing a
@ -39,7 +40,11 @@ export class ModuleStatusGuardService implements CanActivate, CanActivateChild {
// TODO: Hotfix - remove ALLOWLIST'ing when a generic ErrorComponent is implemented // TODO: Hotfix - remove ALLOWLIST'ing when a generic ErrorComponent is implemented
static readonly ALLOWLIST: string[] = ['501']; static readonly ALLOWLIST: string[] = ['501'];
constructor(private http: HttpClient, private router: Router) {} constructor(
private http: HttpClient,
private router: Router,
private mgrModuleService: MgrModuleService
) {}
canActivate(route: ActivatedRouteSnapshot) { canActivate(route: ActivatedRouteSnapshot) {
return this.doCheck(route); return this.doCheck(route);
@ -54,9 +59,15 @@ export class ModuleStatusGuardService implements CanActivate, CanActivateChild {
return observableOf(true); return observableOf(true);
} }
const config = route.data['moduleStatusGuardConfig']; const config = route.data['moduleStatusGuardConfig'];
let backendCheck = false;
if (config.backend) {
this.mgrModuleService.getConfig('orchestrator').subscribe((resp) => {
backendCheck = config.backend === resp['orchestrator'];
});
}
return this.http.get(`api/${config.apiPath}/status`).pipe( return this.http.get(`api/${config.apiPath}/status`).pipe(
map((resp: any) => { map((resp: any) => {
if (!resp.available) { if (!resp.available && !backendCheck) {
this.router.navigate([config.redirectTo || ''], { this.router.navigate([config.redirectTo || ''], {
state: { state: {
header: config.header, header: config.header,

View File

@ -2038,6 +2038,67 @@ paths:
- jwt: [] - jwt: []
tags: tags:
- Cephfs - Cephfs
/api/cluster:
get:
parameters: []
responses:
'200':
content:
application/vnd.ceph.api.v0.1+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: []
summary: Get the cluster status
tags:
- Cluster
put:
parameters: []
requestBody:
content:
application/json:
schema:
properties:
status:
description: Cluster Status
type: string
required:
- status
type: object
responses:
'200':
content:
application/vnd.ceph.api.v0.1+json:
type: object
description: Resource updated.
'202':
content:
application/vnd.ceph.api.v0.1+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: []
summary: Update the cluster status
tags:
- Cluster
/api/cluster_conf: /api/cluster_conf:
get: get:
parameters: [] parameters: []
@ -10388,6 +10449,8 @@ tags:
name: Auth name: Auth
- description: Cephfs Management API - description: Cephfs Management API
name: Cephfs name: Cephfs
- description: Get Cluster Details
name: Cluster
- description: Manage Cluster Configurations - description: Manage Cluster Configurations
name: ClusterConfiguration name: ClusterConfiguration
- description: Crush Rule Management API - description: Crush Rule Management API

View File

@ -0,0 +1,26 @@
# -*- coding: utf-8 -*-
from enum import Enum
from .. import mgr
class ClusterModel:
class Status(Enum):
INSTALLED = 0
POST_INSTALLED = 1
status: Status
def __init__(self, status=Status.INSTALLED.name):
self.status = self.Status[status]
def dict(self):
return {'status': self.status.name}
def to_db(self):
mgr.set_store('cluster/status', self.status.name)
@classmethod
def from_db(cls):
return cls(status=mgr.get_store('cluster/status', cls.Status.INSTALLED.name))