From 2dec079356d6b6f43bb37c7090e6febe18031801 Mon Sep 17 00:00:00 2001 From: Aashish Sharma Date: Tue, 4 Jun 2024 17:59:34 +0530 Subject: [PATCH] mgr/dashboard: Block -> Images -> Create form improvements Fixes: https://tracker.ceph.com/issues/66348 Signed-off-by: Aashish Sharma (cherry picked from commit e3c656440f69fed1a93ed6fa92e2b9e6adf27e45) --- src/pybind/mgr/dashboard/controllers/rbd.py | 8 +- .../block/rbd-form/rbd-feature.interface.ts | 1 + .../rbd-form/rbd-form-edit-request.model.ts | 1 + .../block/rbd-form/rbd-form.component.html | 265 ++++++++++-------- .../block/rbd-form/rbd-form.component.spec.ts | 2 +- .../ceph/block/rbd-form/rbd-form.component.ts | 109 +++++-- src/pybind/mgr/dashboard/openapi.yaml | 2 + src/pybind/mgr/dashboard/services/rbd.py | 19 +- 8 files changed, 244 insertions(+), 163 deletions(-) diff --git a/src/pybind/mgr/dashboard/controllers/rbd.py b/src/pybind/mgr/dashboard/controllers/rbd.py index f803ab1a18ae5..767d23577b655 100644 --- a/src/pybind/mgr/dashboard/controllers/rbd.py +++ b/src/pybind/mgr/dashboard/controllers/rbd.py @@ -137,12 +137,12 @@ class Rbd(RESTController): @RbdTask('edit', ['{image_spec}', '{name}'], 4.0) def set(self, image_spec, name=None, size=None, features=None, configuration=None, metadata=None, enable_mirror=None, primary=None, - force=False, resync=False, mirror_mode=None, schedule_interval='', - remove_scheduling=False): + force=False, resync=False, mirror_mode=None, image_mirror_mode=None, + schedule_interval='', remove_scheduling=False): return RbdService.set(image_spec, name, size, features, configuration, metadata, enable_mirror, primary, - force, resync, mirror_mode, schedule_interval, - remove_scheduling) + force, resync, mirror_mode, image_mirror_mode, + schedule_interval, remove_scheduling) @RbdTask('copy', {'src_image_spec': '{image_spec}', diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-form/rbd-feature.interface.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-form/rbd-feature.interface.ts index 825b1d2bb39b3..898bc45231968 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-form/rbd-feature.interface.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-form/rbd-feature.interface.ts @@ -7,4 +7,5 @@ export interface RbdImageFeature { key?: string; initDisabled?: boolean; helperHtml?: string; + helperText?: string; } 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 2eede58521f1e..670203dd5f0ab 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 @@ -12,4 +12,5 @@ export class RbdFormEditRequestModel { force?: boolean; schedule_interval: string; remove_scheduling? = false; + image_mirror_mode?: string; } 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 cb199fe4af4ae..4c86ef15e27a4 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 @@ -88,6 +88,135 @@ +
+
+ +
+ + + Allow data to be asynchronously mirrored between two Ceph clusters + Mirroring can not be disabled on Pool mirror mode. + You need to change the mirror mode to enable this option. + + You need to set mirror mode in the selected pool to enable mirroring. + + +
+
+
+ + + {{ option.text}} + You need to set mode as Image in the selected pool to enable snapshot mirroring. + + +
+

