@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}',
key?: string;
initDisabled?: boolean;
helperHtml?: string;
+ helperText?: string;
}
force?: boolean;
schedule_interval: string;
remove_scheduling? = false;
+ image_mirror_mode?: string;
}
</div>
</div>
+ <div class="form-group row">
+ <div class="cd-col-form-offset">
+ <!-- Mirroring -->
+ <div class="custom-control custom-checkbox">
+ <input type="checkbox"
+ class="custom-control-input"
+ id="mirroring"
+ name="mirroring"
+ (change)="setMirrorMode()"
+ [(ngModel)]="mirroring && this.currentPoolName"
+ formControlName="mirroring">
+ <label class="custom-control-label"
+ for="mirroring">Mirroring</label>
+ <cd-help-text>Allow data to be asynchronously mirrored between two Ceph clusters</cd-help-text>
+ <cd-alert-panel *ngIf="showMirrorDisableMessage"
+ [showTitle]="false"
+ type="info">Mirroring can not be disabled on <b>Pool</b> mirror mode.
+ You need to change the mirror mode to enable this option.
+ </cd-alert-panel>
+ <cd-alert-panel *ngIf="currentPoolMirrorMode === 'disabled'"
+ type="info"
+ [showTitle]="false"
+ i18n>You need to set <b>mirror mode</b> in the selected pool to enable mirroring.
+ <button class="btn btn-light"
+ type="button"
+ [routerLink]="['/block/mirroring', {outlets: {modal: ['edit', rbdForm.getValue('pool')]}}]">Set Mode</button>
+ </cd-alert-panel>
+ </div>
+ <div *ngIf="mirroring && currentPoolMirrorMode !== 'disabled'">
+ <div class="custom-control custom-radio ms-2"
+ *ngFor="let option of mirroringOptions">
+ <input type="radio"
+ class="form-check-input"
+ [id]="option.value"
+ [value]="option.value"
+ name="mirroringMode"
+ (change)="setExclusiveLock()"
+ formControlName="mirroringMode"
+ [attr.disabled]="shouldDisable(option.value)">
+ <label class="form-check-label"
+ [for]="option.value">{{ option.value | titlecase }}</label>
+ <cd-help-text> {{ option.text}} </cd-help-text>
+ <cd-alert-panel *ngIf="shouldDisable(option.value) && mode !== 'editing'"
+ type="info"
+ [showTitle]="false"
+ i18n>You need to set mode as <b>Image</b> in the selected pool to enable snapshot mirroring.
+ <button class="btn btn-light mx-2"
+ type="button"
+ [routerLink]="['/block/mirroring', {outlets: {modal: ['edit', rbdForm.getValue('pool')]}}]">Set Mode</button>
+ </cd-alert-panel>
+ </div>
+ </div><br>
+ <div class="form-group row"
+ *ngIf="rbdForm.getValue('mirroringMode') === 'snapshot' && mirroring">
+ <label class="cd-col-form-label required"
+ [ngClass]="{'required': mode !== 'editing'}"
+ i18n>Schedule Interval</label>
+ <div class="cd-col-form-input">
+ <input id="schedule"
+ name="schedule"
+ class="form-control"
+ type="text"
+ formControlName="schedule"
+ i18n-placeholder
+ placeholder="12h or 1d or 10m"
+ [attr.disabled]="(peerConfigured === false) ? true : null">
+ <cd-help-text>
+ <span i18n>Specify the interval to create mirror snapshots automatically. The interval can be specified in days, hours, or minutes using d, h, m suffix respectively</span>
+ </cd-help-text>
+ <span *ngIf="rbdForm.showError('schedule', formDir, 'required')"
+ class="invalid-feedback"
+ i18n>This field is required.</span>
+ </div>
+ </div>
+ <!-- Use a dedicated pool -->
+ <div class="custom-control custom-checkbox"
+ *ngIf="allDataPools.length > 1 || mode === 'editing'">
+ <input type="checkbox"
+ class="custom-control-input"
+ id="useDataPool"
+ name="useDataPool"
+ formControlName="useDataPool"
+ (change)="onUseDataPoolChange()">
+ <label class="custom-control-label"
+ for="useDataPool"
+ i18n>Dedicated data pool</label>
+ <cd-help-text>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.</cd-help-text>
+ <cd-helper *ngIf="allDataPools.length <= 1 && mode !== 'editing'">
+ <span i18n>You need more than one pool with the rbd application label use to use a dedicated data pool.</span>
+ </cd-helper>
+ </div>
+ <!-- Data Pool -->
+ <div class="form-group row"
+ *ngIf="rbdForm.getValue('useDataPool')">
+ <div class="cd-col-form-input pt-2 ms-4">
+ <input class="form-control"
+ type="text"
+ placeholder="Data pool name..."
+ id="dataPool"
+ name="dataPool"
+ formControlName="dataPool"
+ *ngIf="mode === 'editing' || !poolPermission.read">
+ <select id="dataPool"
+ name="dataPool"
+ class="form-select"
+ formControlName="dataPool"
+ (change)="onDataPoolChange($event.target.value)"
+ *ngIf="mode !== 'editing' && poolPermission.read">
+ <option *ngIf="dataPools === null"
+ [ngValue]="null"
+ i18n>Loading...</option>
+ <option *ngIf="dataPools !== null && dataPools.length === 0"
+ [ngValue]="null"
+ i18n>-- No data pools available --</option>
+ <option *ngIf="dataPools !== null && dataPools.length > 0"
+ [ngValue]="null">-- Select a data pool --
+ </option>
+ <option *ngFor="let dataPool of dataPools"
+ [value]="dataPool.pool_name">{{ dataPool.pool_name }}</option>
+ </select>
+ <cd-help-text>Dedicated pool that stores the object-data of the RBD.</cd-help-text>
+ <span class="invalid-feedback"
+ *ngIf="rbdForm.showError('dataPool', formDir, 'required')"
+ i18n>This field is required.</span>
+ </div>
+ </div>
+ </div>
+ </div>
+
<!-- Namespace -->
<div class="form-group row"
*ngIf="mode !== 'editing' && rbdForm.getValue('pool') && namespaces === null">
<option *ngFor="let namespace of namespaces"
[value]="namespace">{{ namespace }}</option>
</select>
- </div>
- </div>
-
- <!-- Use a dedicated pool -->
- <div class="form-group row">
- <div class="cd-col-form-offset">
- <div class="custom-control custom-checkbox">
- <input type="checkbox"
- class="custom-control-input"
- id="useDataPool"
- name="useDataPool"
- formControlName="useDataPool"
- (change)="onUseDataPoolChange()">
- <label class="custom-control-label"
- for="useDataPool"
- i18n>Use a dedicated data pool</label>
- <cd-helper *ngIf="allDataPools.length <= 1">
- <span i18n>You need more than one pool with the rbd application label use to use a dedicated data pool.</span>
- </cd-helper>
- </div>
- </div>
- </div>
-
- <!-- Data Pool -->
- <div class="form-group row"
- *ngIf="rbdForm.getValue('useDataPool')">
- <label class="cd-col-form-label"
- for="dataPool">
- <span [ngClass]="{'required': mode !== 'editing'}"
- i18n>Data pool</span>
- <cd-helper i18n-html
- html="Dedicated pool that stores the object-data of the RBD.">
- </cd-helper>
- </label>
- <div class="cd-col-form-input">
- <input class="form-control"
- type="text"
- placeholder="Data pool name..."
- id="dataPool"
- name="dataPool"
- formControlName="dataPool"
- *ngIf="mode === 'editing' || !poolPermission.read">
- <select id="dataPool"
- name="dataPool"
- class="form-select"
- formControlName="dataPool"
- (change)="onDataPoolChange($event.target.value)"
- *ngIf="mode !== 'editing' && poolPermission.read">
- <option *ngIf="dataPools === null"
- [ngValue]="null"
- i18n>Loading...</option>
- <option *ngIf="dataPools !== null && dataPools.length === 0"
- [ngValue]="null"
- i18n>-- No data pools available --</option>
- <option *ngIf="dataPools !== null && dataPools.length > 0"
- [ngValue]="null">-- Select a data pool --
- </option>
- <option *ngFor="let dataPool of dataPools"
- [value]="dataPool.pool_name">{{ dataPool.pool_name }}</option>
- </select>
- <span class="invalid-feedback"
- *ngIf="rbdForm.showError('dataPool', formDir, 'required')"
- i18n>This field is required.</span>
+ <cd-help-text>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</cd-help-text>
</div>
</div>
type="text"
formControlName="size"
i18n-placeholder
- placeholder="e.g., 10GiB"
+ placeholder="10 GiB"
defaultUnit="GiB"
cdDimlessBinary>
<span class="invalid-feedback"
<span *ngIf="rbdForm.showError('size', formDir, 'pattern')"
class="invalid-feedback"
i18n>Size must be a number or in a valid format. eg: 5 GiB</span>
- </div>
- </div>
-
- <!-- Mirroring -->
- <div class="form-group row">
- <div class="cd-col-form-offset">
- <div class="custom-control custom-checkbox">
- <input type="checkbox"
- class="custom-control-input"
- id="mirroring"
- name="mirroring"
- (change)="setMirrorMode()"
- formControlName="mirroring">
- <label class="custom-control-label"
- for="mirroring">Mirroring</label>
- <cd-helper *ngIf="mirroring === false && this.currentPoolName">
- <span i18n>You need to enable a <b>mirror mode</b> in the selected pool. Please <a [routerLink]="['/block/mirroring', {outlets: {modal: ['edit', currentPoolName]}}]">click here to select a mode and enable it in this pool.</a></span>
- </cd-helper>
- </div>
- <div *ngIf="mirroring">
- <div class="custom-control custom-radio ms-2"
- *ngFor="let option of mirroringOptions">
- <input type="radio"
- class="form-check-input"
- [id]="option"
- [value]="option"
- name="mirroringMode"
- (change)="setExclusiveLock()"
- formControlName="mirroringMode"
- [attr.disabled]="(poolMirrorMode === 'pool' && option === 'snapshot') ? true : null">
- <label class="form-check-label"
- [for]="option">{{ option | titlecase }}</label>
- <cd-helper *ngIf="poolMirrorMode === 'pool' && option === 'snapshot'">
- <span i18n>You need to enable <b>image mirror mode</b> in the selected pool. Please <a [routerLink]="['/block/mirroring', {outlets: {modal: ['edit', currentPoolName]}}]">click here to select a mode and enable it in this pool.</a></span>
- </cd-helper>
- </div>
- </div>
- </div>
- </div>
-
- <div class="form-group row"
- *ngIf="rbdForm.getValue('mirroringMode') === 'snapshot' && mirroring">
- <label class="cd-col-form-label"
- i18n>Schedule Interval
- <cd-helper i18n-html
- html="Create Mirror-Snapshots automatically on a periodic basis. The interval can be specified in days, hours, or minutes using d, h, m suffix respectively. To create mirror snapshots, you must import or create and have available peers to mirror">
- </cd-helper></label>
- <div class="cd-col-form-input">
- <input id="schedule"
- name="schedule"
- class="form-control"
- type="text"
- formControlName="schedule"
- i18n-placeholder
- placeholder="e.g., 12h or 1d or 10m"
- [attr.disabled]="(peerConfigured === false) ? true : null">
+ <cd-help-text>Supported Units: KiB, MiB, GiB, TiB, PiB etc</cd-help-text>
</div>
</div>
name="{{ feature.key }}"
formControlName="{{ feature.key }}">
<label class="custom-control-label"
- for="{{ feature.key }}">{{ feature.desc }}</label>
- <cd-helper *ngIf="feature.helperHtml"
- html="{{ feature.helperHtml }}">
- </cd-helper>
+ for="{{ feature.key }}">{{ feature.desc }}</label><br>
+ <cd-help-text *ngIf="feature.helperText">
+ {{ feature.helperText }}
+ </cd-help-text>
+ <cd-alert-panel type="warning"
+ *ngIf="feature.helperHtml && rbdForm.getValue(feature.key) === false">
+ {{ feature.helperHtml }}
+ </cd-alert-panel>
</div>
</div>
</div>
});
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;
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;
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<string> = [
'4 KiB',
private routerUrl: string;
icons = Icons;
+ currentImageMirrorMode = '';
+ showMirrorDisableMessage = false;
constructor(
private authStorageService: AuthStorageService,
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)`,
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);
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),
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();
}
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();
}
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);
}
sizeControlErrors = { required: true };
} else {
const sizeInBytes = formatter.toBytes(sizeControl.value);
- if (stripingCount * objectSizeInBytes > sizeInBytes) {
+ if (stripingCount * objectSizeInBytes >= sizeInBytes) {
sizeControlErrors = { invalidSizeObject: true };
}
}
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;
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;
}
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)
});
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 {
});
}
+ shouldDisable(option: string): boolean {
+ return this.currentPoolMirrorMode === 'pool' && option === 'snapshot' ? true : null;
+ }
+
submit() {
if (!this.mode) {
this.rbdImage.next('create');
force:
default: false
type: boolean
+ image_mirror_mode:
+ type: string
metadata:
type: string
mirror_mode:
@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)
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: