summaryrefslogtreecommitdiffstats
path: root/src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/pool-form/pool-form.component.ts
diff options
context:
space:
mode:
Diffstat (limited to 'src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/pool-form/pool-form.component.ts')
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/pool-form/pool-form.component.ts919
1 files changed, 919 insertions, 0 deletions
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/pool-form/pool-form.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/pool-form/pool-form.component.ts
new file mode 100644
index 000000000..4778562cb
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/pool-form/pool-form.component.ts
@@ -0,0 +1,919 @@
+import { Component, OnInit, Type, ViewChild } from '@angular/core';
+import { FormControl, Validators } from '@angular/forms';
+import { ActivatedRoute, Router } from '@angular/router';
+
+import { NgbNav, NgbTooltip } from '@ng-bootstrap/ng-bootstrap';
+import _ from 'lodash';
+import { Observable, ReplaySubject, Subscription } from 'rxjs';
+
+import { DashboardNotFoundError } from '~/app/core/error/error';
+import { CrushRuleService } from '~/app/shared/api/crush-rule.service';
+import { ErasureCodeProfileService } from '~/app/shared/api/erasure-code-profile.service';
+import { PoolService } from '~/app/shared/api/pool.service';
+import { CrushNodeSelectionClass } from '~/app/shared/classes/crush.node.selection.class';
+import { CriticalConfirmationModalComponent } from '~/app/shared/components/critical-confirmation-modal/critical-confirmation-modal.component';
+import { SelectOption } from '~/app/shared/components/select/select-option.model';
+import { ActionLabelsI18n, URLVerbs } from '~/app/shared/constants/app.constants';
+import { Icons } from '~/app/shared/enum/icons.enum';
+import { CdForm } from '~/app/shared/forms/cd-form';
+import { CdFormGroup } from '~/app/shared/forms/cd-form-group';
+import { CdValidators } from '~/app/shared/forms/cd-validators';
+import {
+ RbdConfigurationEntry,
+ RbdConfigurationSourceField
+} from '~/app/shared/models/configuration';
+import { CrushRule } from '~/app/shared/models/crush-rule';
+import { CrushStep } from '~/app/shared/models/crush-step';
+import { ErasureCodeProfile } from '~/app/shared/models/erasure-code-profile';
+import { FinishedTask } from '~/app/shared/models/finished-task';
+import { Permission } from '~/app/shared/models/permissions';
+import { PoolFormInfo } from '~/app/shared/models/pool-form-info';
+import { DimlessBinaryPipe } from '~/app/shared/pipes/dimless-binary.pipe';
+import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
+import { FormatterService } from '~/app/shared/services/formatter.service';
+import { ModalService } from '~/app/shared/services/modal.service';
+import { TaskWrapperService } from '~/app/shared/services/task-wrapper.service';
+import { CrushRuleFormModalComponent } from '../crush-rule-form-modal/crush-rule-form-modal.component';
+import { ErasureCodeProfileFormModalComponent } from '../erasure-code-profile-form/erasure-code-profile-form-modal.component';
+import { Pool } from '../pool';
+import { PoolFormData } from './pool-form-data';
+
+interface FormFieldDescription {
+ externalFieldName: string;
+ formControlName: string;
+ attr?: string;
+ replaceFn?: Function;
+ editable?: boolean;
+ resetValue?: any;
+}
+
+@Component({
+ selector: 'cd-pool-form',
+ templateUrl: './pool-form.component.html',
+ styleUrls: ['./pool-form.component.scss']
+})
+export class PoolFormComponent extends CdForm implements OnInit {
+ @ViewChild('crushInfoTabs') crushInfoTabs: NgbNav;
+ @ViewChild('crushDeletionBtn') crushDeletionBtn: NgbTooltip;
+ @ViewChild('ecpInfoTabs') ecpInfoTabs: NgbNav;
+ @ViewChild('ecpDeletionBtn') ecpDeletionBtn: NgbTooltip;
+
+ permission: Permission;
+ form: CdFormGroup;
+ ecProfiles: ErasureCodeProfile[];
+ info: PoolFormInfo;
+ routeParamsSubscribe: any;
+ editing = false;
+ isReplicated = false;
+ isErasure = false;
+ data = new PoolFormData();
+ externalPgChange = false;
+ current: Record<string, any> = {
+ rules: []
+ };
+ initializeConfigData = new ReplaySubject<{
+ initialData: RbdConfigurationEntry[];
+ sourceType: RbdConfigurationSourceField;
+ }>(1);
+ currentConfigurationValues: { [configKey: string]: any } = {};
+ action: string;
+ resource: string;
+ icons = Icons;
+ pgAutoscaleModes: string[];
+ crushUsage: string[] = undefined; // Will only be set if a rule is used by some pool
+ ecpUsage: string[] = undefined; // Will only be set if a rule is used by some pool
+
+ private modalSubscription: Subscription;
+
+ constructor(
+ private dimlessBinaryPipe: DimlessBinaryPipe,
+ private route: ActivatedRoute,
+ private router: Router,
+ private modalService: ModalService,
+ private poolService: PoolService,
+ private authStorageService: AuthStorageService,
+ private formatter: FormatterService,
+ private taskWrapper: TaskWrapperService,
+ private ecpService: ErasureCodeProfileService,
+ private crushRuleService: CrushRuleService,
+ public actionLabels: ActionLabelsI18n
+ ) {
+ super();
+ this.editing = this.router.url.startsWith(`/pool/${URLVerbs.EDIT}`);
+ this.action = this.editing ? this.actionLabels.EDIT : this.actionLabels.CREATE;
+ this.resource = $localize`pool`;
+ this.authenticate();
+ this.createForm();
+ }
+
+ authenticate() {
+ this.permission = this.authStorageService.getPermissions().pool;
+ if (
+ !this.permission.read ||
+ (!this.permission.update && this.editing) ||
+ (!this.permission.create && !this.editing)
+ ) {
+ throw new DashboardNotFoundError();
+ }
+ }
+
+ private createForm() {
+ const compressionForm = new CdFormGroup({
+ mode: new FormControl('none'),
+ algorithm: new FormControl(''),
+ minBlobSize: new FormControl('', {
+ updateOn: 'blur'
+ }),
+ maxBlobSize: new FormControl('', {
+ updateOn: 'blur'
+ }),
+ ratio: new FormControl('', {
+ updateOn: 'blur'
+ })
+ });
+
+ this.form = new CdFormGroup(
+ {
+ name: new FormControl('', {
+ validators: [
+ Validators.pattern(/^[.A-Za-z0-9_/-]+$/),
+ Validators.required,
+ CdValidators.custom('rbdPool', () => {
+ return (
+ this.form &&
+ this.form.getValue('name').includes('/') &&
+ this.data &&
+ this.data.applications.selected.indexOf('rbd') !== -1
+ );
+ })
+ ]
+ }),
+ poolType: new FormControl('', {
+ validators: [Validators.required]
+ }),
+ crushRule: new FormControl(null, {
+ validators: [
+ 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
+ )
+ ]
+ }),
+ size: new FormControl('', {
+ updateOn: 'blur'
+ }),
+ erasureProfile: new FormControl(null),
+ pgNum: new FormControl('', {
+ validators: [Validators.required]
+ }),
+ pgAutoscaleMode: new FormControl(null),
+ ecOverwrites: new FormControl(false),
+ compression: compressionForm,
+ max_bytes: new FormControl(''),
+ max_objects: new FormControl(0)
+ },
+ [CdValidators.custom('form', (): null => null)]
+ );
+ }
+
+ ngOnInit() {
+ this.poolService.getInfo().subscribe((info: PoolFormInfo) => {
+ this.initInfo(info);
+ if (this.editing) {
+ this.initEditMode();
+ } else {
+ this.setAvailableApps();
+ this.loadingReady();
+ }
+ 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[]) {
+ this.setListControlStatus('erasureProfile', ecProfiles);
+ this.ecProfiles = ecProfiles;
+ }
+
+ /**
+ * Used to update the crush rule or erasure code profile listings.
+ *
+ * If only one rule or profile exists it will be selected.
+ * If nothing exists null will be selected.
+ * If more than one rule or profile exists the listing will be enabled,
+ * otherwise disabled.
+ */
+ private setListControlStatus(controlName: string, arr: any[]) {
+ const control = this.form.get(controlName);
+ const value = control.value;
+ if (arr.length === 1 && (!value || !_.isEqual(value, arr[0]))) {
+ control.setValue(arr[0]);
+ } else if (arr.length === 0 && value) {
+ control.setValue(null);
+ }
+ if (arr.length <= 1) {
+ if (control.enabled) {
+ control.disable();
+ }
+ } else if (control.disabled) {
+ control.enable();
+ }
+ }
+
+ private initEditMode() {
+ this.disableForEdit();
+ this.routeParamsSubscribe = this.route.params.subscribe((param: { name: string }) =>
+ this.poolService.get(param.name).subscribe((pool: Pool) => {
+ this.data.pool = pool;
+ this.initEditFormData(pool);
+ this.loadingReady();
+ })
+ );
+ }
+
+ private disableForEdit() {
+ ['poolType', 'crushRule', 'size', 'erasureProfile', 'ecOverwrites'].forEach((controlName) =>
+ this.form.get(controlName).disable()
+ );
+ }
+
+ private initEditFormData(pool: Pool) {
+ this.initializeConfigData.next({
+ initialData: pool.configuration,
+ sourceType: RbdConfigurationSourceField.pool
+ });
+ this.poolTypeChange(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: 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,
+ pgNum: pool.pg_num,
+ ecOverwrites: pool.flags_names.includes('ec_overwrites'),
+ mode: pool.options.compression_mode,
+ algorithm: pool.options.compression_algorithm,
+ minBlobSize: this.dimlessBinaryPipe.transform(pool.options.compression_min_blob_size),
+ maxBlobSize: this.dimlessBinaryPipe.transform(pool.options.compression_max_blob_size),
+ ratio: pool.options.compression_required_ratio,
+ 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.form.silentSet(controlName, value);
+ }
+ });
+ this.data.pgs = this.form.getValue('pgNum');
+ this.setAvailableApps(this.data.applications.default.concat(pool.application_metadata));
+ this.data.applications.selected = pool.application_metadata;
+ }
+
+ private setAvailableApps(apps: string[] = this.data.applications.default) {
+ this.data.applications.available = _.uniq(apps.sort()).map(
+ (x: string) => new SelectOption(false, x, '')
+ );
+ }
+
+ private listenToChanges() {
+ this.listenToChangesDuringAddEdit();
+ if (!this.editing) {
+ this.listenToChangesDuringAdd();
+ }
+ }
+
+ private listenToChangesDuringAddEdit() {
+ this.form.get('pgNum').valueChanges.subscribe((pgs) => {
+ const change = pgs - this.data.pgs;
+ if (Math.abs(change) !== 1 || pgs === 2) {
+ this.data.pgs = pgs;
+ return;
+ }
+ this.doPgPowerJump(change as 1 | -1);
+ });
+ }
+
+ private doPgPowerJump(jump: 1 | -1) {
+ const power = this.calculatePgPower() + jump;
+ this.setPgs(jump === -1 ? Math.round(power) : Math.floor(power));
+ }
+
+ private calculatePgPower(pgs = this.form.getValue('pgNum')): number {
+ return Math.log(pgs) / Math.log(2);
+ }
+
+ private setPgs(power: number) {
+ const pgs = Math.pow(2, power < 0 ? 0 : power); // Set size the nearest accurate size.
+ this.data.pgs = pgs;
+ this.form.silentSet('pgNum', pgs);
+ }
+
+ private listenToChangesDuringAdd() {
+ this.form.get('poolType').valueChanges.subscribe((poolType) => {
+ this.poolTypeChange(poolType);
+ });
+ this.form.get('crushRule').valueChanges.subscribe((rule) => {
+ // The crush rule can only be changed if type 'replicated' is set.
+ if (this.crushDeletionBtn && this.crushDeletionBtn.isOpen()) {
+ this.crushDeletionBtn.close();
+ }
+ if (!rule) {
+ return;
+ }
+ this.setCorrectMaxSize(rule);
+ this.crushRuleIsUsedBy(rule.rule_name);
+ 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((profile) => {
+ // The ec profile can only be changed if type 'erasure' is set.
+ if (this.ecpDeletionBtn && this.ecpDeletionBtn.isOpen()) {
+ this.ecpDeletionBtn.close();
+ }
+ if (!profile) {
+ return;
+ }
+ this.ecpIsUsedBy(profile.name);
+ this.pgCalc();
+ });
+ this.form.get('mode').valueChanges.subscribe(() => {
+ ['minBlobSize', 'maxBlobSize', 'ratio'].forEach((name) => {
+ this.form.get(name).updateValueAndValidity({ emitEvent: false });
+ });
+ });
+ this.form.get('minBlobSize').valueChanges.subscribe(() => {
+ this.form.get('maxBlobSize').updateValueAndValidity({ emitEvent: false });
+ });
+ this.form.get('maxBlobSize').valueChanges.subscribe(() => {
+ this.form.get('minBlobSize').updateValueAndValidity({ emitEvent: false });
+ });
+ }
+
+ private poolTypeChange(poolType: string) {
+ if (poolType === 'replicated') {
+ this.setTypeBooleans(true, false);
+ } else if (poolType === 'erasure') {
+ this.setTypeBooleans(false, true);
+ } else {
+ this.setTypeBooleans(false, false);
+ }
+ if (!poolType || !this.info) {
+ this.current.rules = [];
+ return;
+ }
+ const rules = this.info['crush_rules_' + poolType] || [];
+ this.current.rules = rules;
+ if (this.editing) {
+ return;
+ }
+ if (this.isReplicated) {
+ this.setListControlStatus('crushRule', rules);
+ }
+ this.replicatedRuleChange();
+ this.pgCalc();
+ }
+
+ private setTypeBooleans(replicated: boolean, erasure: boolean) {
+ this.isReplicated = replicated;
+ this.isErasure = erasure;
+ }
+
+ private replicatedRuleChange() {
+ if (!this.isReplicated) {
+ return;
+ }
+ const control = this.form.get('size');
+ let size = this.form.getValue('size') || 3;
+ const min = this.getMinSize();
+ const max = this.getMaxSize();
+ if (size < min) {
+ size = min;
+ } else if (size > max) {
+ size = max;
+ }
+ if (size !== control.value) {
+ this.form.silentSet('size', size);
+ }
+ }
+
+ getMinSize(): number {
+ if (!this.info || this.info.osd_count < 1) {
+ return 0;
+ }
+ const rule = this.form.getValue('crushRule');
+ if (rule) {
+ return rule.min_size;
+ }
+ return 1;
+ }
+
+ getMaxSize(): number {
+ const rule = this.form.getValue('crushRule');
+ if (!this.info) {
+ return 0;
+ }
+ if (!rule) {
+ const osds = this.info.osd_count;
+ const defaultSize = 3;
+ return Math.min(osds, defaultSize);
+ }
+ return rule.usable_size;
+ }
+
+ private pgCalc() {
+ const poolType = this.form.getValue('poolType');
+ if (!this.info || this.form.get('pgNum').dirty || !poolType) {
+ return;
+ }
+ const pgMax = this.info.osd_count * 100;
+ const pgs = this.isReplicated ? this.replicatedPgCalc(pgMax) : this.erasurePgCalc(pgMax);
+ if (!pgs) {
+ return;
+ }
+ const oldValue = this.data.pgs;
+ this.alignPgs(pgs);
+ const newValue = this.data.pgs;
+ if (!this.externalPgChange) {
+ this.externalPgChange = oldValue !== newValue;
+ }
+ }
+
+ private setCorrectMaxSize(rule: CrushRule = this.form.getValue('crushRule')) {
+ if (!rule) {
+ return;
+ }
+ const domains = CrushNodeSelectionClass.searchFailureDomains(
+ this.info.nodes,
+ rule.steps[0].item_name
+ );
+ const currentDomain = domains[rule.steps[1].type];
+ const usable = currentDomain ? currentDomain.length : rule.max_size;
+ rule.usable_size = Math.min(usable, rule.max_size);
+ }
+
+ private replicatedPgCalc(pgs: number): number {
+ const sizeControl = this.form.get('size');
+ const size = sizeControl.value;
+ return sizeControl.valid && size > 0 ? pgs / size : 0;
+ }
+
+ private erasurePgCalc(pgs: number): number {
+ const ecpControl = this.form.get('erasureProfile');
+ const ecp = ecpControl.value;
+ return (ecpControl.valid || ecpControl.disabled) && ecp ? pgs / (ecp.k + ecp.m) : 0;
+ }
+
+ alignPgs(pgs = this.form.getValue('pgNum')) {
+ this.setPgs(Math.round(this.calculatePgPower(pgs < 1 ? 1 : pgs)));
+ }
+
+ private setComplexValidators() {
+ if (this.editing) {
+ this.form
+ .get('name')
+ .setValidators([
+ this.form.get('name').validator,
+ CdValidators.custom(
+ 'uniqueName',
+ (name: string) =>
+ this.data.pool &&
+ this.info &&
+ this.info.pool_names.indexOf(name) !== -1 &&
+ this.info.pool_names.indexOf(name) !==
+ this.info.pool_names.indexOf(this.data.pool.pool_name)
+ )
+ ]);
+ } else {
+ CdValidators.validateIf(this.form.get('size'), () => this.isReplicated, [
+ CdValidators.custom(
+ 'min',
+ (value: number) => this.form.getValue('size') && value < this.getMinSize()
+ ),
+ CdValidators.custom(
+ 'max',
+ (value: number) => this.form.getValue('size') && this.getMaxSize() < value
+ )
+ ]);
+ this.form
+ .get('name')
+ .setValidators([
+ this.form.get('name').validator,
+ CdValidators.custom(
+ 'uniqueName',
+ (name: string) => this.info && this.info.pool_names.indexOf(name) !== -1
+ )
+ ]);
+ }
+ this.setCompressionValidators();
+ }
+
+ private setCompressionValidators() {
+ CdValidators.validateIf(this.form.get('minBlobSize'), () => this.hasCompressionEnabled(), [
+ Validators.min(0),
+ CdValidators.custom('maximum', (size: string) =>
+ this.oddBlobSize(size, this.form.getValue('maxBlobSize'))
+ )
+ ]);
+ CdValidators.validateIf(this.form.get('maxBlobSize'), () => this.hasCompressionEnabled(), [
+ Validators.min(0),
+ CdValidators.custom('minimum', (size: string) =>
+ this.oddBlobSize(this.form.getValue('minBlobSize'), size)
+ )
+ ]);
+ CdValidators.validateIf(this.form.get('ratio'), () => this.hasCompressionEnabled(), [
+ Validators.min(0),
+ Validators.max(1)
+ ]);
+ }
+
+ private oddBlobSize(minimum: string, maximum: string) {
+ const min = this.formatter.toBytes(minimum);
+ const max = this.formatter.toBytes(maximum);
+ return Boolean(min && max && min >= max);
+ }
+
+ hasCompressionEnabled() {
+ return this.form.getValue('mode') && this.form.get('mode').value.toLowerCase() !== 'none';
+ }
+
+ describeCrushStep(step: CrushStep) {
+ return [
+ step.op.replace('_', ' '),
+ step.item_name || '',
+ step.type ? step.num + ' type ' + step.type : ''
+ ].join(' ');
+ }
+
+ addErasureCodeProfile() {
+ this.addModal(ErasureCodeProfileFormModalComponent, (name) => this.reloadECPs(name));
+ }
+
+ private addModal(modalComponent: Type<any>, reload: (name: string) => void) {
+ this.hideOpenTooltips();
+ const modalRef = this.modalService.show(modalComponent);
+ modalRef.componentInstance.submitAction.subscribe((item: any) => {
+ reload(item.name);
+ });
+ }
+
+ private hideOpenTooltips() {
+ const hideTooltip = (btn: NgbTooltip) => btn && btn.isOpen() && btn.close();
+ hideTooltip(this.ecpDeletionBtn);
+ hideTooltip(this.crushDeletionBtn);
+ }
+
+ private reloadECPs(profileName?: string) {
+ this.reloadList({
+ newItemName: profileName,
+ getInfo: () => this.ecpService.list(),
+ initInfo: (profiles) => this.initEcp(profiles),
+ findNewItem: () => this.ecProfiles.find((p) => p.name === profileName),
+ controlName: 'erasureProfile'
+ });
+ }
+
+ private reloadList({
+ newItemName,
+ getInfo,
+ initInfo,
+ findNewItem,
+ controlName
+ }: {
+ newItemName: string;
+ getInfo: () => Observable<any>;
+ initInfo: (items: any) => void;
+ findNewItem: () => any;
+ controlName: string;
+ }) {
+ if (this.modalSubscription) {
+ this.modalSubscription.unsubscribe();
+ }
+ getInfo().subscribe((items: any) => {
+ initInfo(items);
+ if (!newItemName) {
+ return;
+ }
+ const item = findNewItem();
+ if (item) {
+ this.form.get(controlName).setValue(item);
+ }
+ });
+ }
+
+ deleteErasureCodeProfile() {
+ this.deletionModal({
+ value: this.form.getValue('erasureProfile'),
+ usage: this.ecpUsage,
+ deletionBtn: this.ecpDeletionBtn,
+ dataName: 'erasureInfo',
+ getTabs: () => this.ecpInfoTabs,
+ tabPosition: 'used-by-pools',
+ nameAttribute: 'name',
+ itemDescription: $localize`erasure code profile`,
+ reloadFn: () => this.reloadECPs(),
+ deleteFn: (name) => this.ecpService.delete(name),
+ taskName: 'ecp/delete'
+ });
+ }
+
+ private deletionModal({
+ value,
+ usage,
+ deletionBtn,
+ dataName,
+ getTabs,
+ tabPosition,
+ nameAttribute,
+ itemDescription,
+ reloadFn,
+ deleteFn,
+ taskName
+ }: {
+ value: any;
+ usage: string[];
+ deletionBtn: NgbTooltip;
+ dataName: string;
+ getTabs: () => NgbNav;
+ tabPosition: string;
+ nameAttribute: string;
+ itemDescription: string;
+ reloadFn: Function;
+ deleteFn: (name: string) => Observable<any>;
+ taskName: string;
+ }) {
+ if (!value) {
+ return;
+ }
+ if (usage) {
+ deletionBtn.animation = false;
+ deletionBtn.toggle();
+ this.data[dataName] = true;
+ setTimeout(() => {
+ const tabs = getTabs();
+ if (tabs) {
+ tabs.select(tabPosition);
+ }
+ }, 50);
+ return;
+ }
+ const name = value[nameAttribute];
+ this.modalService.show(CriticalConfirmationModalComponent, {
+ itemDescription,
+ itemNames: [name],
+ submitActionObservable: () => {
+ const deletion = deleteFn(name);
+ deletion.subscribe(() => reloadFn());
+ return this.taskWrapper.wrapTaskAroundCall({
+ task: new FinishedTask(taskName, { name: name }),
+ call: deletion
+ });
+ }
+ });
+ }
+
+ addCrushRule() {
+ this.addModal(CrushRuleFormModalComponent, (name) => this.reloadCrushRules(name));
+ }
+
+ private reloadCrushRules(ruleName?: string) {
+ this.reloadList({
+ newItemName: ruleName,
+ getInfo: () => this.poolService.getInfo(),
+ initInfo: (info) => {
+ this.initInfo(info);
+ this.poolTypeChange('replicated');
+ },
+ findNewItem: () =>
+ this.info.crush_rules_replicated.find((rule) => rule.rule_name === ruleName),
+ controlName: 'crushRule'
+ });
+ }
+
+ deleteCrushRule() {
+ this.deletionModal({
+ value: this.form.getValue('crushRule'),
+ usage: this.crushUsage,
+ deletionBtn: this.crushDeletionBtn,
+ dataName: 'crushInfo',
+ getTabs: () => this.crushInfoTabs,
+ tabPosition: 'used-by-pools',
+ nameAttribute: 'rule_name',
+ itemDescription: $localize`crush rule`,
+ reloadFn: () => this.reloadCrushRules(),
+ deleteFn: (name) => this.crushRuleService.delete(name),
+ taskName: 'crushRule/delete'
+ });
+ }
+
+ crushRuleIsUsedBy(ruleName: string) {
+ this.crushUsage = ruleName ? this.info.used_rules[ruleName] : undefined;
+ }
+
+ ecpIsUsedBy(profileName: string) {
+ this.ecpUsage = profileName ? this.info.used_profiles[profileName] : undefined;
+ }
+
+ submit() {
+ if (this.form.invalid) {
+ this.form.setErrors({ cdSubmitButton: true });
+ return;
+ }
+
+ const pool = { pool: this.form.getValue('name') };
+
+ this.assignFormFields(pool, [
+ { externalFieldName: 'pool_type', formControlName: 'poolType' },
+ {
+ externalFieldName: 'pg_autoscale_mode',
+ formControlName: 'pgAutoscaleMode',
+ editable: true
+ },
+ {
+ externalFieldName: 'pg_num',
+ formControlName: 'pgNum',
+ replaceFn: (value: number) => (this.form.getValue('pgAutoscaleMode') === 'on' ? 1 : value),
+ editable: true
+ },
+ this.isReplicated
+ ? { externalFieldName: 'size', formControlName: 'size' }
+ : {
+ externalFieldName: 'erasure_code_profile',
+ formControlName: 'erasureProfile',
+ attr: 'name'
+ },
+ {
+ externalFieldName: 'rule_name',
+ formControlName: 'crushRule',
+ replaceFn: (value: CrushRule) => (this.isReplicated ? value && value.rule_name : undefined)
+ },
+ {
+ externalFieldName: 'quota_max_bytes',
+ formControlName: 'max_bytes',
+ replaceFn: this.formatter.toBytes,
+ editable: true,
+ resetValue: this.editing ? 0 : undefined
+ },
+ {
+ externalFieldName: 'quota_max_objects',
+ formControlName: 'max_objects',
+ editable: true,
+ resetValue: this.editing ? 0 : undefined
+ }
+ ]);
+
+ if (this.info.is_all_bluestore) {
+ this.assignFormField(pool, {
+ externalFieldName: 'flags',
+ formControlName: 'ecOverwrites',
+ replaceFn: () => (this.isErasure ? ['ec_overwrites'] : undefined)
+ });
+
+ if (this.form.getValue('mode') !== 'none') {
+ this.assignFormFields(pool, [
+ {
+ externalFieldName: 'compression_mode',
+ formControlName: 'mode',
+ editable: true,
+ replaceFn: (value: boolean) => this.hasCompressionEnabled() && value
+ },
+ {
+ externalFieldName: 'compression_algorithm',
+ formControlName: 'algorithm',
+ editable: true
+ },
+ {
+ externalFieldName: 'compression_min_blob_size',
+ formControlName: 'minBlobSize',
+ replaceFn: this.formatter.toBytes,
+ editable: true,
+ resetValue: 0
+ },
+ {
+ externalFieldName: 'compression_max_blob_size',
+ formControlName: 'maxBlobSize',
+ replaceFn: this.formatter.toBytes,
+ editable: true,
+ resetValue: 0
+ },
+ {
+ externalFieldName: 'compression_required_ratio',
+ formControlName: 'ratio',
+ editable: true,
+ resetValue: 0
+ }
+ ]);
+ } else if (this.editing) {
+ this.assignFormFields(pool, [
+ {
+ externalFieldName: 'compression_mode',
+ formControlName: 'mode',
+ editable: true,
+ replaceFn: () => 'unset' // Is used if no compression is set
+ },
+ {
+ externalFieldName: 'srcpool',
+ formControlName: 'name',
+ editable: true,
+ replaceFn: () => this.data.pool.pool_name
+ }
+ ]);
+ }
+ }
+
+ const apps = this.data.applications.selected;
+ if (apps.length > 0 || this.editing) {
+ pool['application_metadata'] = apps;
+ }
+
+ // Only collect configuration data for replicated pools, as QoS cannot be configured on EC
+ // pools. EC data pools inherit their settings from the corresponding replicated metadata pool.
+ if (this.isReplicated && !_.isEmpty(this.currentConfigurationValues)) {
+ pool['configuration'] = this.currentConfigurationValues;
+ }
+
+ this.triggerApiTask(pool);
+ }
+
+ /**
+ * Retrieves the values for the given form field descriptions and assigns the values to the given
+ * object. This method differentiates between `add` and `edit` mode and acts differently on one or
+ * the other.
+ */
+ private assignFormFields(pool: object, formFieldDescription: FormFieldDescription[]): void {
+ formFieldDescription.forEach((item) => this.assignFormField(pool, item));
+ }
+
+ /**
+ * Retrieves the value for the given form field description and assigns the values to the given
+ * object. This method differentiates between `add` and `edit` mode and acts differently on one or
+ * the other.
+ */
+ private assignFormField(
+ pool: object,
+ {
+ externalFieldName,
+ formControlName,
+ attr,
+ replaceFn,
+ editable,
+ resetValue
+ }: FormFieldDescription
+ ): void {
+ if (this.editing && (!editable || this.form.get(formControlName).pristine)) {
+ return;
+ }
+ const value = this.form.getValue(formControlName);
+ let apiValue = replaceFn ? replaceFn(value) : attr ? _.get(value, attr) : value;
+ if (!value || !apiValue) {
+ if (editable && !_.isUndefined(resetValue)) {
+ apiValue = resetValue;
+ } else {
+ return;
+ }
+ }
+ pool[externalFieldName] = apiValue;
+ }
+
+ private triggerApiTask(pool: Record<string, any>) {
+ this.taskWrapper
+ .wrapTaskAroundCall({
+ task: new FinishedTask('pool/' + (this.editing ? URLVerbs.EDIT : URLVerbs.CREATE), {
+ pool_name: pool.hasOwnProperty('srcpool') ? pool.srcpool : pool.pool
+ }),
+ call: this.poolService[this.editing ? URLVerbs.UPDATE : URLVerbs.CREATE](pool)
+ })
+ .subscribe({
+ error: (resp) => {
+ if (_.isObject(resp.error) && resp.error.code === '34') {
+ this.form.get('pgNum').setErrors({ '34': true });
+ }
+ this.form.setErrors({ cdSubmitButton: true });
+ },
+ complete: () => this.router.navigate(['/pool'])
+ });
+ }
+
+ appSelection() {
+ this.form.get('name').updateValueAndValidity({ emitEvent: false, onlySelf: true });
+ }
+}