import { CrushRule } from '../../../shared/models/crush-rule';
import { ErasureCodeProfile } from '../../../shared/models/erasure-code-profile';
import { Permission } from '../../../shared/models/permissions';
+import { PoolFormInfo } from '../../../shared/models/pool-form-info';
import { AuthStorageService } from '../../../shared/services/auth-storage.service';
import { TaskWrapperService } from '../../../shared/services/task-wrapper.service';
import { Pool } from '../pool';
op: 'emit'
}
];
- component.info['crush_rules_' + type].push(rule);
return rule;
};
const expectValidSubmit = (
pool: any,
- taskName: string,
- poolServiceMethod: 'create' | 'update'
+ taskName = 'pool/create',
+ poolServiceMethod: 'create' | 'update' = 'create'
) => {
spyOn(poolService, poolServiceMethod).and.stub();
const taskWrapper = TestBed.get(TaskWrapperService);
});
};
- const setUpPoolComponent = () => {
- fixture = TestBed.createComponent(PoolFormComponent);
- fixtureHelper = new FixtureHelper(fixture);
- component = fixture.componentInstance;
- component.info = {
- pool_names: [],
+ let infoReturn: PoolFormInfo;
+ const setInfo = () => {
+ const ecp1 = new ErasureCodeProfile();
+ ecp1.name = 'ecp1';
+ infoReturn = {
+ pool_names: ['someExistingPoolName'],
osd_count: OSDS,
is_all_bluestore: true,
bluestore_compression_algorithm: 'snappy',
compression_algorithms: ['snappy'],
compression_modes: ['none', 'passive'],
- crush_rules_replicated: [],
- crush_rules_erasure: [],
+ crush_rules_replicated: [
+ createCrushRule({ id: 0, min: 2, max: 4, name: 'rep1', type: 'replicated' }),
+ createCrushRule({ id: 1, min: 3, max: 18, name: 'rep2', type: 'replicated' })
+ ],
+ crush_rules_erasure: [
+ createCrushRule({ id: 3, min: 1, max: 1, name: 'ecp1', type: 'erasure' })
+ ],
+ erasure_code_profiles: [ecp1],
pg_autoscale_default_mode: 'off',
- pg_autoscale_modes: ['off', 'warn', 'on']
+ pg_autoscale_modes: ['off', 'warn', 'on'],
+ pg_autoscale_config: { default: 'off', enum_values: ['on', 'warn', 'off'], value: [] },
+ used_rules: {}
};
- const ecp1 = new ErasureCodeProfile();
- ecp1.name = 'ecp1';
- component.ecProfiles = [ecp1];
+ };
+
+ const setUpPoolComponent = () => {
+ fixture = TestBed.createComponent(PoolFormComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+
+ fixtureHelper = new FixtureHelper(fixture);
form = component.form;
formHelper = new FormHelper(form);
};
]
});
+ let navigationSpy: jasmine.Spy;
+
beforeEach(() => {
- setUpPoolComponent();
poolService = TestBed.get(PoolService);
- spyOn(poolService, 'getInfo').and.callFake(() => [component.info]);
+ setInfo();
+ spyOn(poolService, 'getInfo').and.callFake(() => of(infoReturn));
+
ecpService = TestBed.get(ErasureCodeProfileService);
- spyOn(ecpService, 'list').and.callFake(() => [component.ecProfiles]);
+
router = TestBed.get(Router);
- spyOn(router, 'navigate').and.stub();
+ navigationSpy = spyOn(router, 'navigate').and.stub();
+
+ setUpPoolComponent();
});
it('should create', () => {
let poolPermissions: Permission;
let authStorageService: AuthStorageService;
- const testForRedirect = (times: number) => {
+ const expectRedirect = (redirected = true) => {
+ navigationSpy.calls.reset();
component.authenticate();
- expect(router.navigate).toHaveBeenCalledTimes(times);
+ expect(navigationSpy).toHaveBeenCalledTimes(redirected ? 1 : 0);
};
beforeEach(() => {
});
it('navigates if user is not allowed', () => {
- testForRedirect(1);
+ expectRedirect();
poolPermissions.read = true;
- testForRedirect(2);
+ expectRedirect();
poolPermissions.delete = true;
- testForRedirect(3);
+ expectRedirect();
poolPermissions.update = true;
- testForRedirect(4);
+ expectRedirect();
component.editing = true;
poolPermissions.update = false;
poolPermissions.create = true;
- testForRedirect(5);
+ expectRedirect();
});
it('does not navigate users with right permissions', () => {
poolPermissions.read = true;
poolPermissions.create = true;
- testForRedirect(0);
+ expectRedirect(false);
component.editing = true;
poolPermissions.update = true;
- testForRedirect(0);
+ expectRedirect(false);
poolPermissions.create = false;
- testForRedirect(0);
+ expectRedirect(false);
});
});
it('is invalid at the beginning all sub forms are valid', () => {
expect(form.valid).toBeFalsy();
['name', 'poolType', 'pgNum'].forEach((name) => formHelper.expectError(name, 'required'));
- ['crushRule', 'size', 'erasureProfile', 'ecOverwrites'].forEach((name) =>
+ ['size', 'crushRule', 'erasureProfile', 'ecOverwrites'].forEach((name) =>
formHelper.expectValid(name)
);
expect(component.form.get('compression').valid).toBeTruthy();
formHelper.expectError('name', 'required');
formHelper.expectValidChange('name', 'some-name');
formHelper.expectValidChange('name', 'name/with/slash');
- component.info.pool_names.push('someExistingPoolName');
formHelper.expectErrorChange('name', 'someExistingPoolName', 'uniqueName');
formHelper.expectErrorChange('name', 'wrong format with spaces', 'pattern');
});
expect(form.valid).toBeTruthy();
});
- it('validates crushRule', () => {
+ it('validates crushRule with multiple crush rules', () => {
+ formHelper.expectValidChange('poolType', 'replicated');
+ formHelper.expectError('crushRule', 'required'); // As multiple rules exist
+ formHelper.expectErrorChange('crushRule', { min_size: 20 }, 'tooFewOsds');
+ });
+
+ it('validates crushRule with no crush rules', () => {
+ infoReturn.crush_rules_replicated = [];
+ setUpPoolComponent();
+ formHelper.expectValidChange('poolType', 'replicated');
formHelper.expectValid('crushRule');
formHelper.expectErrorChange('crushRule', { min_size: 20 }, 'tooFewOsds');
});
});
it('has no effect if pool type is not set', () => {
- component['rulesChange']();
+ component['rulesChange']('');
expect(component.current.rules).toEqual([]);
});
});
describe('crushRule', () => {
+ const selectRuleById = (n: number) => {
+ formHelper.setValue('crushRule', component.info.crush_rules_replicated[n]);
+ };
+
beforeEach(() => {
- createCrushRule({ name: 'replicatedRule' });
- fixture.detectChanges();
formHelper.setValue('poolType', 'replicated');
+ selectRuleById(0);
fixture.detectChanges();
});
});
it('should show info if the info button is clicked', () => {
- fixture.detectChanges();
const infoButton = fixture.debugElement.query(By.css('#crush-info-button'));
infoButton.triggerEventHandler('click', null);
expect(component.data.crushInfo).toBeTruthy();
formHelper.setValue(name, settings[name]);
});
};
- const testCreate = (pool: object) => {
- expectValidSubmit(pool, 'pool/create', 'create');
- };
-
- beforeEach(() => {
- createCrushRule({ name: 'replicatedRule' });
- createCrushRule({ name: 'erasureRule', type: 'erasure', id: 1 });
- });
describe('erasure coded pool', () => {
- it('minimum requirements', () => {
+ const expectEcSubmit = (o: any) =>
+ expectValidSubmit(
+ Object.assign(
+ {
+ pool: 'ecPool',
+ pool_type: 'erasure',
+ pg_autoscale_mode: 'off',
+ erasure_code_profile: 'ecp1',
+ pg_num: 4
+ },
+ o
+ )
+ );
+
+ beforeEach(() => {
+ setMultipleValues({
+ name: 'ecPool',
+ poolType: 'erasure',
+ pgNum: 4
+ });
+ });
+
+ it('minimum requirements without ECP to create ec pool', () => {
+ // Mock that no ec profiles exist
+ infoReturn.erasure_code_profiles = [];
+ setUpPoolComponent();
setMultipleValues({
name: 'minECPool',
poolType: 'erasure',
pgNum: 4
});
- testCreate({
+ expectValidSubmit({
pool: 'minECPool',
pool_type: 'erasure',
+ pg_autoscale_mode: 'off',
pg_num: 4
});
});
- it('with erasure coded profile', () => {
+ it('creates ec pool with erasure coded profile', () => {
const ecp = { name: 'ecpMinimalMock' };
setMultipleValues({
- name: 'ecpPool',
- poolType: 'erasure',
- pgNum: 16,
- size: 2, // Will be ignored
erasureProfile: ecp
});
- testCreate({
- pool: 'ecpPool',
- pool_type: 'erasure',
- pg_num: 16,
+ expectEcSubmit({
erasure_code_profile: ecp.name
});
});
- it('with ec_overwrite flag', () => {
+ it('creates ec pool with ec_overwrite flag', () => {
setMultipleValues({
- name: 'ecOverwrites',
- poolType: 'erasure',
- pgNum: 32,
ecOverwrites: true
});
- testCreate({
- pool: 'ecOverwrites',
- pool_type: 'erasure',
- pg_num: 32,
+ expectEcSubmit({
flags: ['ec_overwrites']
});
});
- it('with rbd qos settings', () => {
+ it('should ignore replicated set settings for ec pools', () => {
setMultipleValues({
- name: 'replicatedRbdQos',
- poolType: 'replicated',
- size: 2,
- pgNum: 32
+ size: 2 // will be ignored
});
- component.currentConfigurationValues = {
- rbd_qos_bps_limit: 55
- };
- testCreate({
- pool: 'replicatedRbdQos',
- pool_type: 'replicated',
- size: 2,
- pg_num: 32,
- configuration: {
- rbd_qos_bps_limit: 55
- }
+ expectEcSubmit({});
+ });
+
+ it('creates a pool with compression', () => {
+ setMultipleValues({
+ mode: 'passive',
+ algorithm: 'lz4',
+ minBlobSize: '4 K',
+ maxBlobSize: '4 M',
+ ratio: 0.7
+ });
+ expectEcSubmit({
+ compression_mode: 'passive',
+ compression_algorithm: 'lz4',
+ compression_min_blob_size: 4096,
+ compression_max_blob_size: 4194304,
+ compression_required_ratio: 0.7
+ });
+ });
+
+ it('creates a pool with application metadata', () => {
+ component.data.applications.selected = ['cephfs', 'rgw'];
+ expectEcSubmit({
+ application_metadata: ['cephfs', 'rgw']
});
});
});
- describe('replicated coded pool', () => {
- it('minimum requirements', () => {
- const ecp = { name: 'ecpMinimalMock' };
+ describe('with replicated pool', () => {
+ const expectReplicatedSubmit = (o: any) =>
+ expectValidSubmit(
+ Object.assign(
+ {
+ pool: 'repPool',
+ pool_type: 'replicated',
+ pg_autoscale_mode: 'off',
+ pg_num: 16,
+ rule_name: 'rep1',
+ size: 3
+ },
+ o
+ )
+ );
+ beforeEach(() => {
+ setMultipleValues({
+ name: 'repPool',
+ poolType: 'replicated',
+ crushRule: infoReturn.crush_rules_replicated[0],
+ size: 3,
+ pgNum: 16
+ });
+ });
+
+ it('uses the minimum requirements for replicated pools', () => {
+ // Mock that no replicated rules exist
+ infoReturn.crush_rules_replicated = [];
+ setUpPoolComponent();
+
setMultipleValues({
name: 'minRepPool',
poolType: 'replicated',
size: 2,
- erasureProfile: ecp, // Will be ignored
- pgNum: 8
+ pgNum: 32
});
- testCreate({
+ expectValidSubmit({
pool: 'minRepPool',
pool_type: 'replicated',
- pg_num: 8,
+ pg_num: 32,
+ pg_autoscale_mode: 'off',
size: 2
});
});
- it('with quotas', () => {
+ it('ignores erasure only set settings for replicated pools', () => {
setMultipleValues({
- name: 'RepPoolWithQuotas',
- poolType: 'replicated',
- max_bytes: 1024 * 1024,
- max_objects: 3000,
- pgNum: 8
+ erasureProfile: { name: 'ecpMinimalMock' }, // Will be ignored
+ ecOverwrites: true // Will be ignored
});
- testCreate({
- pool: 'RepPoolWithQuotas',
- pool_type: 'replicated',
- quota_max_bytes: 1024 * 1024,
- quota_max_objects: 3000,
- pg_num: 8
+ /**
+ * As pgCalc is triggered through profile changes, which is normally not possible,
+ * if type `replicated` is set, pgNum will be set to 256 with the current rule for
+ * a replicated pool.
+ */
+ expectReplicatedSubmit({
+ pg_num: 256
});
});
- });
- it('pool with compression', () => {
- setMultipleValues({
- name: 'compression',
- poolType: 'erasure',
- pgNum: 64,
- mode: 'passive',
- algorithm: 'lz4',
- minBlobSize: '4 K',
- maxBlobSize: '4 M',
- ratio: 0.7
- });
- testCreate({
- pool: 'compression',
- pool_type: 'erasure',
- pg_num: 64,
- compression_mode: 'passive',
- compression_algorithm: 'lz4',
- compression_min_blob_size: 4096,
- compression_max_blob_size: 4194304,
- compression_required_ratio: 0.7
+ it('creates a pool with quotas', () => {
+ setMultipleValues({
+ max_bytes: 1024 * 1024,
+ max_objects: 3000
+ });
+ expectReplicatedSubmit({
+ quota_max_bytes: 1024 * 1024,
+ quota_max_objects: 3000
+ });
});
- });
- it('pool with application metadata', () => {
- setMultipleValues({
- name: 'apps',
- poolType: 'erasure',
- pgNum: 128
- });
- component.data.applications.selected = ['cephfs', 'rgw'];
- testCreate({
- pool: 'apps',
- pool_type: 'erasure',
- pg_num: 128,
- application_metadata: ['cephfs', 'rgw']
+ it('creates a pool with rbd qos settings', () => {
+ component.currentConfigurationValues = {
+ rbd_qos_bps_limit: 55
+ };
+ expectReplicatedSubmit({
+ configuration: {
+ rbd_qos_bps_limit: 55
+ }
+ });
});
});
});
pool = new Pool('somePoolName');
pool.type = 'replicated';
pool.size = 3;
- pool.crush_rule = 'someRule';
+ pool.crush_rule = 'rep1';
pool.pg_num = 32;
pool.options = {};
pool.options.compression_mode = 'passive';
describe('after ngOnInit', () => {
beforeEach(() => {
- component.editing = true;
+ setUrl('/pool/edit/somePoolName');
fixture.detectChanges();
});
it('disabled inputs', () => {
+ fixture.detectChanges();
const disabled = ['poolType', 'crushRule', 'size', 'erasureProfile', 'ecOverwrites'];
disabled.forEach((controlName) => {
return expect(form.get(controlName).disabled).toBeTruthy();
-import { Component, EventEmitter, OnInit } from '@angular/core';
+import { Component, EventEmitter, OnInit, ViewChild } from '@angular/core';
import { FormControl, Validators } from '@angular/forms';
import { ActivatedRoute, Router } from '@angular/router';
import { I18n } from '@ngx-translate/i18n-polyfill';
import * as _ from 'lodash';
import { BsModalService } from 'ngx-bootstrap/modal';
-import { forkJoin, Subscription } from 'rxjs';
+import { Subscription } from 'rxjs';
import { ErasureCodeProfileService } from '../../../shared/api/erasure-code-profile.service';
import { PoolService } from '../../../shared/api/pool.service';
isErasure = false;
data = new PoolFormData(this.i18n);
externalPgChange = false;
- private modalSubscription: Subscription;
current: Record<string, any> = {
rules: []
};
icons = Icons;
pgAutoscaleModes: string[];
+ private modalSubscription: Subscription;
+
constructor(
private dimlessBinaryPipe: DimlessBinaryPipe,
private route: ActivatedRoute,
CdValidators.custom(
'tooFewOsds',
(rule: any) => this.info && rule && this.info.osd_count < rule.min_size
+ ),
+ CdValidators.custom(
+ 'required',
+ (rule: CrushRule) =>
+ this.isReplicated && this.info.crush_rules_replicated.length > 0 && !rule
)
]
}),
}
ngOnInit() {
- forkJoin(this.poolService.getInfo(), this.ecpService.list()).subscribe(
- (data: [PoolFormInfo, ErasureCodeProfile[]]) => {
- this.pgAutoscaleModes = data[0].pg_autoscale_modes;
- this.form.silentSet('pgAutoscaleMode', data[0].pg_autoscale_default_mode);
- this.initInfo(data[0]);
- this.initEcp(data[1]);
- if (this.editing) {
- this.initEditMode();
- } else {
- this.setAvailableApps();
- }
- this.listenToChanges();
- this.setComplexValidators();
+ this.poolService.getInfo().subscribe((info: PoolFormInfo) => {
+ this.initInfo(info);
+ if (this.editing) {
+ this.initEditMode();
+ } else {
+ this.setAvailableApps();
}
- );
+ this.listenToChanges();
+ this.setComplexValidators();
+ });
}
private initInfo(info: PoolFormInfo) {
+ this.pgAutoscaleModes = info.pg_autoscale_modes;
+ this.form.silentSet('pgAutoscaleMode', info.pg_autoscale_default_mode);
this.form.silentSet('algorithm', info.bluestore_compression_algorithm);
this.info = info;
+ this.initEcp(info.erasure_code_profiles);
}
private initEcp(ecProfiles: ErasureCodeProfile[]) {
- const control = this.form.get('erasureProfile');
- if (ecProfiles.length <= 1) {
- control.disable();
+ this.setListControlStatus('erasureProfile', ecProfiles);
+ this.ecProfiles = ecProfiles;
+ }
+
+ private setListControlStatus(controlName: string, arr: any[]) {
+ const control = this.form.get(controlName);
+ if (arr.length === 1) {
+ control.setValue(arr[0]);
}
- if (ecProfiles.length === 1) {
- control.setValue(ecProfiles[0]);
- } else if (ecProfiles.length > 1 && control.disabled) {
+ if (arr.length <= 1) {
+ control.disable();
+ } else if (control.disabled) {
control.enable();
}
- this.ecProfiles = ecProfiles;
}
private initEditMode() {
sourceType: RbdConfigurationSourceField.pool
});
this.rulesChange(pool.type);
+ const rules = this.info.crush_rules_replicated.concat(this.info.crush_rules_erasure);
const dataMap = {
name: pool.pool_name,
poolType: pool.type,
- crushRule: this.info['crush_rules_' + pool.type].find(
- (rule: CrushRule) => rule.rule_name === pool.crush_rule
- ),
+ crushRule: rules.find((rule: CrushRule) => rule.rule_name === pool.crush_rule),
size: pool.size,
erasureProfile: this.ecProfiles.find((ecp) => ecp.name === pool.erasure_code_profile),
pgAutoscaleMode: pool.pg_autoscale_mode,
max_bytes: this.dimlessBinaryPipe.transform(pool.quota_max_bytes),
max_objects: pool.quota_max_objects
};
-
Object.keys(dataMap).forEach((controlName: string) => {
const value = dataMap[controlName];
if (!_.isUndefined(value) && value !== '') {
this.rulesChange(poolType);
});
this.form.get('crushRule').valueChanges.subscribe(() => {
- if (this.isReplicated) {
- this.replicatedRuleChange();
- }
+ // The crush rule can only be changed if type 'replicated' is set.
+ this.replicatedRuleChange();
this.pgCalc();
});
this.form.get('size').valueChanges.subscribe(() => {
+ // The size can only be changed if type 'replicated' is set.
this.pgCalc();
});
this.form.get('erasureProfile').valueChanges.subscribe(() => {
+ // The ec profile can only be changed if type 'erasure' is set.
this.pgCalc();
});
this.form.get('mode').valueChanges.subscribe(() => {
control.setValue(null);
control.enable();
}
- this.current.rules = rules;
+ this.replicatedRuleChange();
+ this.pgCalc();
}
private setTypeBooleans(replicated: boolean, erasure: boolean) {