]> git.apps.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard: configure rbd mirroring
authorNizamudeen A <nia@redhat.com>
Mon, 6 Jun 2022 05:51:29 +0000 (11:21 +0530)
committerNizamudeen A <nia@redhat.com>
Fri, 17 Jun 2022 07:44:53 +0000 (13:14 +0530)
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 <nia@redhat.com>
src/pybind/mgr/dashboard/controllers/pool.py
src/pybind/mgr/dashboard/controllers/rbd_mirroring.py
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/block.module.ts
src/pybind/mgr/dashboard/frontend/src/app/core/error/error.component.html
src/pybind/mgr/dashboard/frontend/src/app/core/error/error.component.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/services/module-status-guard.service.ts
src/pybind/mgr/dashboard/tests/test_rbd_mirroring.py

index 269abfbc8ab7f867c49b05a8080e3a67262fe3fb..1e2e04e1b14da5548abe221d7c654a1b470a8b38 100644 (file)
@@ -345,3 +345,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'])
index f5bda8c8926f23d870291fdf6bf4af1f1f9001da..9730bc85f02d36996325f9ff1206107029404d66 100644 (file)
@@ -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')
 
@@ -574,3 +578,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()
index a4b95f1f2ed3f33331b31c6e11237361cd53295f..8a13f1c6925cbb83c367ed6551a412fd272d0c73 100644 (file)
@@ -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`,
index d8444902b86b3e010164ebba914cd6958d021bcf..0d63566acff364ffa50268ac72cbab83e52dd9b7 100644 (file)
         the {{ section_info }} management functionality.</h4>
     </div>
     <br><br>
-    <div *ngIf="button_name && button_route; else dashboardButton">
+    <div *ngIf="(button_name && button_route) || bootstrap; else dashboardButton">
       <button class="btn btn-primary"
               [routerLink]="button_route"
+              *ngIf="button_name && button_route && !bootstrap"
+              i18n>{{ button_name }}</button>
+
+      <button class="btn btn-primary"
+              (click)="doBootstrap()"
+              *ngIf="bootstrap"
               i18n>{{ button_name }}</button>
     </div>
     <ng-template #dashboardButton>
index 9b49ac9d678374f99e9c7a994b379a7bc0fc7926..f06df9959127cafc1387dd4cff99b42c4fbbd4ce 100644 (file)
@@ -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',
@@ -16,14 +19,18 @@ 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;
   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 +41,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;
   }
@@ -45,10 +78,12 @@ 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.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']);
index c237fb8f7e10c1755102329452391cfc0fd35128..9d677aee53f7f1010c1bea64d4ccf39faf55dcd2 100644 (file)
@@ -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
             }
           });
index a7660475d48f7daae92d58ac6c50bce04b18ddd9..60571d8e5543f2ec4a9be4c62a516babff2702d2 100644 (file)
@@ -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)