]> git.proxmox.com Git - ceph.git/blob - ceph/src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/pool-form/pool-form.component.spec.ts
import 14.2.4 nautilus point release
[ceph.git] / ceph / src / pybind / mgr / dashboard / frontend / src / app / ceph / pool / pool-form / pool-form.component.spec.ts
1 import { HttpClientTestingModule } from '@angular/common/http/testing';
2 import { ComponentFixture, TestBed } from '@angular/core/testing';
3 import { AbstractControl } from '@angular/forms';
4 import { By } from '@angular/platform-browser';
5 import { ActivatedRoute, Router, Routes } from '@angular/router';
6 import { RouterTestingModule } from '@angular/router/testing';
7
8 import { BsModalService } from 'ngx-bootstrap/modal';
9 import { TabsModule } from 'ngx-bootstrap/tabs';
10 import { ToastrModule } from 'ngx-toastr';
11 import { of } from 'rxjs';
12
13 import {
14 configureTestBed,
15 FixtureHelper,
16 FormHelper,
17 i18nProviders
18 } from '../../../../testing/unit-test-helper';
19 import { NotFoundComponent } from '../../../core/not-found/not-found.component';
20 import { ErasureCodeProfileService } from '../../../shared/api/erasure-code-profile.service';
21 import { PoolService } from '../../../shared/api/pool.service';
22 import { CriticalConfirmationModalComponent } from '../../../shared/components/critical-confirmation-modal/critical-confirmation-modal.component';
23 import { SelectBadgesComponent } from '../../../shared/components/select-badges/select-badges.component';
24 import { CdFormGroup } from '../../../shared/forms/cd-form-group';
25 import { CrushRule } from '../../../shared/models/crush-rule';
26 import { ErasureCodeProfile } from '../../../shared/models/erasure-code-profile';
27 import { Permission } from '../../../shared/models/permissions';
28 import { AuthStorageService } from '../../../shared/services/auth-storage.service';
29 import { TaskWrapperService } from '../../../shared/services/task-wrapper.service';
30 import { Pool } from '../pool';
31 import { PoolModule } from '../pool.module';
32 import { PoolFormComponent } from './pool-form.component';
33
34 describe('PoolFormComponent', () => {
35 const OSDS = 8;
36 let formHelper: FormHelper;
37 let fixtureHelper: FixtureHelper;
38 let component: PoolFormComponent;
39 let fixture: ComponentFixture<PoolFormComponent>;
40 let poolService: PoolService;
41 let form: CdFormGroup;
42 let router: Router;
43 let ecpService: ErasureCodeProfileService;
44
45 const setPgNum = (pgs): AbstractControl => {
46 const control = formHelper.setValue('pgNum', pgs);
47 fixture.debugElement.query(By.css('#pgNum')).nativeElement.dispatchEvent(new Event('blur'));
48 return control;
49 };
50
51 const createCrushRule = ({
52 id = 0,
53 name = 'somePoolName',
54 min = 1,
55 max = 10,
56 type = 'replicated'
57 }: {
58 max?: number;
59 min?: number;
60 id?: number;
61 name?: string;
62 type?: string;
63 }) => {
64 const typeNumber = type === 'erasure' ? 3 : 1;
65 const rule = new CrushRule();
66 rule.max_size = max;
67 rule.min_size = min;
68 rule.rule_id = id;
69 rule.ruleset = typeNumber;
70 rule.rule_name = name;
71 rule.steps = [
72 {
73 item_name: 'default',
74 item: -1,
75 op: 'take'
76 },
77 {
78 num: 0,
79 type: 'osd',
80 op: 'choose_firstn'
81 },
82 {
83 op: 'emit'
84 }
85 ];
86 component.info['crush_rules_' + type].push(rule);
87 return rule;
88 };
89
90 const expectValidSubmit = (
91 pool: any,
92 taskName: string,
93 poolServiceMethod: 'create' | 'update'
94 ) => {
95 spyOn(poolService, poolServiceMethod).and.stub();
96 const taskWrapper = TestBed.get(TaskWrapperService);
97 spyOn(taskWrapper, 'wrapTaskAroundCall').and.callThrough();
98 component.submit();
99 expect(poolService[poolServiceMethod]).toHaveBeenCalledWith(pool);
100 expect(taskWrapper.wrapTaskAroundCall).toHaveBeenCalledWith({
101 task: {
102 name: taskName,
103 metadata: {
104 pool_name: pool.pool
105 }
106 },
107 call: undefined // because of stub
108 });
109 };
110
111 const setUpPoolComponent = () => {
112 fixture = TestBed.createComponent(PoolFormComponent);
113 fixtureHelper = new FixtureHelper(fixture);
114 component = fixture.componentInstance;
115 component.info = {
116 pool_names: [],
117 osd_count: OSDS,
118 is_all_bluestore: true,
119 bluestore_compression_algorithm: 'snappy',
120 compression_algorithms: ['snappy'],
121 compression_modes: ['none', 'passive'],
122 crush_rules_replicated: [],
123 crush_rules_erasure: []
124 };
125 const ecp1 = new ErasureCodeProfile();
126 ecp1.name = 'ecp1';
127 component.ecProfiles = [ecp1];
128 form = component.form;
129 formHelper = new FormHelper(form);
130 };
131
132 const routes: Routes = [{ path: '404', component: NotFoundComponent }];
133
134 configureTestBed({
135 declarations: [NotFoundComponent],
136 imports: [
137 HttpClientTestingModule,
138 RouterTestingModule.withRoutes(routes),
139 ToastrModule.forRoot(),
140 TabsModule.forRoot(),
141 PoolModule
142 ],
143 providers: [
144 ErasureCodeProfileService,
145 SelectBadgesComponent,
146 { provide: ActivatedRoute, useValue: { params: of({ name: 'somePoolName' }) } },
147 i18nProviders
148 ]
149 });
150
151 beforeEach(() => {
152 setUpPoolComponent();
153 poolService = TestBed.get(PoolService);
154 spyOn(poolService, 'getInfo').and.callFake(() => [component.info]);
155 ecpService = TestBed.get(ErasureCodeProfileService);
156 spyOn(ecpService, 'list').and.callFake(() => [component.ecProfiles]);
157 router = TestBed.get(Router);
158 spyOn(router, 'navigate').and.stub();
159 });
160
161 it('should create', () => {
162 expect(component).toBeTruthy();
163 });
164
165 describe('redirect not allowed users', () => {
166 let poolPermissions: Permission;
167 let authStorageService: AuthStorageService;
168
169 const testForRedirect = (times: number) => {
170 component.authenticate();
171 expect(router.navigate).toHaveBeenCalledTimes(times);
172 };
173
174 beforeEach(() => {
175 poolPermissions = {
176 create: false,
177 update: false,
178 read: false,
179 delete: false
180 };
181 authStorageService = TestBed.get(AuthStorageService);
182 spyOn(authStorageService, 'getPermissions').and.callFake(() => ({
183 pool: poolPermissions
184 }));
185 });
186
187 it('navigates to 404 if not allowed', () => {
188 component.authenticate();
189 expect(router.navigate).toHaveBeenCalledWith(['/404']);
190 });
191
192 it('navigates if user is not allowed', () => {
193 testForRedirect(1);
194 poolPermissions.read = true;
195 testForRedirect(2);
196 poolPermissions.delete = true;
197 testForRedirect(3);
198 poolPermissions.update = true;
199 testForRedirect(4);
200 component.editing = true;
201 poolPermissions.update = false;
202 poolPermissions.create = true;
203 testForRedirect(5);
204 });
205
206 it('does not navigate users with right permissions', () => {
207 poolPermissions.read = true;
208 poolPermissions.create = true;
209 testForRedirect(0);
210 component.editing = true;
211 poolPermissions.update = true;
212 testForRedirect(0);
213 poolPermissions.create = false;
214 testForRedirect(0);
215 });
216 });
217
218 describe('pool form validation', () => {
219 beforeEach(() => {
220 fixture.detectChanges();
221 });
222
223 it('is invalid at the beginning all sub forms are valid', () => {
224 expect(form.valid).toBeFalsy();
225 ['name', 'poolType', 'pgNum'].forEach((name) => formHelper.expectError(name, 'required'));
226 ['crushRule', 'size', 'erasureProfile', 'ecOverwrites'].forEach((name) =>
227 formHelper.expectValid(name)
228 );
229 expect(component.form.get('compression').valid).toBeTruthy();
230 });
231
232 it('validates name', () => {
233 expect(component.editing).toBeFalsy();
234 formHelper.expectError('name', 'required');
235 formHelper.expectValidChange('name', 'some-name');
236 formHelper.expectValidChange('name', 'name/with/slash');
237 component.info.pool_names.push('someExistingPoolName');
238 formHelper.expectErrorChange('name', 'someExistingPoolName', 'uniqueName');
239 formHelper.expectErrorChange('name', 'wrong format with spaces', 'pattern');
240 });
241
242 it('should validate with dots in pool name', () => {
243 formHelper.expectValidChange('name', 'pool.default.bar', true);
244 });
245
246 it('validates poolType', () => {
247 formHelper.expectError('poolType', 'required');
248 formHelper.expectValidChange('poolType', 'erasure');
249 formHelper.expectValidChange('poolType', 'replicated');
250 });
251
252 it('validates that pgNum is required creation mode', () => {
253 formHelper.expectError(form.get('pgNum'), 'required');
254 });
255
256 it('validates pgNum in edit mode', () => {
257 component.data.pool = new Pool('test');
258 component.data.pool.pg_num = 16;
259 component.editing = true;
260 component.ngOnInit(); // Switches form into edit mode
261 formHelper.setValue('poolType', 'erasure');
262 fixture.detectChanges();
263 formHelper.expectError(setPgNum('8'), 'noDecrease');
264 });
265
266 it('is valid if pgNum, poolType and name are valid', () => {
267 formHelper.setValue('name', 'some-name');
268 formHelper.setValue('poolType', 'erasure');
269 fixture.detectChanges();
270 setPgNum(1);
271 expect(form.valid).toBeTruthy();
272 });
273
274 it('validates crushRule', () => {
275 formHelper.expectValid('crushRule');
276 formHelper.expectErrorChange('crushRule', { min_size: 20 }, 'tooFewOsds');
277 });
278
279 it('validates size', () => {
280 formHelper.setValue('poolType', 'replicated');
281 formHelper.expectValid('size');
282 formHelper.setValue('crushRule', {
283 min_size: 2,
284 max_size: 6
285 });
286 formHelper.expectErrorChange('size', 1, 'min');
287 formHelper.expectErrorChange('size', 8, 'max');
288 formHelper.expectValidChange('size', 6);
289 });
290
291 it('validates compression mode default value', () => {
292 expect(form.getValue('mode')).toBe('none');
293 });
294
295 describe('compression form', () => {
296 beforeEach(() => {
297 formHelper.setValue('poolType', 'replicated');
298 formHelper.setValue('mode', 'passive');
299 });
300
301 it('is valid', () => {
302 expect(component.form.get('compression').valid).toBeTruthy();
303 });
304
305 it('validates minBlobSize to be only valid between 0 and maxBlobSize', () => {
306 formHelper.expectErrorChange('minBlobSize', -1, 'min');
307 formHelper.expectValidChange('minBlobSize', 0);
308 formHelper.setValue('maxBlobSize', '2 KiB');
309 formHelper.expectErrorChange('minBlobSize', '3 KiB', 'maximum');
310 formHelper.expectValidChange('minBlobSize', '1.9 KiB');
311 });
312
313 it('validates minBlobSize converts numbers', () => {
314 const control = formHelper.setValue('minBlobSize', '1');
315 fixture.detectChanges();
316 formHelper.expectValid(control);
317 expect(control.value).toBe('1 KiB');
318 });
319
320 it('validates maxBlobSize to be only valid bigger than minBlobSize', () => {
321 formHelper.expectErrorChange('maxBlobSize', -1, 'min');
322 formHelper.setValue('minBlobSize', '1 KiB');
323 formHelper.expectErrorChange('maxBlobSize', '0.5 KiB', 'minimum');
324 formHelper.expectValidChange('maxBlobSize', '1.5 KiB');
325 });
326
327 it('s valid to only use one blob size', () => {
328 formHelper.expectValid(formHelper.setValue('minBlobSize', '1 KiB'));
329 formHelper.expectValid(formHelper.setValue('maxBlobSize', ''));
330 formHelper.expectValid(formHelper.setValue('minBlobSize', ''));
331 formHelper.expectValid(formHelper.setValue('maxBlobSize', '1 KiB'));
332 });
333
334 it('dismisses any size error if one of the blob sizes is changed into a valid state', () => {
335 const min = formHelper.setValue('minBlobSize', '10 KiB');
336 const max = formHelper.setValue('maxBlobSize', '1 KiB');
337 fixture.detectChanges();
338 max.setValue('');
339 formHelper.expectValid(min);
340 formHelper.expectValid(max);
341 max.setValue('1 KiB');
342 fixture.detectChanges();
343 min.setValue('0.5 KiB');
344 formHelper.expectValid(min);
345 formHelper.expectValid(max);
346 });
347
348 it('validates maxBlobSize converts numbers', () => {
349 const control = formHelper.setValue('maxBlobSize', '2');
350 fixture.detectChanges();
351 expect(control.value).toBe('2 KiB');
352 });
353
354 it('validates that odd size validator works as expected', () => {
355 const odd = (min, max) => component['oddBlobSize'](min, max);
356 expect(odd('10', '8')).toBe(true);
357 expect(odd('8', '-')).toBe(false);
358 expect(odd('8', '10')).toBe(false);
359 expect(odd(null, '8')).toBe(false);
360 expect(odd('10', '')).toBe(false);
361 expect(odd('10', null)).toBe(false);
362 expect(odd(null, null)).toBe(false);
363 });
364
365 it('validates ratio to be only valid between 0 and 1', () => {
366 formHelper.expectValid('ratio');
367 formHelper.expectErrorChange('ratio', -0.1, 'min');
368 formHelper.expectValidChange('ratio', 0);
369 formHelper.expectValidChange('ratio', 1);
370 formHelper.expectErrorChange('ratio', 1.1, 'max');
371 });
372 });
373
374 it('validates application metadata name', () => {
375 formHelper.setValue('poolType', 'replicated');
376 fixture.detectChanges();
377 const selectBadges = fixture.debugElement.query(By.directive(SelectBadgesComponent))
378 .componentInstance;
379 const control = selectBadges.cdSelect.filter;
380 formHelper.expectValid(control);
381 control.setValue('?');
382 formHelper.expectError(control, 'pattern');
383 control.setValue('Ab3_');
384 formHelper.expectValid(control);
385 control.setValue('a'.repeat(129));
386 formHelper.expectError(control, 'maxlength');
387 });
388 });
389
390 describe('pool type changes', () => {
391 beforeEach(() => {
392 component.ngOnInit();
393 createCrushRule({ id: 3, min: 1, max: 1, name: 'ep1', type: 'erasure' });
394 createCrushRule({ id: 0, min: 2, max: 4, name: 'rep1', type: 'replicated' });
395 createCrushRule({ id: 1, min: 3, max: 18, name: 'rep2', type: 'replicated' });
396 });
397
398 it('should have a default replicated size of 3', () => {
399 formHelper.setValue('poolType', 'replicated');
400 expect(form.getValue('size')).toBe(3);
401 });
402
403 describe('replicatedRuleChange', () => {
404 beforeEach(() => {
405 formHelper.setValue('poolType', 'replicated');
406 formHelper.setValue('size', 99);
407 });
408
409 it('should not set size if a replicated pool is not set', () => {
410 formHelper.setValue('poolType', 'erasure');
411 expect(form.getValue('size')).toBe(99);
412 formHelper.setValue('crushRule', component.info.crush_rules_replicated[1]);
413 expect(form.getValue('size')).toBe(99);
414 });
415
416 it('should set size to maximum if size exceeds maximum', () => {
417 formHelper.setValue('crushRule', component.info.crush_rules_replicated[0]);
418 expect(form.getValue('size')).toBe(4);
419 });
420
421 it('should set size to minimum if size is lower than minimum', () => {
422 formHelper.setValue('size', -1);
423 formHelper.setValue('crushRule', component.info.crush_rules_replicated[0]);
424 expect(form.getValue('size')).toBe(2);
425 });
426 });
427
428 describe('rulesChange', () => {
429 it('has no effect if info is not there', () => {
430 delete component.info;
431 formHelper.setValue('poolType', 'replicated');
432 expect(component.current.rules).toEqual([]);
433 });
434
435 it('has no effect if pool type is not set', () => {
436 component['rulesChange']();
437 expect(component.current.rules).toEqual([]);
438 });
439
440 it('shows all replicated rules when pool type is "replicated"', () => {
441 formHelper.setValue('poolType', 'replicated');
442 expect(component.current.rules).toEqual(component.info.crush_rules_replicated);
443 expect(component.current.rules.length).toBe(2);
444 });
445
446 it('shows all erasure code rules when pool type is "erasure"', () => {
447 formHelper.setValue('poolType', 'erasure');
448 expect(component.current.rules).toEqual(component.info.crush_rules_erasure);
449 expect(component.current.rules.length).toBe(1);
450 });
451
452 it('disables rule field if only one rule exists which is used in the disabled field', () => {
453 formHelper.setValue('poolType', 'erasure');
454 const control = form.get('crushRule');
455 expect(control.value).toEqual(component.info.crush_rules_erasure[0]);
456 expect(control.disabled).toBe(true);
457 });
458
459 it('does not select the first rule if more than one exist', () => {
460 formHelper.setValue('poolType', 'replicated');
461 const control = form.get('crushRule');
462 expect(control.value).toEqual(null);
463 expect(control.disabled).toBe(false);
464 });
465
466 it('changing between both types will not leave crushRule in a bad state', () => {
467 formHelper.setValue('poolType', 'erasure');
468 formHelper.setValue('poolType', 'replicated');
469 const control = form.get('crushRule');
470 expect(control.value).toEqual(null);
471 expect(control.disabled).toBe(false);
472 formHelper.setValue('poolType', 'erasure');
473 expect(control.value).toEqual(component.info.crush_rules_erasure[0]);
474 expect(control.disabled).toBe(true);
475 });
476 });
477 });
478
479 describe('getMaxSize and getMinSize', () => {
480 const setCrushRule = ({ min, max }: { min?: number; max?: number }) => {
481 formHelper.setValue('crushRule', {
482 min_size: min,
483 max_size: max
484 });
485 };
486
487 it('returns nothing if osd count is 0', () => {
488 component.info.osd_count = 0;
489 expect(component.getMinSize()).toBe(undefined);
490 expect(component.getMaxSize()).toBe(undefined);
491 });
492
493 it('returns nothing if info is not there', () => {
494 delete component.info;
495 expect(component.getMinSize()).toBe(undefined);
496 expect(component.getMaxSize()).toBe(undefined);
497 });
498
499 it('returns minimum and maximum of rule', () => {
500 setCrushRule({ min: 2, max: 6 });
501 expect(component.getMinSize()).toBe(2);
502 expect(component.getMaxSize()).toBe(6);
503 });
504
505 it('returns 1 as minimum and the osd count as maximum if no crush rule is available', () => {
506 expect(component.getMinSize()).toBe(1);
507 expect(component.getMaxSize()).toBe(OSDS);
508 });
509
510 it('returns the osd count as maximum if the rule maximum exceeds it', () => {
511 setCrushRule({ max: 100 });
512 expect(component.getMaxSize()).toBe(OSDS);
513 });
514
515 it('should return the osd count as minimum if its lower the the rule minimum', () => {
516 setCrushRule({ min: 10 });
517 expect(component.getMinSize()).toBe(10);
518 const control = form.get('crushRule');
519 expect(control.invalid).toBe(true);
520 formHelper.expectError(control, 'tooFewOsds');
521 });
522 });
523
524 describe('application metadata', () => {
525 let selectBadges: SelectBadgesComponent;
526
527 const testAddApp = (app?: string, result?: string[]) => {
528 selectBadges.cdSelect.filter.setValue(app);
529 selectBadges.cdSelect.updateFilter();
530 selectBadges.cdSelect.selectOption();
531 expect(component.data.applications.selected).toEqual(result);
532 };
533
534 const testRemoveApp = (app: string, result: string[]) => {
535 selectBadges.cdSelect.removeItem(app);
536 expect(component.data.applications.selected).toEqual(result);
537 };
538
539 const setCurrentApps = (apps: string[]) => {
540 component.data.applications.selected = apps;
541 fixture.detectChanges();
542 selectBadges.cdSelect.ngOnInit();
543 return apps;
544 };
545
546 beforeEach(() => {
547 formHelper.setValue('poolType', 'replicated');
548 fixture.detectChanges();
549 selectBadges = fixture.debugElement.query(By.directive(SelectBadgesComponent))
550 .componentInstance;
551 });
552
553 it('adds all predefined and a custom applications to the application metadata array', () => {
554 testAddApp('g', ['rgw']);
555 testAddApp('b', ['rbd', 'rgw']);
556 testAddApp('c', ['cephfs', 'rbd', 'rgw']);
557 testAddApp('something', ['cephfs', 'rbd', 'rgw', 'something']);
558 });
559
560 it('only allows 4 apps to be added to the array', () => {
561 const apps = setCurrentApps(['d', 'c', 'b', 'a']);
562 testAddApp('e', apps);
563 });
564
565 it('can remove apps', () => {
566 setCurrentApps(['a', 'b', 'c', 'd']);
567 testRemoveApp('c', ['a', 'b', 'd']);
568 testRemoveApp('a', ['b', 'd']);
569 testRemoveApp('d', ['b']);
570 testRemoveApp('b', []);
571 });
572
573 it('does not remove any app that is not in the array', () => {
574 const apps = ['a', 'b', 'c', 'd'];
575 setCurrentApps(apps);
576 testRemoveApp('e', apps);
577 testRemoveApp('0', apps);
578 });
579 });
580
581 describe('pg number changes', () => {
582 const setPgs = (pgs) => {
583 formHelper.setValue('pgNum', pgs);
584 fixture.debugElement.query(By.css('#pgNum')).nativeElement.dispatchEvent(new Event('blur'));
585 };
586
587 const testPgUpdate = (pgs, jump, returnValue) => {
588 if (pgs) {
589 setPgs(pgs);
590 }
591 if (jump) {
592 setPgs(form.getValue('pgNum') + jump);
593 }
594 expect(form.getValue('pgNum')).toBe(returnValue);
595 };
596
597 beforeEach(() => {
598 formHelper.setValue('crushRule', {
599 min_size: 1,
600 max_size: 20
601 });
602 formHelper.setValue('poolType', 'erasure');
603 fixture.detectChanges();
604 setPgNum(256);
605 });
606
607 it('updates by value', () => {
608 testPgUpdate(10, undefined, 8);
609 testPgUpdate(22, undefined, 16);
610 testPgUpdate(26, undefined, 32);
611 testPgUpdate(200, undefined, 256);
612 testPgUpdate(300, undefined, 256);
613 testPgUpdate(350, undefined, 256);
614 });
615
616 it('updates by jump -> a magnitude of the power of 2', () => {
617 testPgUpdate(undefined, 1, 512);
618 testPgUpdate(undefined, -1, 256);
619 });
620
621 it('returns 1 as minimum for false numbers', () => {
622 testPgUpdate(-26, undefined, 1);
623 testPgUpdate(0, undefined, 1);
624 testPgUpdate(0, -1, 1);
625 testPgUpdate(undefined, -20, 1);
626 });
627
628 it('changes the value and than jumps', () => {
629 testPgUpdate(230, 1, 512);
630 testPgUpdate(3500, -1, 2048);
631 });
632
633 describe('pg power jump', () => {
634 it('should jump correctly at the beginning', () => {
635 testPgUpdate(1, -1, 1);
636 testPgUpdate(1, 1, 2);
637 testPgUpdate(2, -1, 1);
638 testPgUpdate(2, 1, 4);
639 testPgUpdate(4, -1, 2);
640 testPgUpdate(4, 1, 8);
641 testPgUpdate(4, 1, 8);
642 });
643
644 it('increments pg power if difference to the current number is 1', () => {
645 testPgUpdate(undefined, 1, 512);
646 testPgUpdate(undefined, 1, 1024);
647 testPgUpdate(undefined, 1, 2048);
648 testPgUpdate(undefined, 1, 4096);
649 });
650
651 it('decrements pg power if difference to the current number is -1', () => {
652 testPgUpdate(undefined, -1, 128);
653 testPgUpdate(undefined, -1, 64);
654 testPgUpdate(undefined, -1, 32);
655 testPgUpdate(undefined, -1, 16);
656 testPgUpdate(undefined, -1, 8);
657 });
658 });
659
660 describe('pgCalc', () => {
661 const PGS = 1;
662
663 const getValidCase = () => ({
664 type: 'replicated',
665 osds: OSDS,
666 size: 4,
667 ecp: {
668 k: 2,
669 m: 2
670 },
671 expected: 256
672 });
673
674 const testPgCalc = ({ type, osds, size, ecp, expected }) => {
675 component.info.osd_count = osds;
676 formHelper.setValue('poolType', type);
677 if (type === 'replicated') {
678 formHelper.setValue('size', size);
679 } else {
680 formHelper.setValue('erasureProfile', ecp);
681 }
682 expect(form.getValue('pgNum')).toBe(expected);
683 expect(component.externalPgChange).toBe(PGS !== expected);
684 };
685
686 beforeEach(() => {
687 setPgNum(PGS);
688 });
689
690 it('does not change anything if type is not valid', () => {
691 const test = getValidCase();
692 test.type = '';
693 test.expected = PGS;
694 testPgCalc(test);
695 });
696
697 it('does not change anything if ecp is not valid', () => {
698 const test = getValidCase();
699 test.expected = PGS;
700 test.type = 'erasure';
701 test.ecp = null;
702 testPgCalc(test);
703 });
704
705 it('calculates some replicated values', () => {
706 const test = getValidCase();
707 testPgCalc(test);
708 test.osds = 16;
709 test.expected = 512;
710 testPgCalc(test);
711 test.osds = 8;
712 test.size = 8;
713 test.expected = 128;
714 testPgCalc(test);
715 });
716
717 it('calculates erasure code values even if selection is disabled', () => {
718 component['initEcp']([{ k: 2, m: 2, name: 'bla', plugin: '', technique: '' }]);
719 const test = getValidCase();
720 test.type = 'erasure';
721 testPgCalc(test);
722 expect(form.get('erasureProfile').disabled).toBeTruthy();
723 });
724
725 it('calculates some erasure code values', () => {
726 const test = getValidCase();
727 test.type = 'erasure';
728 testPgCalc(test);
729 test.osds = 16;
730 test.ecp.m = 5;
731 test.expected = 256;
732 testPgCalc(test);
733 test.ecp.k = 5;
734 test.expected = 128;
735 testPgCalc(test);
736 });
737
738 it('should not change a manual set pg number', () => {
739 form.get('pgNum').markAsDirty();
740 const test = getValidCase();
741 test.expected = PGS;
742 testPgCalc(test);
743 });
744 });
745 });
746
747 describe('crushRule', () => {
748 beforeEach(() => {
749 createCrushRule({ name: 'replicatedRule' });
750 fixture.detectChanges();
751 formHelper.setValue('poolType', 'replicated');
752 fixture.detectChanges();
753 });
754
755 it('should not show info per default', () => {
756 fixtureHelper.expectElementVisible('#crushRule', true);
757 fixtureHelper.expectElementVisible('#crush-info-block', false);
758 });
759
760 it('should show info if the info button is clicked', () => {
761 fixture.detectChanges();
762 const infoButton = fixture.debugElement.query(By.css('#crush-info-button'));
763 infoButton.triggerEventHandler('click', null);
764 expect(component.data.crushInfo).toBeTruthy();
765 fixture.detectChanges();
766 expect(infoButton.classes['active']).toBeTruthy();
767 fixtureHelper.expectIdElementsVisible(['crushRule', 'crush-info-block'], true);
768 });
769 });
770
771 describe('erasure code profile', () => {
772 const setSelectedEcp = (name: string) => {
773 formHelper.setValue('erasureProfile', { name: name });
774 };
775
776 beforeEach(() => {
777 formHelper.setValue('poolType', 'erasure');
778 fixture.detectChanges();
779 });
780
781 it('should not show info per default', () => {
782 fixtureHelper.expectElementVisible('#erasureProfile', true);
783 fixtureHelper.expectElementVisible('#ecp-info-block', false);
784 });
785
786 it('should show info if the info button is clicked', () => {
787 const infoButton = fixture.debugElement.query(By.css('#ecp-info-button'));
788 infoButton.triggerEventHandler('click', null);
789 expect(component.data.erasureInfo).toBeTruthy();
790 fixture.detectChanges();
791 expect(infoButton.classes['active']).toBeTruthy();
792 fixtureHelper.expectIdElementsVisible(['erasureProfile', 'ecp-info-block'], true);
793 });
794
795 describe('ecp deletion', () => {
796 let taskWrapper: TaskWrapperService;
797 let deletion: CriticalConfirmationModalComponent;
798
799 const callDeletion = () => {
800 component.deleteErasureCodeProfile();
801 deletion.submitActionObservable();
802 };
803
804 const testPoolDeletion = (name) => {
805 setSelectedEcp(name);
806 callDeletion();
807 expect(ecpService.delete).toHaveBeenCalledWith(name);
808 expect(taskWrapper.wrapTaskAroundCall).toHaveBeenCalledWith({
809 task: {
810 name: 'ecp/delete',
811 metadata: {
812 name: name
813 }
814 },
815 call: undefined // because of stub
816 });
817 };
818
819 beforeEach(() => {
820 spyOn(TestBed.get(BsModalService), 'show').and.callFake((deletionClass, config) => {
821 deletion = Object.assign(new deletionClass(), config.initialState);
822 return {
823 content: deletion
824 };
825 });
826 spyOn(ecpService, 'delete').and.stub();
827 taskWrapper = TestBed.get(TaskWrapperService);
828 spyOn(taskWrapper, 'wrapTaskAroundCall').and.callThrough();
829 });
830
831 it('should delete two different erasure code profiles', () => {
832 testPoolDeletion('someEcpName');
833 testPoolDeletion('aDifferentEcpName');
834 });
835 });
836 });
837
838 describe('submit - create', () => {
839 const setMultipleValues = (settings: {}) => {
840 Object.keys(settings).forEach((name) => {
841 formHelper.setValue(name, settings[name]);
842 });
843 };
844 const testCreate = (pool) => {
845 expectValidSubmit(pool, 'pool/create', 'create');
846 };
847
848 beforeEach(() => {
849 createCrushRule({ name: 'replicatedRule' });
850 createCrushRule({ name: 'erasureRule', type: 'erasure', id: 1 });
851 });
852
853 describe('erasure coded pool', () => {
854 it('minimum requirements', () => {
855 setMultipleValues({
856 name: 'minECPool',
857 poolType: 'erasure',
858 pgNum: 4
859 });
860 testCreate({
861 pool: 'minECPool',
862 pool_type: 'erasure',
863 pg_num: 4
864 });
865 });
866
867 it('with erasure coded profile', () => {
868 const ecp = { name: 'ecpMinimalMock' };
869 setMultipleValues({
870 name: 'ecpPool',
871 poolType: 'erasure',
872 pgNum: 16,
873 size: 2, // Will be ignored
874 erasureProfile: ecp
875 });
876 testCreate({
877 pool: 'ecpPool',
878 pool_type: 'erasure',
879 pg_num: 16,
880 erasure_code_profile: ecp.name
881 });
882 });
883
884 it('with ec_overwrite flag', () => {
885 setMultipleValues({
886 name: 'ecOverwrites',
887 poolType: 'erasure',
888 pgNum: 32,
889 ecOverwrites: true
890 });
891 testCreate({
892 pool: 'ecOverwrites',
893 pool_type: 'erasure',
894 pg_num: 32,
895 flags: ['ec_overwrites']
896 });
897 });
898
899 it('with rbd qos settings', () => {
900 setMultipleValues({
901 name: 'replicatedRbdQos',
902 poolType: 'replicated',
903 size: 2,
904 pgNum: 32
905 });
906 component.currentConfigurationValues = {
907 rbd_qos_bps_limit: 55
908 };
909 testCreate({
910 pool: 'replicatedRbdQos',
911 pool_type: 'replicated',
912 size: 2,
913 pg_num: 32,
914 configuration: {
915 rbd_qos_bps_limit: 55
916 }
917 });
918 });
919 });
920
921 describe('replicated coded pool', () => {
922 it('minimum requirements', () => {
923 const ecp = { name: 'ecpMinimalMock' };
924 setMultipleValues({
925 name: 'minRepPool',
926 poolType: 'replicated',
927 size: 2,
928 erasureProfile: ecp, // Will be ignored
929 pgNum: 8
930 });
931 testCreate({
932 pool: 'minRepPool',
933 pool_type: 'replicated',
934 pg_num: 8,
935 size: 2
936 });
937 });
938 });
939
940 it('pool with compression', () => {
941 setMultipleValues({
942 name: 'compression',
943 poolType: 'erasure',
944 pgNum: 64,
945 mode: 'passive',
946 algorithm: 'lz4',
947 minBlobSize: '4 K',
948 maxBlobSize: '4 M',
949 ratio: 0.7
950 });
951 testCreate({
952 pool: 'compression',
953 pool_type: 'erasure',
954 pg_num: 64,
955 compression_mode: 'passive',
956 compression_algorithm: 'lz4',
957 compression_min_blob_size: 4096,
958 compression_max_blob_size: 4194304,
959 compression_required_ratio: 0.7
960 });
961 });
962
963 it('pool with application metadata', () => {
964 setMultipleValues({
965 name: 'apps',
966 poolType: 'erasure',
967 pgNum: 128
968 });
969 component.data.applications.selected = ['cephfs', 'rgw'];
970 testCreate({
971 pool: 'apps',
972 pool_type: 'erasure',
973 pg_num: 128,
974 application_metadata: ['cephfs', 'rgw']
975 });
976 });
977 });
978
979 describe('edit mode', () => {
980 const setUrl = (url) => {
981 Object.defineProperty(router, 'url', { value: url });
982 setUpPoolComponent(); // Renew of component needed because the constructor has to be called
983 };
984
985 let pool: Pool;
986 beforeEach(() => {
987 pool = new Pool('somePoolName');
988 pool.type = 'replicated';
989 pool.size = 3;
990 pool.crush_rule = 'someRule';
991 pool.pg_num = 32;
992 pool.options = {};
993 pool.options.compression_mode = 'passive';
994 pool.options.compression_algorithm = 'lz4';
995 pool.options.compression_min_blob_size = 1024 * 512;
996 pool.options.compression_max_blob_size = 1024 * 1024;
997 pool.options.compression_required_ratio = 0.8;
998 pool.flags_names = 'someFlag1,someFlag2';
999 pool.application_metadata = ['rbd', 'rgw'];
1000
1001 createCrushRule({ name: 'someRule' });
1002 spyOn(poolService, 'get').and.callFake(() => of(pool));
1003 });
1004
1005 it('is not in edit mode if edit is not included in url', () => {
1006 setUrl('/pool/add');
1007 expect(component.editing).toBeFalsy();
1008 });
1009
1010 it('is in edit mode if edit is included in url', () => {
1011 setUrl('/pool/edit/somePoolName');
1012 expect(component.editing).toBeTruthy();
1013 });
1014
1015 describe('after ngOnInit', () => {
1016 beforeEach(() => {
1017 component.editing = true;
1018 fixture.detectChanges();
1019 });
1020
1021 it('disabled inputs', () => {
1022 const disabled = ['poolType', 'crushRule', 'size', 'erasureProfile', 'ecOverwrites'];
1023 disabled.forEach((controlName) => {
1024 return expect(form.get(controlName).disabled).toBeTruthy();
1025 });
1026 const enabled = [
1027 'name',
1028 'pgNum',
1029 'mode',
1030 'algorithm',
1031 'minBlobSize',
1032 'maxBlobSize',
1033 'ratio'
1034 ];
1035 enabled.forEach((controlName) => {
1036 return expect(form.get(controlName).enabled).toBeTruthy();
1037 });
1038 });
1039
1040 it('set all control values to the given pool', () => {
1041 expect(form.getValue('name')).toBe(pool.pool_name);
1042 expect(form.getValue('poolType')).toBe(pool.type);
1043 expect(form.getValue('crushRule')).toEqual(component.info.crush_rules_replicated[0]);
1044 expect(form.getValue('size')).toBe(pool.size);
1045 expect(form.getValue('pgNum')).toBe(pool.pg_num);
1046 expect(form.getValue('mode')).toBe(pool.options.compression_mode);
1047 expect(form.getValue('algorithm')).toBe(pool.options.compression_algorithm);
1048 expect(form.getValue('minBlobSize')).toBe('512 KiB');
1049 expect(form.getValue('maxBlobSize')).toBe('1 MiB');
1050 expect(form.getValue('ratio')).toBe(pool.options.compression_required_ratio);
1051 });
1052
1053 it('is only be possible to use the same or more pgs like before', () => {
1054 formHelper.expectValid(setPgNum(64));
1055 formHelper.expectError(setPgNum(4), 'noDecrease');
1056 });
1057
1058 describe('submit', () => {
1059 const markControlAsPreviouslySet = (controlName) => form.get(controlName).markAsPristine();
1060
1061 beforeEach(() => {
1062 ['algorithm', 'maxBlobSize', 'minBlobSize', 'mode', 'pgNum', 'ratio', 'name'].forEach(
1063 (name) => markControlAsPreviouslySet(name)
1064 );
1065 fixture.detectChanges();
1066 });
1067
1068 it(`always provides the application metadata array with submit even if it's empty`, () => {
1069 expect(form.get('mode').dirty).toBe(false);
1070 component.data.applications.selected = [];
1071 expectValidSubmit(
1072 {
1073 application_metadata: [],
1074 pool: 'somePoolName'
1075 },
1076 'pool/edit',
1077 'update'
1078 );
1079 });
1080
1081 it(`will always provide reset value for compression options`, () => {
1082 formHelper.setValue('minBlobSize', '').markAsDirty();
1083 formHelper.setValue('maxBlobSize', '').markAsDirty();
1084 formHelper.setValue('ratio', '').markAsDirty();
1085 expectValidSubmit(
1086 {
1087 application_metadata: ['rbd', 'rgw'],
1088 compression_max_blob_size: 0,
1089 compression_min_blob_size: 0,
1090 compression_required_ratio: 0,
1091 pool: 'somePoolName'
1092 },
1093 'pool/edit',
1094 'update'
1095 );
1096 });
1097
1098 it(`will unset mode not used anymore`, () => {
1099 formHelper.setValue('mode', 'none').markAsDirty();
1100 expectValidSubmit(
1101 {
1102 application_metadata: ['rbd', 'rgw'],
1103 compression_mode: 'unset',
1104 pool: 'somePoolName'
1105 },
1106 'pool/edit',
1107 'update'
1108 );
1109 });
1110 });
1111 });
1112 });
1113
1114 describe('test pool configuration component', () => {
1115 it('is visible for replicated pools with rbd application', () => {
1116 const poolType = component.form.get('poolType');
1117 poolType.markAsDirty();
1118 poolType.setValue('replicated');
1119 component.data.applications.selected = ['rbd'];
1120 fixture.detectChanges();
1121 expect(
1122 fixture.debugElement.query(By.css('cd-rbd-configuration-form')).nativeElement.parentElement
1123 .hidden
1124 ).toBe(false);
1125 });
1126
1127 it('is invisible for erasure coded pools', () => {
1128 const poolType = component.form.get('poolType');
1129 poolType.markAsDirty();
1130 poolType.setValue('erasure');
1131 fixture.detectChanges();
1132 expect(
1133 fixture.debugElement.query(By.css('cd-rbd-configuration-form')).nativeElement.parentElement
1134 .hidden
1135 ).toBe(true);
1136 });
1137 });
1138 });