File

src/app/ceph/pool/pool-form/pool-form.component.ts

Implements

OnInit

Metadata

selector cd-pool-form
styleUrls ./pool-form.component.scss
templateUrl ./pool-form.component.html

Index

Properties
Methods

Constructor

constructor(dimlessBinaryPipe: DimlessBinaryPipe, route: ActivatedRoute, router: Router, modalService: BsModalService, poolService: PoolService, authStorageService: AuthStorageService, formatter: FormatterService, bsModalService: BsModalService, taskWrapper: TaskWrapperService, ecpService: ErasureCodeProfileService, i18n: I18n, actionLabels: ActionLabelsI18n)
Parameters :
Name Type Optional
dimlessBinaryPipe DimlessBinaryPipe No
route ActivatedRoute No
router Router No
modalService BsModalService No
poolService PoolService No
authStorageService AuthStorageService No
formatter FormatterService No
bsModalService BsModalService No
taskWrapper TaskWrapperService No
ecpService ErasureCodeProfileService No
i18n I18n No
actionLabels ActionLabelsI18n No

Methods

addErasureCodeProfile
addErasureCodeProfile()
Returns : void
Private alignPgs
alignPgs(pgs)
Parameters :
Name Optional Default value
pgs No this.form.getValue('pgNum')
Returns : void
appSelection
appSelection()
Returns : void
Private assignFormField
assignFormField(pool: object, undefined: FormFieldDescription)

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.

Parameters :
Name Type Optional
pool object No
FormFieldDescription No
Returns : void
Private assignFormFields
assignFormFields(pool: object, formFieldDescription: FormFieldDescription[])

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.

Parameters :
Name Type Optional
pool object No
formFieldDescription FormFieldDescription[] No
Returns : void
authenticate
authenticate()
Returns : void
Private calculatePgPower
calculatePgPower(pgs)
Parameters :
Name Optional Default value
pgs No this.form.getValue('pgNum')
Returns : number
Private createForm
createForm()
Returns : void
deleteErasureCodeProfile
deleteErasureCodeProfile()
Returns : void
describeCrushStep
describeCrushStep(step: CrushStep)
Parameters :
Name Type Optional
step CrushStep No
Returns : any
Private disableForEdit
disableForEdit()
Returns : void
Private doPgPowerJump
doPgPowerJump(jump: "1" | "undefined")
Parameters :
Name Type Optional
jump "1" | "undefined" No
Returns : void
Private erasurePgCalc
erasurePgCalc(pgs)
Parameters :
Name Optional
pgs No
Returns : number
getMaxSize
getMaxSize()
Returns : number
getMinSize
getMinSize()
Returns : number
hasCompressionEnabled
hasCompressionEnabled()
Returns : boolean
Private initEcp
initEcp(ecProfiles: ErasureCodeProfile[])
Parameters :
Name Type Optional
ecProfiles ErasureCodeProfile[] No
Returns : void
Private initEditFormData
initEditFormData(pool: Pool)
Parameters :
Name Type Optional
pool Pool No
Returns : void
Private initEditMode
initEditMode()
Returns : void
Private initInfo
initInfo(info: PoolFormInfo)
Parameters :
Name Type Optional
info PoolFormInfo No
Returns : void
Private listenToChanges
listenToChanges()
Returns : void
Private listenToChangesDuringAdd
listenToChangesDuringAdd()
Returns : void
Private listenToChangesDuringAddEdit
listenToChangesDuringAddEdit()
Returns : void
ngOnInit
ngOnInit()
Returns : void
Private oddBlobSize
oddBlobSize(minimum, maximum)
Parameters :
Name Optional
minimum No
maximum No
Returns : any
Private pgCalc
pgCalc()
Returns : void
Private reloadECPs
reloadECPs()
Returns : void
Private replicatedPgCalc
replicatedPgCalc(pgs)
Parameters :
Name Optional
pgs No
Returns : number
Private replicatedRuleChange
replicatedRuleChange()
Returns : void
Private rulesChange
rulesChange()
Returns : void
Private setComplexValidators
setComplexValidators()
Returns : void
Private setCompressionValidators
setCompressionValidators()
Returns : void
Private setPgs
setPgs(power: number)
Parameters :
Name Type Optional
power number No
Returns : void
submit
submit()
Returns : void
Private triggerApiTask
triggerApiTask(pool)
Parameters :
Name Optional
pool No
Returns : void

Properties

action
Type : string
Public actionLabels
Type : ActionLabelsI18n
current
Type : object
Default value : { rules: [] }
currentConfigurationValues
Type : literal type
Default value : {}
data
Default value : new PoolFormData(this.i18n)
ecProfiles
Type : ErasureCodeProfile[]
editing
Default value : false
externalPgChange
Default value : false
form
Type : CdFormGroup
info
Type : PoolFormInfo
initializeConfigData
Default value : new EventEmitter<{ initialData: RbdConfigurationEntry[]; sourceType: RbdConfigurationSourceField; }>()
Private modalSubscription
Type : Subscription
permission
Type : Permission
resource
Type : string
routeParamsSubscribe
Type : any
import { Component, EventEmitter, OnInit } 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 { ErasureCodeProfileService } from '../../../shared/api/erasure-code-profile.service';
import { PoolService } from '../../../shared/api/pool.service';
import { CriticalConfirmationModalComponent } from '../../../shared/components/critical-confirmation-modal/critical-confirmation-modal.component';
import { ActionLabelsI18n, URLVerbs } from '../../../shared/constants/app.constants';
import { CdFormGroup } from '../../../shared/forms/cd-form-group';
import { CdValidators } from '../../../shared/forms/cd-validators';
import {
  RbdConfigurationEntry,
  RbdConfigurationSourceField
} from '../../../shared/models/configuration';
import { CrushRule } from '../../../shared/models/crush-rule';
import { CrushStep } from '../../../shared/models/crush-step';
import { ErasureCodeProfile } from '../../../shared/models/erasure-code-profile';
import { FinishedTask } from '../../../shared/models/finished-task';
import { Permission } from '../../../shared/models/permissions';
import { PoolFormInfo } from '../../../shared/models/pool-form-info';
import { DimlessBinaryPipe } from '../../../shared/pipes/dimless-binary.pipe';
import { AuthStorageService } from '../../../shared/services/auth-storage.service';
import { FormatterService } from '../../../shared/services/formatter.service';
import { TaskWrapperService } from '../../../shared/services/task-wrapper.service';
import { ErasureCodeProfileFormComponent } from '../erasure-code-profile-form/erasure-code-profile-form.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 implements OnInit {
  permission: Permission;
  form: CdFormGroup;
  ecProfiles: ErasureCodeProfile[];
  info: PoolFormInfo;
  routeParamsSubscribe: any;
  editing = false;
  data = new PoolFormData(this.i18n);
  externalPgChange = false;
  private modalSubscription: Subscription;
  current = {
    rules: []
  };
  initializeConfigData = new EventEmitter<{
    initialData: RbdConfigurationEntry[];
    sourceType: RbdConfigurationSourceField;
  }>();
  currentConfigurationValues: { [configKey: string]: any } = {};
  action: string;
  resource: string;

  constructor(
    private dimlessBinaryPipe: DimlessBinaryPipe,
    private route: ActivatedRoute,
    private router: Router,
    private modalService: BsModalService,
    private poolService: PoolService,
    private authStorageService: AuthStorageService,
    private formatter: FormatterService,
    private bsModalService: BsModalService,
    private taskWrapper: TaskWrapperService,
    private ecpService: ErasureCodeProfileService,
    private i18n: I18n,
    public actionLabels: ActionLabelsI18n
  ) {
    this.editing = this.router.url.startsWith(`/pool/${URLVerbs.EDIT}`);
    this.action = this.editing ? this.actionLabels.EDIT : this.actionLabels.CREATE;
    this.resource = this.i18n('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))
    ) {
      this.router.navigate(['/404']);
    }
  }

  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]
        }),
        poolType: new FormControl('', {
          validators: [Validators.required]
        }),
        crushRule: new FormControl(null, {
          validators: [
            CdValidators.custom(
              'tooFewOsds',
              (rule) => this.info && rule && this.info.osd_count < rule.min_size
            )
          ]
        }),
        size: new FormControl('', {
          updateOn: 'blur'
        }),
        erasureProfile: new FormControl(null),
        pgNum: new FormControl('', {
          validators: [Validators.required, Validators.min(1)]
        }),
        ecOverwrites: new FormControl(false),
        compression: compressionForm
      },
      [
        CdValidators.custom('form', () => null),
        CdValidators.custom('rbdPool', () => {
          return (
            this.form &&
            this.form.getValue('name').includes('/') &&
            this.data &&
            this.data.applications.selected.indexOf('rbd') !== -1
          );
        })
      ]
    );
  }

  ngOnInit() {
    forkJoin(this.poolService.getInfo(), this.ecpService.list()).subscribe(
      (data: [PoolFormInfo, ErasureCodeProfile[]]) => {
        this.initInfo(data[0]);
        this.initEcp(data[1]);
        if (this.editing) {
          this.initEditMode();
        }
        this.listenToChanges();
        this.setComplexValidators();
      }
    );
  }

  private initInfo(info: PoolFormInfo) {
    this.form.silentSet('algorithm', info.bluestore_compression_algorithm);
    info.compression_modes.push('unset');
    this.info = info;
  }

  private initEcp(ecProfiles: ErasureCodeProfile[]) {
    const control = this.form.get('erasureProfile');
    if (ecProfiles.length <= 1) {
      control.disable();
    }
    if (ecProfiles.length === 1) {
      control.setValue(ecProfiles[0]);
    } else if (ecProfiles.length > 1 && control.disabled) {
      control.enable();
    }
    this.ecProfiles = ecProfiles;
  }

  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);
      })
    );
  }

  private disableForEdit() {
    ['poolType', 'crushRule', 'size', 'erasureProfile', 'ecOverwrites'].forEach((controlName) =>
      this.form.get(controlName).disable()
    );
  }

  private initEditFormData(pool: Pool) {
    this.initializeConfigData.emit({
      initialData: pool.configuration,
      sourceType: RbdConfigurationSourceField.pool
    });

    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
      ),
      size: pool.size,
      erasureProfile: this.ecProfiles.find((ecp) => ecp.name === pool.erasure_code_profile),
      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
    };

    Object.keys(dataMap).forEach((controlName: string) => {
      const value = dataMap[controlName];
      if (!_.isUndefined(value) && value !== '') {
        this.form.silentSet(controlName, value);
      }
    });
    this.data.applications.selected = pool.application_metadata;
  }

  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.form.get('size').updateValueAndValidity();
      this.rulesChange();
      if (poolType === 'replicated') {
        this.replicatedRuleChange();
      }
      this.pgCalc();
    });
    this.form.get('crushRule').valueChanges.subscribe(() => {
      if (this.form.getValue('poolType') === 'replicated') {
        this.replicatedRuleChange();
      }
      this.pgCalc();
    });
    this.form.get('size').valueChanges.subscribe(() => {
      this.pgCalc();
    });
    this.form.get('erasureProfile').valueChanges.subscribe(() => {
      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 rulesChange() {
    const poolType = this.form.getValue('poolType');
    if (!poolType || !this.info) {
      this.current.rules = [];
      return;
    }
    const rules = this.info['crush_rules_' + poolType] || [];
    const control = this.form.get('crushRule');
    if (rules.length === 1) {
      control.setValue(rules[0]);
      control.disable();
    } else {
      control.setValue(null);
      control.enable();
    }
    this.current.rules = rules;
  }

  private replicatedRuleChange() {
    if (this.form.getValue('poolType') !== 'replicated') {
      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;
    }
    const rule = this.form.getValue('crushRule');
    if (rule) {
      return rule.min_size;
    }
    return 1;
  }

  getMaxSize(): number {
    if (!this.info || this.info.osd_count < 1) {
      return;
    }
    const osds: number = this.info.osd_count;
    if (this.form.getValue('crushRule')) {
      const max: number = this.form.get('crushRule').value.max_size;
      if (max < osds) {
        return max;
      }
    }
    return osds;
  }

  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 =
      poolType === 'replicated' ? 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 replicatedPgCalc(pgs): number {
    const sizeControl = this.form.get('size');
    const size = sizeControl.value;
    if (sizeControl.valid && size > 0) {
      return pgs / size;
    }
  }

  private erasurePgCalc(pgs): number {
    const ecpControl = this.form.get('erasureProfile');
    const ecp = ecpControl.value;
    if ((ecpControl.valid || ecpControl.disabled) && ecp) {
      return pgs / (ecp.k + ecp.m);
    }
  }

  private alignPgs(pgs = this.form.getValue('pgNum')) {
    this.setPgs(Math.round(this.calculatePgPower(pgs < 1 ? 1 : pgs)));
  }

  private setComplexValidators() {
    if (this.editing) {
      this.form
        .get('pgNum')
        .setValidators(
          CdValidators.custom('noDecrease', (pgs) => this.data.pool && pgs < this.data.pool.pg_num)
        );
      this.form
        .get('name')
        .setValidators([
          this.form.get('name').validator,
          CdValidators.custom(
            'uniqueName',
            (name) =>
              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.form.get('poolType').value === 'replicated',
        [
          CdValidators.custom(
            'min',
            (value) => this.form.getValue('size') && value < this.getMinSize()
          ),
          CdValidators.custom(
            'max',
            (value) => this.form.getValue('size') && this.getMaxSize() < value
          )
        ]
      );
      this.form
        .get('name')
        .setValidators([
          this.form.get('name').validator,
          CdValidators.custom(
            'uniqueName',
            (name) => 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) =>
        this.oddBlobSize(size, this.form.getValue('maxBlobSize'))
      )
    ]);
    CdValidators.validateIf(this.form.get('maxBlobSize'), () => this.hasCompressionEnabled(), [
      Validators.min(0),
      CdValidators.custom('minimum', (size) =>
        this.oddBlobSize(this.form.getValue('minBlobSize'), size)
      )
    ]);
    CdValidators.validateIf(this.form.get('ratio'), () => this.hasCompressionEnabled(), [
      Validators.min(0),
      Validators.max(1)
    ]);
  }

  private oddBlobSize(minimum, maximum) {
    minimum = this.formatter.toBytes(minimum);
    maximum = this.formatter.toBytes(maximum);
    return Boolean(minimum && maximum && minimum >= maximum);
  }

  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.modalSubscription = this.modalService.onHide.subscribe(() => this.reloadECPs());
    this.bsModalService.show(ErasureCodeProfileFormComponent);
  }

  private reloadECPs() {
    this.ecpService.list().subscribe((profiles: ErasureCodeProfile[]) => this.initEcp(profiles));
    this.modalSubscription.unsubscribe();
  }

  deleteErasureCodeProfile() {
    const ecp = this.form.getValue('erasureProfile');
    if (!ecp) {
      return;
    }
    const name = ecp.name;
    this.modalSubscription = this.modalService.onHide.subscribe(() => this.reloadECPs());
    this.modalService.show(CriticalConfirmationModalComponent, {
      initialState: {
        itemDescription: this.i18n('erasure code profile'),
        submitActionObservable: () =>
          this.taskWrapper.wrapTaskAroundCall({
            task: new FinishedTask('ecp/delete', { name: name }),
            call: this.ecpService.delete(name)
          })
      }
    });
  }

  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_num', formControlName: 'pgNum', editable: true },
      this.form.getValue('poolType') === 'replicated'
        ? { externalFieldName: 'size', formControlName: 'size' }
        : {
            externalFieldName: 'erasure_code_profile',
            formControlName: 'erasureProfile',
            attr: 'name'
          },
      { externalFieldName: 'rule_name', formControlName: 'crushRule', attr: 'rule_name' }
    ]);

    if (this.info.is_all_bluestore) {
      this.assignFormField(pool, {
        externalFieldName: 'flags',
        formControlName: 'ecOverwrites',
        replaceFn: () => ['ec_overwrites']
      });

      if (this.form.getValue('mode') !== 'none') {
        this.assignFormFields(pool, [
          {
            externalFieldName: 'compression_mode',
            formControlName: 'mode',
            editable: true,
            replaceFn: (value) => 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'
          },
          {
            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.form.get('poolType').value === 'replicated' &&
      !_.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) {
    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(
        undefined,
        (resp) => {
          if (_.isObject(resp.error) && resp.error.code === '34') {
            this.form.get('pgNum').setErrors({ '34': true });
          }
          this.form.setErrors({ cdSubmitButton: true });
        },
        () => this.router.navigate(['/pool'])
      );
  }

  appSelection() {
    this.form.updateValueAndValidity({ emitEvent: false, onlySelf: true });
  }
}
<div class="col-sm-12 col-lg-6">
  <h1 *ngIf="!(info && ecProfiles)"
      class="jumbotron">
    <i class="fa fa-lg fa-pulse fa-spinner text-primary"></i>
    <ng-container i18n>Loading...</ng-container>
  </h1>
  <form name="form"
        *ngIf="info && ecProfiles"
        class="form-horizontal"
        #formDir="ngForm"
        [formGroup]="form"
        novalidate>
    <div class="panel panel-default">
      <div class="panel-heading">
        <h3 i18n="form title|Example: Create Pool@@formTitle"
            class="panel-title">{{ action | titlecase }} {{ resource | upperFirst }}</h3>
      </div>

      <div class="panel-body">
        <!-- Name -->
        <div class="form-group"
             [ngClass]="{'has-error': form.showError('name', formDir)}">
          <label class="control-label col-sm-3"
                 for="name">
            <ng-container i18n>Name</ng-container>
            <span class="required"></span>
          </label>
          <div class="col-sm-9">
            <input id="name"
                   name="name"
                   type="text"
                   class="form-control"
                   placeholder="Name..."
                   i18n-placeholder
                   formControlName="name"
                   autofocus>
            <span class="help-block"
                  *ngIf="form.showError('name', formDir, 'required')"
                  i18n>This field is required!</span>
            <span class="help-block"
                  *ngIf="form.showError('name', formDir, 'uniqueName')"
                  i18n>The chosen Ceph pool name is already in use.</span>
          </div>
        </div>

        <!-- Pool type selection -->
        <div class="form-group"
             [ngClass]="{'has-error': form.showError('poolType', formDir)}">
          <label class="control-label col-sm-3"
                 for="poolType">
            <ng-container i18n>Pool type</ng-container>
            <span class="required"></span>
          </label>
          <div class="col-sm-9">
            <select class="form-control"
                    id="poolType"
                    formControlName="poolType"
                    name="poolType">
              <option ngValue=""
                      i18n>-- Select a pool type --</option>
              <option *ngFor="let poolType of data.poolTypes"
                      [value]="poolType">
                {{ poolType }}
              </option>
            </select>
            <span class="help-block"
                  *ngIf="form.showError('poolType', formDir, 'required')"
                  i18n>This field is required!</span>
          </div>
        </div>

        <div *ngIf="form.getValue('poolType')">
          <!-- Pg number -->
          <div class="form-group"
               [ngClass]="{'has-error': form.showError('pgNum', formDir)}">
            <label class="control-label col-sm-3"
                   for="pgNum">
              <ng-container i18n>Placement groups</ng-container>
              <span class="required"></span>
            </label>
            <div class="col-sm-9">
              <input class="form-control"
                     id="pgNum"
                     name="pgNum"
                     formControlName="pgNum"
                     min="1"
                     type="number"
                     (focus)="externalPgChange = false"
                     (blur)="alignPgs()"
                     required>
              <span class="help-block"
                    *ngIf="form.showError('pgNum', formDir, 'required')"
                    i18n>This field is required!</span>
              <span class="help-block"
                    *ngIf="form.showError('pgNum', formDir, 'min')"
                    i18n>At least one placement group is needed!</span>
              <span class="help-block"
                    *ngIf="form.showError('pgNum', formDir, '34')"
                    i18n>Your cluster can't handle this many PGs. Please recalculate the PG amount needed.</span>
              <span class="help-block"
                    *ngIf="form.showError('pgNum', formDir, 'noDecrease')"
                    i18n>You can only increase the number of PGs of an existing pool.
                Currently your pool has {{ data.pool.pg_num }} PGs.</span>
              <span class="help-block">
                <a i18n
                   target="_blank"
                   href="http://ceph.com/pgcalc">Calculation help</a>
              </span>
              <span class="help-block"
                    *ngIf="externalPgChange"
                    i18n>The current PGs settings were calculated for you, you
                    should make sure the values suit your needs before submit.</span>
            </div>
          </div>

          <!-- Crush ruleset selection -->
          <div class="form-group"
               [ngClass]="{'has-error': form.showError('crushRule', formDir)}"
               *ngIf="form.getValue('poolType') && current.rules.length > 0">
            <label class="control-label col-sm-3"
                   for="crushRule"
                   i18n>Crush ruleset</label>
            <div class="col-sm-9">
              <div class="input-group">
                <select class="form-control"
                        id="crushRule"
                        formControlName="crushRule"
                        name="crushSet">
                  <option [ngValue]="null"
                          i18n>-- Select a crush rule --</option>
                  <option *ngFor="let rule of current.rules"
                          [ngValue]="rule">
                    {{ rule.rule_name }}
                  </option>
                </select>
                <span class="input-group-btn">
                  <button class="btn btn-default"
                          [ngClass]="{'active': data.crushInfo}"
                          id="crush-info-button"
                          type="button"
                          (click)="data.crushInfo = !data.crushInfo">
                    <i class="fa fa-question-circle"
                       aria-hidden="true"></i>
                  </button>
                </span>
              </div>
              <span class="help-block"
                    id="crush-info-block"
                    *ngIf="data.crushInfo && form.getValue('crushRule')">
                <tabset>
                  <tab i18n-heading heading="Crush rule" class="crush-rule-info">
                    <cd-table-key-value [renderObjects]="true"
                                        [data]="form.getValue('crushRule')"
                                        [autoReload]="false">
                    </cd-table-key-value>
                  </tab>
                  <tab i18n-heading heading="Crush steps" class="crush-rule-steps">
                    <ol>
                      <li *ngFor="let step of form.get('crushRule').value.steps">
                        {{ describeCrushStep(step) }}
                      </li>
                    </ol>
                  </tab>
                </tabset>
              </span>
              <span class="help-block"
                    *ngIf="form.showError('crushRule', formDir, 'tooFewOsds')"
                    i18n>The rule can't be used in the current cluster as it has
                to few OSDs to meet the minimum required OSD by this rule.</span>
            </div>
          </div>

          <!-- Replica Size -->
          <div class="form-group"
               [ngClass]="{'has-error': form.showError('size', formDir)}"
               *ngIf="form.getValue('poolType') === 'replicated'">
            <label class="control-label col-sm-3"
                   for="size">
              <ng-container i18n>Replicated size</ng-container>
              <span class="required"></span>
            </label>
            <div class="col-sm-9">
              <input class="form-control"
                     id="size"
                     [max]="getMaxSize()"
                     [min]="getMinSize()"
                     name="size"
                     type="number"
                     formControlName="size">
              <span class="help-block"
                    *ngIf="form.showError('size', formDir)">
                <ul class="list-inline">
                  <li i18n>Minimum: {{ getMinSize() }}</li>
                  <li i18n>Maximum: {{ getMaxSize() }}</li>
                </ul>
              </span>
              <span class="help-block"
                    *ngIf="form.showError('size', formDir)"
                    i18n>The size specified is out of range. A value from
                    {{ getMinSize() }} to {{ getMaxSize() }} is valid.</span>
            </div>
          </div>

          <!-- Erasure Profile select -->
          <div class="form-group"
               *ngIf="form.getValue('poolType') === 'erasure'">
            <label i18n
                   class="control-label col-sm-3"
                   for="erasureProfile">Erasure code profile</label>
            <div class="col-sm-9">
              <div class="input-group">
                <select class="form-control"
                        id="erasureProfile"
                        name="erasureProfile"
                        formControlName="erasureProfile">
                  <option *ngIf="!ecProfiles"
                          ngValue=""
                          i18n>Loading...</option>
                  <option *ngIf="ecProfiles && ecProfiles.length === 0"
                          [ngValue]="null"
                          i18n>-- No erasure code profile available --</option>
                  <option *ngIf="ecProfiles && ecProfiles.length > 0"
                          [ngValue]="null"
                          i18n>-- Select an erasure code profile --</option>
                  <option *ngFor="let ecp of ecProfiles"
                          [ngValue]="ecp">
                    {{ ecp.name }}
                  </option>
                </select>
                <span class="input-group-btn">
                  <button class="btn btn-default"
                          [ngClass]="{'active': data.erasureInfo}"
                          id="ecp-info-button"
                          type="button"
                          (click)="data.erasureInfo = !data.erasureInfo">
                    <i class="fa fa-question-circle"
                       aria-hidden="true"></i>
                  </button>
                  <button class="btn btn-default"
                          type="button"
                          [disabled]="editing"
                          (click)="addErasureCodeProfile()">
                    <i class="fa fa-plus"
                       aria-hidden="true"></i>
                  </button>
                  <button class="btn btn-default"
                          type="button"
                          (click)="deleteErasureCodeProfile()"
                          [disabled]="editing || ecProfiles.length < 1">
                    <i class="fa fa-trash-o"
                       aria-hidden="true"></i>
                  </button>
                </span>
              </div>
              <span class="help-block"
                    id="ecp-info-block"
                    *ngIf="data.erasureInfo && form.getValue('erasureProfile')">
                <cd-table-key-value [renderObjects]="true"
                                    [data]="form.getValue('erasureProfile')"
                                    [autoReload]="false">
                </cd-table-key-value>
              </span>
            </div>
          </div>

          <!-- Flags -->
          <div class="form-group"
               *ngIf="info.is_all_bluestore && form.getValue('poolType') === 'erasure'">
            <label i18n
                   class="control-label col-sm-3">Flags</label>
            <div class="col-sm-9">
              <div class="input-group">
                <div class="checkbox checkbox-primary">
                  <input id="ec-overwrites"
                         type="checkbox"
                         formControlName="ecOverwrites">
                  <label for="ec-overwrites"
                         i18n>EC Overwrites</label>
                </div>
              </div>
            </div>
          </div>

        </div>
        <!-- Applications -->
        <div class="form-group">
          <label i18n
                 class="col-sm-3 control-label"
                 for="applications">Applications</label>
          <div class="col-sm-9">
            <span class="form-control no-border full-height">
              <cd-select-badges id="applications"
                                [customBadges]="true"
                                [customBadgeValidators]="data.applications.validators"
                                [messages]="data.applications.messages"
                                [data]="data.applications.selected"
                                [options]="data.applications.available"
                                [selectionLimit]="4"
                                (selection)="appSelection()">
              </cd-select-badges>
            </span>
          </div>
        </div>

          <!-- Compression -->
          <div *ngIf="info.is_all_bluestore"
               formGroupName="compression">
            <legend i18n>Compression</legend>

            <!-- Compression Mode -->
            <div class="form-group">
              <label i18n
                     class="control-label col-sm-3"
                     for="mode">Mode</label>
              <div class="col-sm-9">
                <select class="form-control"
                        id="mode"
                        name="mode"
                        formControlName="mode">
                  <option *ngFor="let mode of info.compression_modes"
                          [value]="mode">
                    {{ mode }}
                  </option>
                </select>
              </div>
            </div>
            <div *ngIf="hasCompressionEnabled()">
              <!-- Compression algorithm selection -->
              <div class="form-group"
                   [ngClass]="{'has-error': form.showError('algorithm', formDir)}">
                <label i18n
                       class="control-label col-sm-3"
                       for="algorithm">Algorithm</label>
                <div class="col-sm-9">
                  <select class="form-control"
                          id="algorithm"
                          name="algorithm"
                          formControlName="algorithm">
                    <option *ngIf="!info.compression_algorithms"
                            ngValue=""
                            i18n>Loading...</option>
                    <option *ngIf="info.compression_algorithms && info.compression_algorithms.length === 0"
                            i18n
                            ngValue="">-- No erasure compression algorithm available --</option>
                    <option *ngFor="let algorithm of info.compression_algorithms"
                            [value]="algorithm">
                      {{ algorithm }}
                    </option>
                  </select>
                </div>
              </div>

              <!-- Compression min blob size -->
              <div class="form-group"
                   [ngClass]="{'has-error': form.showError('minBlobSize', formDir)}">
                <label i18n
                       class="control-label col-sm-3"
                       for="minBlobSize">Minimum blob size</label>
                <div class="col-sm-9">
                  <input id="minBlobSize"
                         name="minBlobSize"
                         formControlName="minBlobSize"
                         type="text"
                         min="0"
                         class="form-control"
                         i18n-placeholder
                         placeholder="e.g., 128KiB"
                         defaultUnit="KiB"
                         cdDimlessBinary>
                  <span class="help-block"
                        *ngIf="form.showError('minBlobSize', formDir, 'min')"
                        i18n>Value should be greater than 0</span>
                  <span class="help-block"
                        *ngIf="form.showError('minBlobSize', formDir, 'maximum')"
                        i18n>Value should be greater than the maximum blob size</span>
                </div>
              </div>

              <!-- Compression max blob size -->
              <div class="form-group"
                   [ngClass]="{'has-error': form.showError('maxBlobSize', formDir)}">
                <label i18n
                       class="control-label col-sm-3"
                       for="maxBlobSize">Maximum blob size</label>
                <div class="col-sm-9">
                  <input id="maxBlobSize"
                         type="text"
                         min="0"
                         formControlName="maxBlobSize"
                         class="form-control"
                         i18n-placeholder
                         placeholder="e.g., 512KiB"
                         defaultUnit="KiB"
                         cdDimlessBinary>
                  <span class="help-block"
                        *ngIf="form.showError('maxBlobSize', formDir, 'min')"
                        i18n>Value should be greater than 0</span>
                  <span class="help-block"
                        *ngIf="form.showError('maxBlobSize', formDir, 'minimum')"
                        i18n>Value should be greater than the minimum blob size</span>
                </div>
              </div>

              <!-- Compression ratio -->
              <div class="form-group"
                   [ngClass]="{'has-error': form.showError('ratio', formDir)}">
                <label i18n
                       class="control-label col-sm-3"
                       for="ratio">Ratio</label>
                <div class="col-sm-9">
                  <input id="ratio"
                         name="ratio"
                         formControlName="ratio"
                         type="number"
                         min="0"
                         max="1"
                         step="0.1"
                         class="form-control"
                         i18n-placeholder
                         placeholder="Compression ratio">
                  <span class="help-block"
                        *ngIf="form.showError('ratio', formDir, 'min') || form.showError('ratio', formDir, 'max')"
                        i18n>Value should be between 0.0 and 1.0</span>
                </div>
              </div>

            </div>
          </div>

        <!-- Pool configuration -->
        <div [hidden]="form.get('poolType').value !== 'replicated' || data.applications.selected.indexOf('rbd') === -1">
          <cd-rbd-configuration-form [form]="form"
                                     [initializeData]="initializeConfigData"
                                     (changes)="currentConfigurationValues = $event()">
          </cd-rbd-configuration-form>
        </div>

        <div class="form-group has-error">
          <div class="col-sm-offset-3 col-sm-9"
               *ngIf="form.hasError('rbdPool')">
            <br>
            <span class="help-block"
                  i18n>It's not possible to create an RBD pool with '/' in the name.</span>
            <span class="help-block"
                  i18n>Please change the name or remove 'rbd' from the applications list.</span>
          </div>
        </div>
      </div>
      <div class="panel-footer">
        <div class="button-group text-right">
          <cd-submit-button
            [form]="formDir"
            type="button"
            i18n="form action button|Example: Create Pool@@formActionButton"
            (submitAction)="submit()">{{ action | titlecase }} {{ resource | upperFirst }}</cd-submit-button>
          <cd-back-button></cd-back-button>
        </div>
      </div>

    </div>

  </form>
</div>

./pool-form.component.scss

.crush-rule-steps {
  margin-top: 10px;
}
Legend
Html element
Component
Html element with directive

result-matching ""

    No results matching ""