]> git-server-git.apps.pok.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard: Add clay plugin support 37440/head
authorStephan Müller <smueller@suse.com>
Wed, 23 Sep 2020 09:16:44 +0000 (11:16 +0200)
committerStephan Müller <smueller@suse.com>
Tue, 29 Sep 2020 07:59:14 +0000 (09:59 +0200)
The erasure code plugin "clay" is now supported by the dashboard. Now a
clay based profile can be created in the ec profile creation modal
dialog which can be found in the pool form.

The defaults of the plugin are calculated or preselected and shown in the
dashboard, therefore things are made mandatory even if they are not on the
cli, but as they automatically set the user doesn't have to set them,
but sees the defaults instantly before creating the profile.
(This is the same behavior that is used for all other supported
plugins.)

Fixes: https://tracker.ceph.com/issues/44433
Signed-off-by: Stephan Müller <smueller@suse.com>
doc/rados/operations/erasure-code-clay.rst
src/pybind/mgr/dashboard/controllers/erasure_code_profile.py
src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/erasure-code-profile-form/erasure-code-profile-form-modal.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/erasure-code-profile-form/erasure-code-profile-form-modal.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/erasure-code-profile-form/erasure-code-profile-form-modal.component.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/api/erasure-code-profile.service.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/models/erasure-code-profile.ts

index cb330dc1c1af503788ed8d34b46673b825ee9224..a9bb03126820eb7acf355f82698e75f7e35fa7d9 100644 (file)
@@ -88,7 +88,7 @@ Where:
 
 :Description: Number of OSDs requested to send data during recovery of
               a single chunk. *d* needs to be chosen such that
-              k+1 <= d <= k+m-1. Larger the *d*, the better the savings.
+              k+1 <= d <= k+m-1. The larger the *d*, the better the savings.
 
 :Type: Integer
 :Required: No.
