]> git.apps.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard: Disable RBD clone action when conditions are not met 37416/head
authorTiago Melo <tmelo@suse.com>
Wed, 7 Oct 2020 23:01:28 +0000 (23:01 +0000)
committerTiago Melo <tmelo@suse.com>
Thu, 8 Oct 2020 16:45:00 +0000 (16:45 +0000)
Disable clone action when RBD snapshot is not protected
and clone format version is 1.

Fixes: https://tracker.ceph.com/issues/37873
Signed-off-by: Tiago Melo <tmelo@suse.com>
qa/tasks/mgr/dashboard/test_rbd.py
src/pybind/mgr/dashboard/controllers/rbd.py
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-snapshot-list/rbd-snapshot-actions.model.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-snapshot-list/rbd-snapshot-list.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-snapshot-list/rbd-snapshot-list.component.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/api/rbd.service.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/api/rbd.service.ts
src/pybind/mgr/dashboard/openapi.yaml

index 48119383bf3c04ebbf4863f8c705f3e2a91898a6..c6150dd06df2715e4c251c30e523cd5fc4df03a9 100644 (file)
@@ -9,7 +9,7 @@ from .helper import DashboardTestCase, JObj, JLeaf, JList
 
 
 class RbdTest(DashboardTestCase):
-    AUTH_ROLES = ['pool-manager', 'block-manager']
+    AUTH_ROLES = ['pool-manager', 'block-manager', 'cluster-manager']
 
     @classmethod
     def create_pool(cls, name, pg_num, pool_type, application='rbd'):
@@ -754,6 +754,57 @@ class RbdTest(DashboardTestCase):
         self.assertEqual(default_features, [
             'deep-flatten', 'exclusive-lock', 'fast-diff', 'layering', 'object-map'])
 
+    def test_clone_format_version(self):
+        config_name = 'rbd_default_clone_format'
+        def _get_config_by_name(conf_name):
+            data = self._get('/api/cluster_conf/{}'.format(conf_name))
+            if 'value' in data:
+                return data['value']
+            return None
+
+        # with rbd_default_clone_format = auto
+        clone_format_version = self._get('/api/block/image/clone_format_version')
+        self.assertEqual(clone_format_version, 1)
+        self.assertStatus(200)
+
+        # with rbd_default_clone_format = 1
+        value = [{'section': "global", 'value': "1"}]
+        self._post('/api/cluster_conf', {
+            'name': config_name,
+            'value': value
+        })
+        self.wait_until_equal(
+                    lambda: _get_config_by_name(config_name),
+                    value,
+                    timeout=60)
+        clone_format_version = self._get('/api/block/image/clone_format_version')
+        self.assertEqual(clone_format_version, 1)
+        self.assertStatus(200)
+
+        # with rbd_default_clone_format = 2
+        value = [{'section': "global", 'value': "2"}]
+        self._post('/api/cluster_conf', {
+            'name': config_name,
+            'value': value
+        })
+        self.wait_until_equal(
+                    lambda: _get_config_by_name(config_name),
+                    value,
+                    timeout=60)
+        clone_format_version = self._get('/api/block/image/clone_format_version')
+        self.assertEqual(clone_format_version, 2)
+        self.assertStatus(200)
+
+        value = []
+        self._post('/api/cluster_conf', {
+            'name': config_name,
+            'value': value
+        })
+        self.wait_until_equal(
+                    lambda: _get_config_by_name(config_name),
+                    None,
+                    timeout=60)
+
     def test_image_with_namespace(self):
         self.create_namespace('rbd', 'ns')
         self.create_image('rbd', 'ns', 'test', 10240)
index bd72e80bc9b36d4377542dfdd325a2ab197ee9a3..57fe06a00a65a5538ea63dcddd72f07646384992 100644 (file)
@@ -237,6 +237,21 @@ class Rbd(RESTController):
         rbd_default_features = mgr.get('config')['rbd_default_features']
         return format_bitmask(int(rbd_default_features))
 
+    @RESTController.Collection('GET')
+    def clone_format_version(self):
+        """Return the RBD clone format version.
+        """
+        rbd_default_clone_format = mgr.get('config')['rbd_default_clone_format']
+        if rbd_default_clone_format != 'auto':
+            return int(rbd_default_clone_format)
+        osd_map = mgr.get_osdmap().dump()
+        min_compat_client = osd_map.get('min_compat_client', '')
+        require_min_compat_client = osd_map.get('require_min_compat_client', '')
+        if max(min_compat_client, require_min_compat_client) < 'mimic':
+            return 1
+
+        return 2
+
     @RbdTask('trash/move', ['{image_spec}'], 2.0)
     @RESTController.Resource('POST')
     @allow_empty_body
index 396bd45eb0c5a2993eb2370d02db0762ace97bb2..2b22da09aab65b3821e8bb1f86f77f943f65e9d6 100644 (file)
@@ -1,3 +1,5 @@
+import { RbdService } from 'app/shared/api/rbd.service';
+
 import { ActionLabelsI18n } from '../../../shared/constants/app.constants';
 import { Icons } from '../../../shared/enum/icons.enum';
 import { CdTableAction } from '../../../shared/models/cd-table-action';
