]> git.apps.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard: Validate iSCSI controls min/max value 28942/head
authorRicardo Marques <rimarques@suse.com>
Tue, 9 Jul 2019 14:12:24 +0000 (15:12 +0100)
committerRicardo Marques <rimarques@suse.com>
Wed, 17 Jul 2019 09:09:04 +0000 (10:09 +0100)
Fixes: https://tracker.ceph.com/issues/38018
Signed-off-by: Ricardo Marques <rimarques@suse.com>
src/pybind/mgr/dashboard/controllers/iscsi.py
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi-target-form/iscsi-target-form.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi-target-image-settings-modal/iscsi-target-image-settings-modal.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi-target-image-settings-modal/iscsi-target-image-settings-modal.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi-target-image-settings-modal/iscsi-target-image-settings-modal.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi-target-iqn-settings-modal/iscsi-target-iqn-settings-modal.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi-target-iqn-settings-modal/iscsi-target-iqn-settings-modal.component.ts

index df44f889807047039fdaf25b702dbdd8610cd5bc..92a32c3db80c284569f622796e871abd6d4b387d 100644 (file)
@@ -223,7 +223,7 @@ class IscsiTarget(RESTController):
             raise DashboardException(msg='Target already exists',
                                      code='target_already_exists',
                                      component='iscsi')
-        IscsiTarget._validate(target_iqn, portals, disks, groups)
+        IscsiTarget._validate(target_iqn, target_controls, portals, disks, groups)
         IscsiTarget._create(target_iqn, target_controls, acl_enabled, portals, disks, clients,
                             groups, 0, 100, config)
 
@@ -245,7 +245,7 @@ class IscsiTarget(RESTController):
             raise DashboardException(msg='Target IQN already in use',
                                      code='target_iqn_already_in_use',
                                      component='iscsi')
