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