mgr/dashboard: add rbd status endpoint

Show "No RBD pools available" error page when accessing block/rbd if there are no rbd pools.
Add a "button_name" and "button_route" property to `ModuleStatusGuardService` config to customize the button on the error page.
Modify `ModuleStatusGuardService` to execute API calls to `/ui-api/<uiApiPath>/status` which uses the `UIRouter`.

Fixes: https://tracker.ceph.com/issues/42109
Signed-off-by: Melissa Li <melissali@redhat.com>
This commit is contained in:
Melissa Li 2022-05-26 14:07:30 -04:00
parent a74fa9a66f
commit 6ac9b3cfe1
21 changed files with 92 additions and 174 deletions

View File

@ -8,7 +8,7 @@ class OrchestratorControllerTest(DashboardTestCase):
AUTH_ROLES = ['cluster-manager']
URL_STATUS = '/api/orchestrator/status'
URL_STATUS = '/ui-api/orchestrator/status'
ORCHESTRATOR = True

View File

@ -84,7 +84,7 @@ class RgwApiCredentialsTest(RgwTestCase):
# Set the default credentials.
self._ceph_cmd_with_secret(['dashboard', 'set-rgw-api-secret-key'], 'admin')
self._ceph_cmd_with_secret(['dashboard', 'set-rgw-api-access-key'], 'admin')
data = self._get('/api/rgw/status')
data = self._get('/ui-api/rgw/status')
self.assertStatus(200)
self.assertIn('available', data)
self.assertIn('message', data)
@ -480,7 +480,7 @@ class RgwDaemonTest(RgwTestCase):
self.assertTrue(data['rgw_metadata'])
def test_status(self):
data = self._get('/api/rgw/status')
data = self._get('/ui-api/rgw/status')
self.assertStatus(200)
self.assertIn('available', data)
self.assertIn('message', data)

View File

@ -81,31 +81,8 @@ def NfsTask(name, metadata, wait_for): # noqa: N802
return composed_decorator
@APIRouter('/nfs-ganesha', Scope.NFS_GANESHA)
@APIDoc("NFS-Ganesha Cluster Management API", "NFS-Ganesha")
class NFSGanesha(RESTController):
@EndpointDoc("Status of NFS-Ganesha management feature",
responses={200: {
'available': (bool, "Is API available?"),
'message': (str, "Error message")
}})
@Endpoint()
@ReadPermission
def status(self):
status = {'available': True, 'message': None}
try:
mgr.remote('nfs', 'cluster_ls')
except (ImportError, RuntimeError) as error:
logger.exception(error)
status['available'] = False
status['message'] = str(error) # type: ignore
return status
@APIRouter('/nfs-ganesha/cluster', Scope.NFS_GANESHA)
@APIDoc(group="NFS-Ganesha")
@APIDoc("NFS-Ganesha Cluster Management API", "NFS-Ganesha")
class NFSGaneshaCluster(RESTController):
@ReadPermission
@RESTController.MethodMap(version=APIVersion.EXPERIMENTAL)
@ -285,3 +262,16 @@ class NFSGaneshaUi(BaseController):
@ReadPermission
def filesystems(self):
return CephFS.list_filesystems()
@Endpoint()
@ReadPermission
def status(self):
status = {'available': True, 'message': None}
try:
mgr.remote('nfs', 'cluster_ls')
except (ImportError, RuntimeError) as error:
logger.exception(error)
status['available'] = False
status['message'] = str(error) # type: ignore
return status

View File