-        IscsiTarget._validate(new_target_iqn, portals, disks, groups)
+        IscsiTarget._validate(new_target_iqn, target_controls, portals, disks, groups)
         config = IscsiTarget._delete(target_iqn, config, 0, 50, new_target_iqn, target_controls,
                                      portals, disks, clients, groups)
         IscsiTarget._create(new_target_iqn, target_controls, acl_enabled, portals, disks, clients,
@@ -412,7 +412,7 @@ class IscsiTarget(RESTController):
         return False
 
     @staticmethod
-    def _validate(target_iqn, portals, disks, groups):
+    def _validate(target_iqn, target_controls, portals, disks, groups):
         if not target_iqn:
             raise DashboardException(msg='Target IQN is required',
                                      code='target_iqn_required',
@@ -430,6 +430,26 @@ class IscsiTarget(RESTController):
                                      code='portals_required',
                                      component='iscsi')
 
+        # 'target_controls_limits' was introduced in ceph-iscsi > 3.2
+        # When using an older `ceph-iscsi` version these validations will
+        # NOT be executed beforehand
+        if 'target_controls_limits' in settings:
+            for target_control_name, target_control_value in target_controls.items():
+                limits = settings['target_controls_limits'].get(target_control_name)
+                if limits is not None:
+                    min_value = limits.get('min')
+                    if min_value is not None and target_control_value < min_value:
+                        raise DashboardException(msg='Target control {} must be >= '
+                                                     '{}'.format(target_control_name, min_value),
+                                                 code='target_control_invalid_min',
+                                                 component='iscsi')
+                    max_value = limits.get('max')
+                    if max_value is not None and target_control_value > max_value:
+                        raise DashboardException(msg='Target control {} must be <= '
+                                                     '{}'.format(target_control_name, max_value),
+                                                 code='target_control_invalid_max',
+                                                 component='iscsi')
+
         for portal in portals:
             gateway_name = portal['host']
             try:
@@ -449,6 +469,26 @@ class IscsiTarget(RESTController):
             IscsiTarget._validate_image(pool, image, backstore, required_rbd_features,
                                         unsupported_rbd_features)
 
+            # 'disk_controls_limits' was introduced in ceph-iscsi > 3.2
+            # When using an older `ceph-iscsi` version these validations will
+            # NOT be executed beforehand
+            if 'disk_controls_limits' in settings:
+                for disk_control_name, disk_control_value in disk['controls'].items():
+                    limits = settings['disk_controls_limits'][backstore].get(disk_control_name)
+                    if limits is not None:
+                        min_value = limits.get('min')
+                        if min_value is not None and disk_control_value < min_value:
+                            raise DashboardException(msg='Disk control {} must be >= '
+                                                         '{}'.format(disk_control_name, min_value),
+                                                     code='disk_control_invalid_min',
+                                                     component='iscsi')
+                        max_value = limits.get('max')
+                        if max_value is not None and disk_control_value > max_value:
+                            raise DashboardException(msg='Disk control {} must be <= '
+                                                         '{}'.format(disk_control_name, max_value),
+                                                     code='disk_control_invalid_max',
+                                                     component='iscsi')
+
         initiators = []
         for group in groups:
             initiators = initiators + group['members']
index 1092c7c49d52b16293d7575f40e9c94cd9439f02..576752539dc088a8849db1dd805b8f625e25c7a3 100644 (file)
@@ -30,7 +30,9 @@ export class IscsiTargetFormComponent implements OnInit {
   modalRef: BsModalRef;
   minimum_gateways = 1;
   target_default_controls: any;
+  target_controls_limits: any;
   disk_default_controls: any;
+  disk_controls_limits: any;
   backstores: string[];
   default_backstore: string;
   unsupported_rbd_features: any;
@@ -124,7 +126,9 @@ export class IscsiTargetFormComponent implements OnInit {
       // iscsiService.settings()
       this.minimum_gateways = data[3].config.minimum_gateways;
       this.target_default_controls = data[3].target_default_controls;
+      this.target_controls_limits = data[3].target_controls_limits;
       this.disk_default_controls = data[3].disk_default_controls;
+      this.disk_controls_limits = data[3].disk_controls_limits;
       this.backstores = data[3].backstores;
       this.default_backstore = data[3].default_backstore;
       this.unsupported_rbd_features = data[3].unsupported_rbd_features;
@@ -666,7 +670,8 @@ export class IscsiTargetFormComponent implements OnInit {
   targetSettingsModal() {
     const initialState = {
       target_controls: this.targetForm.get('target_controls'),
-      target_default_controls: this.target_default_controls
+      target_default_controls: this.target_default_controls,
+      target_controls_limits: this.target_controls_limits
     };
 
     this.modalRef = this.modalService.show(IscsiTargetIqnSettingsModalComponent, { initialState });
@@ -677,6 +682,7 @@ export class IscsiTargetFormComponent implements OnInit {
       imagesSettings: this.imagesSettings,
       image: image,
       disk_default_controls: this.disk_default_controls,
+      disk_controls_limits: this.disk_controls_limits,
       backstores: this.getValidBackstores(this.getImageById(image))
     };
 
index 438046184e58df64bd6338fcf38a9c12abc8f50c..ecfc41c317b3de5d7c0fc0606a88f9ad65645c76 100644 (file)
@@ -5,54 +5,67 @@
   </ng-container>
 
   <ng-container class="modal-content">
-    <div class="modal-body">
-      <p class="alert-warning"
-         i18n>Changing these parameters from their default values is usually not necessary.</p>
+    <form name="settingsForm"
+          class="form"
+          #formDir="ngForm"
+          [formGroup]="settingsForm"
+          novalidate>
+      <div class="modal-body">
+        <p class="alert-warning"
+           i18n>Changing these parameters from their default values is usually not necessary.</p>
 
-      <!-- BACKSTORE -->
-      <div class="form-group row">
-        <div class="col-sm-12">
-          <label class="col-form-label"
-                 i18n>Backstore</label>
-          <select id="backstore"
-                  name="backstore"
-                  class="form-control custom-select"
-                  [(ngModel)]="model.backstore"
-                  [disabled]="backstores.length == 1">
-            <option *ngFor="let bs of backstores"
-                    [value]="bs">{{ bs | iscsiBackstore }}</option>
-          </select>
+        <!-- BACKSTORE -->
+        <div class="form-group row">
+          <div class="col-sm-12">
+            <label class="col-form-label"
+                   i18n>Backstore</label>
+            <select id="backstore"
+                    name="backstore"
+                    class="form-control custom-select"
+                    formControlName="backstore">
+              <option *ngFor="let bs of backstores"
+                      [value]="bs">{{ bs | iscsiBackstore }}</option>
+            </select>
+          </div>
         </div>
-      </div>
 
-      <!-- CONTROLS -->
-      <ng-container *ngFor="let bs of backstores">
-        <ng-container *ngIf="model.backstore === bs">
-          <div class="form-group row"
-               *ngFor="let setting of disk_default_controls[bs] | keyvalue">
-            <div class="col-sm-12">
-              <label class="col-form-label"
-                     for="{{ setting.key }}">{{ setting.key }}</label>
-              <input type="number"
-                     class="form-control"
-                     [(ngModel)]="model[bs][setting.key]">
-              <span class="form-text text-muted">{{ helpText[setting.key]?.help }}</span>
+        <!-- CONTROLS -->
+        <ng-container *ngFor="let bs of backstores">
+          <ng-container *ngIf="settingsForm.value['backstore'] === bs">
+            <div class="form-group row"
+                 *ngFor="let setting of disk_default_controls[bs] | keyvalue">
+              <div class="col-sm-12">
+                <label class="col-form-label"
+                       for="{{ setting.key }}">{{ setting.key }}</label>
+                <input type="number"
+                       class="form-control"
+                       [formControlName]="setting.key">
+                <span class="invalid-feedback"
+                      *ngIf="settingsForm.showError(setting.key, formDir, 'min')">
+                  <ng-container i18n>Must be greater than or equal to {{ disk_controls_limits[bs][setting.key]['min'] }}.</ng-container>
+                </span>
+                <span class="invalid-feedback"
+                      *ngIf="settingsForm.showError(setting.key, formDir, 'max')">
+                  <ng-container i18n>Must be less than or equal to {{ disk_controls_limits[bs][setting.key]['max'] }}.</ng-container>
+                </span>
+                <span class="form-text text-muted">{{ helpText[setting.key]?.help }}</span>
+              </div>
             </div>
-          </div>
+          </ng-container>
         </ng-container>
-      </ng-container>
-    </div>
+      </div>
 
-    <div class="modal-footer">
-      <div class="button-group text-right">
-        <button class="btn btn-secondary"
-                (click)="save()"
-                i18n>Confirm</button>
-        <cd-back-button [back]="modalRef.hide"
-                        name="Cancel"
-                        i18n-name>
-        </cd-back-button>
+      <div class="modal-footer">
+        <div class="button-group text-right">
+          <cd-submit-button i18n
+                            [form]="settingsForm"
+                            (submitAction)="save()">Confirm</cd-submit-button>
+          <cd-back-button [back]="modalRef.hide"
+                          name="Cancel"
+                          i18n-name>
+          </cd-back-button>
+        </div>
       </div>
-    </div>
+    </form>
   </ng-container>
 </cd-modal>
index 00f4da5fa07dc138e1830d71755cfc775f8e3ef0..c25b73c00575d690da00d2616e847c3541a4ef32 100644 (file)
@@ -1,6 +1,6 @@
 import { HttpClientTestingModule } from '@angular/common/http/testing';
 import { ComponentFixture, TestBed } from '@angular/core/testing';
-import { FormsModule } from '@angular/forms';
+import { ReactiveFormsModule } from '@angular/forms';
 import { RouterTestingModule } from '@angular/router/testing';
 
 import { BsModalRef } from 'ngx-bootstrap/modal';
@@ -15,7 +15,7 @@ describe('IscsiTargetImageSettingsModalComponent', () => {
 
   configureTestBed({
     declarations: [IscsiTargetImageSettingsModalComponent],
-    imports: [SharedModule, FormsModule, HttpClientTestingModule, RouterTestingModule],
+    imports: [SharedModule, ReactiveFormsModule, HttpClientTestingModule, RouterTestingModule],
     providers: [BsModalRef, i18nProviders]
   });
 
@@ -44,16 +44,17 @@ describe('IscsiTargetImageSettingsModalComponent', () => {
     expect(component).toBeTruthy();
   });
 
-  it('should fill the model', () => {
-    expect(component.model).toEqual({
+  it('should fill the form', () => {
+    expect(component.settingsForm.value).toEqual({
       backstore: 'backstore:1',
-      'backstore:1': {},
-      'backstore:2': {}
+      foo: null,
+      bar: null,
+      baz: null
     });
   });
 
   it('should save changes to imagesSettings', () => {
-    component.model['backstore:1'] = { foo: 1234 };
+    component.settingsForm.controls['foo'].setValue(1234);
     expect(component.imagesSettings).toEqual({
       'rbd/disk_1': { backstore: 'backstore:1', 'backstore:1': {} }
     });
index 310942a590ab707ef5ddd9ec9779af11b709bcef..e2687300dc9b5591eb382b9eef899dd00848fbcc 100644 (file)
@@ -1,9 +1,11 @@
 import { Component, OnInit } from '@angular/core';
+import { FormControl, Validators } from '@angular/forms';
 
 import * as _ from 'lodash';
 import { BsModalRef } from 'ngx-bootstrap/modal';
 
 import { IscsiService } from '../../../shared/api/iscsi.service';
+import { CdFormGroup } from '../../../shared/forms/cd-form-group';
 
 @Component({
   selector: 'cd-iscsi-target-image-settings-modal',
@@ -14,9 +16,10 @@ export class IscsiTargetImageSettingsModalComponent implements OnInit {
   image: string;
   imagesSettings: any;
   disk_default_controls: any;
+  disk_controls_limits: any;
   backstores: any;
 
-  model: any;
+  settingsForm: CdFormGroup;
   helpText: any;
 
   constructor(public modalRef: BsModalRef, public iscsiService: IscsiService) {}
@@ -24,18 +27,48 @@ export class IscsiTargetImageSettingsModalComponent implements OnInit {
   ngOnInit() {
     this.helpText = this.iscsiService.imageAdvancedSettings;
 
-    this.model = _.cloneDeep(this.imagesSettings[this.image]);
+    const fg = {
+      backstore: new FormControl(this.imagesSettings[this.image]['backstore'])
+    };
     _.forEach(this.backstores, (backstore) => {
-      this.model[backstore] = this.model[backstore] || {};
+      const model = this.imagesSettings[this.image][backstore] || {};
+      _.forIn(this.disk_default_controls[backstore], (_value, key) => {
+        const validators = [];
+        if (this.disk_controls_limits && key in this.disk_controls_limits[backstore]) {
+          if ('min' in this.disk_controls_limits[backstore][key]) {
+            validators.push(Validators.min(this.disk_controls_limits[backstore][key]['min']));
+          }
+          if ('max' in this.disk_controls_limits[backstore][key]) {
+            validators.push(Validators.max(this.disk_controls_limits[backstore][key]['max']));
+          }
+        }
+        fg[key] = new FormControl(model[key], {
+          validators: validators
+        });
+      });
     });
+
+    this.settingsForm = new CdFormGroup(fg);
   }
 
   save() {
-    const backstore = this.model.backstore;
+    const backstore = this.settingsForm.controls['backstore'].value;
     const settings = {};
-    _.forIn(this.model[backstore], (value, key) => {
-      if (!(value === '' || value === null)) {
-        settings[key] = value;
+    _.forIn(this.settingsForm.controls, (control, key) => {
+      if (
+        !(control.value === '' || control.value === null) &&
+        key in this.disk_default_controls[this.settingsForm.value['backstore']]
+      ) {
+        settings[key] = control.value;
+        // If one setting belongs to multiple backstores, we have to update it in all backstores
+        _.forEach(this.backstores, (currentBackstore) => {
+          if (currentBackstore !== backstore) {
+            const model = this.imagesSettings[this.image][currentBackstore] || {};
+            if (key in model) {
+              this.imagesSettings[this.image][currentBackstore][key] = control.value;
+            }
+          }
+        });
       }
     });
     this.imagesSettings[this.image]['backstore'] = backstore;
index abfe37c9d9d97db56566b901e17131008e48e8ee..8d00c80ce4761af5c8fbce795b67c39a6a2b4b8d 100644 (file)
                    *ngIf="!isRadio(setting.key)"
                    type="number"
                    [formControlName]="setting.key">
+            <span class="invalid-feedback"
+                  *ngIf="settingsForm.showError(setting.key, formDir, 'min')">
+              <ng-container i18n>Must be greater than or equal to {{ target_controls_limits[setting.key]['min'] }}.</ng-container>
+            </span>
+            <span class="invalid-feedback"
+                  *ngIf="settingsForm.showError(setting.key, formDir, 'max')">
+              <ng-container i18n>Must be less than or equal to {{ target_controls_limits[setting.key]['max'] }}.</ng-container>
+            </span>
 
             <ng-container *ngIf="isRadio(setting.key)">
               <br>
index eef5276cb73dabb41e921fd0149136284569d76b..622beb4619e4c25f1bcd4a04f1eab6c6e044e69e 100644 (file)
@@ -1,5 +1,5 @@
 import { Component, OnInit } from '@angular/core';
-import { FormControl } from '@angular/forms';
+import { FormControl, Validators } from '@angular/forms';
 
 import * as _ from 'lodash';
 import { BsModalRef } from 'ngx-bootstrap/modal';
@@ -15,6 +15,7 @@ import { CdFormGroup } from '../../../shared/forms/cd-form-group';
 export class IscsiTargetIqnSettingsModalComponent implements OnInit {
   target_controls: FormControl;
   target_default_controls: any;
+  target_controls_limits: any;
 
   settingsForm: CdFormGroup;
   helpText: any;
@@ -26,7 +27,16 @@ export class IscsiTargetIqnSettingsModalComponent implements OnInit {
     this.helpText = this.iscsiService.targetAdvancedSettings;
 
     _.forIn(this.target_default_controls, (_value, key) => {
-      fg[key] = new FormControl(this.target_controls.value[key]);
+      const validators = [];
+      if (this.target_controls_limits && key in this.target_controls_limits) {
+        if ('min' in this.target_controls_limits[key]) {
+          validators.push(Validators.min(this.target_controls_limits[key]['min']));
+        }
+        if ('max' in this.target_controls_limits[key]) {
+          validators.push(Validators.max(this.target_controls_limits[key]['max']));
+        }
+      }
+      fg[key] = new FormControl(this.target_controls.value[key], { validators: validators });
     });
 
     this.settingsForm = new CdFormGroup(fg);