From f2cdb2ef14d2046b7469a20ee427952a82bf0415 Mon Sep 17 00:00:00 2001 From: Nizamudeen A Date: Mon, 6 Jun 2022 11:21:29 +0530 Subject: [PATCH] mgr/dashboard: configure rbd mirroring Resolves: rhbz#1891012 One-click button in the case of an orch cluster for configuring the rbd-mirroring when its not properly setup. This button will create an rbd-mirror service and also an rbd labelled pool(replicated: size-3) (if they are not existing) Fixes: https://tracker.ceph.com/issues/55646 Signed-off-by: Nizamudeen A Conflicts: src/pybind/mgr/dashboard/frontend/src/app/core/error/error.component.html src/pybind/mgr/dashboard/frontend/src/app/shared/services/module-status-guard.service.ts This commits had minor conflicts where the addition of dashboardButton had to be explicitly added and the state of api/orch/status in module-status-guard had to be updated. (cherry picked from commit e918150212074a757c95f05ecb9d528ff1fe6e06) --- src/pybind/mgr/dashboard/controllers/pool.py | 6 +++ .../dashboard/controllers/rbd_mirroring.py | 46 ++++++++++++++++++- .../src/app/ceph/block/block.module.ts | 15 +++++- .../src/app/core/error/error.component.html | 8 +++- .../src/app/core/error/error.component.ts | 41 ++++++++++++++++- .../services/module-status-guard.service.ts | 3 ++ .../mgr/dashboard/tests/test_rbd_mirroring.py | 26 ++++++++++- 7 files changed, 138 insertions(+), 7 deletions(-) diff --git a/src/pybind/mgr/dashboard/controllers/pool.py b/src/pybind/mgr/dashboard/controllers/pool.py index 386d584404c88..f3aa6d0a4bc30 100644 --- a/src/pybind/mgr/dashboard/controllers/pool.py +++ b/src/pybind/mgr/dashboard/controllers/pool.py @@ -343,3 +343,9 @@ class PoolUi(Pool): "used_profiles": used_profiles, 'nodes': mgr.get('osd_map_tree')['nodes'] } + + +class RBDPool(Pool): + def create(self, pool='rbd-mirror'): # pylint: disable=arguments-differ + super().create(pool, pg_num=1, pool_type='replicated', + rule_name='replicated_rule', application_metadata=['rbd']) diff --git a/src/pybind/mgr/dashboard/controllers/rbd_mirroring.py b/src/pybind/mgr/dashboard/controllers/rbd_mirroring.py index 1e8536b09fd8a..17ef0b88b2a36 100644 --- a/src/pybind/mgr/dashboard/controllers/rbd_mirroring.py +++ b/src/pybind/mgr/dashboard/controllers/rbd_mirroring.py @@ -10,13 +10,17 @@ import cherrypy import rbd from .. import mgr +from ..controllers.pool import RBDPool +from ..controllers.service import Service from ..security import Scope from ..services.ceph_service import CephService from ..services.exception import handle_rados_error, handle_rbd_error, serialize_dashboard_exception +from ..services.orchestrator import OrchClient from ..services.rbd import rbd_call from ..tools import ViewCache -from . import APIDoc, APIRouter, BaseController, Endpoint, EndpointDoc, \ - ReadPermission, RESTController, Task, UpdatePermission, allow_empty_body +from . import APIDoc, APIRouter, BaseController, CreatePermission, Endpoint, \ + EndpointDoc, ReadPermission, RESTController, Task, UIRouter, \ + UpdatePermission, allow_empty_body logger = logging.getLogger('controllers.rbd_mirror') @@ -602,3 +606,41 @@ class RbdMirroringPoolPeer(RESTController): rbd.RBD().mirror_peer_set_attributes(ioctx, peer_uuid, attributes) _reset_view_cache() + + +@UIRouter('/block/mirroring', Scope.RBD_MIRRORING) +class RbdMirroringStatus(BaseController): + @EndpointDoc('Display RBD Mirroring Status') + @Endpoint() + @ReadPermission + def status(self): + status = {'available': True, 'message': None} + orch_status = OrchClient.instance().status() + + # if the orch is not available we can't create the service + # using dashboard. + if not orch_status['available']: + return status + if not CephService.get_service_list('rbd-mirror') or not CephService.get_pool_list('rbd'): + status['available'] = False + status['message'] = 'RBD mirroring is not configured' # type: ignore + return status + + @Endpoint('POST') + @EndpointDoc('Configure RBD Mirroring') + @CreatePermission + def configure(self): + rbd_pool = RBDPool() + service = Service() + + service_spec = { + 'service_type': 'rbd-mirror', + 'placement': {}, + 'unmanaged': False + } + + if not CephService.get_service_list('rbd-mirror'): + service.create(service_spec, 'rbd-mirror') + + if not CephService.get_pool_list('rbd'): + rbd_pool.create() diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/block.module.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/block.module.ts index a4b95f1f2ed3f..8a13f1c6925cb 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/block.module.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/block.module.ts @@ -149,8 +149,19 @@ const routes: Routes = [ { path: 'mirroring', component: RbdMirroringComponent, - canActivate: [FeatureTogglesGuardService], - data: { breadcrumbs: 'Mirroring' }, + canActivate: [FeatureTogglesGuardService, ModuleStatusGuardService], + data: { + moduleStatusGuardConfig: { + uiApiPath: 'block/mirroring', + redirectTo: 'error', + header: $localize`RBD mirroring is not configured`, + button_name: $localize`Configure RBD Mirroring`, + button_title: $localize`This will create rbd-mirror service and a replicated RBD pool`, + component: 'RBD Mirroring', + uiConfig: true + }, + breadcrumbs: 'Mirroring' + }, children: [ { path: `${URLVerbs.EDIT}/:pool_name`, diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/error/error.component.html b/src/pybind/mgr/dashboard/frontend/src/app/core/error/error.component.html index d8444902b86b3..0d63566acff36 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/core/error/error.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/core/error/error.component.html @@ -27,9 +27,15 @@ the {{ section_info }} management functionality.

