]> git-server-git.apps.pok.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard: Disable empty data pool selection in RBD form
authorStephan Müller <smueller@suse.com>
Mon, 31 Aug 2020 16:10:43 +0000 (18:10 +0200)
committerStephan Müller <smueller@suse.com>
Tue, 29 Sep 2020 14:40:10 +0000 (16:40 +0200)
Disables empty data pool selections in the RBD form if no or no
other than the selected pool is available as data pool. If that's the
case a hint can be displayed why that happened.

Fixes: https://tracker.ceph.com/issues/37408
Signed-off-by: Stephan Müller <smueller@suse.com>
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-form/rbd-form.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-form/rbd-form.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-form/rbd-form.component.ts

index 4db306f9ebfd27540bf3ee7d6312584e70e50d7d..91a4439b9048498cf58d38dd007a1139082a6d4c 100644 (file)
               <label class="custom-control-label"
                      for="useDataPool"
                      i18n>Use a dedicated data pool</label>
+              <cd-helper *ngIf="allDataPools.length <= 1">
+                <span i18n>You need more than one pool with the rbd application label use to use a dedicated data pool.</span>
+              </cd-helper>
             </div>
           </div>
         </div>
index 8f86c19a5e0f7de02dc16a251a74dcd5f10e8574..f9bbd4c112bdb8ec71fdcd5ad33efe8515a5a121 100644 (file)
@@ -123,6 +123,36 @@ describe('RbdFormComponent', () => {
       tick(DELAY);
     }));
 