+
+ +
+ + + Specify the interval to create mirror snapshots automatically. The interval can be specified in days, hours, or minutes using d, h, m suffix respectively + + This field is required. +
+
+ +
+ + + Use a dedicated pool to store the mirror data. If not selected, the mirror data will be stored in the same pool as the image data. + + You need more than one pool with the rbd application label use to use a dedicated data pool. + +
+ +
+
+ + + Dedicated pool that stores the object-data of the RBD. + This field is required. +
+
+
+
+
@@ -126,69 +255,8 @@ -
- - - -
-
-
- - - - You need more than one pool with the rbd application label use to use a dedicated data pool. - -
-
-
- - -
- -
- - - This field is required. + Namespace allows you to logically group RBD images within your Ceph Cluster. + Choosing a namespace makes it easier to locate and manage related RBD images efficiently
@@ -204,7 +272,7 @@ type="text" formControlName="size" i18n-placeholder - placeholder="e.g., 10GiB" + placeholder="10 GiB" defaultUnit="GiB" cdDimlessBinary> Size must be a number or in a valid format. eg: 5 GiB - - - - -
-
-
- - - - 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. - -
-
-
-
- -
- -
- + Supported Units: KiB, MiB, GiB, TiB, PiB etc
@@ -292,10 +305,14 @@ name="{{ feature.key }}" formControlName="{{ feature.key }}"> - - + for="{{ feature.key }}">{{ feature.desc }}
+ + {{ feature.helperText }} + + + {{ feature.helperHtml }} + 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 7605348d406ba..fbdebde67a7d7 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 @@ -453,7 +453,7 @@ describe('RbdFormComponent', () => { }); it('should verify only snapshot is disabled for pools that are in pool mirror mode', () => { - component.poolMirrorMode = 'pool'; + component.currentPoolMirrorMode = 'pool'; fixture.detectChanges(); const journal = fixture.debugElement.query(By.css('#journal')).nativeElement; const snapshot = fixture.debugElement.query(By.css('#snapshot')).nativeElement; 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 33e67b09bbf76..1a8c7627b8578 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 @@ -33,6 +33,7 @@ import { RbdFormCreateRequestModel } from './rbd-form-create-request.model'; import { RbdFormEditRequestModel } from './rbd-form-edit-request.model'; import { RbdFormMode } from './rbd-form-mode.enum'; import { RbdFormResponseModel } from './rbd-form-response.model'; +import { CdValidators } from '~/app/shared/forms/cd-validators'; class ExternalData { rbd: RbdFormResponseModel; @@ -79,10 +80,22 @@ export class RbdFormComponent extends CdForm implements OnInit { defaultObjectSize = '4 MiB'; - mirroringOptions = ['journal', 'snapshot']; + mirroringOptions = [ + { + value: 'journal', + text: + 'Ensures reliable replication by logging changes before updating the image, but doubles write time, impacting performance. Not recommended for high-speed data processing tasks.' + }, + { + value: 'snapshot', + text: + 'This mode replicates RBD images between clusters using snapshots, efficiently copying data changes but requiring complete delta syncing during failover. Ideal for less demanding tasks due to its less granular approach compared to journaling.' + } + ]; poolMirrorMode: string; mirroring = false; currentPoolName = ''; + currentPoolMirrorMode = ''; objectSizes: Array = [ '4 KiB', @@ -111,6 +124,8 @@ export class RbdFormComponent extends CdForm implements OnInit { private routerUrl: string; icons = Icons; + currentImageMirrorMode = ''; + showMirrorDisableMessage = false; constructor( private authStorageService: AuthStorageService, @@ -134,27 +149,31 @@ export class RbdFormComponent extends CdForm implements OnInit { requires: null, allowEnable: false, allowDisable: true, - helperHtml: $localize`Feature can be disabled but can't be re-enabled later` + helperHtml: $localize`Feature can be disabled but can't be re-enabled later`, + helperText: $localize`Speeds up the process of deleting a clone by removing the dependency on the parent image.` }, layering: { desc: $localize`Layering`, requires: null, allowEnable: false, allowDisable: false, - helperHtml: $localize`Feature flag can't be manipulated after the image is created. Disabling this option will also disable the Protect and Clone actions on Snapshot` + helperHtml: $localize`Feature flag can't be manipulated after the image is created. Disabling this option will also disable the Protect and Clone actions on Snapshot`, + helperText: $localize`Allows the creation of snapshots and clones of an image.` }, 'exclusive-lock': { desc: $localize`Exclusive lock`, requires: null, allowEnable: true, - allowDisable: true + allowDisable: true, + helperText: $localize`Ensures that only one client can write to the image at a time.` }, 'object-map': { desc: $localize`Object map (requires exclusive-lock)`, requires: 'exclusive-lock', allowEnable: true, allowDisable: true, - initDisabled: true + initDisabled: true, + helperText: $localize`Tracks which objects actually exist (have data stored on a device). Enabling object map support speeds up I/O operations for cloning, importing and exporting a sparsely populated image, and deleting.` }, 'fast-diff': { desc: $localize`Fast diff (interlocked with object-map)`, @@ -162,7 +181,8 @@ export class RbdFormComponent extends CdForm implements OnInit { allowEnable: true, allowDisable: true, interlockedWith: 'object-map', - initDisabled: true + initDisabled: true, + helperText: $localize`Speeds up the process of comparing two images.` } }; this.featuresList = this.objToArray(this.features); @@ -196,9 +216,15 @@ export class RbdFormComponent extends CdForm implements OnInit { return acc; }, {}) ), - mirroring: new UntypedFormControl(''), + mirroring: new UntypedFormControl(false), schedule: new UntypedFormControl('', { - validators: [Validators.pattern(/^([0-9]+)d|([0-9]+)h|([0-9]+)m$/)] // check schedule interval to be in format - 1d or 1h or 1m + validators: [ + Validators.pattern(/^([0-9]+)d|([0-9]+)h|([0-9]+)m$/), + CdValidators.requiredIf({ + mirroringMode: 'snapshot', + mirroring: true + }) + ] // check schedule interval to be in format - 1d or 1h or 1m }), mirroringMode: new UntypedFormControl(''), stripingUnit: new UntypedFormControl(this.defaultStripingUnit), @@ -256,14 +282,14 @@ export class RbdFormComponent extends CdForm implements OnInit { 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; + if (this.mirroring) { + this.rbdForm.get('mirroringMode').setValue(this.mirroringOptions[0].value); + } this.setExclusiveLock(); this.checkPeersConfigured(); } @@ -286,14 +312,34 @@ export class RbdFormComponent extends CdForm implements OnInit { 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(); + this.currentPoolMirrorMode = pool.mirror_mode; + if (this.mode === this.rbdFormMode.editing) { + if (this.currentPoolMirrorMode === 'pool') { + this.showMirrorDisableMessage = true; + } else { + this.showMirrorDisableMessage = false; + } + if (this.currentPoolMirrorMode !== 'image') { + this.rbdForm.get('mirroring').disable(); + this.rbdForm.get('mirroringMode').disable(); + } + } else { + if (pool.mirror_mode === 'disabled') { + this.mirroring = false; + this.rbdForm.get('mirroring').setValue(this.mirroring); + this.rbdForm.get('mirroring').disable(); + } else { + this.mirroring = true; + this.rbdForm.get('mirroring').enable(); + this.rbdForm.get('mirroring').setValue(this.mirroring); + this.rbdForm.get('mirroringMode').setValue(this.mirroringOptions[0].value); + } } }); + } else { + if (this.mode !== this.rbdFormMode.editing) { + this.rbdForm.get('mirroring').disable(); + } } this.setExclusiveLock(); } @@ -390,8 +436,9 @@ export class RbdFormComponent extends CdForm implements OnInit { this.allPools = pools; this.dataPools = dataPools; this.allDataPools = dataPools; - if (this.pools.length === 1) { - const poolName = this.pools[0].pool_name; + if (this.pools.length >= 1) { + const allPoolNames = this.pools.map((pool) => pool.pool_name); + const poolName = allPoolNames.includes('rbd') ? 'rbd' : this.pools[0].pool_name; this.rbdForm.get('pool').setValue(poolName); this.onPoolChange(poolName); } @@ -464,7 +511,7 @@ export class RbdFormComponent extends CdForm implements OnInit { sizeControlErrors = { required: true }; } else { const sizeInBytes = formatter.toBytes(sizeControl.value); - if (stripingCount * objectSizeInBytes > sizeInBytes) { + if (stripingCount * objectSizeInBytes >= sizeInBytes) { sizeControlErrors = { invalidSizeObject: true }; } } @@ -616,6 +663,7 @@ export class RbdFormComponent extends CdForm implements OnInit { this.mirroring = true; this.rbdForm.get('mirroring').setValue(this.mirroring); this.rbdForm.get('mirroringMode').setValue(response?.mirror_mode); + this.currentImageMirrorMode = response?.mirror_mode; this.rbdForm.get('schedule').setValue(response?.schedule_interval); } else { this.mirroring = false; @@ -651,12 +699,11 @@ export class RbdFormComponent extends CdForm implements OnInit { 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(); + if (this.mirroring && this.currentPoolMirrorMode === 'image') { + request.mirror_mode = this.rbdForm.getValue('mirroringMode'); + } return request; } @@ -688,7 +735,8 @@ export class RbdFormComponent extends CdForm implements OnInit { namespace: request.namespace, image_name: request.name, schedule_interval: request.schedule_interval, - start_time: request.start_time + start_time: request.start_time, + mirror_mode: request.mirror_mode }), call: this.rbdService.create(request) }); @@ -698,19 +746,20 @@ export class RbdFormComponent extends CdForm implements OnInit { const request = new RbdFormEditRequestModel(); request.name = this.rbdForm.getValue('name'); request.schedule_interval = this.rbdForm.getValue('schedule'); - request.name = this.rbdForm.getValue('name'); + request.enable_mirror = this.mirroring; 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 (request.enable_mirror) { + request.image_mirror_mode = this.currentImageMirrorMode; if (this.rbdForm.getValue('mirroringMode') === 'journal') { + request.mirror_mode = 'journal'; request.features.push('journaling'); } - if (this.poolMirrorMode === 'image') { + if (this.currentPoolMirrorMode === 'image') { request.mirror_mode = this.rbdForm.getValue('mirroringMode'); } } else { @@ -803,6 +852,10 @@ export class RbdFormComponent extends CdForm implements OnInit { }); } + shouldDisable(option: string): boolean { + return this.currentPoolMirrorMode === 'pool' && option === 'snapshot' ? true : null; + } + submit() { if (!this.mode) { this.rbdImage.next('create'); diff --git a/src/pybind/mgr/dashboard/openapi.yaml b/src/pybind/mgr/dashboard/openapi.yaml index 806829033d02a..095e895b526a9 100644 --- a/src/pybind/mgr/dashboard/openapi.yaml +++ b/src/pybind/mgr/dashboard/openapi.yaml @@ -594,6 +594,8 @@ paths: force: default: false type: boolean + image_mirror_mode: + type: string metadata: type: string mirror_mode: diff --git a/src/pybind/mgr/dashboard/services/rbd.py b/src/pybind/mgr/dashboard/services/rbd.py index ec65b1fd5e119..31fdb7c9818e3 100644 --- a/src/pybind/mgr/dashboard/services/rbd.py +++ b/src/pybind/mgr/dashboard/services/rbd.py @@ -559,8 +559,8 @@ class RbdService(object): @ttl_cache_invalidator(RBD_IMAGE_REFS_CACHE_REFERENCE) def set(cls, image_spec, name=None, size=None, features=None, configuration=None, metadata=None, enable_mirror=None, primary=None, - force=False, resync=False, mirror_mode=None, schedule_interval='', - remove_scheduling=False): + force=False, resync=False, mirror_mode=None, image_mirror_mode=None, + schedule_interval='', remove_scheduling=False): # pylint: disable=too-many-branches pool_name, namespace, image_name = parse_image_spec(image_spec) @@ -574,15 +574,22 @@ class RbdService(object): if size and size != image.size(): image.resize(size) + if image_mirror_mode is not None and mirror_mode is not None: + if image_mirror_mode != mirror_mode: + RbdMirroringService.disable_image(image_name, pool_name, namespace) + mirror_image_info = image.mirror_image_get_info() - if enable_mirror and mirror_image_info['state'] == rbd.RBD_MIRROR_IMAGE_DISABLED: + if (enable_mirror is True + and mirror_image_info['state'] == rbd.RBD_MIRROR_IMAGE_DISABLED): RbdMirroringService.enable_image( image_name, pool_name, namespace, - MIRROR_IMAGE_MODE[mirror_mode]) + MIRROR_IMAGE_MODE[mirror_mode] + ) elif (enable_mirror is False - and mirror_image_info['state'] == rbd.RBD_MIRROR_IMAGE_ENABLED): + and mirror_image_info['state'] == rbd.RBD_MIRROR_IMAGE_ENABLED): RbdMirroringService.disable_image( - image_name, pool_name, namespace) + image_name, pool_name, namespace + ) # check enable/disable features if features is not None: -- 2.39.5