mirror of
https://github.com/ceph/ceph
synced 2025-02-24 11:37:37 +00:00
mgr/dashboard: Disable RBD clone action when conditions are not met
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>
This commit is contained in:
parent
98a8f533b8
commit
ed237aa203
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -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();
|
||||
|
@ -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');
|
||||
|
@ -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
|
||||
|
@ -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: []
|
||||
|
Loading…
Reference in New Issue
Block a user