@ -4,7 +4,7 @@ from functools import wraps
from ..exceptions import DashboardException
from ..services.orchestrator import OrchClient
from . import APIDoc, APIRouter, Endpoint, EndpointDoc, ReadPermission, RESTController
from . import APIDoc, Endpoint, EndpointDoc, ReadPermission, RESTController, UIRouter
STATUS_SCHEMA = {
"available": (bool, "Orchestrator status"),
@ -35,7 +35,7 @@ def raise_if_no_orchestrator(features=None):
return inner
@APIRouter('/orchestrator')
@UIRouter('/orchestrator')
@APIDoc("Orchestrator Management API", "Orchestrator")
class Orchestrator(RESTController):

View File

@ -19,8 +19,9 @@ from ..services.rbd import RbdConfiguration, RbdMirroringService, RbdService, \
RbdSnapshotService, format_bitmask, format_features, parse_image_spec, \
rbd_call, rbd_image_call
from ..tools import ViewCache, str_to_bool
from . import APIDoc, APIRouter, CreatePermission, DeletePermission, \
EndpointDoc, RESTController, Task, UpdatePermission, allow_empty_body
from . import APIDoc, APIRouter, BaseController, CreatePermission, \
DeletePermission, Endpoint, EndpointDoc, ReadPermission, RESTController, \
Task, UIRouter, UpdatePermission, allow_empty_body
logger = logging.getLogger(__name__)
@ -293,6 +294,20 @@ class Rbd(RESTController):
return rbd_call(pool_name, namespace, rbd_inst.trash_move, image_name, delay)
@UIRouter('/block/rbd')
class RbdStatus(BaseController):
@EndpointDoc("Display RBD Image feature status")
@Endpoint()
@ReadPermission
def status(self):
status = {'available': True, 'message': None}
if not CephService.get_pool_list('rbd'):
status['available'] = False
status['message'] = 'No RBD pools in the cluster. Please create a pool '\
'with the "rbd" application label.' # type: ignore
return status
@APIRouter('/block/image/{image_spec}/snap', Scope.RBD_IMAGE)
@APIDoc("RBD Snapshot Management API", "RbdSnapshot")
class RbdSnapshot(RESTController):

View File

@ -13,7 +13,7 @@ from ..services.ceph_service import CephService
from ..services.rgw_client import NoRgwDaemonsException, RgwClient
from ..tools import json_str_to_object, str_to_bool
from . import APIDoc, APIRouter, BaseController, Endpoint, EndpointDoc, \
ReadPermission, RESTController, allow_empty_body
ReadPermission, RESTController, UIRouter, allow_empty_body
from ._version import APIVersion
try:
@ -41,7 +41,7 @@ RGW_USER_SCHEMA = {
}
@APIRouter('/rgw', Scope.RGW)
@UIRouter('/rgw', Scope.RGW)
@APIDoc("RGW Management API", "Rgw")
class Rgw(BaseController):
@Endpoint()

View File

@ -0,0 +1 @@
{ "available": false, "message": "No RBD pools in the cluster. Please create a pool with the \"rbd\" application label." }

View File

@ -35,7 +35,7 @@ export class NavigationPageHelper extends PageHelper {
{
menu: 'Block',
submenus: [
{ menu: 'Images', component: 'cd-rbd-list' },
{ menu: 'Images', component: 'cd-error' },
{ menu: 'Mirroring', component: 'cd-mirroring' },
{ menu: 'iSCSI', component: 'cd-iscsi' }
]
@ -52,9 +52,10 @@ export class NavigationPageHelper extends PageHelper {
}
checkNavigations(navs: any) {
// The nfs-ganesha and RGW status requests are mocked to ensure that this method runs in time
cy.intercept('/api/nfs-ganesha/status', { fixture: 'nfs-ganesha-status.json' });
cy.intercept('/api/rgw/status', { fixture: 'rgw-status.json' });
// The nfs-ganesha, RGW, and block/rbd status requests are mocked to ensure that this method runs in time
cy.intercept('/ui-api/nfs-ganesha/status', { fixture: 'nfs-ganesha-status.json' });
cy.intercept('/ui-api/rgw/status', { fixture: 'rgw-status.json' });
cy.intercept('/ui-api/block/rbd/status', { fixture: 'block-rbd-status.json' });
navs.forEach((nav: any) => {
cy.contains('.simplebar-content li.nav-item a', nav.menu).click();

View File

@ -96,7 +96,7 @@ const routes: Routes = [
canActivate: [ModuleStatusGuardService],
data: {
moduleStatusGuardConfig: {
apiPath: 'orchestrator',
uiApiPath: 'orchestrator',
redirectTo: 'dashboard',
backend: 'cephadm'
},
@ -126,7 +126,7 @@ const routes: Routes = [
canActivate: [ModuleStatusGuardService],
data: {
moduleStatusGuardConfig: {
apiPath: 'orchestrator',
uiApiPath: 'orchestrator',
redirectTo: 'error',
section: 'orch',
section_info: 'Orchestrator',
@ -153,7 +153,7 @@ const routes: Routes = [
component: InventoryComponent,
data: {
moduleStatusGuardConfig: {
apiPath: 'orchestrator',
uiApiPath: 'orchestrator',
redirectTo: 'error',
section: 'orch',
section_info: 'Orchestrator',
@ -298,7 +298,7 @@ const routes: Routes = [
canActivateChild: [FeatureTogglesGuardService, ModuleStatusGuardService],
data: {
moduleStatusGuardConfig: {
apiPath: 'rgw',
uiApiPath: 'rgw',
redirectTo: 'error',
section: 'rgw',
section_info: 'Object Gateway',
@ -335,7 +335,7 @@ const routes: Routes = [
canActivateChild: [FeatureTogglesGuardService, ModuleStatusGuardService],
data: {
moduleStatusGuardConfig: {
apiPath: 'nfs-ganesha',
uiApiPath: 'nfs-ganesha',
redirectTo: 'error',
section: 'nfs-ganesha',
section_info: 'NFS GANESHA',

View File

@ -9,6 +9,7 @@ import { NgxPipeFunctionModule } from 'ngx-pipe-function';
import { ActionLabels, URLVerbs } from '~/app/shared/constants/app.constants';
import { FeatureTogglesGuardService } from '~/app/shared/services/feature-toggles-guard.service';
import { ModuleStatusGuardService } from '~/app/shared/services/module-status-guard.service';
import { SharedModule } from '~/app/shared/shared.module';
import { IscsiSettingComponent } from './iscsi-setting/iscsi-setting.component';
import { IscsiTabsComponent } from './iscsi-tabs/iscsi-tabs.component';
@ -89,8 +90,17 @@ const routes: Routes = [
{ path: '', redirectTo: 'rbd', pathMatch: 'full' },
{
path: 'rbd',
canActivate: [FeatureTogglesGuardService],
data: { breadcrumbs: 'Images' },
canActivate: [FeatureTogglesGuardService, ModuleStatusGuardService],
data: {
moduleStatusGuardConfig: {
uiApiPath: 'block/rbd',
redirectTo: 'error',
header: 'No RBD pools available',
button_name: 'Create RBD pool',
button_route: '/pool/create'
},
breadcrumbs: 'Images'
},
children: [
{ path: '', component: RbdListComponent },
{

View File

@ -27,10 +27,17 @@
the {{ section_info }} management functionality.</h4>
</div>
<br><br>
<div *ngIf="button_name && button_route; else dashboardButton">
<button class="btn btn-primary"
[routerLink]="button_route"
i18n>{{ button_name }}</button>
</div>
<ng-template #dashboardButton>
<div>
<button class="btn btn-primary"
[routerLink]="'/dashboard'"
i18n>Go To Dashboard</button>
</div>
</ng-template>
</div>
</div>

View File

@ -16,6 +16,8 @@ export class ErrorComponent implements OnDestroy, OnInit {
message: string;
section: string;
section_info: string;
button_name: string;
button_route: string;
icon: string;
docUrl: string;
source: string;
@ -43,6 +45,8 @@ export class ErrorComponent implements OnDestroy, OnInit {
this.header = history.state.header;
this.section = history.state.section;
this.section_info = history.state.section_info;
this.button_name = history.state.button_name;
this.button_route = history.state.button_route;
this.icon = history.state.icon;
this.source = history.state.source;
this.docUrl = this.docService.urlGenerator(this.section);

View File

@ -7,7 +7,7 @@ import { OrchestratorService } from './orchestrator.service';
describe('OrchestratorService', () => {
let service: OrchestratorService;
let httpTesting: HttpTestingController;
const apiPath = 'api/orchestrator';
const uiApiPath = 'ui-api/orchestrator';
configureTestBed({
providers: [OrchestratorService],
@ -29,7 +29,7 @@ describe('OrchestratorService', () => {
it('should call status', () => {
service.status().subscribe();
const req = httpTesting.expectOne(`${apiPath}/status`);
const req = httpTesting.expectOne(`${uiApiPath}/status`);
expect(req.request.method).toBe('GET');
});
});

View File

@ -11,7 +11,7 @@ import { OrchestratorStatus } from '../models/orchestrator.interface';
providedIn: 'root'
})
export class OrchestratorService {
private url = 'api/orchestrator';
private url = 'ui-api/orchestrator';
disableMessages = {
noOrchestrator: $localize`The feature is disabled because Orchestrator is not available.`,

View File

@ -67,7 +67,7 @@ describe('ModuleStatusGuardService', () => {
route.url = [];
route.data = {
moduleStatusGuardConfig: {
apiPath: 'bar',
uiApiPath: 'bar',
redirectTo: '/foo',
backend: 'rook'
}

View File

@ -10,7 +10,7 @@ import { Icons } from '~/app/shared/enum/icons.enum';
/**
* This service checks if a route can be activated by executing a
* REST API call to '/api/<apiPath>/status'. If the returned response
* REST API call to '/ui-api/<uiApiPath>/status'. If the returned response
* states that the module is not available, then the user is redirected
* to the specified <redirectTo> URL path.
*
@ -26,7 +26,7 @@ import { Icons } from '~/app/shared/enum/icons.enum';
* canActivate: [AuthGuardService, ModuleStatusGuardService],
* data: {
* moduleStatusGuardConfig: {
* apiPath: 'rgw',
* uiApiPath: 'rgw',
* redirectTo: 'rgw/501'
* }
* }
@ -71,7 +71,7 @@ export class ModuleStatusGuardService implements CanActivate, CanActivateChild {
}
);
}
return this.http.get(`api/${config.apiPath}/status`).pipe(
return this.http.get(`ui-api/${config.uiApiPath}/status`).pipe(
map((resp: any) => {
if (!resp.available && !backendCheck) {
this.router.navigate([config.redirectTo || ''], {
@ -80,6 +80,8 @@ export class ModuleStatusGuardService implements CanActivate, CanActivateChild {
message: resp.message,
section: config.section,
section_info: config.section_info,
button_name: config.button_name,
button_route: config.button_route,
icon: Icons.wrench
}
});

View File

@ -5865,74 +5865,6 @@ paths:
summary: Updates an NFS-Ganesha export
tags:
- NFS-Ganesha
/api/nfs-ganesha/status:
get:
parameters: []
responses:
'200':
content:
application/vnd.ceph.api.v1.0+json:
schema:
properties:
available:
description: Is API available?
type: boolean
message:
description: Error message
type: string
required:
- available
- message
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: Status of NFS-Ganesha management feature
tags:
- NFS-Ganesha
/api/orchestrator/status:
get:
parameters: []
responses:
'200':
content:
application/vnd.ceph.api.v1.0+json:
schema:
properties:
available:
description: Orchestrator status
type: boolean
message:
description: Error message
type: string
required:
- available
- message
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: Display Orchestrator Status
tags:
- Orchestrator
/api/osd:
get:
parameters: []
@ -7952,40 +7884,6 @@ paths:
- jwt: []
tags:
- RgwSite
/api/rgw/status:
get:
parameters: []
responses:
'200':
content:
application/vnd.ceph.api.v1.0+json:
schema:
properties:
available:
description: Is RGW available?
type: boolean
message:
description: Descriptions
type: string
required:
- available
- message
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: Display RGW Status
tags:
- Rgw
/api/rgw/user:
get:
parameters:
@ -10547,8 +10445,6 @@ tags:
name: NFS-Ganesha
- description: OSD management API
name: OSD
- description: Orchestrator Management API
name: Orchestrator
- description: OSD Perf Counters Management API
name: OsdPerfCounter
- description: Perf Counters Management API
@ -10579,8 +10475,6 @@ tags:
name: RbdTrash
- description: Feedback API
name: Report
- description: RGW Management API
name: Rgw
- description: RGW Bucket Management API
name: RgwBucket
- description: RGW Daemon Management API

View File

@ -8,7 +8,7 @@ from mgr_module import CLICommand, Option
from ..controllers.cephfs import CephFS
from ..controllers.iscsi import Iscsi, IscsiTarget
from ..controllers.nfs import NFSGanesha, NFSGaneshaExports
from ..controllers.nfs import NFSGaneshaExports, NFSGaneshaUi
from ..controllers.rbd import Rbd, RbdSnapshot, RbdTrash
from ..controllers.rbd_mirroring import RbdMirroringPoolMode, \
RbdMirroringPoolPeer, RbdMirroringSummary
@ -36,7 +36,7 @@ Feature2Controller = {
Features.ISCSI: [Iscsi, IscsiTarget],
Features.CEPHFS: [CephFS],
Features.RGW: [Rgw, RgwDaemon, RgwBucket, RgwUser],
Features.NFS: [NFSGanesha, NFSGaneshaExports],
Features.NFS: [NFSGaneshaUi, NFSGaneshaExports],
}

View File

@ -6,7 +6,7 @@ from urllib.parse import urlencode
from .. import mgr
from ..controllers._version import APIVersion
from ..controllers.nfs import NFSGanesha, NFSGaneshaExports, NFSGaneshaUi
from ..controllers.nfs import NFSGaneshaExports, NFSGaneshaUi
from ..tests import ControllerTestCase
from ..tools import NotificationQueue, TaskManager
@ -228,19 +228,13 @@ class NFSGaneshaUiControllerTest(ControllerTestCase):
self.assertStatus(200)
self.assertJsonBody({'paths': []})
class NFSGaneshaControllerTest(ControllerTestCase):
@classmethod
def setup_server(cls):
cls.setup_controllers([NFSGanesha])
def test_status_available(self):
self._get('/api/nfs-ganesha/status')
self._get('/ui-api/nfs-ganesha/status')
self.assertStatus(200)
self.assertJsonBody({'available': True, 'message': None})
def test_status_not_available(self):
mgr.remote = Mock(side_effect=RuntimeError('Test'))
self._get('/api/nfs-ganesha/status')
self._get('/ui-api/nfs-ganesha/status')
self.assertStatus(200)
self.assertJsonBody({'available': False, 'message': 'Test'})

View File

@ -10,7 +10,7 @@ from ..tests import ControllerTestCase
class OrchestratorControllerTest(ControllerTestCase):
URL_STATUS = '/api/orchestrator/status'
URL_STATUS = '/ui-api/orchestrator/status'
URL_INVENTORY = '/api/orchestrator/inventory'
@classmethod

View File

@ -20,7 +20,7 @@ class RgwControllerTestCase(ControllerTestCase):
@patch.object(RgwClient, 'is_service_online', Mock(return_value=True))
@patch.object(RgwClient, '_is_system_user', Mock(return_value=True))
def test_status_available(self):
self._get('/test/api/rgw/status')
self._get('/test/ui-api/rgw/status')
self.assertStatus(200)
self.assertJsonBody({'available': True, 'message': None})
@ -28,7 +28,7 @@ class RgwControllerTestCase(ControllerTestCase):
@patch.object(RgwClient, 'is_service_online', Mock(
side_effect=RequestException('My test error')))
def test_status_online_check_error(self):
self._get('/test/api/rgw/status')
self._get('/test/ui-api/rgw/status')
self.assertStatus(200)
self.assertJsonBody({'available': False,
'message': 'My test error'})
@ -36,7 +36,7 @@ class RgwControllerTestCase(ControllerTestCase):
@patch.object(RgwClient, '_get_user_id', Mock(return_value='fake-user'))
@patch.object(RgwClient, 'is_service_online', Mock(return_value=False))
def test_status_not_online(self):
self._get('/test/api/rgw/status')
self._get('/test/ui-api/rgw/status')
self.assertStatus(200)
self.assertJsonBody({'available': False,
'message': "Failed to connect to the Object Gateway's Admin Ops API."})
@ -45,14 +45,14 @@ class RgwControllerTestCase(ControllerTestCase):
@patch.object(RgwClient, 'is_service_online', Mock(return_value=True))
@patch.object(RgwClient, '_is_system_user', Mock(return_value=False))
def test_status_not_system_user(self):
self._get('/test/api/rgw/status')
self._get('/test/ui-api/rgw/status')
self.assertStatus(200)
self.assertJsonBody({'available': False,
'message': 'The system flag is not set for user "fake-user".'})
def test_status_no_service(self):
RgwStub.get_mgr_no_services()
self._get('/test/api/rgw/status')
self._get('/test/ui-api/rgw/status')
self.assertStatus(200)
self.assertJsonBody({'available': False, 'message': 'No RGW service is running.'})