index e9dc01e7e8a3eb76847f8e9cd6e820e8e028465e..40cd6c78e1ab057a268c6ec08d34a2860541a19f 100644 (file)
@@ -58,8 +58,8 @@ class ErasureCodeProfileUi(ErasureCodeProfile):
         """
         config = mgr.get('config')
         return {
-            # Because 'shec' is experimental it's not included
-            'plugins': config['osd_erasure_code_plugins'].split() + ['shec'],
+            # Because 'shec' and 'clay' are experimental they're not included
+            'plugins': config['osd_erasure_code_plugins'].split() + ['shec', 'clay'],
             'directory': config['erasure_code_dir'],
             'nodes': mgr.get('osd_map_tree')['nodes'],
             'names': [name for name, _ in
index 1c98e1aaf91edea2085949914ce3d2a3edf2f965..0ce41105e4e0b9761a1a2e6f469183f2959b18f1 100644 (file)
           </div>
         </div>
 
+        <div class="form-group row"
+             *ngIf="plugin === 'clay'">
+          <label for="d"
+                 class="cd-col-form-label">
+            <span class="required"
+                  i18n>Helper chunks (d)</span>
+            <cd-helper [html]="tooltips.plugins.clay.d">
+            </cd-helper>
+          </label>
+          <div class="cd-col-form-input">
+            <div class="input-group">
+              <input type="number"
+                     id="d"
+                     name="d"
+                     class="form-control"
+                     placeholder="Helper chunks..."
+                     formControlName="d">
+              <span class="input-group-append">
+                <button class="btn btn-light"
+                        id="d-calc-btn"
+                        ngbTooltip="Set d manually or use the plugin's default calculation that maximizes d."
+                        i18n-ngbTooltip
+                        type="button"
+                        (click)="toggleDCalc()">
+                  <i [ngClass]="dCalc ? icons.unlock : icons.lock"
+                     aria-hidden="true"></i>
+                </button>
+              </span>
+            </div>
+            <span class="form-text text-muted"
+                  *ngIf="dCalc"
+                  i18n>D is automatically updated on k and m changes</span>
+            <ng-container
+              *ngIf="!dCalc">
+              <span class="form-text text-muted"
+                    *ngIf="getDMin() < getDMax()"
+                    i18n>D can be set from {{getDMin()}} to {{getDMax()}}</span>
+              <span class="form-text text-muted"
+                    *ngIf="getDMin() === getDMax()"
+                    i18n>D can only be set to {{getDMax()}}</span>
+            </ng-container>
+            <span class="invalid-feedback"
+                  *ngIf="form.showError('d', frm, 'dMin')"
+                  i18n>D has to be greater than k ({{getDMin()}}).</span>
+            <span class="invalid-feedback"
+                  *ngIf="form.showError('d', frm, 'dMax')"
+                  i18n>D has to be lower than k + m ({{getDMax()}}).</span>
+          </div>
+        </div>
+
         <div class="form-group row"
              *ngIf="plugin === PLUGIN.LRC">
           <label class="cd-col-form-label"
         </div>
 
         <div class="form-group row"
-             *ngIf="[PLUGIN.JERASURE, PLUGIN.ISA].includes(plugin)">
+             *ngIf="PLUGIN.CLAY === plugin">
+          <label for="scalar_mds"
+                 class="cd-col-form-label">
+            <ng-container i18n>Scalar mds</ng-container>
+            <cd-helper [html]="tooltips.plugins.clay.scalar_mds">
+            </cd-helper>
+          </label>
+          <div class="cd-col-form-input">
+            <select class="form-control custom-select"
+                    id="scalar_mds"
+                    name="scalar_mds"
+                    formControlName="scalar_mds">
+              <option *ngFor="let plugin of [PLUGIN.JERASURE, PLUGIN.ISA, PLUGIN.SHEC]"
+                      [ngValue]="plugin">
+                {{ plugin }}
+              </option>
+            </select>
+          </div>
+        </div>
+
+        <div class="form-group row"
+             *ngIf="[PLUGIN.JERASURE, PLUGIN.ISA, PLUGIN.CLAY].includes(plugin)">
           <label for="technique"
                  class="cd-col-form-label">
             <ng-container i18n>Technique</ng-container>
index 1ea007a1e320b27b291980701c53240153c080d7..1f01a8f1af05ba5fd785642d770306ef477525c2 100644 (file)
@@ -1,6 +1,5 @@
 import { HttpClientTestingModule } from '@angular/common/http/testing';
 import { ComponentFixture, TestBed } from '@angular/core/testing';
-import { By } from '@angular/platform-browser';
 import { RouterTestingModule } from '@angular/router/testing';
 
 import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
@@ -15,6 +14,7 @@ import {
   Mocks
 } from '../../../../testing/unit-test-helper';
 import { ErasureCodeProfileService } from '../../../shared/api/erasure-code-profile.service';
+import { CrushNode } from '../../../shared/models/crush-node';
 import { ErasureCodeProfile } from '../../../shared/models/erasure-code-profile';
 import { TaskWrapperService } from '../../../shared/services/task-wrapper.service';
 import { PoolModule } from '../pool.module';
@@ -26,7 +26,26 @@ describe('ErasureCodeProfileFormModalComponent', () => {
   let fixture: ComponentFixture<ErasureCodeProfileFormModalComponent>;
   let formHelper: FormHelper;
   let fixtureHelper: FixtureHelper;
-  let data: {};
+  let data: { plugins: string[]; names: string[]; nodes: CrushNode[] };
+
+  const expectTechnique = (current: string) =>
+    expect(component.form.getValue('technique')).toBe(current);
+
+  const expectTechniques = (techniques: string[], current: string) => {
+    expect(component.techniques).toEqual(techniques);
+    expectTechnique(current);
+  };
+
+  const expectRequiredControls = (controlNames: string[]) => {
+    controlNames.forEach((name) => {
+      const value = component.form.getValue(name);
+      formHelper.expectValid(name);
+      formHelper.expectErrorChange(name, null, 'required');
+      // This way other fields won't fail through getting invalid.
+      formHelper.expectValidChange(name, value);
+    });
+    fixtureHelper.expectIdElementsVisible(controlNames, true);
+  };
 
   configureTestBed({
     imports: [
@@ -143,18 +162,46 @@ describe('ErasureCodeProfileFormModalComponent', () => {
       showDefaults('isa');
     });
 
+    it('should change technique to default if not available in other plugin', () => {
+      expectTechnique('reed_sol_van');
+      formHelper.setValue('technique', 'blaum_roth');
+      expectTechnique('blaum_roth');
+      formHelper.setValue('plugin', 'isa');
+      expectTechnique('reed_sol_van');
+      formHelper.setValue('plugin', 'clay');
+      formHelper.expectValidChange('scalar_mds', 'shec');
+      expectTechnique('single');
+    });
+
     describe(`for 'jerasure' plugin (default)`, () => {
       it(`requires 'm' and 'k'`, () => {
-        formHelper.expectErrorChange('k', null, 'required');
-        formHelper.expectErrorChange('m', null, 'required');
+        expectRequiredControls(['k', 'm']);
       });
 
       it(`should show 'packetSize' and 'technique'`, () => {
         fixtureHelper.expectIdElementsVisible(['packetSize', 'technique'], true);
       });
 
+      it('should show available techniques', () => {
+        expectTechniques(
+          [
+            'reed_sol_van',
+            'reed_sol_r6_op',
+            'cauchy_orig',
+            'cauchy_good',
+            'liberation',
+            'blaum_roth',
+            'liber8tion'
+          ],
+          'reed_sol_van'
+        );
+      });
+
       it(`should not show any other plugin specific form control`, () => {
-        fixtureHelper.expectIdElementsVisible(['c', 'l', 'crushLocality'], false);
+        fixtureHelper.expectIdElementsVisible(
+          ['c', 'l', 'crushLocality', 'd', 'scalar_mds'],
+          false
+        );
       });
 
       it('should not allow "k" to be changed more than possible', () => {
@@ -172,17 +219,22 @@ describe('ErasureCodeProfileFormModalComponent', () => {
       });
 
       it(`does require 'm' and 'k'`, () => {
-        formHelper.expectErrorChange('k', null, 'required');
-        formHelper.expectErrorChange('m', null, 'required');
+        expectRequiredControls(['k', 'm']);
       });
 
       it(`should show 'technique'`, () => {
         fixtureHelper.expectIdElementsVisible(['technique'], true);
-        expect(fixture.debugElement.query(By.css('#technique'))).toBeTruthy();
+      });
+
+      it('should show available techniques', () => {
+        expectTechniques(['reed_sol_van', 'cauchy'], 'reed_sol_van');
       });
 
       it(`should not show any other plugin specific form control`, () => {
-        fixtureHelper.expectIdElementsVisible(['c', 'l', 'crushLocality', 'packetSize'], false);
+        fixtureHelper.expectIdElementsVisible(
+          ['c', 'l', 'crushLocality', 'packetSize', 'd', 'scalar_mds'],
+          false
+        );
       });
 
       it('should not allow "k" to be changed more than possible', () => {
@@ -203,9 +255,7 @@ describe('ErasureCodeProfileFormModalComponent', () => {
       });
 
       it(`requires 'm', 'l' and 'k'`, () => {
-        formHelper.expectErrorChange('k', null, 'required');
-        formHelper.expectErrorChange('m', null, 'required');
-        formHelper.expectErrorChange('l', null, 'required');
+        expectRequiredControls(['k', 'm', 'l']);
       });
 
       it(`should show 'l' and 'crushLocality'`, () => {
@@ -213,7 +263,10 @@ describe('ErasureCodeProfileFormModalComponent', () => {
       });
 
       it(`should not show any other plugin specific form control`, () => {
-        fixtureHelper.expectIdElementsVisible(['c', 'packetSize', 'technique'], false);
+        fixtureHelper.expectIdElementsVisible(
+          ['c', 'packetSize', 'technique', 'd', 'scalar_mds'],
+          false
+        );
       });
 
       it('should not allow "k" to be changed more than possible', () => {
@@ -324,18 +377,12 @@ describe('ErasureCodeProfileFormModalComponent', () => {
       });
 
       it(`does require 'm', 'c' and 'k'`, () => {
-        formHelper.expectErrorChange('k', null, 'required');
-        formHelper.expectErrorChange('m', null, 'required');
-        formHelper.expectErrorChange('c', null, 'required');
-      });
-
-      it(`should show 'c'`, () => {
-        fixtureHelper.expectIdElementsVisible(['c'], true);
+        expectRequiredControls(['k', 'm', 'c']);
       });
 
       it(`should not show any other plugin specific form control`, () => {
         fixtureHelper.expectIdElementsVisible(
-          ['l', 'crushLocality', 'packetSize', 'technique'],
+          ['l', 'crushLocality', 'packetSize', 'technique', 'd', 'scalar_mds'],
           false
         );
       });
@@ -360,6 +407,90 @@ describe('ErasureCodeProfileFormModalComponent', () => {
         formHelper.expectValid('k');
       });
     });
+
+    describe(`for 'clay' plugin`, () => {
+      beforeEach(() => {
+        formHelper.setValue('plugin', 'clay');
+        // Through this change d has a valid range from 4 to 7
+        formHelper.expectValidChange('k', 3);
+        formHelper.expectValidChange('m', 5);
+      });
+
+      it(`does require 'm', 'c', 'd', 'scalar_mds' and 'k'`, () => {
+        fixtureHelper.clickElement('#d-calc-btn');
+        expectRequiredControls(['k', 'm', 'd', 'scalar_mds']);
+      });
+
+      it(`should not show any other plugin specific form control`, () => {
+        fixtureHelper.expectIdElementsVisible(['l', 'crushLocality', 'packetSize', 'c'], false);
+      });
+
+      it('should show default values for d and scalar_mds', () => {
+        expect(component.form.getValue('d')).toBe(7); // (k+m-1)
+        expect(component.form.getValue('scalar_mds')).toBe('jerasure');
+      });
+
+      it('should auto change d if auto calculation is enabled (default)', () => {
+        formHelper.expectValidChange('k', 4);
+        expect(component.form.getValue('d')).toBe(8);
+      });
+
+      it('should have specific techniques for scalar_mds jerasure', () => {
+        expectTechniques(
+          ['reed_sol_van', 'reed_sol_r6_op', 'cauchy_orig', 'cauchy_good', 'liber8tion'],
+          'reed_sol_van'
+        );
+      });
+
+      it('should have specific techniques for scalar_mds isa', () => {
+        formHelper.expectValidChange('scalar_mds', 'isa');
+        expectTechniques(['reed_sol_van', 'cauchy'], 'reed_sol_van');
+      });
+
+      it('should have specific techniques for scalar_mds shec', () => {
+        formHelper.expectValidChange('scalar_mds', 'shec');
+        expectTechniques(['single', 'multiple'], 'single');
+      });
+
+      describe('Validity of d', () => {
+        beforeEach(() => {
+          // Don't automatically change d - the only way to get d invalid
+          fixtureHelper.clickElement('#d-calc-btn');
+        });
+
+        it('should not automatically change d if k or m have been changed', () => {
+          formHelper.expectValidChange('m', 4);
+          formHelper.expectValidChange('k', 5);
+          expect(component.form.getValue('d')).toBe(7);
+        });
+
+        it('should trigger dMin through change of d', () => {
+          formHelper.expectErrorChange('d', 3, 'dMin');
+        });
+
+        it('should trigger dMax through change of d', () => {
+          formHelper.expectErrorChange('d', 8, 'dMax');
+        });
+
+        it('should trigger dMin through change of k and m', () => {
+          formHelper.expectValidChange('m', 2);
+          formHelper.expectValidChange('k', 7);
+          formHelper.expectError('d', 'dMin');
+        });
+
+        it('should trigger dMax through change of m', () => {
+          formHelper.expectValidChange('m', 3);
+          formHelper.expectError('d', 'dMax');
+        });
+
+        it('should remove dMax through change of k', () => {
+          formHelper.expectValidChange('m', 3);
+          formHelper.expectError('d', 'dMax');
+          formHelper.expectValidChange('k', 5);
+          formHelper.expectValid('d');
+        });
+      });
+    });
   });
 
   describe('submission', () => {
@@ -519,5 +650,51 @@ describe('ErasureCodeProfileFormModalComponent', () => {
         testCreation();
       });
     });
+
+    describe(`'clay' usage`, () => {
+      beforeEach(() => {
+        ecpChange('name', 'clayProfile');
+        ecpChange('plugin', 'clay');
+        // Setting expectations
+        submittedEcp.k = 4;
+        submittedEcp.m = 2;
+        submittedEcp.d = 5;
+        submittedEcp.scalar_mds = 'jerasure';
+        delete submittedEcp.packetsize;
+      });
+
+      it('should be able to create a profile with only plugin and name', () => {
+        formHelper.setMultipleValues(ecp, true);
+        testCreation();
+      });
+
+      it('should send profile with a changed d', () => {
+        formHelper.setMultipleValues(ecp, true);
+        ecpChange('d', '5');
+        submittedEcp.d = 5;
+        testCreation();
+      });
+
+      it('should send profile with a changed k which automatically changes d', () => {
+        ecpChange('k', 5);
+        formHelper.setMultipleValues(ecp, true);
+        submittedEcp.d = 6;
+        testCreation();
+      });
+
+      it('should send profile with a changed sclara_mds', () => {
+        ecpChange('scalar_mds', 'shec');
+        formHelper.setMultipleValues(ecp, true);
+        submittedEcp.scalar_mds = 'shec';
+        submittedEcp.technique = 'single';
+        testCreation();
+      });
+
+      it('should not send the profile with unsupported fields', () => {
+        formHelper.setMultipleValues(ecp, true);
+        formHelper.setValue('l', 8, true);
+        testCreation();
+      });
+    });
   });
 });
index 4cda73509f5856bbb4a7eb3b6fc845e9df02cef1..e81c5490c71950cf202f0d7b7c4f43fa6c0a1caa 100644 (file)
@@ -6,6 +6,7 @@ import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
 import { ErasureCodeProfileService } from '../../../shared/api/erasure-code-profile.service';
 import { CrushNodeSelectionClass } from '../../../shared/classes/crush.node.selection.class';
 import { ActionLabelsI18n } from '../../../shared/constants/app.constants';
+import { Icons } from '../../../shared/enum/icons.enum';
 import { CdFormBuilder } from '../../../shared/forms/cd-form-builder';
 import { CdFormGroup } from '../../../shared/forms/cd-form-group';
 import { CdValidators } from '../../../shared/forms/cd-validators';
@@ -28,10 +29,12 @@ export class ErasureCodeProfileFormModalComponent extends CrushNodeSelectionClas
   PLUGIN = {
     LRC: 'lrc', // Locally Repairable Erasure Code
     SHEC: 'shec', // Shingled Erasure Code
+    CLAY: 'clay', // Coupled LAYer
     JERASURE: 'jerasure', // default
     ISA: 'isa' // Intel Storage Acceleration
   };
   plugin = this.PLUGIN.JERASURE;
+  icons = Icons;
 
   form: CdFormGroup;
   plugins: string[];
@@ -39,6 +42,7 @@ export class ErasureCodeProfileFormModalComponent extends CrushNodeSelectionClas
   techniques: string[];
   action: string;
   resource: string;
+  dCalc: boolean;
   lrcGroups: number;
   lrcMultiK: number;
 
@@ -92,7 +96,7 @@ export class ErasureCodeProfileFormModalComponent extends CrushNodeSelectionClas
       crushRoot: null, // Will be preselected
       crushDeviceClass: '', // Will be preselected
       directory: '',
-      // Only for 'jerasure' and 'isa' use
+      // Only for 'jerasure', 'clay' and 'isa' use
       technique: 'reed_sol_van',
       // Only for 'jerasure' use
       packetSize: [2048, [Validators.min(1)]],
@@ -114,12 +118,26 @@ export class ErasureCodeProfileFormModalComponent extends CrushNodeSelectionClas
           Validators.min(1),
           CdValidators.custom('cGreaterM', (v: number) => this.shecDurabilityValidation(v))
         ]
-      ]
+      ],
+      // Only for 'clay' use
+      d: [
+        5, // Will be overwritten with plugin defaults (k+m-1) = k+1 <= d <= k+m-1
+        [
+          Validators.required,
+          CdValidators.custom('dMin', (v: number) => this.dMinValidation(v)),
+          CdValidators.custom('dMax', (v: number) => this.dMaxValidation(v))
+        ]
+      ],
+      scalar_mds: [this.PLUGIN.JERASURE, [Validators.required]] // jerasure or isa or shec
     });
-    this.form.get('k').valueChanges.subscribe(() => this.updateValidityOnChange(['m', 'l']));
-    this.form.get('m').valueChanges.subscribe(() => this.updateValidityOnChange(['k', 'l', 'c']));
+    this.toggleDCalc();
+    this.form.get('k').valueChanges.subscribe(() => this.updateValidityOnChange(['m', 'l', 'd']));
+    this.form
+      .get('m')
+      .valueChanges.subscribe(() => this.updateValidityOnChange(['k', 'l', 'c', 'd']));
     this.form.get('l').valueChanges.subscribe(() => this.updateValidityOnChange(['k', 'm']));
     this.form.get('plugin').valueChanges.subscribe((plugin) => this.onPluginChange(plugin));
+    this.form.get('scalar_mds').valueChanges.subscribe(() => this.setClayDefaultsForScalar());
   }
 
   private baseValueValidation(dataChunk: boolean = false): boolean {
@@ -174,8 +192,44 @@ export class ErasureCodeProfileFormModalComponent extends CrushNodeSelectionClas
     }, 'shec');
   }
 
+  private dMinValidation(d: number): boolean {
+    return this.validValidation(() => this.getDMin() > d, 'clay');
+  }
+
+  getDMin(): number {
+    return this.form.getValue('k') + 1;
+  }
+
+  private dMaxValidation(d: number): boolean {
+    return this.validValidation(() => d > this.getDMax(), 'clay');
+  }
+
+  getDMax(): number {
+    const m = this.form.getValue('m');
+    const k = this.form.getValue('k');
+    return k + m - 1;
+  }
+
+  toggleDCalc() {
+    this.dCalc = !this.dCalc;
+    this.form.get('d')[this.dCalc ? 'disable' : 'enable']();
+    this.calculateD();
+  }
+
+  private calculateD() {
+    if (this.plugin !== this.PLUGIN.CLAY || !this.dCalc) {
+      return;
+    }
+    this.form.silentSet('d', this.getDMax());
+  }
+
   private updateValidityOnChange(names: string[]) {
-    names.forEach((name) => this.form.get(name).updateValueAndValidity({ emitEvent: false }));
+    names.forEach((name) => {
+      if (name === 'd') {
+        this.calculateD();
+      }
+      this.form.get(name).updateValueAndValidity({ emitEvent: false });
+    });
   }
 
   private onPluginChange(plugin: string) {
@@ -188,15 +242,13 @@ export class ErasureCodeProfileFormModalComponent extends CrushNodeSelectionClas
       this.setIsaDefaults();
     } else if (plugin === this.PLUGIN.SHEC) {
       this.setShecDefaults();
+    } else if (plugin === this.PLUGIN.CLAY) {
+      this.setClayDefaults();
     }
-    this.updateValidityOnChange(['m']); // Triggers k, m, c and l
+    this.updateValidityOnChange(['m']); // Triggers k, m, c, d and l
   }
 
   private setJerasureDefaults() {
-    this.setDefaults({
-      k: 4,
-      m: 2
-    });
     this.techniques = [
       'reed_sol_van',
       'reed_sol_r6_op',
@@ -206,6 +258,11 @@ export class ErasureCodeProfileFormModalComponent extends CrushNodeSelectionClas
       'blaum_roth',
       'liber8tion'
     ];
+    this.setDefaults({
+      k: 4,
+      m: 2,
+      technique: 'reed_sol_van'
+    });
   }
 
   private setLrcDefaults() {
@@ -222,11 +279,12 @@ export class ErasureCodeProfileFormModalComponent extends CrushNodeSelectionClas
      * if they are not set, therefore it's fine to mark them as required in order to get
      * strange values that weren't set.
      */
+    this.techniques = ['reed_sol_van', 'cauchy'];
     this.setDefaults({
       k: 7,
-      m: 3
+      m: 3,
+      technique: 'reed_sol_van'
     });
-    this.techniques = ['reed_sol_van', 'cauchy'];
   }
 
   private setShecDefaults() {
@@ -242,24 +300,64 @@ export class ErasureCodeProfileFormModalComponent extends CrushNodeSelectionClas
     });
   }
 
+  private setClayDefaults() {
+    /**
+     * Actually d and scalar_mds are not required - but they will be set to show the default values
+     * in case if they are not set, therefore it's fine to mark them as required in order to not get
+     * strange values that weren't set.
+     *
+     * As d would be set to the value k+m-1 for the greatest savings, the form will
+     * automatically update d if the automatic calculation is activated (default).
+     */
+    this.setDefaults({
+      k: 4,
+      m: 2,
+      // d: 5, <- Will be automatically update to 5
+      scalar_mds: this.PLUGIN.JERASURE
+    });
+    this.setClayDefaultsForScalar();
+  }
+
+  private setClayDefaultsForScalar() {
+    const plugin = this.form.getValue('scalar_mds');
+    let defaultTechnique = 'reed_sol_van';
+    if (plugin === this.PLUGIN.JERASURE) {
+      this.techniques = [
+        'reed_sol_van',
+        'reed_sol_r6_op',
+        'cauchy_orig',
+        'cauchy_good',
+        'liber8tion'
+      ];
+    } else if (plugin === this.PLUGIN.ISA) {
+      this.techniques = ['reed_sol_van', 'cauchy'];
+    } else {
+      // this.PLUGIN.SHEC
+      defaultTechnique = 'single';
+      this.techniques = ['single', 'multiple'];
+    }
+    this.setDefaults({ technique: defaultTechnique });
+  }
+
   private setDefaults(defaults: object) {
     Object.keys(defaults).forEach((controlName) => {
       const control = this.form.get(controlName);
       const value = control.value;
-      let overwrite = control.pristine;
       /**
        * As k, m, c and l are now set touched and dirty on the beginning, plugin change will
        * overwrite their values as we can't determine if the user has changed anything.
        * k and m can have two default values where as l and c can only have one,
        * so there is no need to overwrite them.
        */
-      if ('k' === controlName) {
-        overwrite = [4, 7].includes(value);
-      } else if ('m' === controlName) {
-        overwrite = [2, 3].includes(value);
-      }
+      const overwrite =
+        control.pristine ||
+        (controlName === 'technique' && !this.techniques.includes(value)) ||
+        (controlName === 'k' && [4, 7].includes(value)) ||
+        (controlName === 'm' && [2, 3].includes(value));
       if (overwrite) {
-        this.form.get(controlName).setValue(defaults[controlName]);
+        control.setValue(defaults[controlName]); // also validates new value
+      } else {
+        control.updateValueAndValidity();
       }
     });
   }
@@ -298,12 +396,12 @@ export class ErasureCodeProfileFormModalComponent extends CrushNodeSelectionClas
    * fields got changed before by the user.
    */
   private preValidateNumericInputFields() {
-    const kml = ['k', 'm', 'l', 'c'].map((name) => this.form.get(name));
+    const kml = ['k', 'm', 'l', 'c', 'd'].map((name) => this.form.get(name));
     kml.forEach((control) => {
       control.markAsTouched();
       control.markAsDirty();
     });
-    kml[1].updateValueAndValidity(); // Update validity of k, m, c and l
+    kml[1].updateValueAndValidity(); // Update validity of k, m, c, d and l
   }
 
   onSubmit() {
@@ -330,11 +428,13 @@ export class ErasureCodeProfileFormModalComponent extends CrushNodeSelectionClas
 
   private createJson() {
     const pluginControls = {
-      technique: [this.PLUGIN.ISA, this.PLUGIN.JERASURE],
+      technique: [this.PLUGIN.ISA, this.PLUGIN.JERASURE, this.PLUGIN.CLAY],
       packetSize: [this.PLUGIN.JERASURE],
       l: [this.PLUGIN.LRC],
       crushLocality: [this.PLUGIN.LRC],
-      c: [this.PLUGIN.SHEC]
+      c: [this.PLUGIN.SHEC],
+      d: [this.PLUGIN.CLAY],
+      scalar_mds: [this.PLUGIN.CLAY]
     };
     const ecp = new ErasureCodeProfile();
     const plugin = this.form.getValue('plugin');
index 47817367e7262c8e1612c5a51c806e1eb6d6c96a..d2bd131a464c9e77cd02b8d544d4e6fd3a95a558 100644 (file)
@@ -27,7 +27,7 @@ export class ErasureCodeProfileService {
           carefully. All of reed_sol_r6_op, liberation, blaum_roth, liber8tion are RAID6 equivalents
           in the sense that they can only be configured with m=2.`,
         packetSize: $localize`The encoding will be done on packets of bytes size at a time.
-          Chosing the right packet size is difficult.
+          Choosing the right packet size is difficult.
           The jerasure documentation contains extensive information on this topic.`
       },
       lrc: {
@@ -59,6 +59,21 @@ export class ErasureCodeProfileService {
         c: $localize`The number of parity chunks each of which includes each data chunk in its
           calculation range. The number is used as a durability estimator. For instance, if c=2,
           2 OSDs can be down without losing data.`
+      },
+      clay: {
+        description: $localize`CLAY (short for coupled-layer) codes are erasure codes designed to
+          bring about significant savings in terms of network bandwidth and disk IO when a failed
+          node/OSD/rack is being repaired.`,
+        d: $localize`Number of OSDs requested to send data during recovery of a single chunk.
+          d needs to be chosen such that k+1 <= d <= k+m-1. The larger the d, the better
+          the savings.`,
+        scalar_mds: $localize`scalar_mds specifies the plugin that is used as a building block
+          in the layered construction. It can be one of jerasure, isa, shec.`,
+        technique: $localize`technique specifies the technique that will be picked
+          within the 'scalar_mds' plugin specified. Supported techniques
+          are 'reed_sol_van', 'reed_sol_r6_op', 'cauchy_orig',
+          'cauchy_good', 'liber8tion' for jerasure, 'reed_sol_van',
+          'cauchy' for isa and 'single', 'multiple' for shec.`
       }
     },
 
index 17f48acd53beda46dfa6f52c29fea4e81dd5f779..ea9985ccd499e87b8359fde90a53f86ae0a11b0a 100644 (file)
@@ -5,8 +5,10 @@ export class ErasureCodeProfile {
   m?: number;
   c?: number;
   l?: number;
+  d?: number;
   packetsize?: number;
   technique?: string;
+  scalar_mds?: 'jerasure' | 'isa' | 'shec';
   'crush-root'?: string;
   'crush-locality'?: string;
   'crush-failure-domain'?: string;