]> git.apps.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard: Add clay plugin support 38489/head
authorStephan Müller <smueller@suse.com>
Wed, 23 Sep 2020 09:16:44 +0000 (11:16 +0200)
committerTatjana Dehler <tdehler@suse.com>
Tue, 8 Dec 2020 14:10:55 +0000 (15:10 +0100)
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>
(cherry picked from commit b3fd05bbc568cb775d25032ce87ea8dbb5106b3a)

Conflicts:
src/pybind/mgr/dashboard/frontend/src/app/shared/api/erasure-code-profile.service.ts
Fixed conflicts because https://github.com/ceph/ceph/pull/34696 has not
been backported to octopus.

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 ccf3b309c39c5408e8e5d6f5d260ba192ee901fd..3c09314236bed53188554e333f7a5ee21cf0b932 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 3c8ba61f9f8ab9387038d56a879d5e499d630a82..6842943c193efc0397565a8693aebe499547b2e9 100644 (file)
@@ -47,8 +47,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 cbdeb36b1b4a475595fdd09c917d20f8d79f93c3..a6510eccf0f383572d56761bd16ac7bc0c79e16c 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 d404bc63d2a318b5b3c2f1f57873b87a8b7812ea..3bc412af301188d2b99e5f1ac4cbfcf6484d4147 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 { NgBootstrapFormValidationModule } from 'ng-bootstrap-form-validation';
@@ -16,6 +15,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';
@@ -27,7 +27,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: [
@@ -144,18 +163,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', () => {
@@ -173,17 +220,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', () => {
@@ -204,9 +256,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'`, () => {
@@ -214,7 +264,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', () => {
@@ -325,18 +378,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
         );
       });
@@ -361,6 +408,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', () => {
@@ -520,5 +651,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 7f5aa7aed763c6045a33350ea43d828c88f0f791..fefafa0f3f50bb2de739133286a78a42bfc72ffa 100644 (file)
@@ -7,6 +7,7 @@ import { BsModalRef } from 'ngx-bootstrap/modal';
 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';
@@ -29,10 +30,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[];
@@ -40,6 +43,7 @@ export class ErasureCodeProfileFormModalComponent extends CrushNodeSelectionClas
   techniques: string[];
   action: string;
   resource: string;
+  dCalc: boolean;
   lrcGroups: number;
   lrcMultiK: number;
 
@@ -94,7 +98,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)]],
@@ -116,12 +120,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 {
@@ -176,8 +194,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) {
@@ -190,15 +244,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',
@@ -208,6 +260,11 @@ export class ErasureCodeProfileFormModalComponent extends CrushNodeSelectionClas
       'blaum_roth',
       'liber8tion'
     ];
+    this.setDefaults({
+      k: 4,
+      m: 2,
+      technique: 'reed_sol_van'
+    });
   }
 
   private setLrcDefaults() {
@@ -224,11 +281,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() {
@@ -244,24 +302,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();
       }
     });
   }
@@ -300,12 +398,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() {
@@ -333,11 +431,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 a8c414f3c06be389866074d15c1cf6a5f752d80b..c53c1b6e30af8f44258fda5185d8a189e6ee2cdf 100644 (file)
@@ -29,7 +29,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: this.i18n(`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: {
@@ -63,6 +63,21 @@ export class ErasureCodeProfileService {
         c: this.i18n(`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: this.i18n(`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: this.i18n(`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: this.i18n(`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: this.i18n(`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;