mirror of
https://github.com/ceph/ceph
synced 2025-02-23 19:17:37 +00:00
Merge pull request #46527 from rhcs-dashboard/mirroring-workflow
mgr/dashboard: configure rbd mirroring Reviewed-by: Avan Thakkar <athakkar@redhat.com> Reviewed-by: Ernesto Puerta <epuertat@redhat.com> Reviewed-by: Ilya Dryomov <idryomov@redhat.com> Reviewed-by: Pere Diaz Bou <pdiazbou@redhat.com>
This commit is contained in:
commit
e580e3c681
@ -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'])
|
||||
|
@ -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()
|
||||
|
@ -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`,
|
||||
|
@ -2,42 +2,62 @@
|
||||
<title>Error Page</title>
|
||||
<base target="_blank">
|
||||
</head>
|
||||
<div class="dashboard row">
|
||||
<div class="text-center content">
|
||||
<br>
|
||||
<div *ngIf="header && message; else elseBlock">
|
||||
<i class="{{ icon }}"
|
||||
aria-hidden="true"></i>
|
||||
<br><br><br>
|
||||
<h3><b>{{ header }}</b></h3>
|
||||
<br>
|
||||
<h4>{{ message }}</h4>
|
||||
<div class="container h-75">
|
||||
<div class="row h-100 justify-content-center align-items-center">
|
||||
<div class="blank-page">
|
||||
<div *ngIf="header && message; else elseBlock">
|
||||
<i [ngClass]="icon"
|
||||
class="mx-auto d-block"></i>
|
||||
|
||||
<div class="mt-4 text-center">
|
||||
<h3><b>{{ header }}</b></h3>
|
||||
<h4 class="mt-3"
|
||||
*ngIf="header !== message">{{ message }}</h4>
|
||||
<h4 *ngIf="section"
|
||||
i18n>Please consult the <a href="{{ docUrl }}">documentation</a> on how to configure and enable
|
||||
the {{ sectionInfo }} management functionality.
|
||||
</h4>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
<div class="text-center"
|
||||
*ngIf="(buttonName && buttonRoute) || uiConfig; else dashboardButton">
|
||||
<button class="btn btn-primary"
|
||||
[routerLink]="buttonRoute"
|
||||
*ngIf="!uiConfig; else configureButtonTpl"
|
||||
i18n>{{ buttonName }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ng-template #elseBlock>
|
||||
<i class="fa fa-exclamation-triangle"
|
||||
aria-hidden="true"></i>
|
||||
<br><br><br>
|
||||
<h3 i18n><b>Page not Found</b></h3>
|
||||
<br>
|
||||
<h4 i18n>Sorry, we couldn’t find what you were looking for.
|
||||
The page you requested may have been changed or moved.</h4>
|
||||
</ng-template>
|
||||
<div *ngIf="section">
|
||||
<h4 i18n>Please consult the <a href="{{ docUrl }}">documentation</a> on how to configure and enable
|
||||
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>
|
||||
|
||||
<ng-template #configureButtonTpl>
|
||||
<button class="btn btn-primary"
|
||||
(click)="doConfigure()"
|
||||
[attr.title]="buttonTitle"
|
||||
*ngIf="uiConfig"
|
||||
i18n>{{ buttonName }}</button>
|
||||
</ng-template>
|
||||
|
||||
|
||||
<ng-template #elseBlock>
|
||||
<i class="fa fa-exclamation-triangle mx-auto d-block text-danger"></i>
|
||||
|
||||
<div class="mt-4 text-center">
|
||||
<h3 i18n><b>Page not Found</b></h3>
|
||||
|
||||
<h4 class="mt-4"
|
||||
i18n>Sorry, we couldn’t find what you were looking for.
|
||||
The page you requested may have been changed or moved.</h4>
|
||||
</div>
|
||||
</ng-template>
|
||||
|
||||
<ng-template #dashboardButton>
|
||||
<div class="mt-4 text-center">
|
||||
<button class="btn btn-primary"
|
||||
[routerLink]="'/dashboard'"
|
||||
i18n>Go To Dashboard</button>
|
||||
</div>
|
||||
</ng-template>
|
||||
|
@ -9,35 +9,6 @@ i {
|
||||
margin-top: 120px;
|
||||
}
|
||||
|
||||
.text-center {
|
||||
background-color: vv.$body-bg-alt;
|
||||
}
|
||||
|
||||
.dashboard {
|
||||
background-color: vv.$body-bg-alt;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.content {
|
||||
left: 50%;
|
||||
position: absolute;
|
||||
top: 40%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.row {
|
||||
display: block;
|
||||
margin-left: -29px;
|
||||
margin-right: -29px;
|
||||
padding-top: 10em;
|
||||
}
|
||||
|
||||
.fa-exclamation-triangle {
|
||||
color: vv.$danger;
|
||||
}
|
||||
|
||||
.fa-lock {
|
||||
color: vv.$danger;
|
||||
}
|
||||
|
@ -2,6 +2,8 @@ import { HttpClientTestingModule } from '@angular/common/http/testing';
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
|
||||
import { ToastrModule } from 'ngx-toastr';
|
||||
|
||||
import { SharedModule } from '~/app/shared/shared.module';
|
||||
import { configureTestBed } from '~/testing/unit-test-helper';
|
||||
import { ErrorComponent } from './error.component';
|
||||
@ -12,7 +14,7 @@ describe('ErrorComponent', () => {
|
||||
|
||||
configureTestBed({
|
||||
declarations: [ErrorComponent],
|
||||
imports: [HttpClientTestingModule, RouterTestingModule, SharedModule]
|
||||
imports: [HttpClientTestingModule, RouterTestingModule, SharedModule, ToastrModule.forRoot()]
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
|
@ -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',
|
||||
@ -15,15 +18,24 @@ export class ErrorComponent implements OnDestroy, OnInit {
|
||||
header: string;
|
||||
message: string;
|
||||
section: string;
|
||||
section_info: string;
|
||||
button_name: string;
|
||||
button_route: string;
|
||||
sectionInfo: string;
|
||||
icon: string;
|
||||
docUrl: string;
|
||||
source: string;
|
||||
routerSubscription: Subscription;
|
||||
uiConfig: string;
|
||||
uiApiPath: string;
|
||||
buttonRoute: string;
|
||||
buttonName: string;
|
||||
buttonTitle: string;
|
||||
component: 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 +46,23 @@ export class ErrorComponent implements OnDestroy, OnInit {
|
||||
});
|
||||
}
|
||||
|
||||
doConfigure() {
|
||||
this.http.post(`ui-api/${this.uiApiPath}/configure`, {}).subscribe({
|
||||
next: () => {
|
||||
this.notificationService.show(NotificationType.info, `Configuring ${this.component}`);
|
||||
},
|
||||
error: (error: any) => {
|
||||
this.notificationService.show(NotificationType.error, error);
|
||||
},
|
||||
complete: () => {
|
||||
setTimeout(() => {
|
||||
this.router.navigate([this.uiApiPath]);
|
||||
this.notificationService.show(NotificationType.success, `Configured ${this.component}`);
|
||||
}, 3000);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@HostListener('window:beforeunload', ['$event']) unloadHandler(event: Event) {
|
||||
event.returnValue = false;
|
||||
}
|
||||
@ -44,11 +73,15 @@ export class ErrorComponent implements OnDestroy, OnInit {
|
||||
this.message = history.state.message;
|
||||
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.sectionInfo = history.state.section_info;
|
||||
this.icon = history.state.icon;
|
||||
this.source = history.state.source;
|
||||
this.uiConfig = history.state.uiConfig;
|
||||
this.uiApiPath = history.state.uiApiPath;
|
||||
this.buttonRoute = history.state.button_route;
|
||||
this.buttonName = history.state.button_name;
|
||||
this.buttonTitle = history.state.button_title;
|
||||
this.component = history.state.component;
|
||||
this.docUrl = this.docService.urlGenerator(this.section);
|
||||
} catch (error) {
|
||||
this.router.navigate(['/error']);
|
||||
|
@ -82,7 +82,11 @@ export class ModuleStatusGuardService implements CanActivate, CanActivateChild {
|
||||
section_info: config.section_info,
|
||||
button_name: config.button_name,
|
||||
button_route: config.button_route,
|
||||
icon: Icons.wrench
|
||||
button_title: config.button_title,
|
||||
uiConfig: config.uiConfig,
|
||||
uiApiPath: config.uiApiPath,
|
||||
icon: Icons.wrench,
|
||||
component: config.component
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -7228,26 +7228,9 @@ paths:
|
||||
application/json:
|
||||
schema:
|
||||
properties:
|
||||
application_metadata:
|
||||
type: string
|
||||
configuration:
|
||||
type: string
|
||||
erasure_code_profile:
|
||||
type: string
|
||||
flags:
|
||||
type: string
|
||||
pg_num:
|
||||
type: integer
|
||||
pool:
|
||||
default: rbd-mirror
|
||||
type: string
|
||||
pool_type:
|
||||
type: string
|
||||
rule_name:
|
||||
type: string
|
||||
required:
|
||||
- pool
|
||||
- pg_num
|
||||
- pool_type
|
||||
type: object
|
||||
responses:
|
||||
'201':
|
||||
|
@ -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)
|
||||
|
Loading…
Reference in New Issue
Block a user