From b3fd05bbc568cb775d25032ce87ea8dbb5106b3a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Stephan=20M=C3=BCller?= <smueller@suse.com>
Date: Wed, 23 Sep 2020 11:16:44 +0200
Subject: [PATCH] mgr/dashboard: Add clay plugin support
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

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    |   2 +-
 .../controllers/erasure_code_profile.py       |   4 +-
 ...ure-code-profile-form-modal.component.html |  73 +++++-
 ...-code-profile-form-modal.component.spec.ts | 219 ++++++++++++++++--
 ...asure-code-profile-form-modal.component.ts | 146 ++++++++++--
 .../api/erasure-code-profile.service.ts       |  17 +-
 .../app/shared/models/erasure-code-profile.ts |   2 +
 7 files changed, 414 insertions(+), 49 deletions(-)

diff --git a/doc/rados/operations/erasure-code-clay.rst b/doc/rados/operations/erasure-code-clay.rst
index cb330dc1c1a..a9bb0312682 100644
--- a/doc/rados/operations/erasure-code-clay.rst
+++ b/doc/rados/operations/erasure-code-clay.rst
@@ -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.
diff --git a/src/pybind/mgr/dashboard/controllers/erasure_code_profile.py b/src/pybind/mgr/dashboard/controllers/erasure_code_profile.py
index e9dc01e7e8a..40cd6c78e1a 100644
--- a/src/pybind/mgr/dashboard/controllers/erasure_code_profile.py
+++ b/src/pybind/mgr/dashboard/controllers/erasure_code_profile.py
@@ -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
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/erasure-code-profile-form/erasure-code-profile-form-modal.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/erasure-code-profile-form/erasure-code-profile-form-modal.component.html
index 1c98e1aaf91..0ce41105e4e 100644
--- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/erasure-code-profile-form/erasure-code-profile-form-modal.component.html
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/erasure-code-profile-form/erasure-code-profile-form-modal.component.html
@@ -147,6 +147,56 @@
           </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"
@@ -228,7 +278,28 @@
         </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>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/erasure-code-profile-form/erasure-code-profile-form-modal.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/erasure-code-profile-form/erasure-code-profile-form-modal.component.spec.ts
index 1ea007a1e32..1f01a8f1af0 100644
--- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/erasure-code-profile-form/erasure-code-profile-form-modal.component.spec.ts
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/erasure-code-profile-form/erasure-code-profile-form-modal.component.spec.ts
@@ -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();
+      });
+    });
   });
 });
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/erasure-code-profile-form/erasure-code-profile-form-modal.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/erasure-code-profile-form/erasure-code-profile-form-modal.component.ts
index 4cda73509f5..e81c5490c71 100644
--- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/erasure-code-profile-form/erasure-code-profile-form-modal.component.ts
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/erasure-code-profile-form/erasure-code-profile-form-modal.component.ts
@@ -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');
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/erasure-code-profile.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/erasure-code-profile.service.ts
index 47817367e72..d2bd131a464 100644
--- a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/erasure-code-profile.service.ts
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/erasure-code-profile.service.ts
@@ -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.`
       }
     },
 
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/models/erasure-code-profile.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/erasure-code-profile.ts
index 17f48acd53b..ea9985ccd49 100644
--- a/src/pybind/mgr/dashboard/frontend/src/app/shared/models/erasure-code-profile.ts
+++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/erasure-code-profile.ts
@@ -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;