]>
Commit | Line | Data |
---|---|---|
9f95a23c | 1 | import { Component, EventEmitter, OnInit, ViewChild } from '@angular/core'; |
11fdf7f2 TL |
2 | import { FormControl, Validators } from '@angular/forms'; |
3 | import { ActivatedRoute, Router } from '@angular/router'; | |
4 | ||
5 | import { I18n } from '@ngx-translate/i18n-polyfill'; | |
6 | import * as _ from 'lodash'; | |
7 | import { BsModalService } from 'ngx-bootstrap/modal'; | |
9f95a23c TL |
8 | import { TabsetComponent } from 'ngx-bootstrap/tabs'; |
9 | import { TooltipDirective } from 'ngx-bootstrap/tooltip'; | |
10 | import { Subscription } from 'rxjs'; | |
11fdf7f2 | 11 | |
9f95a23c | 12 | import { CrushRuleService } from '../../../shared/api/crush-rule.service'; |
11fdf7f2 TL |
13 | import { ErasureCodeProfileService } from '../../../shared/api/erasure-code-profile.service'; |
14 | import { PoolService } from '../../../shared/api/pool.service'; | |
15 | import { CriticalConfirmationModalComponent } from '../../../shared/components/critical-confirmation-modal/critical-confirmation-modal.component'; | |
9f95a23c | 16 | import { SelectOption } from '../../../shared/components/select/select-option.model'; |
11fdf7f2 | 17 | import { ActionLabelsI18n, URLVerbs } from '../../../shared/constants/app.constants'; |
9f95a23c | 18 | import { Icons } from '../../../shared/enum/icons.enum'; |
11fdf7f2 TL |
19 | import { CdFormGroup } from '../../../shared/forms/cd-form-group'; |
20 | import { CdValidators } from '../../../shared/forms/cd-validators'; | |
21 | import { | |
22 | RbdConfigurationEntry, | |
23 | RbdConfigurationSourceField | |
24 | } from '../../../shared/models/configuration'; | |
9f95a23c | 25 | import { CrushRule, CrushRuleConfig } from '../../../shared/models/crush-rule'; |
11fdf7f2 TL |
26 | import { CrushStep } from '../../../shared/models/crush-step'; |
27 | import { ErasureCodeProfile } from '../../../shared/models/erasure-code-profile'; | |
28 | import { FinishedTask } from '../../../shared/models/finished-task'; | |
29 | import { Permission } from '../../../shared/models/permissions'; | |
30 | import { PoolFormInfo } from '../../../shared/models/pool-form-info'; | |
31 | import { DimlessBinaryPipe } from '../../../shared/pipes/dimless-binary.pipe'; | |
32 | import { AuthStorageService } from '../../../shared/services/auth-storage.service'; | |
33 | import { FormatterService } from '../../../shared/services/formatter.service'; | |
34 | import { TaskWrapperService } from '../../../shared/services/task-wrapper.service'; | |
9f95a23c TL |
35 | import { CrushRuleFormModalComponent } from '../crush-rule-form-modal/crush-rule-form-modal.component'; |
36 | import { ErasureCodeProfileFormModalComponent } from '../erasure-code-profile-form/erasure-code-profile-form-modal.component'; | |
11fdf7f2 TL |
37 | import { Pool } from '../pool'; |
38 | import { PoolFormData } from './pool-form-data'; | |
39 | ||
40 | interface FormFieldDescription { | |
41 | externalFieldName: string; | |
42 | formControlName: string; | |
43 | attr?: string; | |
44 | replaceFn?: Function; | |
45 | editable?: boolean; | |
46 | resetValue?: any; | |
47 | } | |
48 | ||
49 | @Component({ | |
50 | selector: 'cd-pool-form', | |
51 | templateUrl: './pool-form.component.html', | |
52 | styleUrls: ['./pool-form.component.scss'] | |
53 | }) | |
54 | export class PoolFormComponent implements OnInit { | |
9f95a23c TL |
55 | @ViewChild('crushInfoTabs', { static: false }) crushInfoTabs: TabsetComponent; |
56 | @ViewChild('crushDeletionBtn', { static: false }) crushDeletionBtn: TooltipDirective; | |
57 | ||
11fdf7f2 TL |
58 | permission: Permission; |
59 | form: CdFormGroup; | |
60 | ecProfiles: ErasureCodeProfile[]; | |
61 | info: PoolFormInfo; | |
62 | routeParamsSubscribe: any; | |
63 | editing = false; | |
9f95a23c TL |
64 | isReplicated = false; |
65 | isErasure = false; | |
11fdf7f2 TL |
66 | data = new PoolFormData(this.i18n); |
67 | externalPgChange = false; | |
9f95a23c | 68 | current: Record<string, any> = { |
11fdf7f2 TL |
69 | rules: [] |
70 | }; | |
71 | initializeConfigData = new EventEmitter<{ | |
72 | initialData: RbdConfigurationEntry[]; | |
73 | sourceType: RbdConfigurationSourceField; | |
74 | }>(); | |
75 | currentConfigurationValues: { [configKey: string]: any } = {}; | |
76 | action: string; | |
77 | resource: string; | |
9f95a23c TL |
78 | icons = Icons; |
79 | pgAutoscaleModes: string[]; | |
80 | crushUsage: string[] = undefined; // Will only be set if a rule is used by some pool | |
81 | ||
82 | private modalSubscription: Subscription; | |
11fdf7f2 TL |
83 | |
84 | constructor( | |
85 | private dimlessBinaryPipe: DimlessBinaryPipe, | |
86 | private route: ActivatedRoute, | |
87 | private router: Router, | |
88 | private modalService: BsModalService, | |
89 | private poolService: PoolService, | |
90 | private authStorageService: AuthStorageService, | |
91 | private formatter: FormatterService, | |
92 | private bsModalService: BsModalService, | |
93 | private taskWrapper: TaskWrapperService, | |
94 | private ecpService: ErasureCodeProfileService, | |
9f95a23c | 95 | private crushRuleService: CrushRuleService, |
11fdf7f2 TL |
96 | private i18n: I18n, |
97 | public actionLabels: ActionLabelsI18n | |
98 | ) { | |
99 | this.editing = this.router.url.startsWith(`/pool/${URLVerbs.EDIT}`); | |
100 | this.action = this.editing ? this.actionLabels.EDIT : this.actionLabels.CREATE; | |
101 | this.resource = this.i18n('pool'); | |
102 | this.authenticate(); | |
103 | this.createForm(); | |
104 | } | |
105 | ||
106 | authenticate() { | |
107 | this.permission = this.authStorageService.getPermissions().pool; | |
108 | if ( | |
109 | !this.permission.read || | |
110 | ((!this.permission.update && this.editing) || (!this.permission.create && !this.editing)) | |
111 | ) { | |
112 | this.router.navigate(['/404']); | |
113 | } | |
114 | } | |
115 | ||
116 | private createForm() { | |
117 | const compressionForm = new CdFormGroup({ | |
118 | mode: new FormControl('none'), | |
119 | algorithm: new FormControl(''), | |
120 | minBlobSize: new FormControl('', { | |
121 | updateOn: 'blur' | |
122 | }), | |
123 | maxBlobSize: new FormControl('', { | |
124 | updateOn: 'blur' | |
125 | }), | |
126 | ratio: new FormControl('', { | |
127 | updateOn: 'blur' | |
128 | }) | |
129 | }); | |
130 | ||
131 | this.form = new CdFormGroup( | |
132 | { | |
133 | name: new FormControl('', { | |
9f95a23c TL |
134 | validators: [ |
135 | Validators.pattern(/^[.A-Za-z0-9_/-]+$/), | |
136 | Validators.required, | |
137 | CdValidators.custom('rbdPool', () => { | |
138 | return ( | |
139 | this.form && | |
140 | this.form.getValue('name').includes('/') && | |
141 | this.data && | |
142 | this.data.applications.selected.indexOf('rbd') !== -1 | |
143 | ); | |
144 | }) | |
145 | ] | |
11fdf7f2 TL |
146 | }), |
147 | poolType: new FormControl('', { | |
148 | validators: [Validators.required] | |
149 | }), | |
150 | crushRule: new FormControl(null, { | |
151 | validators: [ | |
152 | CdValidators.custom( | |
153 | 'tooFewOsds', | |
9f95a23c TL |
154 | (rule: any) => this.info && rule && this.info.osd_count < rule.min_size |
155 | ), | |
156 | CdValidators.custom( | |
157 | 'required', | |
158 | (rule: CrushRule) => | |
159 | this.isReplicated && this.info.crush_rules_replicated.length > 0 && !rule | |
11fdf7f2 TL |
160 | ) |
161 | ] | |
162 | }), | |
163 | size: new FormControl('', { | |
164 | updateOn: 'blur' | |
165 | }), | |
166 | erasureProfile: new FormControl(null), | |
167 | pgNum: new FormControl('', { | |
168 | validators: [Validators.required, Validators.min(1)] | |
169 | }), | |
9f95a23c | 170 | pgAutoscaleMode: new FormControl(null), |
11fdf7f2 | 171 | ecOverwrites: new FormControl(false), |
9f95a23c TL |
172 | compression: compressionForm, |
173 | max_bytes: new FormControl(''), | |
174 | max_objects: new FormControl(0, { | |
175 | validators: [Validators.min(0)] | |
11fdf7f2 | 176 | }) |
9f95a23c TL |
177 | }, |
178 | [CdValidators.custom('form', (): null => null)] | |
11fdf7f2 TL |
179 | ); |
180 | } | |
181 | ||
182 | ngOnInit() { | |
9f95a23c TL |
183 | this.poolService.getInfo().subscribe((info: PoolFormInfo) => { |
184 | this.initInfo(info); | |
185 | if (this.editing) { | |
186 | this.initEditMode(); | |
187 | } else { | |
188 | this.setAvailableApps(); | |
11fdf7f2 | 189 | } |
9f95a23c TL |
190 | this.listenToChanges(); |
191 | this.setComplexValidators(); | |
192 | }); | |
11fdf7f2 TL |
193 | } |
194 | ||
195 | private initInfo(info: PoolFormInfo) { | |
9f95a23c TL |
196 | this.pgAutoscaleModes = info.pg_autoscale_modes; |
197 | this.form.silentSet('pgAutoscaleMode', info.pg_autoscale_default_mode); | |
11fdf7f2 | 198 | this.form.silentSet('algorithm', info.bluestore_compression_algorithm); |
11fdf7f2 | 199 | this.info = info; |
9f95a23c | 200 | this.initEcp(info.erasure_code_profiles); |
11fdf7f2 TL |
201 | } |
202 | ||
203 | private initEcp(ecProfiles: ErasureCodeProfile[]) { | |
9f95a23c TL |
204 | this.setListControlStatus('erasureProfile', ecProfiles); |
205 | this.ecProfiles = ecProfiles; | |
206 | } | |
207 | ||
208 | /** | |
209 | * Used to update the crush rule or erasure code profile listings. | |
210 | * | |
211 | * If only one rule or profile exists it will be selected. | |
212 | * If nothing exists null will be selected. | |
213 | * If more than one rule or profile exists the listing will be enabled, | |
214 | * otherwise disabled. | |
215 | */ | |
216 | private setListControlStatus(controlName: string, arr: any[]) { | |
217 | const control = this.form.get(controlName); | |
218 | const value = control.value; | |
219 | if (arr.length === 1 && (!value || !_.isEqual(value, arr[0]))) { | |
220 | control.setValue(arr[0]); | |
221 | } else if (arr.length === 0 && value) { | |
222 | control.setValue(null); | |
11fdf7f2 | 223 | } |
9f95a23c TL |
224 | if (arr.length <= 1) { |
225 | if (control.enabled) { | |
226 | control.disable(); | |
227 | } | |
228 | } else if (control.disabled) { | |
11fdf7f2 TL |
229 | control.enable(); |
230 | } | |
11fdf7f2 TL |
231 | } |
232 | ||
233 | private initEditMode() { | |
234 | this.disableForEdit(); | |
235 | this.routeParamsSubscribe = this.route.params.subscribe((param: { name: string }) => | |
236 | this.poolService.get(param.name).subscribe((pool: Pool) => { | |
237 | this.data.pool = pool; | |
238 | this.initEditFormData(pool); | |
239 | }) | |
240 | ); | |
241 | } | |
242 | ||
243 | private disableForEdit() { | |
244 | ['poolType', 'crushRule', 'size', 'erasureProfile', 'ecOverwrites'].forEach((controlName) => | |
245 | this.form.get(controlName).disable() | |
246 | ); | |
247 | } | |
248 | ||
249 | private initEditFormData(pool: Pool) { | |
250 | this.initializeConfigData.emit({ | |
251 | initialData: pool.configuration, | |
252 | sourceType: RbdConfigurationSourceField.pool | |
253 | }); | |
9f95a23c TL |
254 | this.poolTypeChange(pool.type); |
255 | const rules = this.info.crush_rules_replicated.concat(this.info.crush_rules_erasure); | |
11fdf7f2 TL |
256 | const dataMap = { |
257 | name: pool.pool_name, | |
258 | poolType: pool.type, | |
9f95a23c | 259 | crushRule: rules.find((rule: CrushRule) => rule.rule_name === pool.crush_rule), |
11fdf7f2 TL |
260 | size: pool.size, |
261 | erasureProfile: this.ecProfiles.find((ecp) => ecp.name === pool.erasure_code_profile), | |
9f95a23c | 262 | pgAutoscaleMode: pool.pg_autoscale_mode, |
11fdf7f2 TL |
263 | pgNum: pool.pg_num, |
264 | ecOverwrites: pool.flags_names.includes('ec_overwrites'), | |
265 | mode: pool.options.compression_mode, | |
266 | algorithm: pool.options.compression_algorithm, | |
267 | minBlobSize: this.dimlessBinaryPipe.transform(pool.options.compression_min_blob_size), | |
268 | maxBlobSize: this.dimlessBinaryPipe.transform(pool.options.compression_max_blob_size), | |
9f95a23c TL |
269 | ratio: pool.options.compression_required_ratio, |
270 | max_bytes: this.dimlessBinaryPipe.transform(pool.quota_max_bytes), | |
271 | max_objects: pool.quota_max_objects | |
11fdf7f2 | 272 | }; |
11fdf7f2 TL |
273 | Object.keys(dataMap).forEach((controlName: string) => { |
274 | const value = dataMap[controlName]; | |
275 | if (!_.isUndefined(value) && value !== '') { | |
276 | this.form.silentSet(controlName, value); | |
277 | } | |
278 | }); | |
eafe8130 | 279 | this.data.pgs = this.form.getValue('pgNum'); |
9f95a23c | 280 | this.setAvailableApps(this.data.applications.default.concat(pool.application_metadata)); |
11fdf7f2 TL |
281 | this.data.applications.selected = pool.application_metadata; |
282 | } | |
283 | ||
9f95a23c TL |
284 | private setAvailableApps(apps: string[] = this.data.applications.default) { |
285 | this.data.applications.available = _.uniq(apps.sort()).map( | |
286 | (x: string) => new SelectOption(false, x, '') | |
287 | ); | |
288 | } | |
289 | ||
11fdf7f2 TL |
290 | private listenToChanges() { |
291 | this.listenToChangesDuringAddEdit(); | |
292 | if (!this.editing) { | |
293 | this.listenToChangesDuringAdd(); | |
294 | } | |
295 | } | |
296 | ||
297 | private listenToChangesDuringAddEdit() { | |
298 | this.form.get('pgNum').valueChanges.subscribe((pgs) => { | |
299 | const change = pgs - this.data.pgs; | |
300 | if (Math.abs(change) !== 1 || pgs === 2) { | |
301 | this.data.pgs = pgs; | |
302 | return; | |
303 | } | |
304 | this.doPgPowerJump(change as 1 | -1); | |
305 | }); | |
306 | } | |
307 | ||
308 | private doPgPowerJump(jump: 1 | -1) { | |
309 | const power = this.calculatePgPower() + jump; | |
310 | this.setPgs(jump === -1 ? Math.round(power) : Math.floor(power)); | |
311 | } | |
312 | ||
313 | private calculatePgPower(pgs = this.form.getValue('pgNum')): number { | |
314 | return Math.log(pgs) / Math.log(2); | |
315 | } | |
316 | ||
317 | private setPgs(power: number) { | |
318 | const pgs = Math.pow(2, power < 0 ? 0 : power); // Set size the nearest accurate size. | |
319 | this.data.pgs = pgs; | |
320 | this.form.silentSet('pgNum', pgs); | |
321 | } | |
322 | ||
323 | private listenToChangesDuringAdd() { | |
324 | this.form.get('poolType').valueChanges.subscribe((poolType) => { | |
9f95a23c | 325 | this.poolTypeChange(poolType); |
11fdf7f2 | 326 | }); |
9f95a23c TL |
327 | this.form.get('crushRule').valueChanges.subscribe((rule) => { |
328 | // The crush rule can only be changed if type 'replicated' is set. | |
329 | if (this.crushDeletionBtn && this.crushDeletionBtn.isOpen) { | |
330 | this.crushDeletionBtn.hide(); | |
331 | } | |
332 | if (!rule) { | |
333 | return; | |
11fdf7f2 | 334 | } |
9f95a23c TL |
335 | this.crushRuleIsUsedBy(rule.rule_name); |
336 | this.replicatedRuleChange(); | |
11fdf7f2 TL |
337 | this.pgCalc(); |
338 | }); | |
339 | this.form.get('size').valueChanges.subscribe(() => { | |
9f95a23c | 340 | // The size can only be changed if type 'replicated' is set. |
11fdf7f2 TL |
341 | this.pgCalc(); |
342 | }); | |
343 | this.form.get('erasureProfile').valueChanges.subscribe(() => { | |
9f95a23c | 344 | // The ec profile can only be changed if type 'erasure' is set. |
11fdf7f2 TL |
345 | this.pgCalc(); |
346 | }); | |
347 | this.form.get('mode').valueChanges.subscribe(() => { | |
348 | ['minBlobSize', 'maxBlobSize', 'ratio'].forEach((name) => { | |
349 | this.form.get(name).updateValueAndValidity({ emitEvent: false }); | |
350 | }); | |
351 | }); | |
352 | this.form.get('minBlobSize').valueChanges.subscribe(() => { | |
353 | this.form.get('maxBlobSize').updateValueAndValidity({ emitEvent: false }); | |
354 | }); | |
355 | this.form.get('maxBlobSize').valueChanges.subscribe(() => { | |
356 | this.form.get('minBlobSize').updateValueAndValidity({ emitEvent: false }); | |
357 | }); | |
358 | } | |
359 | ||
9f95a23c TL |
360 | private poolTypeChange(poolType: string) { |
361 | if (poolType === 'replicated') { | |
362 | this.setTypeBooleans(true, false); | |
363 | } else if (poolType === 'erasure') { | |
364 | this.setTypeBooleans(false, true); | |
365 | } else { | |
366 | this.setTypeBooleans(false, false); | |
367 | } | |
11fdf7f2 TL |
368 | if (!poolType || !this.info) { |
369 | this.current.rules = []; | |
370 | return; | |
371 | } | |
372 | const rules = this.info['crush_rules_' + poolType] || []; | |
11fdf7f2 | 373 | this.current.rules = rules; |
9f95a23c TL |
374 | if (this.editing) { |
375 | return; | |
376 | } | |
377 | if (this.isReplicated) { | |
378 | this.setListControlStatus('crushRule', rules); | |
379 | } | |
380 | this.replicatedRuleChange(); | |
381 | this.pgCalc(); | |
382 | } | |
383 | ||
384 | private setTypeBooleans(replicated: boolean, erasure: boolean) { | |
385 | this.isReplicated = replicated; | |
386 | this.isErasure = erasure; | |
11fdf7f2 TL |
387 | } |
388 | ||
389 | private replicatedRuleChange() { | |
9f95a23c | 390 | if (!this.isReplicated) { |
11fdf7f2 TL |
391 | return; |
392 | } | |
393 | const control = this.form.get('size'); | |
394 | let size = this.form.getValue('size') || 3; | |
395 | const min = this.getMinSize(); | |
396 | const max = this.getMaxSize(); | |
397 | if (size < min) { | |
398 | size = min; | |
399 | } else if (size > max) { | |
400 | size = max; | |
401 | } | |
402 | if (size !== control.value) { | |
403 | this.form.silentSet('size', size); | |
404 | } | |
405 | } | |
406 | ||
407 | getMinSize(): number { | |
408 | if (!this.info || this.info.osd_count < 1) { | |
9f95a23c | 409 | return 0; |
11fdf7f2 TL |
410 | } |
411 | const rule = this.form.getValue('crushRule'); | |
412 | if (rule) { | |
413 | return rule.min_size; | |
414 | } | |
415 | return 1; | |
416 | } | |
417 | ||
418 | getMaxSize(): number { | |
419 | if (!this.info || this.info.osd_count < 1) { | |
9f95a23c | 420 | return 0; |
11fdf7f2 TL |
421 | } |
422 | const osds: number = this.info.osd_count; | |
423 | if (this.form.getValue('crushRule')) { | |
424 | const max: number = this.form.get('crushRule').value.max_size; | |
425 | if (max < osds) { | |
426 | return max; | |
427 | } | |
428 | } | |
429 | return osds; | |
430 | } | |
431 | ||
432 | private pgCalc() { | |
433 | const poolType = this.form.getValue('poolType'); | |
434 | if (!this.info || this.form.get('pgNum').dirty || !poolType) { | |
435 | return; | |
436 | } | |
437 | const pgMax = this.info.osd_count * 100; | |
9f95a23c | 438 | const pgs = this.isReplicated ? this.replicatedPgCalc(pgMax) : this.erasurePgCalc(pgMax); |
11fdf7f2 TL |
439 | if (!pgs) { |
440 | return; | |
441 | } | |
442 | const oldValue = this.data.pgs; | |
443 | this.alignPgs(pgs); | |
444 | const newValue = this.data.pgs; | |
445 | if (!this.externalPgChange) { | |
446 | this.externalPgChange = oldValue !== newValue; | |
447 | } | |
448 | } | |
449 | ||
9f95a23c | 450 | private replicatedPgCalc(pgs: number): number { |
11fdf7f2 TL |
451 | const sizeControl = this.form.get('size'); |
452 | const size = sizeControl.value; | |
9f95a23c | 453 | return sizeControl.valid && size > 0 ? pgs / size : 0; |
11fdf7f2 TL |
454 | } |
455 | ||
9f95a23c | 456 | private erasurePgCalc(pgs: number): number { |
11fdf7f2 TL |
457 | const ecpControl = this.form.get('erasureProfile'); |
458 | const ecp = ecpControl.value; | |
9f95a23c | 459 | return (ecpControl.valid || ecpControl.disabled) && ecp ? pgs / (ecp.k + ecp.m) : 0; |
11fdf7f2 TL |
460 | } |
461 | ||
462 | private alignPgs(pgs = this.form.getValue('pgNum')) { | |
463 | this.setPgs(Math.round(this.calculatePgPower(pgs < 1 ? 1 : pgs))); | |
464 | } | |
465 | ||
466 | private setComplexValidators() { | |
467 | if (this.editing) { | |
11fdf7f2 TL |
468 | this.form |
469 | .get('name') | |
470 | .setValidators([ | |
471 | this.form.get('name').validator, | |
472 | CdValidators.custom( | |
473 | 'uniqueName', | |
9f95a23c | 474 | (name: string) => |
11fdf7f2 TL |
475 | this.data.pool && |
476 | this.info && | |
477 | this.info.pool_names.indexOf(name) !== -1 && | |
478 | this.info.pool_names.indexOf(name) !== | |
479 | this.info.pool_names.indexOf(this.data.pool.pool_name) | |
480 | ) | |
481 | ]); | |
482 | } else { | |
9f95a23c TL |
483 | CdValidators.validateIf(this.form.get('size'), () => this.isReplicated, [ |
484 | CdValidators.custom( | |
485 | 'min', | |
486 | (value: number) => this.form.getValue('size') && value < this.getMinSize() | |
487 | ), | |
488 | CdValidators.custom( | |
489 | 'max', | |
490 | (value: number) => this.form.getValue('size') && this.getMaxSize() < value | |
491 | ) | |
492 | ]); | |
11fdf7f2 TL |
493 | this.form |
494 | .get('name') | |
495 | .setValidators([ | |
496 | this.form.get('name').validator, | |
497 | CdValidators.custom( | |
498 | 'uniqueName', | |
9f95a23c | 499 | (name: string) => this.info && this.info.pool_names.indexOf(name) !== -1 |
11fdf7f2 TL |
500 | ) |
501 | ]); | |
502 | } | |
503 | this.setCompressionValidators(); | |
504 | } | |
505 | ||
506 | private setCompressionValidators() { | |
507 | CdValidators.validateIf(this.form.get('minBlobSize'), () => this.hasCompressionEnabled(), [ | |
508 | Validators.min(0), | |
9f95a23c | 509 | CdValidators.custom('maximum', (size: string) => |
11fdf7f2 TL |
510 | this.oddBlobSize(size, this.form.getValue('maxBlobSize')) |
511 | ) | |
512 | ]); | |
513 | CdValidators.validateIf(this.form.get('maxBlobSize'), () => this.hasCompressionEnabled(), [ | |
514 | Validators.min(0), | |
9f95a23c | 515 | CdValidators.custom('minimum', (size: string) => |
11fdf7f2 TL |
516 | this.oddBlobSize(this.form.getValue('minBlobSize'), size) |
517 | ) | |
518 | ]); | |
519 | CdValidators.validateIf(this.form.get('ratio'), () => this.hasCompressionEnabled(), [ | |
520 | Validators.min(0), | |
521 | Validators.max(1) | |
522 | ]); | |
523 | } | |
524 | ||
9f95a23c TL |
525 | private oddBlobSize(minimum: string, maximum: string) { |
526 | const min = this.formatter.toBytes(minimum); | |
527 | const max = this.formatter.toBytes(maximum); | |
528 | return Boolean(min && max && min >= max); | |
11fdf7f2 TL |
529 | } |
530 | ||
531 | hasCompressionEnabled() { | |
532 | return this.form.getValue('mode') && this.form.get('mode').value.toLowerCase() !== 'none'; | |
533 | } | |
534 | ||
535 | describeCrushStep(step: CrushStep) { | |
536 | return [ | |
537 | step.op.replace('_', ' '), | |
538 | step.item_name || '', | |
539 | step.type ? step.num + ' type ' + step.type : '' | |
540 | ].join(' '); | |
541 | } | |
542 | ||
543 | addErasureCodeProfile() { | |
544 | this.modalSubscription = this.modalService.onHide.subscribe(() => this.reloadECPs()); | |
9f95a23c | 545 | this.bsModalService.show(ErasureCodeProfileFormModalComponent); |
11fdf7f2 TL |
546 | } |
547 | ||
548 | private reloadECPs() { | |
549 | this.ecpService.list().subscribe((profiles: ErasureCodeProfile[]) => this.initEcp(profiles)); | |
550 | this.modalSubscription.unsubscribe(); | |
551 | } | |
552 | ||
553 | deleteErasureCodeProfile() { | |
554 | const ecp = this.form.getValue('erasureProfile'); | |
555 | if (!ecp) { | |
556 | return; | |
557 | } | |
558 | const name = ecp.name; | |
559 | this.modalSubscription = this.modalService.onHide.subscribe(() => this.reloadECPs()); | |
560 | this.modalService.show(CriticalConfirmationModalComponent, { | |
561 | initialState: { | |
562 | itemDescription: this.i18n('erasure code profile'), | |
eafe8130 | 563 | itemNames: [name], |
11fdf7f2 TL |
564 | submitActionObservable: () => |
565 | this.taskWrapper.wrapTaskAroundCall({ | |
566 | task: new FinishedTask('ecp/delete', { name: name }), | |
567 | call: this.ecpService.delete(name) | |
568 | }) | |
569 | } | |
570 | }); | |
571 | } | |
572 | ||
9f95a23c TL |
573 | addCrushRule() { |
574 | if (this.crushDeletionBtn.isOpen) { | |
575 | this.crushDeletionBtn.hide(); | |
576 | } | |
577 | const modalRef = this.bsModalService.show(CrushRuleFormModalComponent); | |
578 | modalRef.content.submitAction.subscribe((rule: CrushRuleConfig) => { | |
579 | this.reloadCrushRules(rule.name); | |
580 | }); | |
581 | } | |
582 | ||
583 | private reloadCrushRules(ruleName?: string) { | |
584 | if (this.modalSubscription) { | |
585 | this.modalSubscription.unsubscribe(); | |
586 | } | |
587 | this.poolService.getInfo().subscribe((info: PoolFormInfo) => { | |
588 | this.initInfo(info); | |
589 | this.poolTypeChange('replicated'); | |
590 | if (!ruleName) { | |
591 | return; | |
592 | } | |
593 | const newRule = this.info.crush_rules_replicated.find((rule) => rule.rule_name === ruleName); | |
594 | if (newRule) { | |
595 | this.form.get('crushRule').setValue(newRule); | |
596 | } | |
597 | }); | |
598 | } | |
599 | ||
600 | deleteCrushRule() { | |
601 | const rule = this.form.getValue('crushRule'); | |
602 | if (!rule) { | |
603 | return; | |
604 | } | |
605 | if (this.crushUsage) { | |
606 | this.crushDeletionBtn.toggle(); | |
607 | this.data.crushInfo = true; | |
608 | setTimeout(() => { | |
609 | if (this.crushInfoTabs) { | |
610 | this.crushInfoTabs.tabs[2].active = true; | |
611 | } | |
612 | }, 50); | |
613 | return; | |
614 | } | |
615 | const name = rule.rule_name; | |
616 | this.modalSubscription = this.modalService.onHide.subscribe(() => this.reloadCrushRules()); | |
617 | this.modalService.show(CriticalConfirmationModalComponent, { | |
618 | initialState: { | |
619 | itemDescription: this.i18n('crush rule'), | |
620 | itemNames: [name], | |
621 | submitActionObservable: () => | |
622 | this.taskWrapper.wrapTaskAroundCall({ | |
623 | task: new FinishedTask('crushRule/delete', { name: name }), | |
624 | call: this.crushRuleService.delete(name) | |
625 | }) | |
626 | } | |
627 | }); | |
628 | } | |
629 | ||
630 | crushRuleIsUsedBy(ruleName: string) { | |
631 | this.crushUsage = ruleName ? this.info.used_rules[ruleName] : undefined; | |
632 | } | |
633 | ||
11fdf7f2 TL |
634 | submit() { |
635 | if (this.form.invalid) { | |
636 | this.form.setErrors({ cdSubmitButton: true }); | |
637 | return; | |
638 | } | |
639 | ||
640 | const pool = { pool: this.form.getValue('name') }; | |
641 | ||
642 | this.assignFormFields(pool, [ | |
643 | { externalFieldName: 'pool_type', formControlName: 'poolType' }, | |
9f95a23c TL |
644 | { |
645 | externalFieldName: 'pg_autoscale_mode', | |
646 | formControlName: 'pgAutoscaleMode', | |
647 | editable: true | |
648 | }, | |
649 | { | |
650 | externalFieldName: 'pg_num', | |
651 | formControlName: 'pgNum', | |
652 | replaceFn: (value: number) => (this.form.getValue('pgAutoscaleMode') === 'on' ? 1 : value), | |
653 | editable: true | |
654 | }, | |
655 | this.isReplicated | |
11fdf7f2 TL |
656 | ? { externalFieldName: 'size', formControlName: 'size' } |
657 | : { | |
658 | externalFieldName: 'erasure_code_profile', | |
659 | formControlName: 'erasureProfile', | |
660 | attr: 'name' | |
661 | }, | |
9f95a23c TL |
662 | { |
663 | externalFieldName: 'rule_name', | |
664 | formControlName: 'crushRule', | |
665 | replaceFn: (value: CrushRule) => (this.isReplicated ? value && value.rule_name : undefined) | |
666 | }, | |
667 | { | |
668 | externalFieldName: 'quota_max_bytes', | |
669 | formControlName: 'max_bytes', | |
670 | replaceFn: this.formatter.toBytes, | |
671 | editable: true, | |
672 | resetValue: this.editing ? 0 : undefined | |
673 | }, | |
674 | { | |
675 | externalFieldName: 'quota_max_objects', | |
676 | formControlName: 'max_objects', | |
677 | editable: true, | |
678 | resetValue: this.editing ? 0 : undefined | |
679 | } | |
11fdf7f2 TL |
680 | ]); |
681 | ||
682 | if (this.info.is_all_bluestore) { | |
683 | this.assignFormField(pool, { | |
684 | externalFieldName: 'flags', | |
685 | formControlName: 'ecOverwrites', | |
9f95a23c | 686 | replaceFn: () => (this.isErasure ? ['ec_overwrites'] : undefined) |
11fdf7f2 TL |
687 | }); |
688 | ||
689 | if (this.form.getValue('mode') !== 'none') { | |
690 | this.assignFormFields(pool, [ | |
691 | { | |
692 | externalFieldName: 'compression_mode', | |
693 | formControlName: 'mode', | |
694 | editable: true, | |
9f95a23c | 695 | replaceFn: (value: boolean) => this.hasCompressionEnabled() && value |
11fdf7f2 TL |
696 | }, |
697 | { | |
698 | externalFieldName: 'compression_algorithm', | |
699 | formControlName: 'algorithm', | |
700 | editable: true | |
701 | }, | |
702 | { | |
703 | externalFieldName: 'compression_min_blob_size', | |
704 | formControlName: 'minBlobSize', | |
705 | replaceFn: this.formatter.toBytes, | |
706 | editable: true, | |
707 | resetValue: 0 | |
708 | }, | |
709 | { | |
710 | externalFieldName: 'compression_max_blob_size', | |
711 | formControlName: 'maxBlobSize', | |
712 | replaceFn: this.formatter.toBytes, | |
713 | editable: true, | |
714 | resetValue: 0 | |
715 | }, | |
716 | { | |
717 | externalFieldName: 'compression_required_ratio', | |
718 | formControlName: 'ratio', | |
719 | editable: true, | |
720 | resetValue: 0 | |
721 | } | |
722 | ]); | |
723 | } else if (this.editing) { | |
724 | this.assignFormFields(pool, [ | |
725 | { | |
726 | externalFieldName: 'compression_mode', | |
727 | formControlName: 'mode', | |
728 | editable: true, | |
92f5a8d4 | 729 | replaceFn: () => 'unset' // Is used if no compression is set |
11fdf7f2 TL |
730 | }, |
731 | { | |
732 | externalFieldName: 'srcpool', | |
733 | formControlName: 'name', | |
734 | editable: true, | |
735 | replaceFn: () => this.data.pool.pool_name | |
736 | } | |
737 | ]); | |
738 | } | |
739 | } | |
740 | ||
741 | const apps = this.data.applications.selected; | |
742 | if (apps.length > 0 || this.editing) { | |
743 | pool['application_metadata'] = apps; | |
744 | } | |
745 | ||
746 | // Only collect configuration data for replicated pools, as QoS cannot be configured on EC | |
747 | // pools. EC data pools inherit their settings from the corresponding replicated metadata pool. | |
9f95a23c | 748 | if (this.isReplicated && !_.isEmpty(this.currentConfigurationValues)) { |
11fdf7f2 TL |
749 | pool['configuration'] = this.currentConfigurationValues; |
750 | } | |
751 | ||
752 | this.triggerApiTask(pool); | |
753 | } | |
754 | ||
755 | /** | |
756 | * Retrieves the values for the given form field descriptions and assigns the values to the given | |
757 | * object. This method differentiates between `add` and `edit` mode and acts differently on one or | |
758 | * the other. | |
759 | */ | |
760 | private assignFormFields(pool: object, formFieldDescription: FormFieldDescription[]): void { | |
761 | formFieldDescription.forEach((item) => this.assignFormField(pool, item)); | |
762 | } | |
763 | ||
764 | /** | |
765 | * Retrieves the value for the given form field description and assigns the values to the given | |
766 | * object. This method differentiates between `add` and `edit` mode and acts differently on one or | |
767 | * the other. | |
768 | */ | |
769 | private assignFormField( | |
770 | pool: object, | |
771 | { | |
772 | externalFieldName, | |
773 | formControlName, | |
774 | attr, | |
775 | replaceFn, | |
776 | editable, | |
777 | resetValue | |
778 | }: FormFieldDescription | |
779 | ): void { | |
780 | if (this.editing && (!editable || this.form.get(formControlName).pristine)) { | |
781 | return; | |
782 | } | |
783 | const value = this.form.getValue(formControlName); | |
784 | let apiValue = replaceFn ? replaceFn(value) : attr ? _.get(value, attr) : value; | |
785 | if (!value || !apiValue) { | |
786 | if (editable && !_.isUndefined(resetValue)) { | |
787 | apiValue = resetValue; | |
788 | } else { | |
789 | return; | |
790 | } | |
791 | } | |
792 | pool[externalFieldName] = apiValue; | |
793 | } | |
794 | ||
9f95a23c | 795 | private triggerApiTask(pool: Record<string, any>) { |
11fdf7f2 TL |
796 | this.taskWrapper |
797 | .wrapTaskAroundCall({ | |
798 | task: new FinishedTask('pool/' + (this.editing ? URLVerbs.EDIT : URLVerbs.CREATE), { | |
799 | pool_name: pool.hasOwnProperty('srcpool') ? pool.srcpool : pool.pool | |
800 | }), | |
801 | call: this.poolService[this.editing ? URLVerbs.UPDATE : URLVerbs.CREATE](pool) | |
802 | }) | |
803 | .subscribe( | |
804 | undefined, | |
805 | (resp) => { | |
806 | if (_.isObject(resp.error) && resp.error.code === '34') { | |
807 | this.form.get('pgNum').setErrors({ '34': true }); | |
808 | } | |
809 | this.form.setErrors({ cdSubmitButton: true }); | |
810 | }, | |
811 | () => this.router.navigate(['/pool']) | |
812 | ); | |
813 | } | |
814 | ||
815 | appSelection() { | |
9f95a23c | 816 | this.form.get('name').updateValueAndValidity({ emitEvent: false, onlySelf: true }); |
11fdf7f2 TL |
817 | } |
818 | } |