From: Pere Diaz Bou Date: Fri, 13 May 2022 15:15:33 +0000 (+0200) Subject: mgr/dashboard: snapshot mirroring from dashboard X-Git-Url: http://git.apps.os.sepia.ceph.com/?a=commitdiff_plain;h=0e97e9fa6b3fd96630c641773573686d16d9a1e0;p=ceph.git mgr/dashboard: snapshot mirroring from dashboard Resolves: rhbz#1891012 Enable snapshot mirroring from the Pools -> Image Also show the mirror-snapshot in the image where snapshot is enabled When parsing images if an image has the snapshot mode enabled, it will try to run commands that don't work with that mode. The solution was not running those for now and appending the mode in the get call. Fixes: https://tracker.ceph.com/issues/55648 Signed-off-by: Pere Diaz Bou Signed-off-by: Nizamudeen A Signed-off-by: Aashish Sharma Signed-off-by: Avan Thakkar (cherry picked from commit 489a385a95d6ffa5dbd4c5f9c53c1f80ea179142) (cherry picked from commit 3ca9ca7e215562912daf00ca3cca40dbc5d560b5) --- diff --git a/qa/tasks/mgr/dashboard/test_rbd.py b/qa/tasks/mgr/dashboard/test_rbd.py index b8df472a69254..4e0367fd5be77 100644 --- a/qa/tasks/mgr/dashboard/test_rbd.py +++ b/qa/tasks/mgr/dashboard/test_rbd.py @@ -205,6 +205,7 @@ class RbdTest(DashboardTestCase): { "size": 1073741824, "obj_size": 4194304, + "mirror_mode": "journal", "num_objs": 256, "order": 22, "block_name_prefix": "rbd_data.10ae2ae8944a", @@ -245,6 +246,7 @@ class RbdTest(DashboardTestCase): 'source': JLeaf(int), 'value': JLeaf(str), })), + 'mirror_mode': JLeaf(str), }) self.assertSchema(img, schema) diff --git a/src/pybind/mgr/dashboard/__init__.py b/src/pybind/mgr/dashboard/__init__.py index 653474c6304db..d2eab9751fe07 100644 --- a/src/pybind/mgr/dashboard/__init__.py +++ b/src/pybind/mgr/dashboard/__init__.py @@ -48,5 +48,13 @@ else: os.path.dirname(__file__), 'frontend/dist')) + import rbd + + # Api tests do not mock rbd as opposed to dashboard unit tests. Both + # use UNITTEST env variable. + if isinstance(rbd, mock.Mock): + rbd.RBD_MIRROR_IMAGE_MODE_JOURNAL = 0 + rbd.RBD_MIRROR_IMAGE_MODE_SNAPSHOT = 1 + # DO NOT REMOVE: required for ceph-mgr to load a module from .module import Module, StandbyModule # noqa: F401 diff --git a/src/pybind/mgr/dashboard/controllers/rbd.py b/src/pybind/mgr/dashboard/controllers/rbd.py index c065c6d1403e5..2c360333f5ce6 100644 --- a/src/pybind/mgr/dashboard/controllers/rbd.py +++ b/src/pybind/mgr/dashboard/controllers/rbd.py @@ -5,7 +5,6 @@ import logging import math from datetime import datetime -from enum import Enum from functools import partial import rbd @@ -15,9 +14,10 @@ from ..exceptions import DashboardException 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.rbd import RbdConfiguration, RbdMirroringService, RbdService, \ - RbdSnapshotService, format_bitmask, format_features, parse_image_spec, \ - rbd_call, rbd_image_call +from ..services.rbd import MIRROR_IMAGE_MODE, RbdConfiguration, \ + RbdMirroringService, RbdService, RbdSnapshotService, format_bitmask, \ + format_features, get_image_spec, 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 @@ -77,10 +77,6 @@ class Rbd(RESTController): ALLOW_DISABLE_FEATURES = {"exclusive-lock", "object-map", "fast-diff", "deep-flatten", "journaling"} - class MIRROR_IMAGE_MODE(Enum): - journal = rbd.RBD_MIRROR_IMAGE_MODE_JOURNAL - snapshot = rbd.RBD_MIRROR_IMAGE_MODE_SNAPSHOT - def _rbd_list(self, pool_name=None): if pool_name: pools = [pool_name] @@ -114,8 +110,9 @@ class Rbd(RESTController): @RbdTask('create', {'pool_name': '{pool_name}', 'namespace': '{namespace}', 'image_name': '{name}'}, 2.0) - def create(self, name, pool_name, size, namespace=None, obj_size=None, features=None, - stripe_unit=None, stripe_count=None, data_pool=None, configuration=None): + def create(self, name, pool_name, size, namespace=None, schedule_interval='', + obj_size=None, features=None, stripe_unit=None, stripe_count=None, + data_pool=None, configuration=None, mirror_mode=None): size = int(size) @@ -137,6 +134,13 @@ class Rbd(RESTController): image_name=name).set_configuration(configuration) rbd_call(pool_name, namespace, _create) + if mirror_mode: + RbdMirroringService.enable_image(name, pool_name, namespace, + MIRROR_IMAGE_MODE[mirror_mode]) + + if schedule_interval: + image_spec = get_image_spec(pool_name, namespace, name) + RbdMirroringService.snapshot_schedule_add(image_spec, schedule_interval) @RbdTask('delete', ['{image_spec}'], 2.0) def delete(self, image_spec): @@ -153,7 +157,9 @@ class Rbd(RESTController): @RbdTask('edit', ['{image_spec}', '{name}'], 4.0) def set(self, image_spec, name=None, size=None, features=None, configuration=None, enable_mirror=None, primary=None, - resync=False, mirror_mode=None, schedule_interval='', start_time=''): + resync=False, mirror_mode=None, schedule_interval='', + remove_scheduling=False): + pool_name, namespace, image_name = parse_image_spec(image_spec) def _edit(ioctx, image): @@ -193,7 +199,7 @@ class Rbd(RESTController): if enable_mirror and mirror_image_info['state'] == rbd.RBD_MIRROR_IMAGE_DISABLED: RbdMirroringService.enable_image( image_name, pool_name, namespace, - self.MIRROR_IMAGE_MODE[mirror_mode].value) + MIRROR_IMAGE_MODE[mirror_mode]) elif (enable_mirror is False and mirror_image_info['state'] == rbd.RBD_MIRROR_IMAGE_ENABLED): RbdMirroringService.disable_image( @@ -210,7 +216,10 @@ class Rbd(RESTController): RbdMirroringService.resync_image(image_name, pool_name, namespace) if schedule_interval: - RbdMirroringService.snapshot_schedule(image_spec, schedule_interval, start_time) + RbdMirroringService.snapshot_schedule_add(image_spec, schedule_interval) + + if remove_scheduling: + RbdMirroringService.snapshot_schedule_remove(image_spec) return rbd_image_call(pool_name, namespace, image_name, _edit) 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 472fe37f19fa6..ae569aab449da 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 @@ -21,6 +21,7 @@ import { IscsiTargetListComponent } from './iscsi-target-list/iscsi-target-list. import { IscsiComponent } from './iscsi/iscsi.component'; import { MirroringModule } from './mirroring/mirroring.module'; import { OverviewComponent as RbdMirroringComponent } from './mirroring/overview/overview.component'; +import { PoolEditModeModalComponent } from './mirroring/pool-edit-mode-modal/pool-edit-mode-modal.component'; import { RbdConfigurationFormComponent } from './rbd-configuration-form/rbd-configuration-form.component'; import { RbdConfigurationListComponent } from './rbd-configuration-list/rbd-configuration-list.component'; import { RbdDetailsComponent } from './rbd-details/rbd-details.component'; @@ -139,7 +140,14 @@ const routes: Routes = [ path: 'mirroring', component: RbdMirroringComponent, canActivate: [FeatureTogglesGuardService], - data: { breadcrumbs: 'Mirroring' } + data: { breadcrumbs: 'Mirroring' }, + children: [ + { + path: `${URLVerbs.EDIT}/:pool_name`, + component: PoolEditModeModalComponent, + outlet: 'modal' + } + ] }, // iSCSI { diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirroring/pool-edit-mode-modal/pool-edit-mode-modal.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirroring/pool-edit-mode-modal/pool-edit-mode-modal.component.html index 03b49a8030d65..00fe92b32b734 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirroring/pool-edit-mode-modal/pool-edit-mode-modal.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirroring/pool-edit-mode-modal/pool-edit-mode-modal.component.html @@ -1,4 +1,5 @@ - + Edit pool mirror mode diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirroring/pool-edit-mode-modal/pool-edit-mode-modal.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirroring/pool-edit-mode-modal/pool-edit-mode-modal.component.spec.ts index a22a60d5e736c..11ba12334f38c 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirroring/pool-edit-mode-modal/pool-edit-mode-modal.component.spec.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirroring/pool-edit-mode-modal/pool-edit-mode-modal.component.spec.ts @@ -1,6 +1,7 @@ import { HttpClientTestingModule } from '@angular/common/http/testing'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ReactiveFormsModule } from '@angular/forms'; +import { ActivatedRoute } from '@angular/router'; import { RouterTestingModule } from '@angular/router/testing'; import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; @@ -10,6 +11,7 @@ import { of } from 'rxjs'; import { RbdMirroringService } from '~/app/shared/api/rbd-mirroring.service'; import { NotificationService } from '~/app/shared/services/notification.service'; import { SharedModule } from '~/app/shared/shared.module'; +import { ActivatedRouteStub } from '~/testing/activated-route-stub'; import { configureTestBed, FormHelper } from '~/testing/unit-test-helper'; import { PoolEditModeModalComponent } from './pool-edit-mode-modal.component'; @@ -19,6 +21,7 @@ describe('PoolEditModeModalComponent', () => { let notificationService: NotificationService; let rbdMirroringService: RbdMirroringService; let formHelper: FormHelper; + let activatedRoute: ActivatedRouteStub; configureTestBed({ declarations: [PoolEditModeModalComponent], @@ -29,7 +32,13 @@ describe('PoolEditModeModalComponent', () => { SharedModule, ToastrModule.forRoot() ], - providers: [NgbActiveModal] + providers: [ + NgbActiveModal, + { + provide: ActivatedRoute, + useValue: new ActivatedRouteStub({ pool_name: 'somePool' }) + } + ] }); beforeEach(() => { @@ -41,6 +50,7 @@ describe('PoolEditModeModalComponent', () => { spyOn(notificationService, 'show').and.stub(); rbdMirroringService = TestBed.inject(RbdMirroringService); + activatedRoute = TestBed.inject(ActivatedRoute); formHelper = new FormHelper(component.editModeForm); fixture.detectChanges(); @@ -55,11 +65,8 @@ describe('PoolEditModeModalComponent', () => { spyOn(component.activeModal, 'close').and.callThrough(); }); - afterEach(() => { - expect(component.activeModal.close).toHaveBeenCalledTimes(1); - }); - it('should call updatePool', () => { + activatedRoute.setParams({ pool_name: 'somePool' }); spyOn(rbdMirroringService, 'updatePool').and.callFake(() => of('')); component.editModeForm.patchValue({ mirrorMode: 'disabled' }); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirroring/pool-edit-mode-modal/pool-edit-mode-modal.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirroring/pool-edit-mode-modal/pool-edit-mode-modal.component.ts index 137e787174d7a..ef30c888c8ba6 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirroring/pool-edit-mode-modal/pool-edit-mode-modal.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirroring/pool-edit-mode-modal/pool-edit-mode-modal.component.ts @@ -1,5 +1,7 @@ +import { Location } from '@angular/common'; import { Component, OnDestroy, OnInit } from '@angular/core'; import { AbstractControl, FormControl, Validators } from '@angular/forms'; +import { ActivatedRoute } from '@angular/router'; import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; import { Subscription } from 'rxjs'; @@ -40,7 +42,9 @@ export class PoolEditModeModalComponent implements OnInit, OnDestroy { public activeModal: NgbActiveModal, public actionLabels: ActionLabelsI18n, private rbdMirroringService: RbdMirroringService, - private taskWrapper: TaskWrapperService + private taskWrapper: TaskWrapperService, + private route: ActivatedRoute, + private location: Location ) { this.createForm(); } @@ -54,6 +58,9 @@ export class PoolEditModeModalComponent implements OnInit, OnDestroy { } ngOnInit() { + this.route.params.subscribe((params: { pool_name: string }) => { + this.poolName = params.pool_name; + }); this.pattern = `${this.poolName}`; this.rbdMirroringService.getPool(this.poolName).subscribe((resp: PoolEditModeResponseModel) => { this.setResponse(resp); @@ -97,7 +104,7 @@ export class PoolEditModeModalComponent implements OnInit, OnDestroy { error: () => this.editModeForm.setErrors({ cdSubmitButton: true }), complete: () => { this.rbdMirroringService.refresh(); - this.activeModal.close(); + this.location.back(); } }); } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirroring/pool-list/pool-list.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirroring/pool-list/pool-list.component.html index b6081b8e03d63..1e4e72df19651 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirroring/pool-list/pool-list.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirroring/pool-list/pool-list.component.html @@ -20,3 +20,4 @@ let-value="value"> {{ value }} + diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirroring/pool-list/pool-list.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirroring/pool-list/pool-list.component.ts index 29b435900df6f..a5e1c9e4b959b 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirroring/pool-list/pool-list.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/mirroring/pool-list/pool-list.component.ts @@ -1,4 +1,5 @@ import { Component, OnDestroy, OnInit, TemplateRef, ViewChild } from '@angular/core'; +import { Router } from '@angular/router'; import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; import { Observable, Subscriber, Subscription } from 'rxjs'; @@ -6,6 +7,7 @@ import { Observable, Subscriber, Subscription } from 'rxjs'; import { RbdMirroringService } from '~/app/shared/api/rbd-mirroring.service'; import { TableStatusViewCache } from '~/app/shared/classes/table-status-view-cache'; import { CriticalConfirmationModalComponent } from '~/app/shared/components/critical-confirmation-modal/critical-confirmation-modal.component'; +import { URLVerbs } from '~/app/shared/constants/app.constants'; import { Icons } from '~/app/shared/enum/icons.enum'; import { CdTableAction } from '~/app/shared/models/cd-table-action'; import { CdTableSelection } from '~/app/shared/models/cd-table-selection'; @@ -14,9 +16,9 @@ import { Permission } from '~/app/shared/models/permissions'; import { AuthStorageService } from '~/app/shared/services/auth-storage.service'; import { ModalService } from '~/app/shared/services/modal.service'; import { TaskWrapperService } from '~/app/shared/services/task-wrapper.service'; -import { PoolEditModeModalComponent } from '../pool-edit-mode-modal/pool-edit-mode-modal.component'; import { PoolEditPeerModalComponent } from '../pool-edit-peer-modal/pool-edit-peer-modal.component'; +const BASE_URL = '/block/mirroring'; @Component({ selector: 'cd-mirroring-pools', templateUrl: './pool-list.component.html', @@ -43,7 +45,8 @@ export class PoolListComponent implements OnInit, OnDestroy { private authStorageService: AuthStorageService, private rbdMirroringService: RbdMirroringService, private modalService: ModalService, - private taskWrapper: TaskWrapperService + private taskWrapper: TaskWrapperService, + private router: Router ) { this.data = []; this.permission = this.authStorageService.getPermissions().rbdMirroring; @@ -111,10 +114,10 @@ export class PoolListComponent implements OnInit, OnDestroy { } editModeModal() { - const initialState = { - poolName: this.selection.first().name - }; - this.modalRef = this.modalService.show(PoolEditModeModalComponent, initialState); + this.router.navigate([ + BASE_URL, + { outlets: { modal: [URLVerbs.EDIT, this.selection.first().name] } } + ]); } editPeersModal(mode: string) { diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-details/rbd-details.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-details/rbd-details.component.html index 7503863e0fb2c..ab9454cbc15eb 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-details/rbd-details.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-details/rbd-details.component.html @@ -133,6 +133,7 @@ [featuresName]="selection.features_name" [poolName]="selection.pool_name" [namespace]="selection.namespace" + [mirroring]="selection.mirror_mode" [rbdName]="selection.name"> diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-form/rbd-form-edit-request.model.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-form/rbd-form-edit-request.model.ts index 9187b024d9d01..6d1f8c7d3c9fd 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-form/rbd-form-edit-request.model.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-form/rbd-form-edit-request.model.ts @@ -5,4 +5,9 @@ export class RbdFormEditRequestModel { size: number; features: Array = []; configuration: RbdConfigurationEntry[]; + + enable_mirror?: boolean; + mirror_mode?: string; + schedule_interval: string; + remove_scheduling? = false; } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-form/rbd-form.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-form/rbd-form.component.html index 668556fa0e24d..ad55b26ff7a0b 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-form/rbd-form.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-form/rbd-form.component.html @@ -68,7 +68,8 @@ name="pool" class="form-control" formControlName="pool" - *ngIf="mode !== 'editing' && poolPermission.read"> + *ngIf="mode !== 'editing' && poolPermission.read" + (change)="setPoolMirrorMode()"> @@ -238,6 +239,62 @@ + +
+
+
+ + + + You need to enable a mirror mode in the selected pool. Please click here to select a mode and enable it in this pool. + +
+
+
+ + + + You need to enable image mirror mode in the selected pool. Please click here to select a mode and enable it in this pool. + +
+
+
+
+ +
+ +
+ +
+
+
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-form/rbd-form.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-form/rbd-form.component.spec.ts index 8c00c7460ab6a..5df73d93fe1ee 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-form/rbd-form.component.spec.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-form/rbd-form.component.spec.ts @@ -304,12 +304,7 @@ describe('RbdFormComponent', () => { }); describe('tests for feature flags', () => { - let deepFlatten: any, - layering: any, - exclusiveLock: any, - objectMap: any, - journaling: any, - fastDiff: any; + let deepFlatten: any, layering: any, exclusiveLock: any, objectMap: any, fastDiff: any; const defaultFeatures = [ // Supposed to be enabled by default 'deep-flatten', @@ -323,7 +318,6 @@ describe('RbdFormComponent', () => { 'layering', 'exclusive-lock', 'object-map', - 'journaling', 'fast-diff' ]; const setFeatures = (features: Record) => { @@ -359,14 +353,7 @@ describe('RbdFormComponent', () => { spyOn(rbdService, 'defaultFeatures').and.returnValue(of(defaultFeatures)); setRouterUrl('edit', pool, image); fixture.detectChanges(); - [ - deepFlatten, - layering, - exclusiveLock, - objectMap, - journaling, - fastDiff - ] = getFeatureNativeElements(); + [deepFlatten, layering, exclusiveLock, objectMap, fastDiff] = getFeatureNativeElements(); }; it('should have the interlock feature for flags disabled, if one feature is not set', () => { @@ -409,14 +396,7 @@ describe('RbdFormComponent', () => { spyOn(rbdService, 'defaultFeatures').and.returnValue(of(defaultFeatures)); setRouterUrl('create'); fixture.detectChanges(); - [ - deepFlatten, - layering, - exclusiveLock, - objectMap, - journaling, - fastDiff - ] = getFeatureNativeElements(); + [deepFlatten, layering, exclusiveLock, objectMap, fastDiff] = getFeatureNativeElements(); }); it('should initialize the checkboxes correctly', () => { @@ -424,21 +404,18 @@ describe('RbdFormComponent', () => { expect(layering.disabled).toBe(false); expect(exclusiveLock.disabled).toBe(false); expect(objectMap.disabled).toBe(false); - expect(journaling.disabled).toBe(false); expect(fastDiff.disabled).toBe(false); expect(deepFlatten.checked).toBe(true); expect(layering.checked).toBe(true); expect(exclusiveLock.checked).toBe(true); expect(objectMap.checked).toBe(true); - expect(journaling.checked).toBe(false); expect(fastDiff.checked).toBe(true); }); it('should disable features if their requirements are not met (exclusive-lock)', () => { exclusiveLock.click(); // unchecks exclusive-lock expect(objectMap.disabled).toBe(true); - expect(journaling.disabled).toBe(true); expect(fastDiff.disabled).toBe(true); }); @@ -447,5 +424,39 @@ describe('RbdFormComponent', () => { expect(fastDiff.disabled).toBe(true); }); }); + + describe('test mirroring options', () => { + beforeEach(() => { + component.ngOnInit(); + fixture.detectChanges(); + const mirroring = fixture.debugElement.query(By.css('#mirroring')).nativeElement; + mirroring.click(); + fixture.detectChanges(); + }); + + it('should verify two mirroring options are shown', () => { + const journal = fixture.debugElement.query(By.css('#journal')).nativeElement; + const snapshot = fixture.debugElement.query(By.css('#snapshot')).nativeElement; + expect(journal).not.toBeNull(); + expect(snapshot).not.toBeNull(); + }); + + it('should verify only snapshot is disabled for pools that are in pool mirror mode', () => { + component.poolMirrorMode = 'pool'; + fixture.detectChanges(); + const journal = fixture.debugElement.query(By.css('#journal')).nativeElement; + const snapshot = fixture.debugElement.query(By.css('#snapshot')).nativeElement; + expect(journal.disabled).toBe(false); + expect(snapshot.disabled).toBe(true); + }); + + it('should set and disable exclusive-lock only for the journal mode', () => { + component.poolMirrorMode = 'pool'; + fixture.detectChanges(); + const exclusiveLocks = fixture.debugElement.query(By.css('#exclusive-lock')).nativeElement; + expect(exclusiveLocks.checked).toBe(true); + expect(exclusiveLocks.disabled).toBe(true); + }); + }); }); }); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-form/rbd-form.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-form/rbd-form.component.ts index 1dc1df0dd3aba..6b6058468f46e 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-form/rbd-form.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-form/rbd-form.component.ts @@ -8,6 +8,7 @@ import { first, switchMap } from 'rxjs/operators'; import { Pool } from '~/app/ceph/pool/pool'; import { PoolService } from '~/app/shared/api/pool.service'; +import { RbdMirroringService } from '~/app/shared/api/rbd-mirroring.service'; import { RbdService } from '~/app/shared/api/rbd.service'; import { ActionLabelsI18n } from '~/app/shared/constants/app.constants'; import { Icons } from '~/app/shared/enum/icons.enum'; @@ -77,6 +78,11 @@ export class RbdFormComponent extends CdForm implements OnInit { defaultObjectSize = '4 MiB'; + mirroringOptions = ['journal', 'snapshot']; + poolMirrorMode: string; + mirroring = false; + currentPoolName = ''; + objectSizes: Array = [ '4 KiB', '8 KiB', @@ -109,7 +115,8 @@ export class RbdFormComponent extends CdForm implements OnInit { private taskWrapper: TaskWrapperService, private dimlessBinaryPipe: DimlessBinaryPipe, public actionLabels: ActionLabelsI18n, - private router: Router + private router: Router, + private rbdMirroringService: RbdMirroringService ) { super(); this.routerUrl = this.router.url; @@ -141,13 +148,6 @@ export class RbdFormComponent extends CdForm implements OnInit { allowDisable: true, initDisabled: true }, - journaling: { - desc: $localize`Journaling (requires exclusive-lock)`, - requires: 'exclusive-lock', - allowEnable: true, - allowDisable: true, - initDisabled: true - }, 'fast-diff': { desc: $localize`Fast diff (interlocked with object-map)`, requires: 'object-map', @@ -188,6 +188,11 @@ export class RbdFormComponent extends CdForm implements OnInit { return acc; }, {}) ), + mirroring: new FormControl(false), + schedule: new FormControl('', { + validators: [Validators.pattern(/^([0-9]+)d|([0-9]+)h|([0-9]+)m$/)] // check schedule interval to be in format - 1d or 1h or 1m + }), + mirroringMode: new FormControl(this.mirroringOptions[0]), stripingUnit: new FormControl(null), stripingCount: new FormControl(null, { updateOn: 'blur' @@ -232,6 +237,48 @@ export class RbdFormComponent extends CdForm implements OnInit { this.gatherNeededData().subscribe(this.handleExternalData.bind(this)); } + setExclusiveLock() { + if (this.mirroring && this.rbdForm.get('mirroringMode').value === 'journal') { + this.rbdForm.get('exclusive-lock').setValue(true); + this.rbdForm.get('exclusive-lock').disable(); + } else { + this.rbdForm.get('exclusive-lock').enable(); + if (this.poolMirrorMode === 'pool') { + this.rbdForm.get('mirroringMode').setValue(this.mirroringOptions[0]); + } + } + } + + setMirrorMode() { + this.mirroring = !this.mirroring; + this.setExclusiveLock(); + } + + setPoolMirrorMode() { + this.currentPoolName = + this.mode === this.rbdFormMode.editing + ? this.response?.pool_name + : this.rbdForm.getValue('pool'); + if (this.currentPoolName) { + this.rbdMirroringService.refresh(); + this.rbdMirroringService.subscribeSummary((data) => { + const pool = data.content_data.pools.find((o: any) => o.name === this.currentPoolName); + this.poolMirrorMode = pool.mirror_mode; + + if (pool.mirror_mode === 'disabled') { + this.mirroring = false; + this.rbdForm.get('mirroring').setValue(this.mirroring); + this.rbdForm.get('mirroring').disable(); + } else if (this.mode !== this.rbdFormMode.editing) { + this.rbdForm.get('mirroring').enable(); + this.mirroring = true; + this.rbdForm.get('mirroring').setValue(this.mirroring); + } + }); + } + this.setExclusiveLock(); + } + private prepareFormForAction() { const url = this.routerUrl; if (url.startsWith('/block/rbd/edit')) { @@ -285,6 +332,7 @@ export class RbdFormComponent extends CdForm implements OnInit { private handleExternalData(data: ExternalData) { this.handlePoolData(data.pools); + this.setPoolMirrorMode(); if (data.defaultFeatures) { // Fetched only during creation @@ -543,6 +591,16 @@ export class RbdFormComponent extends CdForm implements OnInit { } if (this.mode === this.rbdFormMode.editing) { this.rbdForm.get('name').setValue(response.name); + if (response?.mirror_mode === 'snapshot' || response.features_name.includes('journaling')) { + this.mirroring = true; + this.rbdForm.get('mirroring').setValue(this.mirroring); + this.rbdForm.get('mirroringMode').setValue(response?.mirror_mode); + this.rbdForm.get('schedule').setValue(response?.schedule_interval); + } else { + this.mirroring = false; + this.rbdForm.get('mirroring').setValue(this.mirroring); + } + this.setPoolMirrorMode(); } this.rbdForm.get('pool').setValue(response.pool_name); this.onPoolChange(response.pool_name); @@ -570,7 +628,11 @@ export class RbdFormComponent extends CdForm implements OnInit { request.pool_name = this.rbdForm.getValue('pool'); request.namespace = this.rbdForm.getValue('namespace'); request.name = this.rbdForm.getValue('name'); + request.schedule_interval = this.rbdForm.getValue('schedule'); request.size = this.formatter.toBytes(this.rbdForm.getValue('size')); + if (this.poolMirrorMode === 'image') { + request.mirror_mode = this.rbdForm.getValue('mirroringMode'); + } this.addObjectSizeAndStripingToRequest(request); request.configuration = this.getDirtyConfigurationValues(); return request; @@ -586,6 +648,10 @@ export class RbdFormComponent extends CdForm implements OnInit { } }); + if (this.mirroring && this.rbdForm.getValue('mirroringMode') === 'journal') { + request.features.push('journaling'); + } + /* Striping */ request.stripe_unit = this.formatter.toBytes(this.rbdForm.getValue('stripingUnit')); request.stripe_count = this.rbdForm.getValue('stripingCount'); @@ -598,7 +664,9 @@ export class RbdFormComponent extends CdForm implements OnInit { task: new FinishedTask('rbd/create', { pool_name: request.pool_name, namespace: request.namespace, - image_name: request.name + image_name: request.name, + schedule_interval: request.schedule_interval, + start_time: request.start_time }), call: this.rbdService.create(request) }); @@ -607,12 +675,29 @@ export class RbdFormComponent extends CdForm implements OnInit { editRequest() { const request = new RbdFormEditRequestModel(); request.name = this.rbdForm.getValue('name'); + request.schedule_interval = this.rbdForm.getValue('schedule'); + request.name = this.rbdForm.getValue('name'); request.size = this.formatter.toBytes(this.rbdForm.getValue('size')); _.forIn(this.features, (feature) => { if (this.rbdForm.getValue(feature.key)) { request.features.push(feature.key); } }); + request.enable_mirror = this.rbdForm.getValue('mirroring'); + if (this.poolMirrorMode === 'image') { + if (request.enable_mirror) { + request.mirror_mode = this.rbdForm.getValue('mirroringMode'); + } + } else { + if (request.enable_mirror) { + request.features.push('journaling'); + } else { + const index = request.features.indexOf('journaling', 0); + if (index > -1) { + request.features.splice(index, 1); + } + } + } request.configuration = this.getDirtyConfigurationValues(); return request; } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-form/rbd-form.model.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-form/rbd-form.model.ts index 36c7b5ea86950..262d79c95baec 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-form/rbd-form.model.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-form/rbd-form.model.ts @@ -17,4 +17,10 @@ export class RbdFormModel { /* Deletion process */ source?: string; + + enable_mirror?: boolean; + mirror_mode?: string; + + schedule_interval: string; + start_time: string; } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-list/rbd-list.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-list/rbd-list.component.html index ea2bd668fbaa9..824e05f8034f3 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-list/rbd-list.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-list/rbd-list.component.html @@ -24,7 +24,7 @@ - +
@@ -56,6 +56,18 @@ -
+ + {{ value[0] }}  + {{ value[1] }} + + {{ value }} + + + You are about to flatten diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-list/rbd-list.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-list/rbd-list.component.spec.ts index 02cf636ac89e6..a63ac2379fd59 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-list/rbd-list.component.spec.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-list/rbd-list.component.spec.ts @@ -314,11 +314,19 @@ describe('RbdListComponent', () => { expect(tableActions).toEqual({ 'create,update,delete': { - actions: ['Create', 'Edit', 'Copy', 'Flatten', 'Delete', 'Move to Trash'], + actions: [ + 'Create', + 'Edit', + 'Copy', + 'Flatten', + 'Delete', + 'Move to Trash', + 'Remove Scheduling' + ], primary: { multiple: 'Create', executing: 'Edit', single: 'Edit', no: 'Create' } }, 'create,update': { - actions: ['Create', 'Edit', 'Copy', 'Flatten'], + actions: ['Create', 'Edit', 'Copy', 'Flatten', 'Remove Scheduling'], primary: { multiple: 'Create', executing: 'Edit', single: 'Edit', no: 'Create' } }, 'create,delete': { @@ -330,11 +338,11 @@ describe('RbdListComponent', () => { primary: { multiple: 'Create', executing: 'Copy', single: 'Copy', no: 'Create' } }, 'update,delete': { - actions: ['Edit', 'Flatten', 'Delete', 'Move to Trash'], + actions: ['Edit', 'Flatten', 'Delete', 'Move to Trash', 'Remove Scheduling'], primary: { multiple: 'Edit', executing: 'Edit', single: 'Edit', no: 'Edit' } }, update: { - actions: ['Edit', 'Flatten'], + actions: ['Edit', 'Flatten', 'Remove Scheduling'], primary: { multiple: 'Edit', executing: 'Edit', single: 'Edit', no: 'Edit' } }, delete: { diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-list/rbd-list.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-list/rbd-list.component.ts index 0ffb25695c8d2..c77a627324eed 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-list/rbd-list.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-list/rbd-list.component.ts @@ -2,6 +2,7 @@ import { Component, OnInit, TemplateRef, ViewChild } from '@angular/core'; import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; import _ from 'lodash'; +import { Observable, Subscriber } from 'rxjs'; import { RbdService } from '~/app/shared/api/rbd.service'; import { ListWithDetails } from '~/app/shared/classes/list-with-details.class'; @@ -26,6 +27,7 @@ import { ModalService } from '~/app/shared/services/modal.service'; import { TaskListService } from '~/app/shared/services/task-list.service'; import { TaskWrapperService } from '~/app/shared/services/task-wrapper.service'; import { URLBuilderService } from '~/app/shared/services/url-builder.service'; +import { RbdFormEditRequestModel } from '../rbd-form/rbd-form-edit-request.model'; import { RbdParentModel } from '../rbd-form/rbd-parent.model'; import { RbdTrashMoveModalComponent } from '../rbd-trash-move-modal/rbd-trash-move-modal.component'; import { RBDImageFormat, RbdModel } from './rbd-model'; @@ -50,6 +52,8 @@ export class RbdListComponent extends ListWithDetails implements OnInit { parentTpl: TemplateRef; @ViewChild('nameTpl') nameTpl: TemplateRef; + @ViewChild('mirroringTpl', { static: true }) + mirroringTpl: TemplateRef; @ViewChild('flattenTpl', { static: true }) flattenTpl: TemplateRef; @ViewChild('deleteTpl', { static: true }) @@ -89,6 +93,7 @@ export class RbdListComponent extends ListWithDetails implements OnInit { metadata['dest_image_name'] ) }; + remove_scheduling: boolean; private createRbdFromTaskImageSpec(imageSpecStr: string): RbdModel { const imageSpec = ImageSpec.fromString(imageSpecStr); @@ -180,13 +185,24 @@ export class RbdListComponent extends ListWithDetails implements OnInit { this.getInvalidNameDisable(selection) || selection.first().image_format === RBDImageFormat.V1 }; + const removeSchedulingAction: CdTableAction = { + permission: 'update', + icon: Icons.edit, + click: () => this.removeSchedulingModal(), + name: this.actionLabels.REMOVE_SCHEDULING, + disable: (selection: CdTableSelection) => + this.getRemovingStatusDesc(selection) || + this.getInvalidNameDisable(selection) || + selection.first().schedule_info === undefined + }; this.tableActions = [ addAction, editAction, copyAction, flattenAction, deleteAction, - moveAction + moveAction, + removeSchedulingAction ]; } @@ -250,6 +266,12 @@ export class RbdListComponent extends ListWithDetails implements OnInit { prop: 'parent', flexGrow: 2, cellTemplate: this.parentTpl + }, + { + name: $localize`Mirroring`, + prop: 'mirror_mode', + flexGrow: 3, + cellTemplate: this.mirroringTpl } ]; @@ -345,6 +367,19 @@ export class RbdListComponent extends ListWithDetails implements OnInit { this.tableStatus = new TableStatusViewCache(); } + images.forEach((image) => { + if (image.schedule_info !== undefined) { + let scheduling: any[] = []; + const scheduleStatus = 'scheduled'; + let nextSnapshotDate = +new Date(image.schedule_info.schedule_time); + const offset = new Date().getTimezoneOffset(); + nextSnapshotDate = nextSnapshotDate + Math.abs(offset) * 60000; + scheduling.push(image.mirror_mode, scheduleStatus, nextSnapshotDate); + image.mirror_mode = scheduling; + scheduling = []; + } + }); + return images; } @@ -429,6 +464,44 @@ export class RbdListComponent extends ListWithDetails implements OnInit { this.modalRef = this.modalService.show(ConfirmationModalComponent, initialState); } + editRequest() { + const request = new RbdFormEditRequestModel(); + request.remove_scheduling = !request.remove_scheduling; + return request; + } + + removeSchedulingModal() { + const imageName = this.selection.first().name; + + const imageSpec = new ImageSpec( + this.selection.first().pool_name, + this.selection.first().namespace, + this.selection.first().name + ); + + this.modalRef = this.modalService.show(CriticalConfirmationModalComponent, { + actionDescription: 'remove scheduling on', + itemDescription: $localize`image`, + itemNames: [`${imageName}`], + submitActionObservable: () => + new Observable((observer: Subscriber) => { + this.taskWrapper + .wrapTaskAroundCall({ + task: new FinishedTask('rbd/edit', { + image_spec: imageSpec.toString() + }), + call: this.rbdService.update(imageSpec, this.editRequest()) + }) + .subscribe({ + error: (resp) => observer.error(resp), + complete: () => { + this.modalRef.close(); + } + }); + }) + }); + } + hasSnapshots() { const snapshots = this.selection.first()['snapshots'] || []; return snapshots.length > 0; diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-snapshot-form/rbd-snapshot-form-modal.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-snapshot-form/rbd-snapshot-form-modal.component.html index da8a2cd25b505..598e3fd3843ec 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-snapshot-form/rbd-snapshot-form-modal.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-snapshot-form/rbd-snapshot-form-modal.component.html @@ -19,11 +19,14 @@ placeholder="Snapshot name..." id="snapshotName" name="snapshotName" + [attr.disabled]="(mirroring === 'snapshot') ? true : null" formControlName="snapshotName" autofocus> This field is required. + i18n>This field is required.

+ Snapshot mode is enabled on image {{ imageName }}: snapshot names are auto generated
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-snapshot-form/rbd-snapshot-form-modal.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-snapshot-form/rbd-snapshot-form-modal.component.ts index 83296866c1af7..d5861163f198b 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-snapshot-form/rbd-snapshot-form-modal.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-snapshot-form/rbd-snapshot-form-modal.component.ts @@ -22,6 +22,7 @@ export class RbdSnapshotFormModalComponent { namespace: string; imageName: string; snapName: string; + mirroring: string; snapshotForm: CdFormGroup; @@ -53,7 +54,11 @@ export class RbdSnapshotFormModalComponent { setSnapName(snapName: string) { this.snapName = snapName; - this.snapshotForm.get('snapshotName').setValue(snapName); + if (this.mirroring !== 'snapshot') { + this.snapshotForm.get('snapshotName').setValue(snapName); + } else { + this.snapshotForm.get('snapshotName').clearValidators(); + } } /** diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-snapshot-list/rbd-snapshot-actions.model.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-snapshot-list/rbd-snapshot-actions.model.ts index 01ea8cbdcdb04..cc0d61f91aa07 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-snapshot-list/rbd-snapshot-actions.model.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-snapshot-list/rbd-snapshot-actions.model.ts @@ -34,27 +34,31 @@ export class RbdSnapshotActionsModel { this.rename = { permission: 'update', icon: Icons.edit, - name: actionLabels.RENAME + name: actionLabels.RENAME, + disable: (selection: CdTableSelection) => this.disableForMirrorSnapshot(selection) }; this.protect = { permission: 'update', icon: Icons.lock, visible: (selection: CdTableSelection) => selection.hasSingleSelection && !selection.first().is_protected, - name: actionLabels.PROTECT + name: actionLabels.PROTECT, + disable: (selection: CdTableSelection) => this.disableForMirrorSnapshot(selection) }; this.unprotect = { permission: 'update', icon: Icons.unlock, visible: (selection: CdTableSelection) => selection.hasSingleSelection && selection.first().is_protected, - name: actionLabels.UNPROTECT + name: actionLabels.UNPROTECT, + disable: (selection: CdTableSelection) => this.disableForMirrorSnapshot(selection) }; this.clone = { permission: 'create', canBePrimary: (selection: CdTableSelection) => selection.hasSingleSelection, disable: (selection: CdTableSelection) => - this.getCloneDisableDesc(selection, this.featuresName), + this.getCloneDisableDesc(selection, this.featuresName) || + this.disableForMirrorSnapshot(selection), icon: Icons.clone, name: actionLabels.CLONE }; @@ -62,21 +66,29 @@ export class RbdSnapshotActionsModel { permission: 'create', canBePrimary: (selection: CdTableSelection) => selection.hasSingleSelection, disable: (selection: CdTableSelection) => - !selection.hasSingleSelection || selection.first().cdExecuting, + !selection.hasSingleSelection || + selection.first().cdExecuting || + this.disableForMirrorSnapshot(selection), icon: Icons.copy, name: actionLabels.COPY }; this.rollback = { permission: 'update', icon: Icons.undo, - name: actionLabels.ROLLBACK + name: actionLabels.ROLLBACK, + disable: (selection: CdTableSelection) => this.disableForMirrorSnapshot(selection) }; this.deleteSnap = { permission: 'delete', icon: Icons.destroy, disable: (selection: CdTableSelection) => { const first = selection.first(); - return !selection.hasSingleSelection || first.cdExecuting || first.is_protected; + return ( + !selection.hasSingleSelection || + first.cdExecuting || + first.is_protected || + this.disableForMirrorSnapshot(selection) + ); }, name: actionLabels.DELETE }; @@ -108,4 +120,12 @@ export class RbdSnapshotActionsModel { return true; } + + disableForMirrorSnapshot(selection: CdTableSelection) { + return ( + selection.hasSingleSelection && + selection.first().mirror_mode === 'snapshot' && + selection.first().name.includes('.mirror.') + ); + } } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-snapshot-list/rbd-snapshot-list.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-snapshot-list/rbd-snapshot-list.component.ts index 6a8be5b134a25..df66b0e8842ae 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-snapshot-list/rbd-snapshot-list.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-snapshot-list/rbd-snapshot-list.component.ts @@ -56,6 +56,8 @@ export class RbdSnapshotListComponent implements OnInit, OnChanges { @Input() namespace: string; @Input() + mirroring: string; + @Input() rbdName: string; @ViewChild('nameTpl') nameTpl: TemplateRef; @@ -210,7 +212,10 @@ export class RbdSnapshotListComponent implements OnInit, OnChanges { } private openSnapshotModal(taskName: string, snapName: string = null) { - this.modalRef = this.modalService.show(RbdSnapshotFormModalComponent); + const modalVariables = { + mirroring: this.mirroring + }; + this.modalRef = this.modalService.show(RbdSnapshotFormModalComponent, modalVariables); this.modalRef.componentInstance.poolName = this.poolName; this.modalRef.componentInstance.imageName = this.rbdName; this.modalRef.componentInstance.namespace = this.namespace; diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/constants/app.constants.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/constants/app.constants.ts index e19d007c4a607..dfa25475bc72c 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/constants/app.constants.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/constants/app.constants.ts @@ -130,6 +130,7 @@ export class ActionLabelsI18n { FLAGS: string; ENTER_MAINTENANCE: string; EXIT_MAINTENANCE: string; + REMOVE_SCHEDULING: string; START_DRAIN: string; STOP_DRAIN: string; START: string; @@ -189,6 +190,7 @@ export class ActionLabelsI18n { this.FLAGS = $localize`Flags`; this.ENTER_MAINTENANCE = $localize`Enter Maintenance`; this.EXIT_MAINTENANCE = $localize`Exit Maintenance`; + this.START_DRAIN = $localize`Start Drain`; this.STOP_DRAIN = $localize`Stop Drain`; @@ -200,6 +202,8 @@ export class ActionLabelsI18n { this.STOP = $localize`Stop`; this.REDEPLOY = $localize`Redeploy`; this.RESTART = $localize`Restart`; + + this.REMOVE_SCHEDULING = $localize`Remove Scheduling`; } } diff --git a/src/pybind/mgr/dashboard/frontend/src/styles/ceph-custom/_forms.scss b/src/pybind/mgr/dashboard/frontend/src/styles/ceph-custom/_forms.scss index 3c6ddbf80c998..1d84895c0dd75 100644 --- a/src/pybind/mgr/dashboard/frontend/src/styles/ceph-custom/_forms.scss +++ b/src/pybind/mgr/dashboard/frontend/src/styles/ceph-custom/_forms.scss @@ -27,6 +27,10 @@ padding-top: 7px; } +.custom-radio { + padding-top: 5px; +} + .cd-col-form { @extend .col-12; @extend .col-lg-8; diff --git a/src/pybind/mgr/dashboard/openapi.yaml b/src/pybind/mgr/dashboard/openapi.yaml index 5918c81c17bc0..4c7ce89121411 100644 --- a/src/pybind/mgr/dashboard/openapi.yaml +++ b/src/pybind/mgr/dashboard/openapi.yaml @@ -206,6 +206,8 @@ paths: type: string features: type: string + mirror_mode: + type: string name: type: string namespace: @@ -214,6 +216,9 @@ paths: type: integer pool_name: type: string + schedule_interval: + default: '' + type: string size: type: integer stripe_count: @@ -549,6 +554,9 @@ paths: type: string primary: type: string + remove_scheduling: + default: false + type: boolean resync: default: false type: boolean @@ -557,9 +565,6 @@ paths: type: string size: type: integer - start_time: - default: '' - type: string type: object responses: '200': diff --git a/src/pybind/mgr/dashboard/services/rbd.py b/src/pybind/mgr/dashboard/services/rbd.py index 31160e893d5cd..f9203b2418eb6 100644 --- a/src/pybind/mgr/dashboard/services/rbd.py +++ b/src/pybind/mgr/dashboard/services/rbd.py @@ -1,11 +1,14 @@ # -*- coding: utf-8 -*- # pylint: disable=unused-argument import errno +import json +from enum import IntEnum import cherrypy import rbd from .. import mgr +from ..exceptions import DashboardException from ..tools import ViewCache from .ceph_service import CephService @@ -28,6 +31,20 @@ RBD_FEATURES_NAME_MAPPING = { } +class MIRROR_IMAGE_MODE(IntEnum): + journal = rbd.RBD_MIRROR_IMAGE_MODE_JOURNAL + snapshot = rbd.RBD_MIRROR_IMAGE_MODE_SNAPSHOT + + +def _rbd_support_remote(method_name: str, *args, **kwargs): + try: + return mgr.remote('rbd_support', method_name, *args, **kwargs) + except ImportError as ie: + raise DashboardException(f'rbd_support module not found {ie}') + except RuntimeError as ie: + raise DashboardException(f'rbd_support.{method_name} error: {ie}') + + def format_bitmask(features): """ Formats the bitmask: @@ -245,10 +262,22 @@ class RbdService(object): return total_used_size, snap_map @classmethod - def _rbd_image(cls, ioctx, pool_name, namespace, image_name): + def _rbd_image(cls, ioctx, pool_name, namespace, image_name): # pylint: disable=R0912 with rbd.Image(ioctx, image_name) as img: - stat = img.stat() + mirror_mode = img.mirror_image_get_mode() + if mirror_mode == rbd.RBD_MIRROR_IMAGE_MODE_JOURNAL: + stat['mirror_mode'] = 'journal' + elif mirror_mode == rbd.RBD_MIRROR_IMAGE_MODE_SNAPSHOT: + stat['mirror_mode'] = 'snapshot' + schedule_status = json.loads(_rbd_support_remote( + 'mirror_snapshot_schedule_status')[1]) + for scheduled_image in schedule_status['scheduled_images']: + if scheduled_image['image'] == get_image_spec(pool_name, namespace, image_name): + stat['schedule_info'] = scheduled_image + else: + stat['mirror_mode'] = 'unknown' + stat['name'] = image_name if img.old_format(): stat['unique_id'] = get_image_spec(pool_name, namespace, stat['block_name_prefix']) @@ -290,23 +319,34 @@ class RbdService(object): # snapshots stat['snapshots'] = [] for snap in img.list_snaps(): + try: + snap['mirror_mode'] = MIRROR_IMAGE_MODE(img.mirror_image_get_mode()).name + except ValueError as ex: + raise DashboardException(f'Unknown RBD Mirror mode: {ex}') + snap['timestamp'] = "{}Z".format( img.get_snap_timestamp(snap['id']).isoformat()) - snap['is_protected'] = img.is_protected_snap(snap['name']) + + snap['is_protected'] = None + if mirror_mode != rbd.RBD_MIRROR_IMAGE_MODE_SNAPSHOT: + snap['is_protected'] = img.is_protected_snap(snap['name']) snap['used_bytes'] = None snap['children'] = [] - img.set_snap(snap['name']) - for child_pool_name, child_image_name in img.list_children(): - snap['children'].append({ - 'pool_name': child_pool_name, - 'image_name': child_image_name - }) + + if mirror_mode != rbd.RBD_MIRROR_IMAGE_MODE_SNAPSHOT: + img.set_snap(snap['name']) + for child_pool_name, child_image_name in img.list_children(): + snap['children'].append({ + 'pool_name': child_pool_name, + 'image_name': child_image_name + }) stat['snapshots'].append(snap) # disk usage img_flags = img.flags() if 'fast-diff' in stat['features_name'] and \ - not rbd.RBD_FLAG_FAST_DIFF_INVALID & img_flags: + not rbd.RBD_FLAG_FAST_DIFF_INVALID & img_flags and \ + mirror_mode != rbd.RBD_MIRROR_IMAGE_MODE_SNAPSHOT: snaps = [(s['id'], s['size'], s['name']) for s in stat['snapshots']] snaps.sort(key=lambda s: s[0]) @@ -425,7 +465,7 @@ class RBDSchedulerInterval: class RbdMirroringService: @classmethod - def enable_image(cls, image_name: str, pool_name: str, namespace: str, mode: str): + def enable_image(cls, image_name: str, pool_name: str, namespace: str, mode: MIRROR_IMAGE_MODE): rbd_image_call(pool_name, namespace, image_name, lambda ioctx, image: image.mirror_image_enable(mode)) @@ -450,6 +490,10 @@ class RbdMirroringService: lambda ioctx, image: image.mirror_image_resync()) @classmethod - def snapshot_schedule(cls, image_spec: str, interval: str, start_time: str = ''): - mgr.remote('rbd_support', 'mirror_snapshot_schedule_add', image_spec, - str(RBDSchedulerInterval(interval)), start_time) + def snapshot_schedule_add(cls, image_spec: str, interval: str): + _rbd_support_remote('mirror_snapshot_schedule_add', image_spec, + str(RBDSchedulerInterval(interval))) + + @classmethod + def snapshot_schedule_remove(cls, image_spec: str): + _rbd_support_remote('mirror_snapshot_schedule_remove', image_spec)