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