+    describe('disable data pools', () => {
+      beforeEach(() => {
+        component.ngOnInit();
+      });
+
+      it('should be enabled with more than 1 pool', () => {
+        component['handleExternalData'](mock);
+        expect(component.allDataPools.length).toBe(3);
+        expect(component.rbdForm.get('useDataPool').disabled).toBe(false);
+
+        mock.pools.pop();
+        component['handleExternalData'](mock);
+        expect(component.allDataPools.length).toBe(2);
+        expect(component.rbdForm.get('useDataPool').disabled).toBe(false);
+      });
+
+      it('should be disabled with 1 pool', () => {
+        mock.pools = [mock.pools[0]];
+        component['handleExternalData'](mock);
+        expect(component.rbdForm.get('useDataPool').disabled).toBe(true);
+      });
+
+      // Reason for 2 tests - useDataPool is not re-enabled anywhere else
+      it('should be disabled without any pool', () => {
+        mock.pools = [];
+        component['handleExternalData'](mock);
+        expect(component.rbdForm.get('useDataPool').disabled).toBe(true);
+      });
+    });
+
     it('should edit image after image data is received', () => {
       component.mode = RbdFormMode.editing;
       component.ngOnInit();
index c4146a088e2052cf2e4bb386353be7b65aea8859..676922c43d7b57e6d71ce59d64bda2199ab44cbd 100644 (file)
@@ -33,6 +33,12 @@ import { RbdFormEditRequestModel } from './rbd-form-edit-request.model';
 import { RbdFormMode } from './rbd-form-mode.enum';
 import { RbdFormResponseModel } from './rbd-form-response.model';
 
+class ExternalData {
+  rbd: RbdFormResponseModel;
+  defaultFeatures: string[];
+  pools: Pool[];
+}
+
 @Component({
   selector: 'cd-rbd-form',
   templateUrl: './rbd-form.component.html',
@@ -50,8 +56,8 @@ export class RbdFormComponent extends CdForm implements OnInit {
   namespacesByPoolCache = {};
   pools: Array<Pool> = null;
   allPools: Array<Pool> = null;
-  dataPools: Array<string> = null;
-  allDataPools: Array<string> = null;
+  dataPools: Array<Pool> = null;
+  allDataPools: Array<Pool> = [];
   features: { [key: string]: RbdImageFeature };
   featuresList: RbdImageFeature[] = [];
   initializeConfigData = new EventEmitter<{
@@ -220,98 +226,108 @@ export class RbdFormComponent extends CdForm implements OnInit {
   }
 
   ngOnInit() {
-    if (this.router.url.startsWith('/block/rbd/edit')) {
+    this.prepareFormForAction();
+    this.gatherNeededData().subscribe(this.handleExternalData.bind(this));
+  }
+
+  private prepareFormForAction() {
+    const url = this.router.url;
+    if (url.startsWith('/block/rbd/edit')) {
       this.mode = this.rbdFormMode.editing;
       this.action = this.actionLabels.EDIT;
       this.disableForEdit();
-    } else if (this.router.url.startsWith('/block/rbd/clone')) {
+    } else if (url.startsWith('/block/rbd/clone')) {
       this.mode = this.rbdFormMode.cloning;
       this.disableForClone();
       this.action = this.actionLabels.CLONE;
-    } else if (this.router.url.startsWith('/block/rbd/copy')) {
+    } else if (url.startsWith('/block/rbd/copy')) {
       this.mode = this.rbdFormMode.copying;
       this.action = this.actionLabels.COPY;
       this.disableForCopy();
     } else {
       this.action = this.actionLabels.CREATE;
     }
-    enum Promisse {
-      RbdServiceGet = 'rbdService.get',
-      PoolServiceList = 'poolService.list'
-    }
-    const promisses = {};
-    if (
-      this.mode === this.rbdFormMode.editing ||
-      this.mode === this.rbdFormMode.cloning ||
-      this.mode === this.rbdFormMode.copying
-    ) {
+    _.each(this.features, (feature) => {
+      this.rbdForm
+        .get('features')
+        .get(feature.key)
+        .valueChanges.subscribe((value) => this.featureFormUpdate(feature.key, value));
+    });
+  }
+
+  private gatherNeededData(): Observable<object> {
+    const promises = {};
+    if (this.mode) {
+      // Mode is not set for creation
       this.route.params.subscribe((params: { image_spec: string; snap: string }) => {
         const imageSpec = ImageSpec.fromString(decodeURIComponent(params.image_spec));
         if (params.snap) {
           this.snapName = decodeURIComponent(params.snap);
         }
-        promisses[Promisse.RbdServiceGet] = this.rbdService.get(imageSpec);
+        promises['rbd'] = this.rbdService.get(imageSpec);
       });
     } else {
       // New image
-      this.rbdService.defaultFeatures().subscribe((defaultFeatures: Array<string>) => {
-        this.setFeatures(defaultFeatures);
-      });
+      promises['defaultFeatures'] = this.rbdService.defaultFeatures();
     }
     if (this.mode !== this.rbdFormMode.editing && this.poolPermission.read) {
-      promisses[Promisse.PoolServiceList] = this.poolService.list([
+      promises['pools'] = this.poolService.list([
         'pool_name',
         'type',
         'flags_names',
         'application_metadata'
       ]);
     }
+    return forkJoin(promises);
+  }
 
-    forkJoin(promisses).subscribe((data: object) => {
-      // poolService.list
-      if (data[Promisse.PoolServiceList]) {
-        const pools: Pool[] = [];
-        const dataPools = [];
-        for (const pool of data[Promisse.PoolServiceList]) {
-          if (this.rbdService.isRBDPool(pool)) {
-            if (pool.type === 'replicated') {
-              pools.push(pool);
-              dataPools.push(pool);
-            } else if (
-              pool.type === 'erasure' &&
-              pool.flags_names.indexOf('ec_overwrites') !== -1
-            ) {
-              dataPools.push(pool);
-            }
-          }
-        }
-        this.pools = pools;
-        this.allPools = pools;
-        this.dataPools = dataPools;
-        this.allDataPools = dataPools;
-        if (this.pools.length === 1) {
-          const poolName = this.pools[0].pool_name;
-          this.rbdForm.get('pool').setValue(poolName);
-          this.onPoolChange(poolName);
-        }
-      }
+  private handleExternalData(data: ExternalData) {
+    this.handlePoolData(data.pools);
 
-      // rbdService.get
-      if (data[Promisse.RbdServiceGet]) {
-        const resp: RbdFormResponseModel = data[Promisse.RbdServiceGet];
-        this.setResponse(resp, this.snapName);
-        this.rbdImage.next(resp);
-      }
+    if (data.defaultFeatures) {
+      // Fetched only during creation
+      this.setFeatures(data.defaultFeatures);
+    }
 
-      this.loadingReady();
-    });
+    if (data.rbd) {
+      // Not fetched for creation
+      const resp = data.rbd;
+      this.setResponse(resp, this.snapName);
+      this.rbdImage.next(resp);
+    }
 
-    _.each(this.features, (feature) => {
-      this.rbdForm
-        .get('features')
-        .get(feature.key)
-        .valueChanges.subscribe((value) => this.featureFormUpdate(feature.key, value));
-    });
+    this.loadingReady();
+  }
+
+  private handlePoolData(data: Pool[]) {
+    if (!data) {
+      // Not fetched while editing
+      return;
+    }
+    const pools: Pool[] = [];
+    const dataPools = [];
+    for (const pool of data) {
+      if (this.rbdService.isRBDPool(pool)) {
+        if (pool.type === 'replicated') {
+          pools.push(pool);
+          dataPools.push(pool);
+        } else if (pool.type === 'erasure' && pool.flags_names.indexOf('ec_overwrites') !== -1) {
+          dataPools.push(pool);
+        }
+      }
+    }
+    this.pools = pools;
+    this.allPools = pools;
+    this.dataPools = dataPools;
+    this.allDataPools = dataPools;
+    if (this.pools.length === 1) {
+      const poolName = this.pools[0].pool_name;
+      this.rbdForm.get('pool').setValue(poolName);
+      this.onPoolChange(poolName);
+    }
+    if (this.allDataPools.length <= 1) {
+      this.rbdForm.get('useDataPool').disable();
+    }
   }
 
   onPoolChange(selectedPoolName: string) {