@@ -14,7 +16,13 @@ export class RbdSnapshotActionsModel {
   deleteSnap: CdTableAction;
   ordering: CdTableAction[];
 
-  constructor(actionLabels: ActionLabelsI18n, featuresName: string[]) {
+  cloneFormatVersion = 1;
+
+  constructor(actionLabels: ActionLabelsI18n, featuresName: string[], rbdService: RbdService) {
+    rbdService.cloneFormatVersion().subscribe((version: number) => {
+      this.cloneFormatVersion = version;
+    });
+
     this.create = {
       permission: 'create',
       icon: Icons.add,
@@ -87,6 +95,10 @@ export class RbdSnapshotActionsModel {
         return $localize`Parent image must support Layering`;
       }
 
+      if (this.cloneFormatVersion === 1 && !selection.first().is_protected) {
+        return $localize`Snapshot must be protected in order to clone.`;
+      }
+
       return false;
     }
 
index 1ea292e0b4ffadcd4409c2aa6cefeced2f85e0d4..30e68261b28336b3d17effc4e7e186efc597d1ea 100644 (file)
@@ -31,6 +31,7 @@ import { SummaryService } from '../../../shared/services/summary.service';
 import { TaskListService } from '../../../shared/services/task-list.service';
 import { RbdSnapshotFormModalComponent } from '../rbd-snapshot-form/rbd-snapshot-form-modal.component';
 import { RbdTabsComponent } from '../rbd-tabs/rbd-tabs.component';
+import { RbdSnapshotActionsModel } from './rbd-snapshot-actions.model';
 import { RbdSnapshotListComponent } from './rbd-snapshot-list.component';
 import { RbdSnapshotModel } from './rbd-snapshot.model';
 
@@ -277,4 +278,32 @@ describe('RbdSnapshotListComponent', () => {
       }
     });
   });
+
+  describe('clone button disable state', () => {
+    let actions: RbdSnapshotActionsModel;
+
+    beforeEach(() => {
+      fixture.detectChanges();
+      const rbdService = TestBed.inject(RbdService);
+      const actionLabelsI18n = TestBed.inject(ActionLabelsI18n);
+      actions = new RbdSnapshotActionsModel(actionLabelsI18n, [], rbdService);
+    });
+
+    it('should be disabled with version 1 and protected false', () => {
+      const selection = new CdTableSelection([{ name: 'someName', is_protected: false }]);
+      const disableDesc = actions.getCloneDisableDesc(selection, ['layering']);
+      expect(disableDesc).toBe('Snapshot must be protected in order to clone.');
+    });
+
+    it.each([
+      [1, true],
+      [2, true],
+      [2, false]
+    ])('should be enabled with version %d and protected %s', (version, is_protected) => {
+      actions.cloneFormatVersion = version;
+      const selection = new CdTableSelection([{ name: 'someName', is_protected: is_protected }]);
+      const disableDesc = actions.getCloneDisableDesc(selection, ['layering']);
+      expect(disableDesc).toBe(false);
+    });
+  });
 });
index d2d7378a8f5869fe04da82b8666cd00f89d57b51..d14136c7c888de2ba5758a919b1f99b4d9cb8a82 100644 (file)
@@ -130,7 +130,11 @@ export class RbdSnapshotListComponent implements OnInit, OnChanges {
   ngOnChanges() {
     const imageSpec = new ImageSpec(this.poolName, this.namespace, this.rbdName);
 
-    const actions = new RbdSnapshotActionsModel(this.actionLabels, this.featuresName);
+    const actions = new RbdSnapshotActionsModel(
+      this.actionLabels,
+      this.featuresName,
+      this.rbdService
+    );
     actions.create.click = () => this.openCreateSnapshotModal();
     actions.rename.click = () => this.openEditSnapshotModal();
     actions.protect.click = () => this.toggleProtection();
index 72af80b977d9f1c975e767707608532a51775a44..dae61e7e19254bee5bc96be95fdaac10a84ca691 100644 (file)
@@ -80,6 +80,12 @@ describe('RbdService', () => {
     expect(req.request.method).toBe('GET');
   });
 
+  it('should call cloneFormatVersion', () => {
+    service.cloneFormatVersion().subscribe();
+    const req = httpTesting.expectOne('api/block/image/clone_format_version');
+    expect(req.request.method).toBe('GET');
+  });
+
   it('should call createSnapshot', () => {
     service.createSnapshot(new ImageSpec('poolName', null, 'rbdName'), 'snapshotName').subscribe();
     const req = httpTesting.expectOne('api/block/image/poolName%2FrbdName/snap');
index d65c2356f56900caf439405720da0f7217236ebf..5482f093122869cd38e672062a3d7f2cc2470a24 100644 (file)
@@ -75,6 +75,10 @@ export class RbdService {
     return this.http.get('api/block/image/default_features');
   }
 
+  cloneFormatVersion() {
+    return this.http.get<number>('api/block/image/clone_format_version');
+  }
+
   createSnapshot(imageSpec: ImageSpec, @cdEncodeNot snapshotName: string) {
     const request = {
       snapshot_name: snapshotName
index bba9a054a30eeabfa557ab2265d7742f0dd0eda8..f93dfe5f669cdc1be1962c25a11df74e5b002411 100644 (file)
@@ -226,6 +226,26 @@ paths:
       - jwt: []
       tags:
       - Rbd
+  /api/block/image/clone_format_version:
+    get:
+      description: "Return the RBD clone format version.\n        "
+      parameters: []
+      responses:
+        '200':
+          description: OK
+        '400':
+          description: Operation exception. Please check the response body for details.
+        '401':
+          description: Unauthenticated access. Please login first.
+        '403':
+          description: Unauthorized access. Please check your permissions.
+        '500':
+          description: Unexpected error. Please check the response body for the stack
+            trace.
+      security:
+      - jwt: []
+      tags:
+      - Rbd
   /api/block/image/default_features:
     get:
       parameters: []