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';
8 import * as _ from 'lodash';
9 import { NgBootstrapFormValidationModule } from 'ng-bootstrap-form-validation';
10 import { BsModalService } from 'ngx-bootstrap/modal';
11 import { TabsetComponent, TabsModule } from 'ngx-bootstrap/tabs';
12 import { ToastrModule } from 'ngx-toastr';
13 import { of } from 'rxjs';
20 } from '../../../../testing/unit-test-helper';
21 import { NotFoundComponent } from '../../../core/not-found/not-found.component';
22 import { CrushRuleService } from '../../../shared/api/crush-rule.service';
23 import { ErasureCodeProfileService } from '../../../shared/api/erasure-code-profile.service';
24 import { PoolService } from '../../../shared/api/pool.service';
25 import { CriticalConfirmationModalComponent } from '../../../shared/components/critical-confirmation-modal/critical-confirmation-modal.component';
26 import { SelectBadgesComponent } from '../../../shared/components/select-badges/select-badges.component';
27 import { CdFormGroup } from '../../../shared/forms/cd-form-group';
28 import { CrushRule } from '../../../shared/models/crush-rule';
29 import { ErasureCodeProfile } from '../../../shared/models/erasure-code-profile';
30 import { Permission } from '../../../shared/models/permissions';
31 import { PoolFormInfo } from '../../../shared/models/pool-form-info';
32 import { AuthStorageService } from '../../../shared/services/auth-storage.service';
33 import { TaskWrapperService } from '../../../shared/services/task-wrapper.service';
34 import { Pool } from '../pool';
35 import { PoolModule } from '../pool.module';
36 import { PoolFormComponent } from './pool-form.component';
38 describe('PoolFormComponent', () => {
40 let formHelper: FormHelper;
41 let fixtureHelper: FixtureHelper;
42 let component: PoolFormComponent;
43 let fixture: ComponentFixture<PoolFormComponent>;
44 let poolService: PoolService;
45 let form: CdFormGroup;
47 let ecpService: ErasureCodeProfileService;
48 let crushRuleService: CrushRuleService;
50 const setPgNum = (pgs: number): AbstractControl => {
51 const control = formHelper.setValue('pgNum', pgs);
52 fixture.debugElement.query(By.css('#pgNum')).nativeElement.dispatchEvent(new Event('blur'));
56 const testPgUpdate = (pgs: number, jump: number, returnValue: number) => {
61 setPgNum(form.getValue('pgNum') + jump);
63 expect(form.getValue('pgNum')).toBe(returnValue);
66 const createCrushRule = ({
68 name = 'somePoolName',
79 const typeNumber = type === 'erasure' ? 3 : 1;
80 const rule = new CrushRule();
84 rule.ruleset = typeNumber;
85 rule.rule_name = name;
104 const expectValidSubmit = (
106 taskName = 'pool/create',
107 poolServiceMethod: 'create' | 'update' = 'create'
109 spyOn(poolService, poolServiceMethod).and.stub();
110 const taskWrapper = TestBed.get(TaskWrapperService);
111 spyOn(taskWrapper, 'wrapTaskAroundCall').and.callThrough();
113 expect(poolService[poolServiceMethod]).toHaveBeenCalledWith(pool);
114 expect(taskWrapper.wrapTaskAroundCall).toHaveBeenCalledWith({
121 call: undefined // because of stub
125 let infoReturn: PoolFormInfo;
126 const setInfo = () => {
127 const ecp1 = new ErasureCodeProfile();
130 pool_names: ['someExistingPoolName'],
132 is_all_bluestore: true,
133 bluestore_compression_algorithm: 'snappy',
134 compression_algorithms: ['snappy'],
135 compression_modes: ['none', 'passive'],
136 crush_rules_replicated: [
137 createCrushRule({ id: 0, min: 2, max: 4, name: 'rep1', type: 'replicated' }),
138 createCrushRule({ id: 1, min: 3, max: 18, name: 'rep2', type: 'replicated' }),
139 createCrushRule({ id: 2, min: 1, max: 9, name: 'used_rule', type: 'replicated' })
141 crush_rules_erasure: [
142 createCrushRule({ id: 3, min: 1, max: 1, name: 'ecp1', type: 'erasure' })
144 erasure_code_profiles: [ecp1],
145 pg_autoscale_default_mode: 'off',
146 pg_autoscale_modes: ['off', 'warn', 'on'],
148 used_rule: ['some.pool.uses.it']
153 const setUpPoolComponent = () => {
154 fixture = TestBed.createComponent(PoolFormComponent);
155 component = fixture.componentInstance;
156 fixture.detectChanges();
158 fixtureHelper = new FixtureHelper(fixture);
159 form = component.form;
160 formHelper = new FormHelper(form);
163 const routes: Routes = [{ path: '404', component: NotFoundComponent }];
166 declarations: [NotFoundComponent],
168 HttpClientTestingModule,
169 RouterTestingModule.withRoutes(routes),
170 ToastrModule.forRoot(),
171 TabsModule.forRoot(),
173 NgBootstrapFormValidationModule.forRoot()
176 ErasureCodeProfileService,
177 SelectBadgesComponent,
178 { provide: ActivatedRoute, useValue: { params: of({ name: 'somePoolName' }) } },
183 let navigationSpy: jasmine.Spy;
186 poolService = TestBed.get(PoolService);
188 spyOn(poolService, 'getInfo').and.callFake(() => of(infoReturn));
190 ecpService = TestBed.get(ErasureCodeProfileService);
191 crushRuleService = TestBed.get(CrushRuleService);
193 router = TestBed.get(Router);
194 navigationSpy = spyOn(router, 'navigate').and.stub();
196 setUpPoolComponent();
199 it('should create', () => {
200 expect(component).toBeTruthy();
203 describe('redirect not allowed users', () => {
204 let poolPermissions: Permission;
205 let authStorageService: AuthStorageService;
207 const expectRedirect = (redirected = true) => {
208 navigationSpy.calls.reset();
209 component.authenticate();
210 expect(navigationSpy).toHaveBeenCalledTimes(redirected ? 1 : 0);
220 authStorageService = TestBed.get(AuthStorageService);
221 spyOn(authStorageService, 'getPermissions').and.callFake(() => ({
222 pool: poolPermissions
226 it('navigates to 404 if not allowed', () => {
227 component.authenticate();
228 expect(router.navigate).toHaveBeenCalledWith(['/404']);
231 it('navigates if user is not allowed', () => {
233 poolPermissions.read = true;
235 poolPermissions.delete = true;
237 poolPermissions.update = true;
239 component.editing = true;
240 poolPermissions.update = false;
241 poolPermissions.create = true;
245 it('does not navigate users with right permissions', () => {
246 poolPermissions.read = true;
247 poolPermissions.create = true;
248 expectRedirect(false);
249 component.editing = true;
250 poolPermissions.update = true;
251 expectRedirect(false);
252 poolPermissions.create = false;
253 expectRedirect(false);
257 describe('pool form validation', () => {
259 fixture.detectChanges();
262 it('is invalid at the beginning all sub forms are valid', () => {
263 expect(form.valid).toBeFalsy();
264 ['name', 'poolType', 'pgNum'].forEach((name) => formHelper.expectError(name, 'required'));
265 ['size', 'crushRule', 'erasureProfile', 'ecOverwrites'].forEach((name) =>
266 formHelper.expectValid(name)
268 expect(component.form.get('compression').valid).toBeTruthy();
271 it('validates name', () => {
272 expect(component.editing).toBeFalsy();
273 formHelper.expectError('name', 'required');
274 formHelper.expectValidChange('name', 'some-name');
275 formHelper.expectValidChange('name', 'name/with/slash');
276 formHelper.expectErrorChange('name', 'someExistingPoolName', 'uniqueName');
277 formHelper.expectErrorChange('name', 'wrong format with spaces', 'pattern');
280 it('should validate with dots in pool name', () => {
281 formHelper.expectValidChange('name', 'pool.default.bar', true);
284 it('validates poolType', () => {
285 formHelper.expectError('poolType', 'required');
286 formHelper.expectValidChange('poolType', 'erasure');
287 formHelper.expectValidChange('poolType', 'replicated');
290 it('validates that pgNum is required creation mode', () => {
291 formHelper.expectError(form.get('pgNum'), 'required');
294 it('validates pgNum in edit mode', () => {
295 component.data.pool = new Pool('test');
296 component.data.pool.pg_num = 16;
297 component.editing = true;
298 component.ngOnInit(); // Switches form into edit mode
299 formHelper.setValue('poolType', 'erasure');
300 fixture.detectChanges();
301 formHelper.expectValid(setPgNum(8));
304 it('is valid if pgNum, poolType and name are valid', () => {
305 formHelper.setValue('name', 'some-name');
306 formHelper.setValue('poolType', 'erasure');
307 fixture.detectChanges();
309 expect(form.valid).toBeTruthy();
312 it('validates crushRule with multiple crush rules', () => {
313 formHelper.expectValidChange('poolType', 'replicated');
314 form.get('crushRule').updateValueAndValidity();
315 formHelper.expectError('crushRule', 'required'); // As multiple rules exist
316 formHelper.expectErrorChange('crushRule', { min_size: 20 }, 'tooFewOsds');
319 it('validates crushRule with no crush rules', () => {
320 infoReturn.crush_rules_replicated = [];
321 setUpPoolComponent();
322 formHelper.expectValidChange('poolType', 'replicated');
323 formHelper.expectValid('crushRule');
326 it('validates size', () => {
327 formHelper.setValue('poolType', 'replicated');
328 formHelper.expectValid('size');
329 formHelper.setValue('crushRule', {
333 formHelper.expectErrorChange('size', 1, 'min');
334 formHelper.expectErrorChange('size', 8, 'max');
335 formHelper.expectValidChange('size', 6);
338 it('validates compression mode default value', () => {
339 expect(form.getValue('mode')).toBe('none');
342 it('validate quotas', () => {
343 formHelper.expectValid('max_bytes');
344 formHelper.expectValid('max_objects');
345 formHelper.expectValidChange('max_bytes', '10 Gib');
346 formHelper.expectValidChange('max_bytes', '');
347 formHelper.expectValidChange('max_objects', '');
348 formHelper.expectErrorChange('max_objects', -1, 'min');
351 describe('compression form', () => {
353 formHelper.setValue('poolType', 'replicated');
354 formHelper.setValue('mode', 'passive');
357 it('is valid', () => {
358 expect(component.form.get('compression').valid).toBeTruthy();
361 it('validates minBlobSize to be only valid between 0 and maxBlobSize', () => {
362 formHelper.expectErrorChange('minBlobSize', -1, 'min');
363 formHelper.expectValidChange('minBlobSize', 0);
364 formHelper.setValue('maxBlobSize', '2 KiB');
365 formHelper.expectErrorChange('minBlobSize', '3 KiB', 'maximum');
366 formHelper.expectValidChange('minBlobSize', '1.9 KiB');
369 it('validates minBlobSize converts numbers', () => {
370 const control = formHelper.setValue('minBlobSize', '1');
371 fixture.detectChanges();
372 formHelper.expectValid(control);
373 expect(control.value).toBe('1 KiB');
376 it('validates maxBlobSize to be only valid bigger than minBlobSize', () => {
377 formHelper.expectErrorChange('maxBlobSize', -1, 'min');
378 formHelper.setValue('minBlobSize', '1 KiB');
379 formHelper.expectErrorChange('maxBlobSize', '0.5 KiB', 'minimum');
380 formHelper.expectValidChange('maxBlobSize', '1.5 KiB');
383 it('s valid to only use one blob size', () => {
384 formHelper.expectValid(formHelper.setValue('minBlobSize', '1 KiB'));
385 formHelper.expectValid(formHelper.setValue('maxBlobSize', ''));
386 formHelper.expectValid(formHelper.setValue('minBlobSize', ''));
387 formHelper.expectValid(formHelper.setValue('maxBlobSize', '1 KiB'));
390 it('dismisses any size error if one of the blob sizes is changed into a valid state', () => {
391 const min = formHelper.setValue('minBlobSize', '10 KiB');
392 const max = formHelper.setValue('maxBlobSize', '1 KiB');
393 fixture.detectChanges();
395 formHelper.expectValid(min);
396 formHelper.expectValid(max);
397 max.setValue('1 KiB');
398 fixture.detectChanges();
399 min.setValue('0.5 KiB');
400 formHelper.expectValid(min);
401 formHelper.expectValid(max);
404 it('validates maxBlobSize converts numbers', () => {
405 const control = formHelper.setValue('maxBlobSize', '2');
406 fixture.detectChanges();
407 expect(control.value).toBe('2 KiB');
410 it('validates that odd size validator works as expected', () => {
411 const odd = (min: string, max: string) => component['oddBlobSize'](min, max);
412 expect(odd('10', '8')).toBe(true);
413 expect(odd('8', '-')).toBe(false);
414 expect(odd('8', '10')).toBe(false);
415 expect(odd(null, '8')).toBe(false);
416 expect(odd('10', '')).toBe(false);
417 expect(odd('10', null)).toBe(false);
418 expect(odd(null, null)).toBe(false);
421 it('validates ratio to be only valid between 0 and 1', () => {
422 formHelper.expectValid('ratio');
423 formHelper.expectErrorChange('ratio', -0.1, 'min');
424 formHelper.expectValidChange('ratio', 0);
425 formHelper.expectValidChange('ratio', 1);
426 formHelper.expectErrorChange('ratio', 1.1, 'max');
430 it('validates application metadata name', () => {
431 formHelper.setValue('poolType', 'replicated');
432 fixture.detectChanges();
433 const selectBadges = fixture.debugElement.query(By.directive(SelectBadgesComponent))
435 const control = selectBadges.cdSelect.filter;
436 formHelper.expectValid(control);
437 control.setValue('?');
438 formHelper.expectError(control, 'pattern');
439 control.setValue('Ab3_');
440 formHelper.expectValid(control);
441 control.setValue('a'.repeat(129));
442 formHelper.expectError(control, 'maxlength');
446 describe('pool type changes', () => {
448 component.ngOnInit();
449 createCrushRule({ id: 3, min: 1, max: 1, name: 'ep1', type: 'erasure' });
450 createCrushRule({ id: 0, min: 2, max: 4, name: 'rep1', type: 'replicated' });
451 createCrushRule({ id: 1, min: 3, max: 18, name: 'rep2', type: 'replicated' });
454 it('should have a default replicated size of 3', () => {
455 formHelper.setValue('poolType', 'replicated');
456 expect(form.getValue('size')).toBe(3);
459 describe('replicatedRuleChange', () => {
461 formHelper.setValue('poolType', 'replicated');
462 formHelper.setValue('size', 99);
465 it('should not set size if a replicated pool is not set', () => {
466 formHelper.setValue('poolType', 'erasure');
467 expect(form.getValue('size')).toBe(99);
468 formHelper.setValue('crushRule', component.info.crush_rules_replicated[1]);
469 expect(form.getValue('size')).toBe(99);
472 it('should set size to maximum if size exceeds maximum', () => {
473 formHelper.setValue('crushRule', component.info.crush_rules_replicated[0]);
474 expect(form.getValue('size')).toBe(4);
477 it('should set size to minimum if size is lower than minimum', () => {
478 formHelper.setValue('size', -1);
479 formHelper.setValue('crushRule', component.info.crush_rules_replicated[0]);
480 expect(form.getValue('size')).toBe(2);
484 describe('rulesChange', () => {
485 it('has no effect if info is not there', () => {
486 delete component.info;
487 formHelper.setValue('poolType', 'replicated');
488 expect(component.current.rules).toEqual([]);
491 it('has no effect if pool type is not set', () => {
492 component['poolTypeChange']('');
493 expect(component.current.rules).toEqual([]);
496 it('shows all replicated rules when pool type is "replicated"', () => {
497 formHelper.setValue('poolType', 'replicated');
498 expect(component.current.rules).toEqual(component.info.crush_rules_replicated);
499 expect(component.current.rules.length).toBe(3);
502 it('shows all erasure code rules when pool type is "erasure"', () => {
503 formHelper.setValue('poolType', 'erasure');
504 expect(component.current.rules).toEqual(component.info.crush_rules_erasure);
505 expect(component.current.rules.length).toBe(1);
508 it('disables rule field if only one rule exists which is used in the disabled field', () => {
509 infoReturn.crush_rules_replicated = [
510 createCrushRule({ id: 0, min: 2, max: 4, name: 'rep1', type: 'replicated' })
512 setUpPoolComponent();
513 formHelper.setValue('poolType', 'replicated');
514 const control = form.get('crushRule');
515 expect(control.value).toEqual(component.info.crush_rules_replicated[0]);
516 expect(control.disabled).toBe(true);
519 it('does not select the first rule if more than one exist', () => {
520 formHelper.setValue('poolType', 'replicated');
521 const control = form.get('crushRule');
522 expect(control.value).toEqual(null);
523 expect(control.disabled).toBe(false);
526 it('changing between both pool types will not forget the crush rule selection', () => {
527 formHelper.setValue('poolType', 'replicated');
528 const control = form.get('crushRule');
529 const currentRule = component.info.crush_rules_replicated[0];
530 control.setValue(currentRule);
531 formHelper.setValue('poolType', 'erasure');
532 formHelper.setValue('poolType', 'replicated');
533 expect(control.value).toEqual(currentRule);
538 describe('getMaxSize and getMinSize', () => {
539 const setCrushRule = ({ min, max }: { min?: number; max?: number }) => {
540 formHelper.setValue('crushRule', {
546 it('returns 0 if osd count is 0', () => {
547 component.info.osd_count = 0;
548 expect(component.getMinSize()).toBe(0);
549 expect(component.getMaxSize()).toBe(0);
552 it('returns 0 if info is not there', () => {
553 delete component.info;
554 expect(component.getMinSize()).toBe(0);
555 expect(component.getMaxSize()).toBe(0);
558 it('returns minimum and maximum of rule', () => {
559 setCrushRule({ min: 2, max: 6 });
560 expect(component.getMinSize()).toBe(2);
561 expect(component.getMaxSize()).toBe(6);
564 it('returns 1 as minimum and the osd count as maximum if no crush rule is available', () => {
565 expect(component.getMinSize()).toBe(1);
566 expect(component.getMaxSize()).toBe(OSDS);
569 it('returns the osd count as maximum if the rule maximum exceeds it', () => {
570 setCrushRule({ max: 100 });
571 expect(component.getMaxSize()).toBe(OSDS);
574 it('should return the osd count as minimum if its lower the the rule minimum', () => {
575 setCrushRule({ min: 10 });
576 expect(component.getMinSize()).toBe(10);
577 const control = form.get('crushRule');
578 expect(control.invalid).toBe(true);
579 formHelper.expectError(control, 'tooFewOsds');
583 describe('application metadata', () => {
584 let selectBadges: SelectBadgesComponent;
586 const testAddApp = (app?: string, result?: string[]) => {
587 selectBadges.cdSelect.filter.setValue(app);
588 selectBadges.cdSelect.updateFilter();
589 selectBadges.cdSelect.selectOption();
590 expect(component.data.applications.selected).toEqual(result);
593 const testRemoveApp = (app: string, result: string[]) => {
594 selectBadges.cdSelect.removeItem(app);
595 expect(component.data.applications.selected).toEqual(result);
598 const setCurrentApps = (apps: string[]) => {
599 component.data.applications.selected = apps;
600 fixture.detectChanges();
601 selectBadges.cdSelect.ngOnInit();
606 formHelper.setValue('poolType', 'replicated');
607 fixture.detectChanges();
608 selectBadges = fixture.debugElement.query(By.directive(SelectBadgesComponent))
612 it('adds all predefined and a custom applications to the application metadata array', () => {
613 testAddApp('g', ['rgw']);
614 testAddApp('b', ['rbd', 'rgw']);
615 testAddApp('c', ['cephfs', 'rbd', 'rgw']);
616 testAddApp('ownApp', ['cephfs', 'ownApp', 'rbd', 'rgw']);
619 it('only allows 4 apps to be added to the array', () => {
620 const apps = setCurrentApps(['d', 'c', 'b', 'a']);
621 testAddApp('e', apps);
624 it('can remove apps', () => {
625 setCurrentApps(['a', 'b', 'c', 'd']);
626 testRemoveApp('c', ['a', 'b', 'd']);
627 testRemoveApp('a', ['b', 'd']);
628 testRemoveApp('d', ['b']);
629 testRemoveApp('b', []);
632 it('does not remove any app that is not in the array', () => {
633 const apps = ['a', 'b', 'c', 'd'];
634 setCurrentApps(apps);
635 testRemoveApp('e', apps);
636 testRemoveApp('0', apps);
640 describe('pg number changes', () => {
642 formHelper.setValue('crushRule', {
646 formHelper.setValue('poolType', 'erasure');
647 fixture.detectChanges();
651 it('updates by value', () => {
652 testPgUpdate(10, undefined, 8);
653 testPgUpdate(22, undefined, 16);
654 testPgUpdate(26, undefined, 32);
655 testPgUpdate(200, undefined, 256);
656 testPgUpdate(300, undefined, 256);
657 testPgUpdate(350, undefined, 256);
660 it('updates by jump -> a magnitude of the power of 2', () => {
661 testPgUpdate(undefined, 1, 512);
662 testPgUpdate(undefined, -1, 256);
665 it('returns 1 as minimum for false numbers', () => {
666 testPgUpdate(-26, undefined, 1);
667 testPgUpdate(0, undefined, 1);
668 testPgUpdate(0, -1, 1);
669 testPgUpdate(undefined, -20, 1);
672 it('changes the value and than jumps', () => {
673 testPgUpdate(230, 1, 512);
674 testPgUpdate(3500, -1, 2048);
677 describe('pg power jump', () => {
678 it('should jump correctly at the beginning', () => {
679 testPgUpdate(1, -1, 1);
680 testPgUpdate(1, 1, 2);
681 testPgUpdate(2, -1, 1);
682 testPgUpdate(2, 1, 4);
683 testPgUpdate(4, -1, 2);
684 testPgUpdate(4, 1, 8);
685 testPgUpdate(4, 1, 8);
688 it('increments pg power if difference to the current number is 1', () => {
689 testPgUpdate(undefined, 1, 512);
690 testPgUpdate(undefined, 1, 1024);
691 testPgUpdate(undefined, 1, 2048);
692 testPgUpdate(undefined, 1, 4096);
695 it('decrements pg power if difference to the current number is -1', () => {
696 testPgUpdate(undefined, -1, 128);
697 testPgUpdate(undefined, -1, 64);
698 testPgUpdate(undefined, -1, 32);
699 testPgUpdate(undefined, -1, 16);
700 testPgUpdate(undefined, -1, 8);
704 describe('pgCalc', () => {
707 const getValidCase = () => ({
718 const testPgCalc = ({ type, osds, size, ecp, expected }: Record<string, any>) => {
719 component.info.osd_count = osds;
720 formHelper.setValue('poolType', type);
721 if (type === 'replicated') {
722 formHelper.setValue('size', size);
724 formHelper.setValue('erasureProfile', ecp);
726 expect(form.getValue('pgNum')).toBe(expected);
727 expect(component.externalPgChange).toBe(PGS !== expected);
734 it('does not change anything if type is not valid', () => {
735 const test = getValidCase();
741 it('does not change anything if ecp is not valid', () => {
742 const test = getValidCase();
744 test.type = 'erasure';
749 it('calculates some replicated values', () => {
750 const test = getValidCase();
761 it('calculates erasure code values even if selection is disabled', () => {
762 component['initEcp']([{ k: 2, m: 2, name: 'bla', plugin: '', technique: '' }]);
763 const test = getValidCase();
764 test.type = 'erasure';
766 expect(form.get('erasureProfile').disabled).toBeTruthy();
769 it('calculates some erasure code values', () => {
770 const test = getValidCase();
771 test.type = 'erasure';
782 it('should not change a manual set pg number', () => {
783 form.get('pgNum').markAsDirty();
784 const test = getValidCase();
791 describe('crushRule', () => {
792 const selectRuleByIndex = (n: number) => {
793 formHelper.setValue('crushRule', component.info.crush_rules_replicated[n]);
797 formHelper.setValue('poolType', 'replicated');
798 selectRuleByIndex(0);
799 fixture.detectChanges();
802 it('should not show info per default', () => {
803 fixtureHelper.expectElementVisible('#crushRule', true);
804 fixtureHelper.expectElementVisible('#crush-info-block', false);
807 it('should show info if the info button is clicked', () => {
808 const infoButton = fixture.debugElement.query(By.css('#crush-info-button'));
809 infoButton.triggerEventHandler('click', null);
810 expect(component.data.crushInfo).toBeTruthy();
811 fixture.detectChanges();
812 expect(infoButton.classes['active']).toBeTruthy();
813 fixtureHelper.expectIdElementsVisible(['crushRule', 'crush-info-block'], true);
816 it('should know which rules are in use', () => {
817 selectRuleByIndex(2);
818 expect(component.crushUsage).toEqual(['some.pool.uses.it']);
821 describe('crush rule deletion', () => {
822 let taskWrapper: TaskWrapperService;
823 let deletion: CriticalConfirmationModalComponent;
824 let deleteSpy: jasmine.Spy;
825 let modalSpy: jasmine.Spy;
827 const callDeletion = () => {
828 component.deleteCrushRule();
829 deletion.submitActionObservable();
832 const callDeletionWithRuleByIndex = (index: number) => {
833 deleteSpy.calls.reset();
834 selectRuleByIndex(index);
838 const expectSuccessfulDeletion = (name: string) => {
839 expect(crushRuleService.delete).toHaveBeenCalledWith(name);
840 expect(taskWrapper.wrapTaskAroundCall).toHaveBeenCalledWith({
842 name: 'crushRule/delete',
847 call: undefined // because of stub
852 modalSpy = spyOn(TestBed.get(BsModalService), 'show').and.callFake(
853 (deletionClass, config) => {
854 deletion = Object.assign(new deletionClass(), config.initialState);
860 deleteSpy = spyOn(crushRuleService, 'delete').and.callFake((name) => {
861 const rules = infoReturn.crush_rules_replicated;
862 const index = _.findIndex(rules, (rule) => rule.rule_name === name);
863 rules.splice(index, 1);
865 taskWrapper = TestBed.get(TaskWrapperService);
866 spyOn(taskWrapper, 'wrapTaskAroundCall').and.callThrough();
869 describe('with unused rule', () => {
871 callDeletionWithRuleByIndex(0);
874 it('should have called delete', () => {
875 expectSuccessfulDeletion('rep1');
878 it('should not open the tooltip nor the crush info', () => {
879 expect(component.crushDeletionBtn.isOpen).toBe(false);
880 expect(component.data.crushInfo).toBe(false);
883 it('should reload the rules after deletion', () => {
884 const expected = infoReturn.crush_rules_replicated;
885 const currentRules = component.current.rules;
886 expect(currentRules.length).toBe(expected.length);
887 expect(currentRules).toEqual(expected);
891 describe('rule in use', () => {
893 spyOn(global, 'setTimeout').and.callFake((fn: Function) => fn());
894 component.crushInfoTabs = { tabs: [{}, {}, {}] } as TabsetComponent; // Mock it
895 deleteSpy.calls.reset();
896 selectRuleByIndex(2);
897 component.deleteCrushRule();
900 it('should not have called delete and opened the tooltip', () => {
901 expect(crushRuleService.delete).not.toHaveBeenCalled();
902 expect(component.crushDeletionBtn.isOpen).toBe(true);
903 expect(component.data.crushInfo).toBe(true);
906 it('should open the third crush info tab', () => {
907 expect(component.crushInfoTabs).toEqual({
908 tabs: [{}, {}, { active: true }]
909 } as TabsetComponent);
912 it('should hide the tooltip when clicking on delete again', () => {
913 component.deleteCrushRule();
914 expect(component.crushDeletionBtn.isOpen).toBe(false);
917 it('should hide the tooltip when clicking on add', () => {
918 modalSpy.and.callFake((): any => ({
920 submitAction: of('someRule')
923 component.addCrushRule();
924 expect(component.crushDeletionBtn.isOpen).toBe(false);
927 it('should hide the tooltip when changing the crush rule', () => {
928 selectRuleByIndex(0);
929 expect(component.crushDeletionBtn.isOpen).toBe(false);
935 describe('erasure code profile', () => {
936 const setSelectedEcp = (name: string) => {
937 formHelper.setValue('erasureProfile', { name: name });
941 formHelper.setValue('poolType', 'erasure');
942 fixture.detectChanges();
945 it('should not show info per default', () => {
946 fixtureHelper.expectElementVisible('#erasureProfile', true);
947 fixtureHelper.expectElementVisible('#ecp-info-block', false);
950 it('should show info if the info button is clicked', () => {
951 const infoButton = fixture.debugElement.query(By.css('#ecp-info-button'));
952 infoButton.triggerEventHandler('click', null);
953 expect(component.data.erasureInfo).toBeTruthy();
954 fixture.detectChanges();
955 expect(infoButton.classes['active']).toBeTruthy();
956 fixtureHelper.expectIdElementsVisible(['erasureProfile', 'ecp-info-block'], true);
959 describe('ecp deletion', () => {
960 let taskWrapper: TaskWrapperService;
961 let deletion: CriticalConfirmationModalComponent;
963 const callDeletion = () => {
964 component.deleteErasureCodeProfile();
965 deletion.submitActionObservable();
968 const testPoolDeletion = (name: string) => {
969 setSelectedEcp(name);
971 expect(ecpService.delete).toHaveBeenCalledWith(name);
972 expect(taskWrapper.wrapTaskAroundCall).toHaveBeenCalledWith({
979 call: undefined // because of stub
984 spyOn(TestBed.get(BsModalService), 'show').and.callFake((deletionClass, config) => {
985 deletion = Object.assign(new deletionClass(), config.initialState);
990 spyOn(ecpService, 'delete').and.stub();
991 taskWrapper = TestBed.get(TaskWrapperService);
992 spyOn(taskWrapper, 'wrapTaskAroundCall').and.callThrough();
995 it('should delete two different erasure code profiles', () => {
996 testPoolDeletion('someEcpName');
997 testPoolDeletion('aDifferentEcpName');
1002 describe('submit - create', () => {
1003 const setMultipleValues = (settings: object) => {
1004 Object.keys(settings).forEach((name) => {
1005 formHelper.setValue(name, settings[name]);
1009 describe('erasure coded pool', () => {
1010 const expectEcSubmit = (o: any) =>
1015 pool_type: 'erasure',
1016 pg_autoscale_mode: 'off',
1017 erasure_code_profile: 'ecp1',
1027 poolType: 'erasure',
1032 it('minimum requirements without ECP to create ec pool', () => {
1033 // Mock that no ec profiles exist
1034 infoReturn.erasure_code_profiles = [];
1035 setUpPoolComponent();
1038 poolType: 'erasure',
1043 pool_type: 'erasure',
1044 pg_autoscale_mode: 'off',
1049 it('creates ec pool with erasure coded profile', () => {
1050 const ecp = { name: 'ecpMinimalMock' };
1055 erasure_code_profile: ecp.name
1059 it('creates ec pool with ec_overwrite flag', () => {
1064 flags: ['ec_overwrites']
1068 it('should ignore replicated set settings for ec pools', () => {
1070 size: 2 // will be ignored
1075 it('creates a pool with compression', () => {
1084 compression_mode: 'passive',
1085 compression_algorithm: 'lz4',
1086 compression_min_blob_size: 4096,
1087 compression_max_blob_size: 4194304,
1088 compression_required_ratio: 0.7
1092 it('creates a pool with application metadata', () => {
1093 component.data.applications.selected = ['cephfs', 'rgw'];
1095 application_metadata: ['cephfs', 'rgw']
1100 describe('with replicated pool', () => {
1101 const expectReplicatedSubmit = (o: any) =>
1106 pool_type: 'replicated',
1107 pg_autoscale_mode: 'off',
1118 poolType: 'replicated',
1119 crushRule: infoReturn.crush_rules_replicated[0],
1125 it('uses the minimum requirements for replicated pools', () => {
1126 // Mock that no replicated rules exist
1127 infoReturn.crush_rules_replicated = [];
1128 setUpPoolComponent();
1132 poolType: 'replicated',
1138 pool_type: 'replicated',
1140 pg_autoscale_mode: 'off',
1145 it('ignores erasure only set settings for replicated pools', () => {
1147 erasureProfile: { name: 'ecpMinimalMock' }, // Will be ignored
1148 ecOverwrites: true // Will be ignored
1151 * As pgCalc is triggered through profile changes, which is normally not possible,
1152 * if type `replicated` is set, pgNum will be set to 256 with the current rule for
1153 * a replicated pool.
1155 expectReplicatedSubmit({
1160 it('creates a pool with quotas', () => {
1162 max_bytes: 1024 * 1024,
1165 expectReplicatedSubmit({
1166 quota_max_bytes: 1024 * 1024,
1167 quota_max_objects: 3000
1171 it('creates a pool with rbd qos settings', () => {
1172 component.currentConfigurationValues = {
1173 rbd_qos_bps_limit: 55
1175 expectReplicatedSubmit({
1177 rbd_qos_bps_limit: 55
1184 describe('edit mode', () => {
1185 const setUrl = (url: string) => {
1186 Object.defineProperty(router, 'url', { value: url });
1187 setUpPoolComponent(); // Renew of component needed because the constructor has to be called
1192 pool = new Pool('somePoolName');
1193 pool.type = 'replicated';
1195 pool.crush_rule = 'rep1';
1198 pool.options.compression_mode = 'passive';
1199 pool.options.compression_algorithm = 'lz4';
1200 pool.options.compression_min_blob_size = 1024 * 512;
1201 pool.options.compression_max_blob_size = 1024 * 1024;
1202 pool.options.compression_required_ratio = 0.8;
1203 pool.flags_names = 'someFlag1,someFlag2';
1204 pool.application_metadata = ['rbd', 'ownApp'];
1205 pool.quota_max_bytes = 1024 * 1024 * 1024;
1206 pool.quota_max_objects = 3000;
1208 createCrushRule({ name: 'someRule' });
1209 spyOn(poolService, 'get').and.callFake(() => of(pool));
1212 it('is not in edit mode if edit is not included in url', () => {
1213 setUrl('/pool/add');
1214 expect(component.editing).toBeFalsy();
1217 it('is in edit mode if edit is included in url', () => {
1218 setUrl('/pool/edit/somePoolName');
1219 expect(component.editing).toBeTruthy();
1222 describe('after ngOnInit', () => {
1224 setUrl('/pool/edit/somePoolName');
1225 fixture.detectChanges();
1228 it('disabled inputs', () => {
1229 fixture.detectChanges();
1230 const disabled = ['poolType', 'crushRule', 'size', 'erasureProfile', 'ecOverwrites'];
1231 disabled.forEach((controlName) => {
1232 return expect(form.get(controlName).disabled).toBeTruthy();
1245 enabled.forEach((controlName) => {
1246 return expect(form.get(controlName).enabled).toBeTruthy();
1250 it('should include the custom app as valid option', () => {
1252 component.data.applications.available.map((app: Record<string, any>) => app.name)
1253 ).toEqual(['cephfs', 'ownApp', 'rbd', 'rgw']);
1256 it('set all control values to the given pool', () => {
1257 expect(form.getValue('name')).toBe(pool.pool_name);
1258 expect(form.getValue('poolType')).toBe(pool.type);
1259 expect(form.getValue('crushRule')).toEqual(component.info.crush_rules_replicated[0]);
1260 expect(form.getValue('size')).toBe(pool.size);
1261 expect(form.getValue('pgNum')).toBe(pool.pg_num);
1262 expect(form.getValue('mode')).toBe(pool.options.compression_mode);
1263 expect(form.getValue('algorithm')).toBe(pool.options.compression_algorithm);
1264 expect(form.getValue('minBlobSize')).toBe('512 KiB');
1265 expect(form.getValue('maxBlobSize')).toBe('1 MiB');
1266 expect(form.getValue('ratio')).toBe(pool.options.compression_required_ratio);
1267 expect(form.getValue('max_bytes')).toBe('1 GiB');
1268 expect(form.getValue('max_objects')).toBe(pool.quota_max_objects);
1271 it('updates pgs on every change', () => {
1272 testPgUpdate(undefined, -1, 16);
1273 testPgUpdate(undefined, -1, 8);
1276 it('is possible to use less or more pgs than before', () => {
1277 formHelper.expectValid(setPgNum(64));
1278 formHelper.expectValid(setPgNum(4));
1281 describe('submit', () => {
1282 const markControlAsPreviouslySet = (controlName: string) =>
1283 form.get(controlName).markAsPristine();
1286 ['algorithm', 'maxBlobSize', 'minBlobSize', 'mode', 'pgNum', 'ratio', 'name'].forEach(
1287 (name) => markControlAsPreviouslySet(name)
1289 fixture.detectChanges();
1292 it(`always provides the application metadata array with submit even if it's empty`, () => {
1293 expect(form.get('mode').dirty).toBe(false);
1294 component.data.applications.selected = [];
1297 application_metadata: [],
1298 pool: 'somePoolName'
1305 it(`will always provide reset value for compression options`, () => {
1306 formHelper.setValue('minBlobSize', '').markAsDirty();
1307 formHelper.setValue('maxBlobSize', '').markAsDirty();
1308 formHelper.setValue('ratio', '').markAsDirty();
1311 application_metadata: ['ownApp', 'rbd'],
1312 compression_max_blob_size: 0,
1313 compression_min_blob_size: 0,
1314 compression_required_ratio: 0,
1315 pool: 'somePoolName'
1322 it(`will unset mode not used anymore`, () => {
1323 formHelper.setValue('mode', 'none').markAsDirty();
1326 application_metadata: ['ownApp', 'rbd'],
1327 compression_mode: 'unset',
1328 pool: 'somePoolName'
1338 describe('test pool configuration component', () => {
1339 it('is visible for replicated pools with rbd application', () => {
1340 const poolType = component.form.get('poolType');
1341 poolType.markAsDirty();
1342 poolType.setValue('replicated');
1343 component.data.applications.selected = ['rbd'];
1344 fixture.detectChanges();
1346 fixture.debugElement.query(By.css('cd-rbd-configuration-form')).nativeElement.parentElement
1351 it('is invisible for erasure coded pools', () => {
1352 const poolType = component.form.get('poolType');
1353 poolType.markAsDirty();
1354 poolType.setValue('erasure');
1355 fixture.detectChanges();
1357 fixture.debugElement.query(By.css('cd-rbd-configuration-form')).nativeElement.parentElement