]>
Commit | Line | Data |
---|---|---|
f67539c2 | 1 | import { Component, OnInit } from '@angular/core'; |
aee94f69 | 2 | import { UntypedFormControl, ValidatorFn, Validators } from '@angular/forms'; |
11fdf7f2 TL |
3 | import { ActivatedRoute, Router } from '@angular/router'; |
4 | ||
f67539c2 | 5 | import _ from 'lodash'; |
1911f103 TL |
6 | import { forkJoin, Observable, ReplaySubject } from 'rxjs'; |
7 | import { first, switchMap } from 'rxjs/operators'; | |
11fdf7f2 | 8 | |
f67539c2 TL |
9 | import { Pool } from '~/app/ceph/pool/pool'; |
10 | import { PoolService } from '~/app/shared/api/pool.service'; | |
2a845540 | 11 | import { RbdMirroringService } from '~/app/shared/api/rbd-mirroring.service'; |
f67539c2 TL |
12 | import { RbdService } from '~/app/shared/api/rbd.service'; |
13 | import { ActionLabelsI18n } from '~/app/shared/constants/app.constants'; | |
14 | import { Icons } from '~/app/shared/enum/icons.enum'; | |
15 | import { CdForm } from '~/app/shared/forms/cd-form'; | |
16 | import { CdFormGroup } from '~/app/shared/forms/cd-form-group'; | |
11fdf7f2 TL |
17 | import { |
18 | RbdConfigurationEntry, | |
19 | RbdConfigurationSourceField | |
f67539c2 TL |
20 | } from '~/app/shared/models/configuration'; |
21 | import { FinishedTask } from '~/app/shared/models/finished-task'; | |
22 | import { ImageSpec } from '~/app/shared/models/image-spec'; | |
23 | import { Permission } from '~/app/shared/models/permissions'; | |
24 | import { DimlessBinaryPipe } from '~/app/shared/pipes/dimless-binary.pipe'; | |
25 | import { AuthStorageService } from '~/app/shared/services/auth-storage.service'; | |
26 | import { FormatterService } from '~/app/shared/services/formatter.service'; | |
27 | import { TaskWrapperService } from '~/app/shared/services/task-wrapper.service'; | |
f6b5b4d7 | 28 | import { RBDImageFormat, RbdModel } from '../rbd-list/rbd-model'; |
494da23a | 29 | import { RbdImageFeature } from './rbd-feature.interface'; |
11fdf7f2 TL |
30 | import { RbdFormCloneRequestModel } from './rbd-form-clone-request.model'; |
31 | import { RbdFormCopyRequestModel } from './rbd-form-copy-request.model'; | |
32 | import { RbdFormCreateRequestModel } from './rbd-form-create-request.model'; | |
33 | import { RbdFormEditRequestModel } from './rbd-form-edit-request.model'; | |
34 | import { RbdFormMode } from './rbd-form-mode.enum'; | |
35 | import { RbdFormResponseModel } from './rbd-form-response.model'; | |
36 | ||
f67539c2 TL |
37 | class ExternalData { |
38 | rbd: RbdFormResponseModel; | |
39 | defaultFeatures: string[]; | |
40 | pools: Pool[]; | |
41 | } | |
42 | ||
11fdf7f2 TL |
43 | @Component({ |
44 | selector: 'cd-rbd-form', | |
45 | templateUrl: './rbd-form.component.html', | |
46 | styleUrls: ['./rbd-form.component.scss'] | |
47 | }) | |
f67539c2 | 48 | export class RbdFormComponent extends CdForm implements OnInit { |
11fdf7f2 TL |
49 | poolPermission: Permission; |
50 | rbdForm: CdFormGroup; | |
11fdf7f2 TL |
51 | getDirtyConfigurationValues: ( |
52 | includeLocalField?: boolean, | |
53 | localField?: RbdConfigurationSourceField | |
54 | ) => RbdConfigurationEntry[]; | |
55 | ||
9f95a23c TL |
56 | namespaces: Array<string> = []; |
57 | namespacesByPoolCache = {}; | |
58 | pools: Array<Pool> = null; | |
59 | allPools: Array<Pool> = null; | |
f67539c2 TL |
60 | dataPools: Array<Pool> = null; |
61 | allDataPools: Array<Pool> = []; | |
494da23a TL |
62 | features: { [key: string]: RbdImageFeature }; |
63 | featuresList: RbdImageFeature[] = []; | |
f67539c2 | 64 | initializeConfigData = new ReplaySubject<{ |
11fdf7f2 TL |
65 | initialData: RbdConfigurationEntry[]; |
66 | sourceType: RbdConfigurationSourceField; | |
f67539c2 | 67 | }>(1); |
11fdf7f2 TL |
68 | |
69 | pool: string; | |
39ae355f | 70 | peerConfigured = false; |
11fdf7f2 TL |
71 | |
72 | advancedEnabled = false; | |
73 | ||
74 | public rbdFormMode = RbdFormMode; | |
75 | mode: RbdFormMode; | |
76 | ||
77 | response: RbdFormResponseModel; | |
78 | snapName: string; | |
79 | ||
80 | defaultObjectSize = '4 MiB'; | |
81 | ||
2a845540 TL |
82 | mirroringOptions = ['journal', 'snapshot']; |
83 | poolMirrorMode: string; | |
84 | mirroring = false; | |
85 | currentPoolName = ''; | |
86 | ||
11fdf7f2 TL |
87 | objectSizes: Array<string> = [ |
88 | '4 KiB', | |
89 | '8 KiB', | |
90 | '16 KiB', | |
91 | '32 KiB', | |
92 | '64 KiB', | |
93 | '128 KiB', | |
94 | '256 KiB', | |
95 | '512 KiB', | |
96 | '1 MiB', | |
97 | '2 MiB', | |
98 | '4 MiB', | |
99 | '8 MiB', | |
100 | '16 MiB', | |
101 | '32 MiB' | |
102 | ]; | |
2a845540 TL |
103 | |
104 | defaultStripingUnit = '4 MiB'; | |
105 | ||
106 | defaultStripingCount = 1; | |
107 | ||
11fdf7f2 TL |
108 | action: string; |
109 | resource: string; | |
1911f103 | 110 | private rbdImage = new ReplaySubject(1); |
f67539c2 | 111 | private routerUrl: string; |
11fdf7f2 | 112 | |
9f95a23c TL |
113 | icons = Icons; |
114 | ||
11fdf7f2 TL |
115 | constructor( |
116 | private authStorageService: AuthStorageService, | |
117 | private route: ActivatedRoute, | |
11fdf7f2 TL |
118 | private poolService: PoolService, |
119 | private rbdService: RbdService, | |
120 | private formatter: FormatterService, | |
121 | private taskWrapper: TaskWrapperService, | |
122 | private dimlessBinaryPipe: DimlessBinaryPipe, | |
494da23a | 123 | public actionLabels: ActionLabelsI18n, |
2a845540 TL |
124 | private router: Router, |
125 | private rbdMirroringService: RbdMirroringService | |
11fdf7f2 | 126 | ) { |
f67539c2 TL |
127 | super(); |
128 | this.routerUrl = this.router.url; | |
11fdf7f2 | 129 | this.poolPermission = this.authStorageService.getPermissions().pool; |
f67539c2 | 130 | this.resource = $localize`RBD`; |
11fdf7f2 TL |
131 | this.features = { |
132 | 'deep-flatten': { | |
f67539c2 | 133 | desc: $localize`Deep flatten`, |
11fdf7f2 TL |
134 | requires: null, |
135 | allowEnable: false, | |
aee94f69 TL |
136 | allowDisable: true, |
137 | helperHtml: $localize`Feature can be disabled but can't be re-enabled later` | |
11fdf7f2 TL |
138 | }, |
139 | layering: { | |
f67539c2 | 140 | desc: $localize`Layering`, |
11fdf7f2 TL |
141 | requires: null, |
142 | allowEnable: false, | |
aee94f69 TL |
143 | allowDisable: false, |
144 | 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` | |
11fdf7f2 TL |
145 | }, |
146 | 'exclusive-lock': { | |
f67539c2 | 147 | desc: $localize`Exclusive lock`, |
11fdf7f2 TL |
148 | requires: null, |
149 | allowEnable: true, | |
150 | allowDisable: true | |
151 | }, | |
152 | 'object-map': { | |
f67539c2 | 153 | desc: $localize`Object map (requires exclusive-lock)`, |
11fdf7f2 TL |
154 | requires: 'exclusive-lock', |
155 | allowEnable: true, | |
494da23a TL |
156 | allowDisable: true, |
157 | initDisabled: true | |
11fdf7f2 | 158 | }, |
11fdf7f2 | 159 | 'fast-diff': { |
f67539c2 | 160 | desc: $localize`Fast diff (interlocked with object-map)`, |
11fdf7f2 TL |
161 | requires: 'object-map', |
162 | allowEnable: true, | |
494da23a TL |
163 | allowDisable: true, |
164 | interlockedWith: 'object-map', | |
165 | initDisabled: true | |
11fdf7f2 TL |
166 | } |
167 | }; | |
494da23a | 168 | this.featuresList = this.objToArray(this.features); |
11fdf7f2 | 169 | this.createForm(); |
494da23a TL |
170 | } |
171 | ||
172 | objToArray(obj: { [key: string]: any }) { | |
173 | return _.map(obj, (o, key) => Object.assign(o, { key: key })); | |
11fdf7f2 TL |
174 | } |
175 | ||
176 | createForm() { | |
11fdf7f2 TL |
177 | this.rbdForm = new CdFormGroup( |
178 | { | |
aee94f69 TL |
179 | parent: new UntypedFormControl(''), |
180 | name: new UntypedFormControl('', { | |
11fdf7f2 TL |
181 | validators: [Validators.required, Validators.pattern(/^[^@/]+?$/)] |
182 | }), | |
aee94f69 | 183 | pool: new UntypedFormControl(null, { |
11fdf7f2 TL |
184 | validators: [Validators.required] |
185 | }), | |
aee94f69 TL |
186 | namespace: new UntypedFormControl(null), |
187 | useDataPool: new UntypedFormControl(false), | |
188 | dataPool: new UntypedFormControl(null), | |
189 | size: new UntypedFormControl(null, { | |
11fdf7f2 TL |
190 | updateOn: 'blur' |
191 | }), | |
aee94f69 | 192 | obj_size: new UntypedFormControl(this.defaultObjectSize), |
494da23a | 193 | features: new CdFormGroup( |
9f95a23c | 194 | this.featuresList.reduce((acc: object, e) => { |
aee94f69 | 195 | acc[e.key] = new UntypedFormControl({ value: false, disabled: !!e.initDisabled }); |
494da23a TL |
196 | return acc; |
197 | }, {}) | |
198 | ), | |
aee94f69 TL |
199 | mirroring: new UntypedFormControl(''), |
200 | schedule: new UntypedFormControl('', { | |
2a845540 TL |
201 | validators: [Validators.pattern(/^([0-9]+)d|([0-9]+)h|([0-9]+)m$/)] // check schedule interval to be in format - 1d or 1h or 1m |
202 | }), | |
aee94f69 TL |
203 | mirroringMode: new UntypedFormControl(''), |
204 | stripingUnit: new UntypedFormControl(this.defaultStripingUnit), | |
205 | stripingCount: new UntypedFormControl(this.defaultStripingCount, { | |
11fdf7f2 TL |
206 | updateOn: 'blur' |
207 | }) | |
208 | }, | |
209 | this.validateRbdForm(this.formatter) | |
210 | ); | |
211 | } | |
212 | ||
213 | disableForEdit() { | |
214 | this.rbdForm.get('parent').disable(); | |
215 | this.rbdForm.get('pool').disable(); | |
9f95a23c | 216 | this.rbdForm.get('namespace').disable(); |
11fdf7f2 TL |
217 | this.rbdForm.get('useDataPool').disable(); |
218 | this.rbdForm.get('dataPool').disable(); | |
219 | this.rbdForm.get('obj_size').disable(); | |
220 | this.rbdForm.get('stripingUnit').disable(); | |
221 | this.rbdForm.get('stripingCount').disable(); | |
f6b5b4d7 TL |
222 | |
223 | /* RBD Image Format v1 */ | |
224 | this.rbdImage.subscribe((image: RbdModel) => { | |
225 | if (image.image_format === RBDImageFormat.V1) { | |
226 | this.rbdForm.get('deep-flatten').disable(); | |
227 | this.rbdForm.get('layering').disable(); | |
228 | this.rbdForm.get('exclusive-lock').disable(); | |
aee94f69 TL |
229 | } else { |
230 | if (!this.rbdForm.get('deep-flatten').value) { | |
231 | this.rbdForm.get('deep-flatten').disable(); | |
232 | } | |
233 | this.rbdForm.get('layering').disable(); | |
f6b5b4d7 TL |
234 | } |
235 | }); | |
11fdf7f2 TL |
236 | } |
237 | ||
238 | disableForClone() { | |
239 | this.rbdForm.get('parent').disable(); | |
240 | this.rbdForm.get('size').disable(); | |
241 | } | |
242 | ||
243 | disableForCopy() { | |
244 | this.rbdForm.get('parent').disable(); | |
245 | this.rbdForm.get('size').disable(); | |
246 | } | |
247 | ||
248 | ngOnInit() { | |
f67539c2 TL |
249 | this.prepareFormForAction(); |
250 | this.gatherNeededData().subscribe(this.handleExternalData.bind(this)); | |
251 | } | |
252 | ||
2a845540 TL |
253 | setExclusiveLock() { |
254 | if (this.mirroring && this.rbdForm.get('mirroringMode').value === 'journal') { | |
255 | this.rbdForm.get('exclusive-lock').setValue(true); | |
256 | this.rbdForm.get('exclusive-lock').disable(); | |
257 | } else { | |
258 | this.rbdForm.get('exclusive-lock').enable(); | |
259 | if (this.poolMirrorMode === 'pool') { | |
260 | this.rbdForm.get('mirroringMode').setValue(this.mirroringOptions[0]); | |
261 | } | |
262 | } | |
263 | } | |
264 | ||
265 | setMirrorMode() { | |
266 | this.mirroring = !this.mirroring; | |
267 | this.setExclusiveLock(); | |
39ae355f TL |
268 | this.checkPeersConfigured(); |
269 | } | |
270 | ||
271 | checkPeersConfigured(poolname?: string) { | |
1e59de90 TL |
272 | var Poolname = poolname ? poolname : this.rbdForm.get('pool').value; |
273 | this.rbdMirroringService.getPeerForPool(Poolname).subscribe((resp: any) => { | |
39ae355f TL |
274 | if (resp.length > 0) { |
275 | this.peerConfigured = true; | |
276 | } | |
277 | }); | |
2a845540 TL |
278 | } |
279 | ||
280 | setPoolMirrorMode() { | |
281 | this.currentPoolName = | |
282 | this.mode === this.rbdFormMode.editing | |
283 | ? this.response?.pool_name | |
284 | : this.rbdForm.getValue('pool'); | |
285 | if (this.currentPoolName) { | |
286 | this.rbdMirroringService.refresh(); | |
287 | this.rbdMirroringService.subscribeSummary((data) => { | |
288 | const pool = data.content_data.pools.find((o: any) => o.name === this.currentPoolName); | |
289 | this.poolMirrorMode = pool.mirror_mode; | |
290 | ||
291 | if (pool.mirror_mode === 'disabled') { | |
292 | this.mirroring = false; | |
293 | this.rbdForm.get('mirroring').setValue(this.mirroring); | |
294 | this.rbdForm.get('mirroring').disable(); | |
2a845540 TL |
295 | } |
296 | }); | |
297 | } | |
298 | this.setExclusiveLock(); | |
299 | } | |
300 | ||
f67539c2 TL |
301 | private prepareFormForAction() { |
302 | const url = this.routerUrl; | |
303 | if (url.startsWith('/block/rbd/edit')) { | |
11fdf7f2 TL |
304 | this.mode = this.rbdFormMode.editing; |
305 | this.action = this.actionLabels.EDIT; | |
306 | this.disableForEdit(); | |
f67539c2 | 307 | } else if (url.startsWith('/block/rbd/clone')) { |
11fdf7f2 TL |
308 | this.mode = this.rbdFormMode.cloning; |
309 | this.disableForClone(); | |
310 | this.action = this.actionLabels.CLONE; | |
f67539c2 | 311 | } else if (url.startsWith('/block/rbd/copy')) { |
11fdf7f2 TL |
312 | this.mode = this.rbdFormMode.copying; |
313 | this.action = this.actionLabels.COPY; | |
314 | this.disableForCopy(); | |
315 | } else { | |
316 | this.action = this.actionLabels.CREATE; | |
317 | } | |
f67539c2 TL |
318 | _.each(this.features, (feature) => { |
319 | this.rbdForm | |
320 | .get('features') | |
321 | .get(feature.key) | |
322 | .valueChanges.subscribe((value) => this.featureFormUpdate(feature.key, value)); | |
323 | }); | |
324 | } | |
325 | ||
326 | private gatherNeededData(): Observable<object> { | |
327 | const promises = {}; | |
328 | if (this.mode) { | |
329 | // Mode is not set for creation | |
9f95a23c TL |
330 | this.route.params.subscribe((params: { image_spec: string; snap: string }) => { |
331 | const imageSpec = ImageSpec.fromString(decodeURIComponent(params.image_spec)); | |
11fdf7f2 TL |
332 | if (params.snap) { |
333 | this.snapName = decodeURIComponent(params.snap); | |
334 | } | |
f67539c2 | 335 | promises['rbd'] = this.rbdService.get(imageSpec); |
39ae355f | 336 | this.checkPeersConfigured(imageSpec.poolName); |
11fdf7f2 TL |
337 | }); |
338 | } else { | |
494da23a | 339 | // New image |
f67539c2 | 340 | promises['defaultFeatures'] = this.rbdService.defaultFeatures(); |
11fdf7f2 TL |
341 | } |
342 | if (this.mode !== this.rbdFormMode.editing && this.poolPermission.read) { | |
f67539c2 | 343 | promises['pools'] = this.poolService.list([ |
9f95a23c TL |
344 | 'pool_name', |
345 | 'type', | |
346 | 'flags_names', | |
347 | 'application_metadata' | |
348 | ]); | |
349 | } | |
f67539c2 TL |
350 | return forkJoin(promises); |
351 | } | |
9f95a23c | 352 | |
f67539c2 TL |
353 | private handleExternalData(data: ExternalData) { |
354 | this.handlePoolData(data.pools); | |
2a845540 | 355 | this.setPoolMirrorMode(); |
9f95a23c | 356 | |
f67539c2 TL |
357 | if (data.defaultFeatures) { |
358 | // Fetched only during creation | |
359 | this.setFeatures(data.defaultFeatures); | |
360 | } | |
9f95a23c | 361 | |
f67539c2 TL |
362 | if (data.rbd) { |
363 | // Not fetched for creation | |
364 | const resp = data.rbd; | |
365 | this.setResponse(resp, this.snapName); | |
366 | this.rbdImage.next(resp); | |
367 | } | |
368 | ||
369 | this.loadingReady(); | |
370 | } | |
371 | ||
372 | private handlePoolData(data: Pool[]) { | |
373 | if (!data) { | |
374 | // Not fetched while editing | |
375 | return; | |
376 | } | |
377 | const pools: Pool[] = []; | |
378 | const dataPools = []; | |
379 | for (const pool of data) { | |
380 | if (this.rbdService.isRBDPool(pool)) { | |
381 | if (pool.type === 'replicated') { | |
382 | pools.push(pool); | |
383 | dataPools.push(pool); | |
384 | } else if (pool.type === 'erasure' && pool.flags_names.indexOf('ec_overwrites') !== -1) { | |
385 | dataPools.push(pool); | |
386 | } | |
387 | } | |
388 | } | |
389 | this.pools = pools; | |
390 | this.allPools = pools; | |
391 | this.dataPools = dataPools; | |
392 | this.allDataPools = dataPools; | |
393 | if (this.pools.length === 1) { | |
394 | const poolName = this.pools[0].pool_name; | |
395 | this.rbdForm.get('pool').setValue(poolName); | |
396 | this.onPoolChange(poolName); | |
397 | } | |
398 | if (this.allDataPools.length <= 1) { | |
399 | this.rbdForm.get('useDataPool').disable(); | |
400 | } | |
11fdf7f2 TL |
401 | } |
402 | ||
9f95a23c | 403 | onPoolChange(selectedPoolName: string) { |
f67539c2 TL |
404 | const dataPoolControl = this.rbdForm.get('dataPool'); |
405 | if (dataPoolControl.value === selectedPoolName) { | |
406 | dataPoolControl.setValue(null); | |
407 | } | |
408 | this.dataPools = this.allDataPools | |
9f95a23c TL |
409 | ? this.allDataPools.filter((dataPool: any) => { |
410 | return dataPool.pool_name !== selectedPoolName; | |
411 | }) | |
412 | : []; | |
9f95a23c TL |
413 | this.namespaces = null; |
414 | if (selectedPoolName in this.namespacesByPoolCache) { | |
415 | this.namespaces = this.namespacesByPoolCache[selectedPoolName]; | |
416 | } else { | |
417 | this.rbdService.listNamespaces(selectedPoolName).subscribe((namespaces: any[]) => { | |
418 | namespaces = namespaces.map((namespace) => namespace.namespace); | |
419 | this.namespacesByPoolCache[selectedPoolName] = namespaces; | |
420 | this.namespaces = namespaces; | |
421 | }); | |
422 | } | |
423 | this.rbdForm.get('namespace').setValue(null); | |
11fdf7f2 TL |
424 | } |
425 | ||
426 | onUseDataPoolChange() { | |
427 | if (!this.rbdForm.getValue('useDataPool')) { | |
428 | this.rbdForm.get('dataPool').setValue(null); | |
429 | this.onDataPoolChange(null); | |
430 | } | |
431 | } | |
432 | ||
9f95a23c TL |
433 | onDataPoolChange(selectedDataPoolName: string) { |
434 | const newPools = this.allPools.filter((pool: Pool) => { | |
11fdf7f2 TL |
435 | return pool.pool_name !== selectedDataPoolName; |
436 | }); | |
437 | if (this.rbdForm.getValue('pool') === selectedDataPoolName) { | |
438 | this.rbdForm.get('pool').setValue(null); | |
439 | } | |
440 | this.pools = newPools; | |
441 | } | |
442 | ||
443 | validateRbdForm(formatter: FormatterService): ValidatorFn { | |
444 | return (formGroup: CdFormGroup) => { | |
445 | // Data Pool | |
446 | const useDataPoolControl = formGroup.get('useDataPool'); | |
447 | const dataPoolControl = formGroup.get('dataPool'); | |
448 | let dataPoolControlErrors = null; | |
449 | if (useDataPoolControl.value && dataPoolControl.value == null) { | |
450 | dataPoolControlErrors = { required: true }; | |
451 | } | |
452 | dataPoolControl.setErrors(dataPoolControlErrors); | |
453 | // Size | |
454 | const sizeControl = formGroup.get('size'); | |
455 | const objectSizeControl = formGroup.get('obj_size'); | |
456 | const objectSizeInBytes = formatter.toBytes( | |
457 | objectSizeControl.value != null ? objectSizeControl.value : this.defaultObjectSize | |
458 | ); | |
459 | const stripingCountControl = formGroup.get('stripingCount'); | |
2a845540 TL |
460 | const stripingCount = |
461 | stripingCountControl.value != null ? stripingCountControl.value : this.defaultStripingCount; | |
11fdf7f2 TL |
462 | let sizeControlErrors = null; |
463 | if (sizeControl.value === null) { | |
464 | sizeControlErrors = { required: true }; | |
465 | } else { | |
466 | const sizeInBytes = formatter.toBytes(sizeControl.value); | |
467 | if (stripingCount * objectSizeInBytes > sizeInBytes) { | |
468 | sizeControlErrors = { invalidSizeObject: true }; | |
469 | } | |
470 | } | |
471 | sizeControl.setErrors(sizeControlErrors); | |
472 | // Striping Unit | |
473 | const stripingUnitControl = formGroup.get('stripingUnit'); | |
474 | let stripingUnitControlErrors = null; | |
475 | if (stripingUnitControl.value === null && stripingCountControl.value !== null) { | |
476 | stripingUnitControlErrors = { required: true }; | |
477 | } else if (stripingUnitControl.value !== null) { | |
478 | const stripingUnitInBytes = formatter.toBytes(stripingUnitControl.value); | |
479 | if (stripingUnitInBytes > objectSizeInBytes) { | |
480 | stripingUnitControlErrors = { invalidStripingUnit: true }; | |
481 | } | |
482 | } | |
483 | stripingUnitControl.setErrors(stripingUnitControlErrors); | |
484 | // Striping Count | |
485 | let stripingCountControlErrors = null; | |
486 | if (stripingCountControl.value === null && stripingUnitControl.value !== null) { | |
487 | stripingCountControlErrors = { required: true }; | |
488 | } else if (stripingCount < 1) { | |
489 | stripingCountControlErrors = { min: true }; | |
490 | } | |
491 | stripingCountControl.setErrors(stripingCountControlErrors); | |
492 | return null; | |
493 | }; | |
494 | } | |
495 | ||
9f95a23c | 496 | deepBoxCheck(key: string, checked: boolean) { |
f67539c2 | 497 | const childFeatures = this.getDependentChildFeatures(key); |
494da23a TL |
498 | childFeatures.forEach((feature) => { |
499 | const featureControl = this.rbdForm.get(feature.key); | |
500 | if (checked) { | |
501 | featureControl.enable({ emitEvent: false }); | |
502 | } else { | |
503 | featureControl.disable({ emitEvent: false }); | |
504 | featureControl.setValue(false, { emitEvent: false }); | |
505 | this.deepBoxCheck(feature.key, checked); | |
11fdf7f2 | 506 | } |
494da23a TL |
507 | |
508 | const featureFormGroup = this.rbdForm.get('features'); | |
509 | if (this.mode === this.rbdFormMode.editing && featureFormGroup.get(feature.key).enabled) { | |
510 | if (this.response.features_name.indexOf(feature.key) !== -1 && !feature.allowDisable) { | |
511 | featureFormGroup.get(feature.key).disable(); | |
512 | } else if ( | |
513 | this.response.features_name.indexOf(feature.key) === -1 && | |
514 | !feature.allowEnable | |
515 | ) { | |
516 | featureFormGroup.get(feature.key).disable(); | |
11fdf7f2 TL |
517 | } |
518 | } | |
519 | }); | |
520 | } | |
521 | ||
f67539c2 TL |
522 | protected getDependentChildFeatures(featureKey: string) { |
523 | return _.filter(this.features, (f) => f.requires === featureKey) || []; | |
524 | } | |
525 | ||
9f95a23c | 526 | interlockCheck(key: string, checked: boolean) { |
494da23a TL |
527 | // Adds a compatibility layer for Ceph cluster where the feature interlock of features hasn't |
528 | // been implemented yet. It disables the feature interlock for images which only have one of | |
529 | // both interlocked features (at the time of this writing: object-map and fast-diff) enabled. | |
530 | const feature = this.featuresList.find((f) => f.key === key); | |
531 | if (this.response) { | |
532 | // Ignore `create` page | |
533 | const hasInterlockedFeature = feature.interlockedWith != null; | |
534 | const dependentInterlockedFeature = this.featuresList.find( | |
535 | (f) => f.interlockedWith === feature.key | |
536 | ); | |
537 | const isOriginFeatureEnabled = !!this.response.features_name.find((e) => e === feature.key); // in this case: fast-diff | |
538 | if (hasInterlockedFeature) { | |
539 | const isLinkedEnabled = !!this.response.features_name.find( | |
540 | (e) => e === feature.interlockedWith | |
541 | ); // depends: object-map | |
542 | if (isOriginFeatureEnabled !== isLinkedEnabled) { | |
543 | return; // Ignore incompatible setting because it's from a previous cluster version | |
544 | } | |
545 | } else if (dependentInterlockedFeature) { | |
546 | const isOtherInterlockedFeatureEnabled = !!this.response.features_name.find( | |
547 | (e) => e === dependentInterlockedFeature.key | |
548 | ); | |
549 | if (isOtherInterlockedFeatureEnabled !== isOriginFeatureEnabled) { | |
550 | return; // Ignore incompatible setting because it's from a previous cluster version | |
551 | } | |
552 | } | |
553 | } | |
554 | ||
555 | if (checked) { | |
556 | _.filter(this.features, (f) => f.interlockedWith === key).forEach((f) => | |
557 | this.rbdForm.get(f.key).setValue(true, { emitEvent: false }) | |
558 | ); | |
559 | } else { | |
560 | if (feature.interlockedWith) { | |
561 | // Don't skip emitting the event here, as it prevents `fast-diff` from | |
562 | // becoming disabled when manually unchecked. This is because it | |
563 | // triggers an update on `object-map` and if `object-map` doesn't emit, | |
564 | // `fast-diff` will not be automatically disabled. | |
e306af50 | 565 | this.rbdForm.get('features').get(feature.interlockedWith).setValue(false); |
494da23a TL |
566 | } |
567 | } | |
568 | } | |
569 | ||
9f95a23c | 570 | featureFormUpdate(key: string, checked: boolean) { |
11fdf7f2 TL |
571 | if (checked) { |
572 | const required = this.features[key].requires; | |
573 | if (required && !this.rbdForm.getValue(required)) { | |
494da23a | 574 | this.rbdForm.get(`features.${key}`).setValue(false); |
11fdf7f2 TL |
575 | return; |
576 | } | |
577 | } | |
578 | this.deepBoxCheck(key, checked); | |
494da23a | 579 | this.interlockCheck(key, checked); |
11fdf7f2 TL |
580 | } |
581 | ||
582 | setFeatures(features: Array<string>) { | |
583 | const featuresControl = this.rbdForm.get('features'); | |
584 | _.forIn(this.features, (feature) => { | |
585 | if (features.indexOf(feature.key) !== -1) { | |
586 | featuresControl.get(feature.key).setValue(true); | |
587 | } | |
494da23a | 588 | this.featureFormUpdate(feature.key, featuresControl.get(feature.key).value); |
11fdf7f2 TL |
589 | }); |
590 | } | |
591 | ||
592 | setResponse(response: RbdFormResponseModel, snapName: string) { | |
593 | this.response = response; | |
9f95a23c TL |
594 | const imageSpec = new ImageSpec( |
595 | response.pool_name, | |
596 | response.namespace, | |
597 | response.name | |
598 | ).toString(); | |
11fdf7f2 | 599 | if (this.mode === this.rbdFormMode.cloning) { |
9f95a23c | 600 | this.rbdForm.get('parent').setValue(`${imageSpec}@${snapName}`); |
11fdf7f2 TL |
601 | } else if (this.mode === this.rbdFormMode.copying) { |
602 | if (snapName) { | |
9f95a23c | 603 | this.rbdForm.get('parent').setValue(`${imageSpec}@${snapName}`); |
11fdf7f2 | 604 | } else { |
9f95a23c | 605 | this.rbdForm.get('parent').setValue(`${imageSpec}`); |
11fdf7f2 TL |
606 | } |
607 | } else if (response.parent) { | |
608 | const parent = response.parent; | |
609 | this.rbdForm | |
610 | .get('parent') | |
611 | .setValue(`${parent.pool_name}/${parent.image_name}@${parent.snap_name}`); | |
612 | } | |
613 | if (this.mode === this.rbdFormMode.editing) { | |
614 | this.rbdForm.get('name').setValue(response.name); | |
2a845540 TL |
615 | if (response?.mirror_mode === 'snapshot' || response.features_name.includes('journaling')) { |
616 | this.mirroring = true; | |
617 | this.rbdForm.get('mirroring').setValue(this.mirroring); | |
618 | this.rbdForm.get('mirroringMode').setValue(response?.mirror_mode); | |
619 | this.rbdForm.get('schedule').setValue(response?.schedule_interval); | |
620 | } else { | |
621 | this.mirroring = false; | |
622 | this.rbdForm.get('mirroring').setValue(this.mirroring); | |
623 | } | |
624 | this.setPoolMirrorMode(); | |
11fdf7f2 TL |
625 | } |
626 | this.rbdForm.get('pool').setValue(response.pool_name); | |
9f95a23c TL |
627 | this.onPoolChange(response.pool_name); |
628 | this.rbdForm.get('namespace').setValue(response.namespace); | |
11fdf7f2 TL |
629 | if (response.data_pool) { |
630 | this.rbdForm.get('useDataPool').setValue(true); | |
631 | this.rbdForm.get('dataPool').setValue(response.data_pool); | |
632 | } | |
633 | this.rbdForm.get('size').setValue(this.dimlessBinaryPipe.transform(response.size)); | |
634 | this.rbdForm.get('obj_size').setValue(this.dimlessBinaryPipe.transform(response.obj_size)); | |
635 | this.setFeatures(response.features_name); | |
636 | this.rbdForm | |
637 | .get('stripingUnit') | |
638 | .setValue(this.dimlessBinaryPipe.transform(response.stripe_unit)); | |
639 | this.rbdForm.get('stripingCount').setValue(response.stripe_count); | |
11fdf7f2 | 640 | /* Configuration */ |
f67539c2 | 641 | this.initializeConfigData.next({ |
11fdf7f2 TL |
642 | initialData: this.response.configuration, |
643 | sourceType: RbdConfigurationSourceField.image | |
644 | }); | |
645 | } | |
646 | ||
647 | createRequest() { | |
648 | const request = new RbdFormCreateRequestModel(); | |
649 | request.pool_name = this.rbdForm.getValue('pool'); | |
9f95a23c | 650 | request.namespace = this.rbdForm.getValue('namespace'); |
11fdf7f2 | 651 | request.name = this.rbdForm.getValue('name'); |
2a845540 | 652 | request.schedule_interval = this.rbdForm.getValue('schedule'); |
11fdf7f2 | 653 | request.size = this.formatter.toBytes(this.rbdForm.getValue('size')); |
39ae355f | 654 | |
2a845540 TL |
655 | if (this.poolMirrorMode === 'image') { |
656 | request.mirror_mode = this.rbdForm.getValue('mirroringMode'); | |
657 | } | |
f67539c2 TL |
658 | this.addObjectSizeAndStripingToRequest(request); |
659 | request.configuration = this.getDirtyConfigurationValues(); | |
660 | return request; | |
661 | } | |
662 | ||
663 | private addObjectSizeAndStripingToRequest( | |
664 | request: RbdFormCreateRequestModel | RbdFormCloneRequestModel | RbdFormCopyRequestModel | |
665 | ) { | |
11fdf7f2 TL |
666 | request.obj_size = this.formatter.toBytes(this.rbdForm.getValue('obj_size')); |
667 | _.forIn(this.features, (feature) => { | |
668 | if (this.rbdForm.getValue(feature.key)) { | |
669 | request.features.push(feature.key); | |
670 | } | |
671 | }); | |
672 | ||
2a845540 TL |
673 | if (this.mirroring && this.rbdForm.getValue('mirroringMode') === 'journal') { |
674 | request.features.push('journaling'); | |
675 | } | |
676 | ||
11fdf7f2 TL |
677 | /* Striping */ |
678 | request.stripe_unit = this.formatter.toBytes(this.rbdForm.getValue('stripingUnit')); | |
679 | request.stripe_count = this.rbdForm.getValue('stripingCount'); | |
680 | request.data_pool = this.rbdForm.getValue('dataPool'); | |
11fdf7f2 TL |
681 | } |
682 | ||
683 | createAction(): Observable<any> { | |
684 | const request = this.createRequest(); | |
685 | return this.taskWrapper.wrapTaskAroundCall({ | |
686 | task: new FinishedTask('rbd/create', { | |
687 | pool_name: request.pool_name, | |
9f95a23c | 688 | namespace: request.namespace, |
2a845540 TL |
689 | image_name: request.name, |
690 | schedule_interval: request.schedule_interval, | |
691 | start_time: request.start_time | |
11fdf7f2 TL |
692 | }), |
693 | call: this.rbdService.create(request) | |
694 | }); | |
695 | } | |
696 | ||
697 | editRequest() { | |
698 | const request = new RbdFormEditRequestModel(); | |
699 | request.name = this.rbdForm.getValue('name'); | |
2a845540 TL |
700 | request.schedule_interval = this.rbdForm.getValue('schedule'); |
701 | request.name = this.rbdForm.getValue('name'); | |
11fdf7f2 TL |
702 | request.size = this.formatter.toBytes(this.rbdForm.getValue('size')); |
703 | _.forIn(this.features, (feature) => { | |
704 | if (this.rbdForm.getValue(feature.key)) { | |
705 | request.features.push(feature.key); | |
706 | } | |
707 | }); | |
2a845540 | 708 | request.enable_mirror = this.rbdForm.getValue('mirroring'); |
39ae355f TL |
709 | if (request.enable_mirror) { |
710 | if (this.rbdForm.getValue('mirroringMode') === 'journal') { | |
711 | request.features.push('journaling'); | |
712 | } | |
713 | if (this.poolMirrorMode === 'image') { | |
2a845540 TL |
714 | request.mirror_mode = this.rbdForm.getValue('mirroringMode'); |
715 | } | |
716 | } else { | |
39ae355f TL |
717 | const index = request.features.indexOf('journaling', 0); |
718 | if (index > -1) { | |
719 | request.features.splice(index, 1); | |
2a845540 TL |
720 | } |
721 | } | |
11fdf7f2 | 722 | request.configuration = this.getDirtyConfigurationValues(); |
11fdf7f2 TL |
723 | return request; |
724 | } | |
725 | ||
726 | cloneRequest(): RbdFormCloneRequestModel { | |
727 | const request = new RbdFormCloneRequestModel(); | |
728 | request.child_pool_name = this.rbdForm.getValue('pool'); | |
9f95a23c | 729 | request.child_namespace = this.rbdForm.getValue('namespace'); |
11fdf7f2 | 730 | request.child_image_name = this.rbdForm.getValue('name'); |
f67539c2 | 731 | this.addObjectSizeAndStripingToRequest(request); |
11fdf7f2 TL |
732 | request.configuration = this.getDirtyConfigurationValues( |
733 | true, | |
734 | RbdConfigurationSourceField.image | |
735 | ); | |
11fdf7f2 TL |
736 | return request; |
737 | } | |
738 | ||
739 | editAction(): Observable<any> { | |
9f95a23c TL |
740 | const imageSpec = new ImageSpec( |
741 | this.response.pool_name, | |
742 | this.response.namespace, | |
743 | this.response.name | |
744 | ); | |
11fdf7f2 TL |
745 | return this.taskWrapper.wrapTaskAroundCall({ |
746 | task: new FinishedTask('rbd/edit', { | |
9f95a23c | 747 | image_spec: imageSpec.toString() |
11fdf7f2 | 748 | }), |
9f95a23c | 749 | call: this.rbdService.update(imageSpec, this.editRequest()) |
11fdf7f2 TL |
750 | }); |
751 | } | |
752 | ||
753 | cloneAction(): Observable<any> { | |
754 | const request = this.cloneRequest(); | |
9f95a23c TL |
755 | const imageSpec = new ImageSpec( |
756 | this.response.pool_name, | |
757 | this.response.namespace, | |
758 | this.response.name | |
759 | ); | |
11fdf7f2 TL |
760 | return this.taskWrapper.wrapTaskAroundCall({ |
761 | task: new FinishedTask('rbd/clone', { | |
9f95a23c | 762 | parent_image_spec: imageSpec.toString(), |
11fdf7f2 TL |
763 | parent_snap_name: this.snapName, |
764 | child_pool_name: request.child_pool_name, | |
9f95a23c | 765 | child_namespace: request.child_namespace, |
11fdf7f2 TL |
766 | child_image_name: request.child_image_name |
767 | }), | |
9f95a23c | 768 | call: this.rbdService.cloneSnapshot(imageSpec, this.snapName, request) |
11fdf7f2 TL |
769 | }); |
770 | } | |
771 | ||
772 | copyRequest(): RbdFormCopyRequestModel { | |
773 | const request = new RbdFormCopyRequestModel(); | |
774 | if (this.snapName) { | |
775 | request.snapshot_name = this.snapName; | |
776 | } | |
777 | request.dest_pool_name = this.rbdForm.getValue('pool'); | |
9f95a23c | 778 | request.dest_namespace = this.rbdForm.getValue('namespace'); |
11fdf7f2 | 779 | request.dest_image_name = this.rbdForm.getValue('name'); |
f67539c2 | 780 | this.addObjectSizeAndStripingToRequest(request); |
11fdf7f2 TL |
781 | request.configuration = this.getDirtyConfigurationValues( |
782 | true, | |
783 | RbdConfigurationSourceField.image | |
784 | ); | |
11fdf7f2 TL |
785 | return request; |
786 | } | |
787 | ||
788 | copyAction(): Observable<any> { | |
789 | const request = this.copyRequest(); | |
9f95a23c TL |
790 | const imageSpec = new ImageSpec( |
791 | this.response.pool_name, | |
792 | this.response.namespace, | |
793 | this.response.name | |
794 | ); | |
11fdf7f2 TL |
795 | return this.taskWrapper.wrapTaskAroundCall({ |
796 | task: new FinishedTask('rbd/copy', { | |
9f95a23c | 797 | src_image_spec: imageSpec.toString(), |
11fdf7f2 | 798 | dest_pool_name: request.dest_pool_name, |
9f95a23c | 799 | dest_namespace: request.dest_namespace, |
11fdf7f2 TL |
800 | dest_image_name: request.dest_image_name |
801 | }), | |
9f95a23c | 802 | call: this.rbdService.copy(imageSpec, request) |
11fdf7f2 TL |
803 | }); |
804 | } | |
805 | ||
806 | submit() { | |
eafe8130 TL |
807 | if (!this.mode) { |
808 | this.rbdImage.next('create'); | |
11fdf7f2 | 809 | } |
eafe8130 TL |
810 | this.rbdImage |
811 | .pipe( | |
1911f103 | 812 | first(), |
eafe8130 TL |
813 | switchMap(() => { |
814 | if (this.mode === this.rbdFormMode.editing) { | |
815 | return this.editAction(); | |
816 | } else if (this.mode === this.rbdFormMode.cloning) { | |
817 | return this.cloneAction(); | |
818 | } else if (this.mode === this.rbdFormMode.copying) { | |
819 | return this.copyAction(); | |
820 | } else { | |
821 | return this.createAction(); | |
822 | } | |
823 | }) | |
824 | ) | |
825 | .subscribe( | |
f67539c2 | 826 | () => undefined, |
eafe8130 TL |
827 | () => this.rbdForm.setErrors({ cdSubmitButton: true }), |
828 | () => this.router.navigate(['/block/rbd']) | |
829 | ); | |
11fdf7f2 TL |
830 | } |
831 | } |