]> 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)
committerPere Diaz Bou <pdiazbou@redhat.com>
Thu, 30 Jun 2022 14:56:37 +0000 (16:56 +0200)
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>
 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
src/pybind/mgr/dashboard/tests/test_rbd_mirroring.py

error component and module-status-guard.service.ts had one minor
conflict which was resolved by getting incoming changes (add missing
logic).
test_rbd_mirroring had an import conflict which was resolved by
accepting incoming changes which had the same imports with new ones.

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 c653575dea5593109484cbd245378bd38383dd61..8e8a87b98c914e9fc05d4c4832b22d8459dadd7b 100644 (file)
@@ -344,3 +344,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 53210a3180372dcf57b12e0305f9497df3aa6575..b2c0527cc72ff2d5614350cb4e42a6877696dce7 100644 (file)
@@ -11,13 +11,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
 
 try:
     from typing import no_type_check
@@ -595,3 +599,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 ae569aab449da910cc9f3be23fa3d848b165a52c..40fffc2bce749e0c3350077d6ff06842bb1c065f 100644 (file)
@@ -139,8 +139,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 93438532e74034050ff17a7db8bbc0c95152b47f..68436b26aea3fcb5236089b2053f244d65dd691f 100644 (file)
         the {{ section_info }} management functionality.</h4>
     </div>
     <br><br>
+    <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>
     <div>
       <button class="btn btn-primary"
               [routerLink]="'/dashboard'"
index 1f2cf57c5088780a25fcf11c4bcc8cb69ac2e855..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',
@@ -20,8 +23,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();
@@ -32,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,6 +80,10 @@ export class ErrorComponent implements OnDestroy, OnInit {
       this.section_info = history.state.section_info;
       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 97c7251fbfac1a2da5651d0cff2183800e1c924f..e49879bbb6fa4f2ba175787fd9a436f3261ba0ce 100644 (file)
@@ -80,6 +80,11 @@ 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,
+              button_title: config.button_title,
+              uiConfig: config.uiConfig,
+              uiApiPath: config.uiApiPath,
               icon: Icons.wrench
             }
           });
index e064089706a644a1cde5ae5f7bbbd0193d55310b..69e3f11e247962f46b5e024f92f9706ff564b45b 100644 (file)
@@ -8,7 +8,9 @@ except ImportError:
     import unittest.mock as mock
 
 from .. import mgr
-from ..controllers.rbd_mirroring import RbdMirroring, RbdMirroringPoolBootstrap, RbdMirroringSummary
+from ..controllers.orchestrator import Orchestrator
+from ..controllers.rbd_mirroring import RbdMirroring, \
+    RbdMirroringPoolBootstrap, RbdMirroringStatus, RbdMirroringSummary
 from ..controllers.summary import Summary
 from ..services import progress
 from ..tests import ControllerTestCase
@@ -169,3 +171,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)