-
+
+ +
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/error/error.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/core/error/error.component.ts index 9b49ac9d67837..ae57035693431 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/core/error/error.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/core/error/error.component.ts @@ -1,10 +1,13 @@ +import { HttpClient } from '@angular/common/http'; import { Component, HostListener, OnDestroy, OnInit } from '@angular/core'; import { NavigationEnd, Router, RouterEvent } from '@angular/router'; import { Subscription } from 'rxjs'; import { filter } from 'rxjs/operators'; +import { NotificationType } from '~/app/shared/enum/notification-type.enum'; import { DocService } from '~/app/shared/services/doc.service'; +import { NotificationService } from '~/app/shared/services/notification.service'; @Component({ selector: 'cd-error', @@ -22,8 +25,14 @@ export class ErrorComponent implements OnDestroy, OnInit { docUrl: string; source: string; routerSubscription: Subscription; + bootstrap: string; + uiApiPath: string; + button_route: string; + button_name: string; - constructor(private router: Router, private docService: DocService) {} + constructor(private router: Router, private docService: DocService, + private http: HttpClient, + private notificationService: NotificationService, ) {} ngOnInit() { this.fetchData(); @@ -34,6 +43,32 @@ export class ErrorComponent implements OnDestroy, OnInit { }); } + doBootstrap() { + this.http.post(`ui-api/${this.uiApiPath}/configure`, {}).subscribe({ + next: () => { + this.notificationService.show( + NotificationType.info, + 'Configuring RBD Mirroring' + ); + }, + error: (error: any) => { + this.notificationService.show( + NotificationType.error, + error + ); + }, + complete: () => { + setTimeout(() => { + this.router.navigate([this.uiApiPath]); + this.notificationService.show( + NotificationType.success, + 'Configured RBD Mirroring' + ); + }, 3000); + } + }); + } + @HostListener('window:beforeunload', ['$event']) unloadHandler(event: Event) { event.returnValue = false; } @@ -49,6 +84,10 @@ export class ErrorComponent implements OnDestroy, OnInit { this.button_route = history.state.button_route; this.icon = history.state.icon; this.source = history.state.source; + this.bootstrap = history.state.bootstrap; + this.uiApiPath = history.state.uiApiPath; + this.button_route = history.state.button_route; + this.button_name = history.state.button_name; this.docUrl = this.docService.urlGenerator(this.section); } catch (error) { this.router.navigate(['/error']); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/module-status-guard.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/module-status-guard.service.ts index c237fb8f7e10c..9d677aee53f7f 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/module-status-guard.service.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/module-status-guard.service.ts @@ -82,6 +82,9 @@ export class ModuleStatusGuardService implements CanActivate, CanActivateChild { section_info: config.section_info, button_name: config.button_name, button_route: config.button_route, + button_title: config.button_title, + uiConfig: config.uiConfig, + uiApiPath: config.uiApiPath, icon: Icons.wrench } }); diff --git a/src/pybind/mgr/dashboard/tests/test_rbd_mirroring.py b/src/pybind/mgr/dashboard/tests/test_rbd_mirroring.py index a7660475d48f7..60571d8e5543f 100644 --- a/src/pybind/mgr/dashboard/tests/test_rbd_mirroring.py +++ b/src/pybind/mgr/dashboard/tests/test_rbd_mirroring.py @@ -10,8 +10,10 @@ except ImportError: import unittest.mock as mock from .. import mgr +from ..controllers.orchestrator import Orchestrator from ..controllers.rbd_mirroring import RbdMirroring, \ - RbdMirroringPoolBootstrap, RbdMirroringSummary, get_daemons, get_pools + RbdMirroringPoolBootstrap, RbdMirroringStatus, RbdMirroringSummary, \ + get_daemons, get_pools from ..controllers.summary import Summary from ..services import progress from ..tests import ControllerTestCase @@ -279,3 +281,25 @@ class RbdMirroringSummaryControllerTest(ControllerTestCase): summary = self.json_body()['rbd_mirroring'] self.assertEqual(summary, {'errors': 0, 'warnings': 1}) + + +class RbdMirroringStatusControllerTest(ControllerTestCase): + + @classmethod + def setup_server(cls): + cls.setup_controllers([RbdMirroringStatus, Orchestrator]) + + @mock.patch('dashboard.controllers.orchestrator.OrchClient.instance') + def test_status(self, instance): + status = {'available': False, 'description': ''} + fake_client = mock.Mock() + fake_client.status.return_value = status + instance.return_value = fake_client + + self._get('/ui-api/block/mirroring/status') + self.assertStatus(200) + self.assertJsonBody({'available': True, 'message': None}) + + def test_configure(self): + self._post('/ui-api/block/mirroring/configure') + self.assertStatus(200) -- 2.39.5