From 688218cdbb86bd42d7733f0d6784ac386e3d5fb6 Mon Sep 17 00:00:00 2001 From: Ricardo Marques Date: Wed, 31 Jul 2019 10:34:57 +0100 Subject: [PATCH] mgr/dashboard: Support RBD namespaces Fixes: https://tracker.ceph.com/issues/25125 Signed-off-by: Ricardo Marques --- qa/tasks/mgr/dashboard/test_rbd.py | 388 ++++++++++-------- src/pybind/mgr/dashboard/controllers/rbd.py | 331 +++++++-------- .../dashboard/controllers/rbd_mirroring.py | 2 +- .../frontend/e2e/block/images.e2e-spec.ts | 9 +- .../dashboard/frontend/e2e/block/images.po.ts | 2 +- .../frontend/src/app/app.component.spec.ts | 3 +- .../src/app/ceph/block/block.module.ts | 13 +- .../iscsi-target-form.component.ts | 4 + .../rbd-details/rbd-details.component.html | 6 +- .../rbd-form/rbd-form-clone-request.model.ts | 1 + .../rbd-form/rbd-form-copy-request.model.ts | 1 + .../block/rbd-form/rbd-form.component.html | 41 ++ .../block/rbd-form/rbd-form.component.spec.ts | 14 +- .../ceph/block/rbd-form/rbd-form.component.ts | 173 +++++--- .../app/ceph/block/rbd-form/rbd-form.model.ts | 1 + .../ceph/block/rbd-form/rbd-parent.model.ts | 1 + .../rbd-images/rbd-images.component.html | 4 + .../rbd-images/rbd-images.component.spec.ts | 2 + .../block/rbd-list/rbd-list.component.html | 2 +- .../block/rbd-list/rbd-list.component.spec.ts | 10 +- .../ceph/block/rbd-list/rbd-list.component.ts | 150 ++++--- .../src/app/ceph/block/rbd-list/rbd-model.ts | 1 + .../rbd-namespace-form.component.html | 92 +++++ .../rbd-namespace-form.component.scss | 0 .../rbd-namespace-form.component.spec.ts | 41 ++ .../rbd-namespace-form.component.ts | 146 +++++++ .../rbd-namespace-list.component.html | 16 + .../rbd-namespace-list.component.scss | 0 .../rbd-namespace-list.component.spec.ts | 31 ++ .../rbd-namespace-list.component.ts | 162 ++++++++ .../rbd-snapshot-form.component.ts | 11 +- .../rbd-snapshot-list.component.html | 2 +- .../rbd-snapshot-list.component.spec.ts | 3 +- .../rbd-snapshot-list.component.ts | 24 +- .../rbd-trash-list.component.spec.ts | 5 +- .../rbd-trash-list.component.ts | 40 +- .../rbd-trash-move-modal.component.html | 2 +- .../rbd-trash-move-modal.component.spec.ts | 6 +- .../rbd-trash-move-modal.component.ts | 8 +- .../rbd-trash-restore-modal.component.html | 2 +- .../rbd-trash-restore-modal.component.spec.ts | 6 +- .../rbd-trash-restore-modal.component.ts | 8 +- .../src/app/shared/api/rbd.service.spec.ts | 74 ++-- .../src/app/shared/api/rbd.service.ts | 132 ++++-- .../notifications-sidebar.component.spec.ts | 7 +- .../services/notification.service.spec.ts | 8 +- .../prometheus-alert-formatter.spec.ts | 3 +- .../shared/services/task-list.service.spec.ts | 3 +- .../services/task-message.service.spec.ts | 23 +- .../shared/services/task-message.service.ts | 59 ++- src/pybind/mgr/dashboard/services/progress.py | 12 +- src/pybind/mgr/dashboard/services/rbd.py | 162 +++++++- .../mgr/dashboard/tests/test_rbd_service.py | 18 + 53 files changed, 1634 insertions(+), 631 deletions(-) create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-namespace-form/rbd-namespace-form.component.html create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-namespace-form/rbd-namespace-form.component.scss create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-namespace-form/rbd-namespace-form.component.spec.ts create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-namespace-form/rbd-namespace-form.component.ts create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-namespace-list/rbd-namespace-list.component.html create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-namespace-list/rbd-namespace-list.component.scss create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-namespace-list/rbd-namespace-list.component.spec.ts create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-namespace-list/rbd-namespace-list.component.ts create mode 100644 src/pybind/mgr/dashboard/tests/test_rbd_service.py diff --git a/qa/tasks/mgr/dashboard/test_rbd.py b/qa/tasks/mgr/dashboard/test_rbd.py index 570c864682916..7ac313a565008 100644 --- a/qa/tasks/mgr/dashboard/test_rbd.py +++ b/qa/tasks/mgr/dashboard/test_rbd.py @@ -27,100 +27,135 @@ class RbdTest(DashboardTestCase): def test_read_access_permissions(self): self._get('/api/block/image') self.assertStatus(403) - self._get('/api/block/image/pool/image') + self.get_image('pool', None, 'image') self.assertStatus(403) @DashboardTestCase.RunAs('test', 'test', [{'rbd-image': ['read', 'update', 'delete']}]) def test_create_access_permissions(self): - self.create_image('pool', 'name', 0) + self.create_image('pool', None, 'name', 0) self.assertStatus(403) - self.create_snapshot('pool', 'image', 'snapshot') + self.create_snapshot('pool', None, 'image', 'snapshot') self.assertStatus(403) - self.copy_image('src_pool', 'src_image', 'dest_pool', 'dest_image') + self.copy_image('src_pool', None, 'src_image', 'dest_pool', None, 'dest_image') self.assertStatus(403) - self.clone_image('parent_pool', 'parent_image', 'parent_snap', 'pool', 'name') + self.clone_image('parent_pool', None, 'parent_image', 'parent_snap', 'pool', None, 'name') self.assertStatus(403) @DashboardTestCase.RunAs('test', 'test', [{'rbd-image': ['read', 'create', 'delete']}]) def test_update_access_permissions(self): - self.edit_image('pool', 'image') + self.edit_image('pool', None, 'image') self.assertStatus(403) - self.update_snapshot('pool', 'image', 'snapshot', None, None) + self.update_snapshot('pool', None, 'image', 'snapshot', None, None) self.assertStatus(403) - self._task_post('/api/block/image/rbd/rollback_img/snap/snap1/rollback') + self.rollback_snapshot('rbd', None, 'rollback_img', 'snap1') self.assertStatus(403) - self.flatten_image('pool', 'image') + self.flatten_image('pool', None, 'image') self.assertStatus(403) @DashboardTestCase.RunAs('test', 'test', [{'rbd-image': ['read', 'create', 'update']}]) def test_delete_access_permissions(self): - self.remove_image('pool', 'image') + self.remove_image('pool', None, 'image') self.assertStatus(403) - self.remove_snapshot('pool', 'image', 'snapshot') + self.remove_snapshot('pool', None, 'image', 'snapshot') self.assertStatus(403) @classmethod - def create_image(cls, pool, name, size, **kwargs): - data = {'name': name, 'pool_name': pool, 'size': size} + def create_namespace(cls, pool, namespace): + data = {'namespace': namespace} + return cls._post('/api/block/pool/{}/namespace'.format(pool), data) + + @classmethod + def remove_namespace(cls, pool, namespace): + return cls._delete('/api/block/pool/{}/namespace/{}'.format(pool, namespace)) + + @classmethod + def create_image(cls, pool, namespace, name, size, **kwargs): + data = {'name': name, 'pool_name': pool, 'namespace': namespace, 'size': size} data.update(kwargs) return cls._task_post('/api/block/image', data) @classmethod - def clone_image(cls, parent_pool, parent_image, parent_snap, pool, name, - **kwargs): + def get_image(cls, pool, namespace, name): + namespace = '{}%2F'.format(namespace) if namespace else '' + return cls._get('/api/block/image/{}%2F{}{}'.format(pool, namespace, name)) + + @classmethod + def clone_image(cls, parent_pool, parent_namespace, parent_image, parent_snap, pool, namespace, + name, **kwargs): # pylint: disable=too-many-arguments - data = {'child_image_name': name, 'child_pool_name': pool} + data = {'child_image_name': name, 'child_namespace': namespace, 'child_pool_name': pool} data.update(kwargs) - return cls._task_post('/api/block/image/{}/{}/snap/{}/clone' - .format(parent_pool, parent_image, parent_snap), + parent_namespace = '{}%2F'.format(parent_namespace) if parent_namespace else '' + return cls._task_post('/api/block/image/{}%2F{}{}/snap/{}/clone' + .format(parent_pool, parent_namespace, parent_image, parent_snap), data) @classmethod - def copy_image(cls, src_pool, src_image, dest_pool, dest_image, **kwargs): + def copy_image(cls, src_pool, src_namespace, src_image, dest_pool, dest_namespace, dest_image, + **kwargs): # pylint: disable=too-many-arguments - data = {'dest_image_name': dest_image, 'dest_pool_name': dest_pool} + data = {'dest_image_name': dest_image, + 'dest_pool_name': dest_pool, + 'dest_namespace': dest_namespace} data.update(kwargs) - return cls._task_post('/api/block/image/{}/{}/copy' - .format(src_pool, src_image), data) + src_namespace = '{}%2F'.format(src_namespace) if src_namespace else '' + return cls._task_post('/api/block/image/{}%2F{}{}/copy' + .format(src_pool, src_namespace, src_image), data) @classmethod - def remove_image(cls, pool, image): - return cls._task_delete('/api/block/image/{}/{}'.format(pool, image)) + def remove_image(cls, pool, namespace, image): + namespace = '{}%2F'.format(namespace) if namespace else '' + return cls._task_delete('/api/block/image/{}%2F{}{}'.format(pool, namespace, image)) # pylint: disable=too-many-arguments @classmethod - def edit_image(cls, pool, image, name=None, size=None, features=None, **kwargs): + def edit_image(cls, pool, namespace, image, name=None, size=None, features=None, **kwargs): kwargs.update({'name': name, 'size': size, 'features': features}) - return cls._task_put('/api/block/image/{}/{}'.format(pool, image), kwargs) + namespace = '{}%2F'.format(namespace) if namespace else '' + return cls._task_put('/api/block/image/{}%2F{}{}'.format(pool, namespace, image), kwargs) @classmethod - def flatten_image(cls, pool, image): - return cls._task_post('/api/block/image/{}/{}/flatten'.format(pool, image)) + def flatten_image(cls, pool, namespace, image): + namespace = '{}%2F'.format(namespace) if namespace else '' + return cls._task_post('/api/block/image/{}%2F{}{}/flatten'.format(pool, namespace, image)) @classmethod - def create_snapshot(cls, pool, image, snapshot): - return cls._task_post('/api/block/image/{}/{}/snap'.format(pool, image), + def create_snapshot(cls, pool, namespace, image, snapshot): + namespace = '{}%2F'.format(namespace) if namespace else '' + return cls._task_post('/api/block/image/{}%2F{}{}/snap'.format(pool, namespace, image), {'snapshot_name': snapshot}) @classmethod - def remove_snapshot(cls, pool, image, snapshot): - return cls._task_delete('/api/block/image/{}/{}/snap/{}'.format(pool, image, snapshot)) + def remove_snapshot(cls, pool, namespace, image, snapshot): + namespace = '{}%2F'.format(namespace) if namespace else '' + return cls._task_delete('/api/block/image/{}%2F{}{}/snap/{}'.format(pool, namespace, image, + snapshot)) @classmethod - def update_snapshot(cls, pool, image, snapshot, new_name, is_protected): - return cls._task_put('/api/block/image/{}/{}/snap/{}'.format(pool, image, snapshot), + def update_snapshot(cls, pool, namespace, image, snapshot, new_name, is_protected): + namespace = '{}%2F'.format(namespace) if namespace else '' + return cls._task_put('/api/block/image/{}%2F{}{}/snap/{}'.format(pool, namespace, image, + snapshot), {'new_snap_name': new_name, 'is_protected': is_protected}) + @classmethod + def rollback_snapshot(cls, pool, namespace, image, snapshot): + namespace = '{}%2F'.format(namespace) if namespace else '' + return cls._task_post('/api/block/image/{}%2F{}{}/snap/{}/rollback'.format(pool, + namespace, + image, + snapshot)) + @classmethod def setUpClass(cls): super(RbdTest, cls).setUpClass() cls.create_pool('rbd', 2**3, 'replicated') cls.create_pool('rbd_iscsi', 2**3, 'replicated') - cls.create_image('rbd', 'img1', 2**30) - cls.create_image('rbd', 'img2', 2*2**30) - cls.create_image('rbd_iscsi', 'img1', 2**30) - cls.create_image('rbd_iscsi', 'img2', 2*2**30) + cls.create_image('rbd', None, 'img1', 2**30) + cls.create_image('rbd', None, 'img2', 2*2**30) + cls.create_image('rbd_iscsi', None, 'img1', 2**30) + cls.create_image('rbd_iscsi', None, 'img2', 2*2**30) osd_metadata = cls.ceph_cluster.mon_manager.get_osd_metadata() cls.bluestore_support = True @@ -140,18 +175,30 @@ class RbdTest(DashboardTestCase): @classmethod def create_image_in_trash(cls, pool, name, delay=0): - cls.create_image(pool, name, 10240) - img = cls._get('/api/block/image/{}/{}'.format(pool, name)) + cls.create_image(pool, None, name, 10240) + img = cls._get('/api/block/image/{}%2F{}'.format(pool, name)) - cls._task_post("/api/block/image/{}/{}/move_trash".format(pool, name), + cls._task_post("/api/block/image/{}%2F{}/move_trash".format(pool, name), {'delay': delay}) return img['id'] @classmethod - def remove_trash(cls, pool, image_id, image_name, force=False): - return cls._task_delete('/api/block/image/trash/{}/{}/?image_name={}&force={}'.format( - 'rbd', image_id, image_name, force)) + def remove_trash(cls, pool, image_id, force=False): + return cls._task_delete('/api/block/image/trash/{}%2F{}/?force={}'.format( + pool, image_id, force)) + + @classmethod + def restore_trash(cls, pool, namespace, image_id, new_image_name): + data = {'new_image_name': new_image_name} + namespace = '{}%2F'.format(namespace) if namespace else '' + return cls._task_post('/api/block/image/trash/{}%2F{}{}/restore'.format(pool, + namespace, + image_id), data) + + @classmethod + def purge_trash(cls, pool): + return cls._task_post('/api/block/image/trash/purge?pool_name={}'.format(pool)) @classmethod def get_trash(cls, pool, image_id): @@ -190,11 +237,13 @@ class RbdTest(DashboardTestCase): 'name': JLeaf(str), 'id': JLeaf(str), 'pool_name': JLeaf(str), + 'namespace': JLeaf(str, none=True), 'features': JLeaf(int), 'features_name': JList(JLeaf(str)), 'stripe_count': JLeaf(int, none=True), 'stripe_unit': JLeaf(int, none=True), 'parent': JObj(sub_elems={'pool_name': JLeaf(str), + 'pool_namespace': JLeaf(str, none=True), 'image_name': JLeaf(str), 'snap_name': JLeaf(str)}, none=True), 'data_pool': JLeaf(str, none=True), @@ -279,10 +328,10 @@ class RbdTest(DashboardTestCase): def test_create(self): rbd_name = 'test_rbd' - self.create_image('rbd', rbd_name, 10240) + self.create_image('rbd', None, rbd_name, 10240) self.assertStatus(201) - img = self._get('/api/block/image/rbd/test_rbd') + img = self.get_image('rbd', None, 'test_rbd') self.assertStatus(200) self._validate_image(img, name=rbd_name, size=10240, @@ -292,7 +341,7 @@ class RbdTest(DashboardTestCase): 'fast-diff', 'layering', 'object-map']) - self.remove_image('rbd', rbd_name) + self.remove_image('rbd', None, rbd_name) def test_create_with_configuration(self): pool = 'rbd' @@ -312,14 +361,14 @@ class RbdTest(DashboardTestCase): 'value': str(10240 * 2), }] - self.create_image(pool, image_name, size, configuration=configuration) + self.create_image(pool, None, image_name, size, configuration=configuration) self.assertStatus(201) - img = self._get('/api/block/image/rbd/{}'.format(image_name)) + img = self.get_image('rbd', None, image_name) self.assertStatus(200) for conf in expected: self.assertIn(conf, img['configuration']) - self.remove_image(pool, image_name) + self.remove_image(pool, None, image_name) def test_create_rbd_in_data_pool(self): if not self.bluestore_support: @@ -328,10 +377,10 @@ class RbdTest(DashboardTestCase): self.create_pool('data_pool', 2**4, 'erasure') rbd_name = 'test_rbd_in_data_pool' - self.create_image('rbd', rbd_name, 10240, data_pool='data_pool') + self.create_image('rbd', None, rbd_name, 10240, data_pool='data_pool') self.assertStatus(201) - img = self._get('/api/block/image/rbd/test_rbd_in_data_pool') + img = self.get_image('rbd', None, 'test_rbd_in_data_pool') self.assertStatus(200) self._validate_image(img, name=rbd_name, size=10240, @@ -342,31 +391,31 @@ class RbdTest(DashboardTestCase): 'fast-diff', 'layering', 'object-map']) - self.remove_image('rbd', rbd_name) + self.remove_image('rbd', None, rbd_name) self.assertStatus(204) self._ceph_cmd(['osd', 'pool', 'delete', 'data_pool', 'data_pool', '--yes-i-really-really-mean-it']) def test_create_rbd_twice(self): - res = self.create_image('rbd', 'test_rbd_twice', 10240) + res = self.create_image('rbd', None, 'test_rbd_twice', 10240) - res = self.create_image('rbd', 'test_rbd_twice', 10240) + res = self.create_image('rbd', None, 'test_rbd_twice', 10240) self.assertStatus(400) self.assertEqual(res, {"code": '17', 'status': 400, "component": "rbd", "detail": "[errno 17] RBD image already exists (error creating image)", 'task': {'name': 'rbd/create', - 'metadata': {'pool_name': 'rbd', + 'metadata': {'pool_name': 'rbd', 'namespace': None, 'image_name': 'test_rbd_twice'}}}) - self.remove_image('rbd', 'test_rbd_twice') + self.remove_image('rbd', None, 'test_rbd_twice') self.assertStatus(204) def test_snapshots_and_clone_info(self): - self.create_snapshot('rbd', 'img1', 'snap1') - self.create_snapshot('rbd', 'img1', 'snap2') + self.create_snapshot('rbd', None, 'img1', 'snap1') + self.create_snapshot('rbd', None, 'img1', 'snap2') self._rbd_cmd(['snap', 'protect', 'rbd/img1@snap1']) self._rbd_cmd(['clone', 'rbd/img1@snap1', 'rbd_iscsi/img1_clone']) - img = self._get('/api/block/image/rbd/img1') + img = self.get_image('rbd', None, 'img1') self.assertStatus(200) self._validate_image(img, name='img1', size=1073741824, num_objs=256, obj_size=4194304, parent=None, @@ -383,116 +432,115 @@ class RbdTest(DashboardTestCase): elif snap['name'] == 'snap2': self._validate_snapshot(snap, is_protected=False) - img = self._get('/api/block/image/rbd_iscsi/img1_clone') + img = self.get_image('rbd_iscsi', None, 'img1_clone') self.assertStatus(200) self._validate_image(img, name='img1_clone', size=1073741824, num_objs=256, obj_size=4194304, - parent={'pool_name': 'rbd', 'image_name': 'img1', - 'snap_name': 'snap1'}, + parent={'pool_name': 'rbd', 'pool_namespace': '', + 'image_name': 'img1', 'snap_name': 'snap1'}, features_name=['deep-flatten', 'exclusive-lock', 'fast-diff', 'layering', 'object-map']) - self.remove_image('rbd_iscsi', 'img1_clone') + self.remove_image('rbd_iscsi', None, 'img1_clone') self.assertStatus(204) def test_disk_usage(self): self._rbd_cmd(['bench', '--io-type', 'write', '--io-total', '50M', 'rbd/img2']) - self.create_snapshot('rbd', 'img2', 'snap1') + self.create_snapshot('rbd', None, 'img2', 'snap1') self._rbd_cmd(['bench', '--io-type', 'write', '--io-total', '20M', 'rbd/img2']) - self.create_snapshot('rbd', 'img2', 'snap2') + self.create_snapshot('rbd', None, 'img2', 'snap2') self._rbd_cmd(['bench', '--io-type', 'write', '--io-total', '10M', 'rbd/img2']) - self.create_snapshot('rbd', 'img2', 'snap3') + self.create_snapshot('rbd', None, 'img2', 'snap3') self._rbd_cmd(['bench', '--io-type', 'write', '--io-total', '5M', 'rbd/img2']) - img = self._get('/api/block/image/rbd/img2') + img = self.get_image('rbd', None, 'img2') self.assertStatus(200) self._validate_image(img, name='img2', size=2147483648, total_disk_usage=268435456, disk_usage=67108864) def test_delete_non_existent_image(self): - res = self.remove_image('rbd', 'i_dont_exist') + res = self.remove_image('rbd', None, 'i_dont_exist') self.assertStatus(400) self.assertEqual(res, {u'code': u'2', "status": 400, "component": "rbd", "detail": "[errno 2] RBD image not found (error removing image)", 'task': {'name': 'rbd/delete', - 'metadata': {'pool_name': 'rbd', - 'image_name': 'i_dont_exist'}}}) + 'metadata': {'image_spec': 'rbd/i_dont_exist'}}}) def test_image_delete(self): - self.create_image('rbd', 'delete_me', 2**30) + self.create_image('rbd', None, 'delete_me', 2**30) self.assertStatus(201) - self.create_snapshot('rbd', 'delete_me', 'snap1') + self.create_snapshot('rbd', None, 'delete_me', 'snap1') self.assertStatus(201) - self.create_snapshot('rbd', 'delete_me', 'snap2') + self.create_snapshot('rbd', None, 'delete_me', 'snap2') self.assertStatus(201) - img = self._get('/api/block/image/rbd/delete_me') + img = self.get_image('rbd', None, 'delete_me') self.assertStatus(200) self._validate_image(img, name='delete_me', size=2**30) self.assertEqual(len(img['snapshots']), 2) - self.remove_snapshot('rbd', 'delete_me', 'snap1') + self.remove_snapshot('rbd', None, 'delete_me', 'snap1') self.assertStatus(204) - self.remove_snapshot('rbd', 'delete_me', 'snap2') + self.remove_snapshot('rbd', None, 'delete_me', 'snap2') self.assertStatus(204) - img = self._get('/api/block/image/rbd/delete_me') + img = self.get_image('rbd', None, 'delete_me') self.assertStatus(200) self._validate_image(img, name='delete_me', size=2**30) self.assertEqual(len(img['snapshots']), 0) - self.remove_image('rbd', 'delete_me') + self.remove_image('rbd', None, 'delete_me') self.assertStatus(204) def test_image_rename(self): - self.create_image('rbd', 'edit_img', 2**30) + self.create_image('rbd', None, 'edit_img', 2**30) self.assertStatus(201) - self._get('/api/block/image/rbd/edit_img') + self.get_image('rbd', None, 'edit_img') self.assertStatus(200) - self.edit_image('rbd', 'edit_img', 'new_edit_img') + self.edit_image('rbd', None, 'edit_img', 'new_edit_img') self.assertStatus(200) - self._get('/api/block/image/rbd/edit_img') + self.get_image('rbd', None, 'edit_img') self.assertStatus(404) - self._get('/api/block/image/rbd/new_edit_img') + self.get_image('rbd', None, 'new_edit_img') self.assertStatus(200) - self.remove_image('rbd', 'new_edit_img') + self.remove_image('rbd', None, 'new_edit_img') self.assertStatus(204) def test_image_resize(self): - self.create_image('rbd', 'edit_img', 2**30) + self.create_image('rbd', None, 'edit_img', 2**30) self.assertStatus(201) - img = self._get('/api/block/image/rbd/edit_img') + img = self.get_image('rbd', None, 'edit_img') self.assertStatus(200) self._validate_image(img, size=2**30) - self.edit_image('rbd', 'edit_img', size=2*2**30) + self.edit_image('rbd', None, 'edit_img', size=2*2**30) self.assertStatus(200) - img = self._get('/api/block/image/rbd/edit_img') + img = self.get_image('rbd', None, 'edit_img') self.assertStatus(200) self._validate_image(img, size=2*2**30) - self.remove_image('rbd', 'edit_img') + self.remove_image('rbd', None, 'edit_img') self.assertStatus(204) def test_image_change_features(self): - self.create_image('rbd', 'edit_img', 2**30, features=["layering"]) + self.create_image('rbd', None, 'edit_img', 2**30, features=["layering"]) self.assertStatus(201) - img = self._get('/api/block/image/rbd/edit_img') + img = self.get_image('rbd', None, 'edit_img') self.assertStatus(200) self._validate_image(img, features_name=["layering"]) - self.edit_image('rbd', 'edit_img', + self.edit_image('rbd', None, 'edit_img', features=["fast-diff", "object-map", "exclusive-lock"]) self.assertStatus(200) - img = self._get('/api/block/image/rbd/edit_img') + img = self.get_image('rbd', None, 'edit_img') self.assertStatus(200) self._validate_image(img, features_name=['exclusive-lock', 'fast-diff', 'layering', 'object-map']) - self.edit_image('rbd', 'edit_img', + self.edit_image('rbd', None, 'edit_img', features=["journaling", "exclusive-lock"]) self.assertStatus(200) - img = self._get('/api/block/image/rbd/edit_img') + img = self.get_image('rbd', None, 'edit_img') self.assertStatus(200) self._validate_image(img, features_name=['exclusive-lock', 'journaling', 'layering']) - self.remove_image('rbd', 'edit_img') + self.remove_image('rbd', None, 'edit_img') self.assertStatus(204) def test_image_change_config(self): @@ -530,113 +578,113 @@ class RbdTest(DashboardTestCase): 'value': '0', }] - self.create_image(pool, image, 2**30, configuration=initial_conf) + self.create_image(pool, None, image, 2**30, configuration=initial_conf) self.assertStatus(201) - img = self._get('/api/block/image/{}/{}'.format(pool, image)) + img = self.get_image(pool, None, image) self.assertStatus(200) for conf in initial_expect: self.assertIn(conf, img['configuration']) - self.edit_image(pool, image, configuration=new_conf) - img = self._get('/api/block/image/{}/{}'.format(pool, image)) + self.edit_image(pool, None, image, configuration=new_conf) + img = self.get_image(pool, None, image) self.assertStatus(200) for conf in new_expect: self.assertIn(conf, img['configuration']) - self.remove_image(pool, image) + self.remove_image(pool, None, image) self.assertStatus(204) def test_update_snapshot(self): - self.create_snapshot('rbd', 'img1', 'snap5') + self.create_snapshot('rbd', None, 'img1', 'snap5') self.assertStatus(201) - img = self._get('/api/block/image/rbd/img1') + img = self.get_image('rbd', None, 'img1') self._validate_snapshot_list(img['snapshots'], 'snap5', is_protected=False) - self.update_snapshot('rbd', 'img1', 'snap5', 'snap6', None) + self.update_snapshot('rbd', None, 'img1', 'snap5', 'snap6', None) self.assertStatus(200) - img = self._get('/api/block/image/rbd/img1') + img = self.get_image('rbd', None, 'img1') self._validate_snapshot_list(img['snapshots'], 'snap6', is_protected=False) - self.update_snapshot('rbd', 'img1', 'snap6', None, True) + self.update_snapshot('rbd', None, 'img1', 'snap6', None, True) self.assertStatus(200) - img = self._get('/api/block/image/rbd/img1') + img = self.get_image('rbd', None, 'img1') self._validate_snapshot_list(img['snapshots'], 'snap6', is_protected=True) - self.update_snapshot('rbd', 'img1', 'snap6', 'snap5', False) + self.update_snapshot('rbd', None, 'img1', 'snap6', 'snap5', False) self.assertStatus(200) - img = self._get('/api/block/image/rbd/img1') + img = self.get_image('rbd', None, 'img1') self._validate_snapshot_list(img['snapshots'], 'snap5', is_protected=False) - self.remove_snapshot('rbd', 'img1', 'snap5') + self.remove_snapshot('rbd', None, 'img1', 'snap5') self.assertStatus(204) def test_snapshot_rollback(self): - self.create_image('rbd', 'rollback_img', 2**30, + self.create_image('rbd', None, 'rollback_img', 2**30, features=["layering", "exclusive-lock", "fast-diff", "object-map"]) self.assertStatus(201) - self.create_snapshot('rbd', 'rollback_img', 'snap1') + self.create_snapshot('rbd', None, 'rollback_img', 'snap1') self.assertStatus(201) - img = self._get('/api/block/image/rbd/rollback_img') + img = self.get_image('rbd', None, 'rollback_img') self.assertStatus(200) self.assertEqual(img['disk_usage'], 0) self._rbd_cmd(['bench', '--io-type', 'write', '--io-total', '5M', 'rbd/rollback_img']) - img = self._get('/api/block/image/rbd/rollback_img') + img = self.get_image('rbd', None, 'rollback_img') self.assertStatus(200) self.assertGreater(img['disk_usage'], 0) - self._task_post('/api/block/image/rbd/rollback_img/snap/snap1/rollback') + self.rollback_snapshot('rbd', None, 'rollback_img', 'snap1') self.assertStatus([201, 200]) - img = self._get('/api/block/image/rbd/rollback_img') + img = self.get_image('rbd', None, 'rollback_img') self.assertStatus(200) self.assertEqual(img['disk_usage'], 0) - self.remove_snapshot('rbd', 'rollback_img', 'snap1') + self.remove_snapshot('rbd', None, 'rollback_img', 'snap1') self.assertStatus(204) - self.remove_image('rbd', 'rollback_img') + self.remove_image('rbd', None, 'rollback_img') self.assertStatus(204) def test_clone(self): - self.create_image('rbd', 'cimg', 2**30, features=["layering"]) + self.create_image('rbd', None, 'cimg', 2**30, features=["layering"]) self.assertStatus(201) - self.create_snapshot('rbd', 'cimg', 'snap1') + self.create_snapshot('rbd', None, 'cimg', 'snap1') self.assertStatus(201) - self.update_snapshot('rbd', 'cimg', 'snap1', None, True) + self.update_snapshot('rbd', None, 'cimg', 'snap1', None, True) self.assertStatus(200) - self.clone_image('rbd', 'cimg', 'snap1', 'rbd', 'cimg-clone', + self.clone_image('rbd', None, 'cimg', 'snap1', 'rbd', None, 'cimg-clone', features=["layering", "exclusive-lock", "fast-diff", "object-map"]) self.assertStatus([200, 201]) - img = self._get('/api/block/image/rbd/cimg-clone') + img = self.get_image('rbd', None, 'cimg-clone') self.assertStatus(200) self._validate_image(img, features_name=['exclusive-lock', 'fast-diff', 'layering', 'object-map'], - parent={'pool_name': 'rbd', 'image_name': 'cimg', - 'snap_name': 'snap1'}) + parent={'pool_name': 'rbd', 'pool_namespace': '', + 'image_name': 'cimg', 'snap_name': 'snap1'}) - res = self.remove_image('rbd', 'cimg') + res = self.remove_image('rbd', None, 'cimg') self.assertStatus(400) self.assertIn('code', res) self.assertEqual(res['code'], '39') - self.remove_image('rbd', 'cimg-clone') + self.remove_image('rbd', None, 'cimg-clone') self.assertStatus(204) - self.update_snapshot('rbd', 'cimg', 'snap1', None, False) + self.update_snapshot('rbd', None, 'cimg', 'snap1', None, False) self.assertStatus(200) - self.remove_snapshot('rbd', 'cimg', 'snap1') + self.remove_snapshot('rbd', None, 'cimg', 'snap1') self.assertStatus(204) - self.remove_image('rbd', 'cimg') + self.remove_image('rbd', None, 'cimg') self.assertStatus(204) def test_copy(self): - self.create_image('rbd', 'coimg', 2**30, + self.create_image('rbd', None, 'coimg', 2**30, features=["layering", "exclusive-lock", "fast-diff", "object-map"]) self.assertStatus(201) @@ -644,48 +692,48 @@ class RbdTest(DashboardTestCase): self._rbd_cmd(['bench', '--io-type', 'write', '--io-total', '5M', 'rbd/coimg']) - self.copy_image('rbd', 'coimg', 'rbd_iscsi', 'coimg-copy', + self.copy_image('rbd', None, 'coimg', 'rbd_iscsi', None, 'coimg-copy', features=["layering", "fast-diff", "exclusive-lock", "object-map"]) self.assertStatus([200, 201]) - img = self._get('/api/block/image/rbd/coimg') + img = self.get_image('rbd', None, 'coimg') self.assertStatus(200) self._validate_image(img, features_name=['layering', 'exclusive-lock', 'fast-diff', 'object-map']) - img_copy = self._get('/api/block/image/rbd_iscsi/coimg-copy') + img_copy = self.get_image('rbd_iscsi', None, 'coimg-copy') self._validate_image(img_copy, features_name=['exclusive-lock', 'fast-diff', 'layering', 'object-map'], disk_usage=img['disk_usage']) - self.remove_image('rbd', 'coimg') + self.remove_image('rbd', None, 'coimg') self.assertStatus(204) - self.remove_image('rbd_iscsi', 'coimg-copy') + self.remove_image('rbd_iscsi', None, 'coimg-copy') self.assertStatus(204) def test_flatten(self): - self.create_snapshot('rbd', 'img1', 'snapf') - self.update_snapshot('rbd', 'img1', 'snapf', None, True) - self.clone_image('rbd', 'img1', 'snapf', 'rbd_iscsi', 'img1_snapf_clone') + self.create_snapshot('rbd', None, 'img1', 'snapf') + self.update_snapshot('rbd', None, 'img1', 'snapf', None, True) + self.clone_image('rbd', None, 'img1', 'snapf', 'rbd_iscsi', None, 'img1_snapf_clone') - img = self._get('/api/block/image/rbd_iscsi/img1_snapf_clone') + img = self.get_image('rbd_iscsi', None, 'img1_snapf_clone') self.assertStatus(200) self.assertIsNotNone(img['parent']) - self.flatten_image('rbd_iscsi', 'img1_snapf_clone') + self.flatten_image('rbd_iscsi', None, 'img1_snapf_clone') self.assertStatus([200, 201]) - img = self._get('/api/block/image/rbd_iscsi/img1_snapf_clone') + img = self.get_image('rbd_iscsi', None, 'img1_snapf_clone') self.assertStatus(200) self.assertIsNone(img['parent']) - self.update_snapshot('rbd', 'img1', 'snapf', None, False) - self.remove_snapshot('rbd', 'img1', 'snapf') + self.update_snapshot('rbd', None, 'img1', 'snapf', None, False) + self.remove_snapshot('rbd', None, 'img1', 'snapf') self.assertStatus(204) - self.remove_image('rbd_iscsi', 'img1_snapf_clone') + self.remove_image('rbd_iscsi', None, 'img1_snapf_clone') self.assertStatus(204) def test_default_features(self): @@ -693,30 +741,30 @@ class RbdTest(DashboardTestCase): self.assertEqual(default_features, [ 'deep-flatten', 'exclusive-lock', 'fast-diff', 'layering', 'object-map']) - def test_image_with_special_name(self): - rbd_name = 'test/rbd' - rbd_name_encoded = 'test%2Frbd' - - self.create_image('rbd', rbd_name, 10240) + def test_image_with_namespace(self): + self.create_namespace('rbd', 'ns') + self.create_image('rbd', 'ns', 'test', 10240) self.assertStatus(201) - img = self._get("/api/block/image/rbd/" + rbd_name_encoded) + img = self.get_image('rbd', 'ns', 'test') self.assertStatus(200) - self._validate_image(img, name=rbd_name, size=10240, + self._validate_image(img, name='test', size=10240, + pool_name='rbd', namespace='ns', num_objs=1, obj_size=4194304, features_name=['deep-flatten', 'exclusive-lock', 'fast-diff', 'layering', 'object-map']) - self.remove_image('rbd', rbd_name_encoded) + self.remove_image('rbd', 'ns', 'test') + self.remove_namespace('rbd', 'ns') def test_move_image_to_trash(self): id = self.create_image_in_trash('rbd', 'test_rbd') self.assertStatus(200) - self._get('/api/block/image/rbd/test_rbd') + img = self.get_image('rbd', None, 'test_rbd') self.assertStatus(404) time.sleep(1) @@ -724,7 +772,7 @@ class RbdTest(DashboardTestCase): image = self.get_trash('rbd', id) self.assertIsNotNone(image) - self.remove_trash('rbd', id, 'test_rbd') + self.remove_trash('rbd', id) def test_list_trash(self): id = self.create_image_in_trash('rbd', 'test_rbd', 0) @@ -733,26 +781,25 @@ class RbdTest(DashboardTestCase): self.assertIsInstance(data, list) self.assertIsNotNone(data) - self.remove_trash('rbd', id, 'test_rbd') + self.remove_trash('rbd', id) self.assertStatus(204) def test_restore_trash(self): id = self.create_image_in_trash('rbd', 'test_rbd') - self._task_post('/api/block/image/trash/{}/{}/restore'.format('rbd', id), - {'new_image_name': 'test_rbd'}) + self.restore_trash('rbd', None, id, 'test_rbd') - self._get('/api/block/image/rbd/test_rbd') + self.get_image('rbd', None, 'test_rbd') self.assertStatus(200) image = self.get_trash('rbd', id) self.assertIsNone(image) - self.remove_image('rbd', 'test_rbd') + self.remove_image('rbd', None, 'test_rbd') def test_remove_expired_trash(self): id = self.create_image_in_trash('rbd', 'test_rbd', 0) - self.remove_trash('rbd', id, 'test_rbd', False) + self.remove_trash('rbd', id, False) self.assertStatus(204) image = self.get_trash('rbd', id) @@ -760,7 +807,7 @@ class RbdTest(DashboardTestCase): def test_remove_not_expired_trash(self): id = self.create_image_in_trash('rbd', 'test_rbd', 9999) - self.remove_trash('rbd', id, 'test_rbd', False) + self.remove_trash('rbd', id, False) self.assertStatus(400) time.sleep(1) @@ -768,11 +815,11 @@ class RbdTest(DashboardTestCase): image = self.get_trash('rbd', id) self.assertIsNotNone(image) - self.remove_trash('rbd', id, 'test_rbd', True) + self.remove_trash('rbd', id, True) def test_remove_not_expired_trash_with_force(self): id = self.create_image_in_trash('rbd', 'test_rbd', 9999) - self.remove_trash('rbd', id, 'test_rbd', True) + self.remove_trash('rbd', id, True) self.assertStatus(204) image = self.get_trash('rbd', id) @@ -784,7 +831,7 @@ class RbdTest(DashboardTestCase): time.sleep(1) - self._task_post('/api/block/image/trash/purge?pool_name={}'.format('rbd')) + self.purge_trash('rbd') self.assertStatus(200) time.sleep(1) @@ -794,3 +841,12 @@ class RbdTest(DashboardTestCase): trash_expired = self.get_trash('rbd', id_expired) self.assertIsNone(trash_expired) + + def test_list_namespaces(self): + self.create_namespace('rbd', 'ns') + + namespaces = self._get('/api/block/pool/rbd/namespace') + self.assertStatus(200) + self.assertEqual(len(namespaces), 1) + + self.remove_namespace('rbd', 'ns') diff --git a/src/pybind/mgr/dashboard/controllers/rbd.py b/src/pybind/mgr/dashboard/controllers/rbd.py index 133a359b71395..7ea98360240bd 100644 --- a/src/pybind/mgr/dashboard/controllers/rbd.py +++ b/src/pybind/mgr/dashboard/controllers/rbd.py @@ -12,14 +12,16 @@ import cherrypy import rbd from . import ApiController, RESTController, Task, UpdatePermission, \ - DeletePermission, CreatePermission + DeletePermission, CreatePermission from .. import mgr +from ..exceptions import DashboardException from ..security import Scope from ..services.ceph_service import CephService -from ..services.rbd import RbdConfiguration, format_bitmask, format_features +from ..services.rbd import RbdConfiguration, RbdService, format_bitmask, format_features,\ + parse_image_spec from ..tools import ViewCache, str_to_bool from ..services.exception import handle_rados_error, handle_rbd_error, \ - serialize_dashboard_exception + serialize_dashboard_exception # pylint: disable=not-callable @@ -32,17 +34,18 @@ def RbdTask(name, metadata, wait_for): # noqa: N802 return composed_decorator -def _rbd_call(pool_name, func, *args, **kwargs): +def _rbd_call(pool_name, namespace, func, *args, **kwargs): with mgr.rados.open_ioctx(pool_name) as ioctx: + ioctx.set_namespace(namespace if namespace is not None else '') func(ioctx, *args, **kwargs) -def _rbd_image_call(pool_name, image_name, func, *args, **kwargs): +def _rbd_image_call(pool_name, namespace, image_name, func, *args, **kwargs): def _ioctx_func(ioctx, image_name, func, *args, **kwargs): with rbd.Image(ioctx, image_name) as img: func(ioctx, img, *args, **kwargs) - return _rbd_call(pool_name, _ioctx_func, image_name, func, *args, **kwargs) + return _rbd_call(pool_name, namespace, _ioctx_func, image_name, func, *args, **kwargs) def _sort_features(features, enable=True): @@ -67,8 +70,6 @@ def _sort_features(features, enable=True): @ApiController('/block/image', Scope.RBD_IMAGE) class Rbd(RESTController): - RESOURCE_ID = "pool_name/image_name" - # set of image features that can be enable on existing images ALLOW_ENABLE_FEATURES = {"exclusive-lock", "object-map", "fast-diff", "journaling"} @@ -76,127 +77,6 @@ class Rbd(RESTController): ALLOW_DISABLE_FEATURES = {"exclusive-lock", "object-map", "fast-diff", "deep-flatten", "journaling"} - @classmethod - def _rbd_disk_usage(cls, image, snaps, whole_object=True): - class DUCallback(object): - def __init__(self): - self.used_size = 0 - - def __call__(self, offset, length, exists): - if exists: - self.used_size += length - - snap_map = {} - prev_snap = None - total_used_size = 0 - for _, size, name in snaps: - image.set_snap(name) - du_callb = DUCallback() - image.diff_iterate(0, size, prev_snap, du_callb, - whole_object=whole_object) - snap_map[name] = du_callb.used_size - total_used_size += du_callb.used_size - prev_snap = name - - return total_used_size, snap_map - - @classmethod - def _rbd_image(cls, ioctx, pool_name, image_name): - with rbd.Image(ioctx, image_name) as img: - stat = img.stat() - stat['name'] = image_name - stat['id'] = img.id() - stat['pool_name'] = pool_name - features = img.features() - stat['features'] = features - stat['features_name'] = format_bitmask(features) - - # the following keys are deprecated - del stat['parent_pool'] - del stat['parent_name'] - - stat['timestamp'] = "{}Z".format(img.create_timestamp() - .isoformat()) - - stat['stripe_count'] = img.stripe_count() - stat['stripe_unit'] = img.stripe_unit() - - data_pool_name = CephService.get_pool_name_from_id( - img.data_pool_id()) - if data_pool_name == pool_name: - data_pool_name = None - stat['data_pool'] = data_pool_name - - try: - parent_info = img.parent_info() - stat['parent'] = { - 'pool_name': parent_info[0], - 'image_name': parent_info[1], - 'snap_name': parent_info[2] - } - except rbd.ImageNotFound: - # no parent image - stat['parent'] = None - - # snapshots - stat['snapshots'] = [] - for snap in img.list_snaps(): - snap['timestamp'] = "{}Z".format( - img.get_snap_timestamp(snap['id']).isoformat()) - snap['is_protected'] = img.is_protected_snap(snap['name']) - snap['used_bytes'] = None - snap['children'] = [] - img.set_snap(snap['name']) - for child_pool_name, child_image_name in img.list_children(): - snap['children'].append({ - 'pool_name': child_pool_name, - 'image_name': child_image_name - }) - stat['snapshots'].append(snap) - - # disk usage - img_flags = img.flags() - if 'fast-diff' in stat['features_name'] and \ - not rbd.RBD_FLAG_FAST_DIFF_INVALID & img_flags: - snaps = [(s['id'], s['size'], s['name']) - for s in stat['snapshots']] - snaps.sort(key=lambda s: s[0]) - snaps += [(snaps[-1][0]+1 if snaps else 0, stat['size'], None)] - total_prov_bytes, snaps_prov_bytes = cls._rbd_disk_usage( - img, snaps, True) - stat['total_disk_usage'] = total_prov_bytes - for snap, prov_bytes in snaps_prov_bytes.items(): - if snap is None: - stat['disk_usage'] = prov_bytes - continue - for ss in stat['snapshots']: - if ss['name'] == snap: - ss['disk_usage'] = prov_bytes - break - else: - stat['total_disk_usage'] = None - stat['disk_usage'] = None - - stat['configuration'] = RbdConfiguration(pool_ioctx=ioctx, image_name=image_name).list() - - return stat - - @classmethod - @ViewCache() - def _rbd_pool_list(cls, pool_name): - rbd_inst = rbd.RBD() - with mgr.rados.open_ioctx(pool_name) as ioctx: - names = rbd_inst.list(ioctx) - result = [] - for name in names: - try: - stat = cls._rbd_image(ioctx, pool_name, name) - except rbd.ImageNotFound: - # may have been removed in the meanwhile - continue - result.append(stat) - return result - def _rbd_list(self, pool_name=None): if pool_name: pools = [pool_name] @@ -206,9 +86,10 @@ class Rbd(RESTController): result = [] for pool in pools: # pylint: disable=unbalanced-tuple-unpacking - status, value = self._rbd_pool_list(pool) + status, value = RbdService.rbd_pool_list(pool) for i, image in enumerate(value): - value[i]['configuration'] = RbdConfiguration(pool, image['name']).list() + value[i]['configuration'] = RbdConfiguration( + pool, image['namespace'], image['name']).list() result.append({'status': status, 'value': value, 'pool_name': pool}) return result @@ -219,16 +100,19 @@ class Rbd(RESTController): @handle_rbd_error() @handle_rados_error('pool') - def get(self, pool_name, image_name): + def get(self, image_spec): + pool_name, namespace, image_name = parse_image_spec(image_spec) ioctx = mgr.rados.open_ioctx(pool_name) + if namespace: + ioctx.set_namespace(namespace) try: - return self._rbd_image(ioctx, pool_name, image_name) + return RbdService.rbd_image(ioctx, pool_name, namespace, image_name) except rbd.ImageNotFound: raise cherrypy.HTTPError(404) @RbdTask('create', - {'pool_name': '{pool_name}', 'image_name': '{name}'}, 2.0) - def create(self, name, pool_name, size, obj_size=None, features=None, + {'pool_name': '{pool_name}', 'namespace': '{namespace}', 'image_name': '{name}'}, 2.0) + def create(self, name, pool_name, size, namespace=None, obj_size=None, features=None, stripe_unit=None, stripe_count=None, data_pool=None, configuration=None): size = int(size) @@ -247,17 +131,21 @@ class Rbd(RESTController): rbd_inst.create(ioctx, name, size, order=l_order, old_format=False, features=feature_bitmask, stripe_unit=stripe_unit, stripe_count=stripe_count, data_pool=data_pool) - RbdConfiguration(pool_ioctx=ioctx, image_name=name).set_configuration(configuration) + RbdConfiguration(pool_ioctx=ioctx, namespace=namespace, + image_name=name).set_configuration(configuration) - _rbd_call(pool_name, _create) + _rbd_call(pool_name, namespace, _create) - @RbdTask('delete', ['{pool_name}', '{image_name}'], 2.0) - def delete(self, pool_name, image_name): + @RbdTask('delete', ['{image_spec}'], 2.0) + def delete(self, image_spec): + pool_name, namespace, image_name = parse_image_spec(image_spec) rbd_inst = rbd.RBD() - return _rbd_call(pool_name, rbd_inst.remove, image_name) + return _rbd_call(pool_name, namespace, rbd_inst.remove, image_name) + + @RbdTask('edit', ['{image_spec}', '{name}'], 4.0) + def set(self, image_spec, name=None, size=None, features=None, configuration=None): + pool_name, namespace, image_name = parse_image_spec(image_spec) - @RbdTask('edit', ['{pool_name}', '{image_name}', '{name}'], 4.0) - def set(self, pool_name, image_name, name=None, size=None, features=None, configuration=None): def _edit(ioctx, image): rbd_inst = rbd.RBD() # check rename image @@ -291,17 +179,18 @@ class Rbd(RESTController): RbdConfiguration(pool_ioctx=ioctx, image_name=image_name).set_configuration( configuration) - return _rbd_image_call(pool_name, image_name, _edit) + return _rbd_image_call(pool_name, namespace, image_name, _edit) @RbdTask('copy', - {'src_pool_name': '{pool_name}', - 'src_image_name': '{image_name}', + {'src_image_spec': '{image_spec}', 'dest_pool_name': '{dest_pool_name}', + 'dest_namespace': '{dest_namespace}', 'dest_image_name': '{dest_image_name}'}, 2.0) @RESTController.Resource('POST') - def copy(self, pool_name, image_name, dest_pool_name, dest_image_name, - snapshot_name=None, obj_size=None, features=None, stripe_unit=None, - stripe_count=None, data_pool=None, configuration=None): + def copy(self, image_spec, dest_pool_name, dest_namespace, dest_image_name, + snapshot_name=None, obj_size=None, features=None, + stripe_unit=None, stripe_count=None, data_pool=None, configuration=None): + pool_name, namespace, image_name = parse_image_spec(image_spec) def _src_copy(s_ioctx, s_img): def _copy(d_ioctx): @@ -321,62 +210,67 @@ class Rbd(RESTController): RbdConfiguration(pool_ioctx=d_ioctx, image_name=dest_image_name).set_configuration( configuration) - return _rbd_call(dest_pool_name, _copy) + return _rbd_call(dest_pool_name, dest_namespace, _copy) - return _rbd_image_call(pool_name, image_name, _src_copy) + return _rbd_image_call(pool_name, namespace, image_name, _src_copy) - @RbdTask('flatten', ['{pool_name}', '{image_name}'], 2.0) + @RbdTask('flatten', ['{image_spec}'], 2.0) @RESTController.Resource('POST') @UpdatePermission - def flatten(self, pool_name, image_name): + def flatten(self, image_spec): def _flatten(ioctx, image): image.flatten() - return _rbd_image_call(pool_name, image_name, _flatten) + pool_name, namespace, image_name = parse_image_spec(image_spec) + return _rbd_image_call(pool_name, namespace, image_name, _flatten) @RESTController.Collection('GET') def default_features(self): rbd_default_features = mgr.get('config')['rbd_default_features'] return format_bitmask(int(rbd_default_features)) - @RbdTask('trash/move', ['{pool_name}', '{image_name}'], 2.0) + @RbdTask('trash/move', ['{image_spec}'], 2.0) @RESTController.Resource('POST') - def move_trash(self, pool_name, image_name, delay=0): + def move_trash(self, image_spec, delay=0): """Move an image to the trash. Images, even ones actively in-use by clones, can be moved to the trash and deleted at a later time. """ + pool_name, namespace, image_name = parse_image_spec(image_spec) rbd_inst = rbd.RBD() - return _rbd_call(pool_name, rbd_inst.trash_move, image_name, delay) + return _rbd_call(pool_name, namespace, rbd_inst.trash_move, image_name, delay) -@ApiController('/block/image/{pool_name}/{image_name}/snap', Scope.RBD_IMAGE) +@ApiController('/block/image/{image_spec}/snap', Scope.RBD_IMAGE) class RbdSnapshot(RESTController): RESOURCE_ID = "snapshot_name" @RbdTask('snap/create', - ['{pool_name}', '{image_name}', '{snapshot_name}'], 2.0) - def create(self, pool_name, image_name, snapshot_name): + ['{image_spec}', '{snapshot_name}'], 2.0) + def create(self, image_spec, snapshot_name): + pool_name, namespace, image_name = parse_image_spec(image_spec) + def _create_snapshot(ioctx, img, snapshot_name): img.create_snap(snapshot_name) - return _rbd_image_call(pool_name, image_name, _create_snapshot, + return _rbd_image_call(pool_name, namespace, image_name, _create_snapshot, snapshot_name) @RbdTask('snap/delete', - ['{pool_name}', '{image_name}', '{snapshot_name}'], 2.0) - def delete(self, pool_name, image_name, snapshot_name): + ['{image_spec}', '{snapshot_name}'], 2.0) + def delete(self, image_spec, snapshot_name): def _remove_snapshot(ioctx, img, snapshot_name): img.remove_snap(snapshot_name) - return _rbd_image_call(pool_name, image_name, _remove_snapshot, + pool_name, namespace, image_name = parse_image_spec(image_spec) + return _rbd_image_call(pool_name, namespace, image_name, _remove_snapshot, snapshot_name) @RbdTask('snap/edit', - ['{pool_name}', '{image_name}', '{snapshot_name}'], 4.0) - def set(self, pool_name, image_name, snapshot_name, new_snap_name=None, + ['{image_spec}', '{snapshot_name}'], 4.0) + def set(self, image_spec, snapshot_name, new_snap_name=None, is_protected=None): def _edit(ioctx, img, snapshot_name): if new_snap_name and new_snap_name != snapshot_name: @@ -389,31 +283,35 @@ class RbdSnapshot(RESTController): else: img.unprotect_snap(snapshot_name) - return _rbd_image_call(pool_name, image_name, _edit, snapshot_name) + pool_name, namespace, image_name = parse_image_spec(image_spec) + return _rbd_image_call(pool_name, namespace, image_name, _edit, snapshot_name) @RbdTask('snap/rollback', - ['{pool_name}', '{image_name}', '{snapshot_name}'], 5.0) + ['{image_spec}', '{snapshot_name}'], 5.0) @RESTController.Resource('POST') @UpdatePermission - def rollback(self, pool_name, image_name, snapshot_name): + def rollback(self, image_spec, snapshot_name): def _rollback(ioctx, img, snapshot_name): img.rollback_to_snap(snapshot_name) - return _rbd_image_call(pool_name, image_name, _rollback, snapshot_name) + + pool_name, namespace, image_name = parse_image_spec(image_spec) + return _rbd_image_call(pool_name, namespace, image_name, _rollback, snapshot_name) @RbdTask('clone', - {'parent_pool_name': '{pool_name}', - 'parent_image_name': '{image_name}', - 'parent_snap_name': '{snapshot_name}', + {'parent_image_spec': '{image_spec}', 'child_pool_name': '{child_pool_name}', + 'child_namespace': '{child_namespace}', 'child_image_name': '{child_image_name}'}, 2.0) @RESTController.Resource('POST') - def clone(self, pool_name, image_name, snapshot_name, child_pool_name, - child_image_name, obj_size=None, features=None, stripe_unit=None, stripe_count=None, - data_pool=None, configuration=None): + def clone(self, image_spec, snapshot_name, child_pool_name, + child_image_name, child_namespace=None, obj_size=None, features=None, + stripe_unit=None, stripe_count=None, data_pool=None, configuration=None): """ Clones a snapshot to an image """ + pool_name, namespace, image_name = parse_image_spec(image_spec) + def _parent_clone(p_ioctx): def _clone(ioctx): # Set order @@ -432,26 +330,33 @@ class RbdSnapshot(RESTController): RbdConfiguration(pool_ioctx=ioctx, image_name=child_image_name).set_configuration( configuration) - return _rbd_call(child_pool_name, _clone) + return _rbd_call(child_pool_name, child_namespace, _clone) - _rbd_call(pool_name, _parent_clone) + _rbd_call(pool_name, namespace, _parent_clone) @ApiController('/block/image/trash', Scope.RBD_IMAGE) class RbdTrash(RESTController): - RESOURCE_ID = "pool_name/image_id" + RESOURCE_ID = "image_id_spec" rbd_inst = rbd.RBD() @ViewCache() def _trash_pool_list(self, pool_name): with mgr.rados.open_ioctx(pool_name) as ioctx: - images = self.rbd_inst.trash_list(ioctx) result = [] - for trash in images: - trash['pool_name'] = pool_name - trash['deletion_time'] = "{}Z".format(trash['deletion_time'].isoformat()) - trash['deferment_end_time'] = "{}Z".format(trash['deferment_end_time'].isoformat()) - result.append(trash) + namespaces = self.rbd_inst.namespace_list(ioctx) + # images without namespace + namespaces.append('') + for namespace in namespaces: + ioctx.set_namespace(namespace) + images = self.rbd_inst.trash_list(ioctx) + for trash in images: + trash['pool_name'] = pool_name + trash['namespace'] = namespace + trash['deletion_time'] = "{}Z".format(trash['deletion_time'].isoformat()) + trash['deferment_end_time'] = "{}Z".format( + trash['deferment_end_time'].isoformat()) + result.append(trash) return result def _trash_list(self, pool_name=None): @@ -486,19 +391,63 @@ class RbdTrash(RESTController): for pool in pools: for image in pool['value']: if image['deferment_end_time'] < now: - _rbd_call(pool['pool_name'], self.rbd_inst.trash_remove, image['id'], 0) + _rbd_call(pool['pool_name'], image['namespace'], + self.rbd_inst.trash_remove, image['id'], 0) - @RbdTask('trash/restore', ['{pool_name}', '{image_id}', '{new_image_name}'], 2.0) + @RbdTask('trash/restore', ['{image_id_spec}', '{new_image_name}'], 2.0) @RESTController.Resource('POST') @CreatePermission - def restore(self, pool_name, image_id, new_image_name): + def restore(self, image_id_spec, new_image_name): """Restore an image from trash.""" - return _rbd_call(pool_name, self.rbd_inst.trash_restore, image_id, new_image_name) + pool_name, namespace, image_id = parse_image_spec(image_id_spec) + return _rbd_call(pool_name, namespace, self.rbd_inst.trash_restore, image_id, + new_image_name) - @RbdTask('trash/remove', ['{pool_name}', '{image_id}', '{image_name}'], 2.0) - def delete(self, pool_name, image_id, image_name, force=False): + @RbdTask('trash/remove', ['{image_id_spec}'], 2.0) + def delete(self, image_id_spec, force=False): """Delete an image from trash. If image deferment time has not expired you can not removed it unless use force. But an actively in-use by clones or has snapshots can not be removed. """ - return _rbd_call(pool_name, self.rbd_inst.trash_remove, image_id, int(str_to_bool(force))) + pool_name, namespace, image_id = parse_image_spec(image_id_spec) + return _rbd_call(pool_name, namespace, self.rbd_inst.trash_remove, image_id, + int(str_to_bool(force))) + + +@ApiController('/block/pool/{pool_name}/namespace', Scope.RBD_IMAGE) +class RbdNamespace(RESTController): + rbd_inst = rbd.RBD() + + def create(self, pool_name, namespace): + with mgr.rados.open_ioctx(pool_name) as ioctx: + namespaces = self.rbd_inst.namespace_list(ioctx) + if namespace in namespaces: + raise DashboardException( + msg='Namespace already exists', + code='namespace_already_exists', + component='rbd') + return self.rbd_inst.namespace_create(ioctx, namespace) + + def delete(self, pool_name, namespace): + with mgr.rados.open_ioctx(pool_name) as ioctx: + # pylint: disable=unbalanced-tuple-unpacking + _, images = RbdService.rbd_pool_list(pool_name, namespace) + if images: + raise DashboardException( + msg='Namespace contains images which must be deleted first', + code='namespace_contains_images', + component='rbd') + return self.rbd_inst.namespace_remove(ioctx, namespace) + + def list(self, pool_name): + with mgr.rados.open_ioctx(pool_name) as ioctx: + result = [] + namespaces = self.rbd_inst.namespace_list(ioctx) + for namespace in namespaces: + # pylint: disable=unbalanced-tuple-unpacking + _, images = RbdService.rbd_pool_list(pool_name, namespace) + result.append({ + 'namespace': namespace, + 'num_images': len(images) if images else 0 + }) + return result diff --git a/src/pybind/mgr/dashboard/controllers/rbd_mirroring.py b/src/pybind/mgr/dashboard/controllers/rbd_mirroring.py index 5c5d2c592cda9..1d0a360b497f7 100644 --- a/src/pybind/mgr/dashboard/controllers/rbd_mirroring.py +++ b/src/pybind/mgr/dashboard/controllers/rbd_mirroring.py @@ -369,7 +369,7 @@ class RbdMirroringPoolMode(RESTController): rbd.RBD().mirror_mode_set(ioctx, mode_enum) _reset_view_cache() - return _rbd_call(pool_name, _edit, mirror_mode) + return _rbd_call(pool_name, None, _edit, mirror_mode) @ApiController('/block/mirroring/pool/{pool_name}/peer', Scope.RBD_MIRRORING) diff --git a/src/pybind/mgr/dashboard/frontend/e2e/block/images.e2e-spec.ts b/src/pybind/mgr/dashboard/frontend/e2e/block/images.e2e-spec.ts index 943c7877d8fdb..f031a3ea72f75 100644 --- a/src/pybind/mgr/dashboard/frontend/e2e/block/images.e2e-spec.ts +++ b/src/pybind/mgr/dashboard/frontend/e2e/block/images.e2e-spec.ts @@ -23,14 +23,15 @@ describe('Images page', () => { await images.waitTextToBePresent(images.getBreadcrumb(), 'Images'); }); - it('should show three tabs', async () => { - await expect(images.getTabsCount()).toEqual(3); + it('should show four tabs', async () => { + await expect(images.getTabsCount()).toEqual(4); }); it('should show text for all tabs', async () => { await expect(images.getTabText(0)).toEqual('Images'); - await expect(images.getTabText(1)).toEqual('Trash'); - await expect(images.getTabText(2)).toEqual('Overall Performance'); + await expect(images.getTabText(1)).toEqual('Namespaces'); + await expect(images.getTabText(2)).toEqual('Trash'); + await expect(images.getTabText(3)).toEqual('Overall Performance'); }); }); diff --git a/src/pybind/mgr/dashboard/frontend/e2e/block/images.po.ts b/src/pybind/mgr/dashboard/frontend/e2e/block/images.po.ts index c9878c08998bb..33b3661d49e07 100644 --- a/src/pybind/mgr/dashboard/frontend/e2e/block/images.po.ts +++ b/src/pybind/mgr/dashboard/frontend/e2e/block/images.po.ts @@ -36,7 +36,7 @@ export class ImagesPageHelper extends PageHelper { const base_url = '/#/block/rbd/edit/'; const editURL = base_url .concat(pool) - .concat('/') + .concat('%2F') .concat(name); await browser.get(editURL); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/app.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/app.component.spec.ts index cdef9faf56cd1..c3ef688492888 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/app.component.spec.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/app.component.spec.ts @@ -8,6 +8,7 @@ import { ToastrModule } from 'ngx-toastr'; import { configureTestBed, i18nProviders } from '../testing/unit-test-helper'; import { AppComponent } from './app.component'; +import { RbdService } from './shared/api/rbd.service'; import { PipesModule } from './shared/pipes/pipes.module'; import { AuthStorageService } from './shared/services/auth-storage.service'; import { NotificationService } from './shared/services/notification.service'; @@ -26,7 +27,7 @@ describe('AppComponent', () => { ], declarations: [AppComponent], schemas: [NO_ERRORS_SCHEMA], - providers: [AuthStorageService, i18nProviders] + providers: [AuthStorageService, i18nProviders, RbdService] }); beforeEach(() => { diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/block.module.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/block.module.ts index a2ba84cacfe97..217bd98e5575f 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/block.module.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/block.module.ts @@ -32,6 +32,8 @@ import { RbdDetailsComponent } from './rbd-details/rbd-details.component'; import { RbdFormComponent } from './rbd-form/rbd-form.component'; import { RbdImagesComponent } from './rbd-images/rbd-images.component'; import { RbdListComponent } from './rbd-list/rbd-list.component'; +import { RbdNamespaceFormComponent } from './rbd-namespace-form/rbd-namespace-form.component'; +import { RbdNamespaceListComponent } from './rbd-namespace-list/rbd-namespace-list.component'; import { RbdSnapshotFormComponent } from './rbd-snapshot-form/rbd-snapshot-form.component'; import { RbdSnapshotListComponent } from './rbd-snapshot-list/rbd-snapshot-list.component'; import { RbdTrashListComponent } from './rbd-trash-list/rbd-trash-list.component'; @@ -42,6 +44,7 @@ import { RbdTrashRestoreModalComponent } from './rbd-trash-restore-modal/rbd-tra @NgModule({ entryComponents: [ RbdDetailsComponent, + RbdNamespaceFormComponent, RbdSnapshotFormComponent, RbdTrashMoveModalComponent, RbdTrashRestoreModalComponent, @@ -75,6 +78,8 @@ import { RbdTrashRestoreModalComponent } from './rbd-trash-restore-modal/rbd-tra IscsiTargetListComponent, RbdDetailsComponent, RbdFormComponent, + RbdNamespaceFormComponent, + RbdNamespaceListComponent, RbdSnapshotListComponent, RbdSnapshotFormComponent, RbdTrashListComponent, @@ -112,22 +117,22 @@ const routes: Routes = [ data: { breadcrumbs: ActionLabels.CREATE } }, { - path: `${URLVerbs.EDIT}/:pool/:name`, + path: `${URLVerbs.EDIT}/:image_spec`, component: RbdFormComponent, data: { breadcrumbs: ActionLabels.EDIT } }, { - path: `${URLVerbs.CLONE}/:pool/:name/:snap`, + path: `${URLVerbs.CLONE}/:image_spec/:snap`, component: RbdFormComponent, data: { breadcrumbs: ActionLabels.CLONE } }, { - path: `${URLVerbs.COPY}/:pool/:name`, + path: `${URLVerbs.COPY}/:image_spec`, component: RbdFormComponent, data: { breadcrumbs: ActionLabels.COPY } }, { - path: `${URLVerbs.COPY}/:pool/:name/:snap`, + path: `${URLVerbs.COPY}/:image_spec/:snap`, component: RbdFormComponent, data: { breadcrumbs: ActionLabels.COPY } } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi-target-form/iscsi-target-form.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi-target-form/iscsi-target-form.component.ts index 1f0541f383804..ea0eaf030ac0e 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi-target-form/iscsi-target-form.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi-target-form/iscsi-target-form.component.ts @@ -144,6 +144,10 @@ export class IscsiTargetFormComponent implements OnInit { this.imagesAll = _(data[1]) .flatMap((pool) => pool.value) .filter((image) => { + // Namespaces are not supported by ceph-iscsi + if (image.namespace) { + return false; + } const imageId = `${image.pool_name}/${image.name}`; if (usedImages.indexOf(imageId) !== -1) { return false; diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-details/rbd-details.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-details/rbd-details.component.html index aeed1654a9578..c5c776613a3ce 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-details/rbd-details.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-details/rbd-details.component.html @@ -2,7 +2,6 @@ Only available for RBD images with fast-diff enabled - @@ -96,9 +95,7 @@ Parent - {{ selectedItem.parent.pool_name }} - /{{ selectedItem.parent.image_name }} - @{{ selectedItem.parent.snap_name }} + {{ selectedItem.parent.pool_name }}/{{ selectedItem.parent.pool_namespace }}/{{ selectedItem.parent.image_name }}@{{ selectedItem.parent.snap_name }} - @@ -120,6 +117,7 @@ = []; diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-form/rbd-form-copy-request.model.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-form/rbd-form-copy-request.model.ts index c1b290dca283c..6642237f144bf 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-form/rbd-form-copy-request.model.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-form/rbd-form-copy-request.model.ts @@ -2,6 +2,7 @@ import { RbdConfigurationEntry } from '../../../shared/models/configuration'; export class RbdFormCopyRequestModel { dest_pool_name: string; + dest_namespace: string; dest_image_name: string; snapshot_name: string; obj_size: number; diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-form/rbd-form.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-form/rbd-form.component.html index c14ddb2bebc9b..8e19837e72a68 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-form/rbd-form.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-form/rbd-form.component.html @@ -90,6 +90,47 @@ + +
+
+ +
+
+
+ +
+ + +
+
+
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-form/rbd-form.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-form/rbd-form.component.spec.ts index 78926ba7521ba..04635367646ae 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-form/rbd-form.component.spec.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-form/rbd-form.component.spec.ts @@ -178,17 +178,23 @@ describe('RbdFormComponent', () => { spyOn(rbdService, 'get').and.callThrough(); }); + it('with namespace', () => { + activatedRoute.setParams({ image_spec: 'foo%2Fbar%2Fbaz' }); + + expect(rbdService.get).toHaveBeenCalledWith('foo', 'bar', 'baz'); + }); + it('without snapName', () => { - activatedRoute.setParams({ pool: 'foo%2Ffoo', name: 'bar%2Fbar', snap: undefined }); + activatedRoute.setParams({ image_spec: 'foo%2Fbar', snap: undefined }); - expect(rbdService.get).toHaveBeenCalledWith('foo/foo', 'bar/bar'); + expect(rbdService.get).toHaveBeenCalledWith('foo', null, 'bar'); expect(component.snapName).toBeUndefined(); }); it('with snapName', () => { - activatedRoute.setParams({ pool: 'foo%2Ffoo', name: 'bar%2Fbar', snap: 'baz%2Fbaz' }); + activatedRoute.setParams({ image_spec: 'foo%2Fbar', snap: 'baz%2Fbaz' }); - expect(rbdService.get).toHaveBeenCalledWith('foo/foo', 'bar/bar'); + expect(rbdService.get).toHaveBeenCalledWith('foo', null, 'bar'); expect(component.snapName).toBe('baz/baz'); }); }); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-form/rbd-form.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-form/rbd-form.component.ts index 6e9bb294ce1d4..78a0a9f36b2aa 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-form/rbd-form.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-form/rbd-form.component.ts @@ -4,12 +4,14 @@ import { ActivatedRoute, Router } from '@angular/router'; import { I18n } from '@ngx-translate/i18n-polyfill'; import * as _ from 'lodash'; -import { AsyncSubject, Observable } from 'rxjs'; + +import { AsyncSubject, forkJoin, Observable } from 'rxjs'; import { switchMap } from 'rxjs/operators'; import { PoolService } from '../../../shared/api/pool.service'; import { RbdService } from '../../../shared/api/rbd.service'; import { ActionLabelsI18n } from '../../../shared/constants/app.constants'; +import { Icons } from '../../../shared/enum/icons.enum'; import { CdFormGroup } from '../../../shared/forms/cd-form-group'; import { RbdConfigurationEntry, @@ -42,6 +44,8 @@ export class RbdFormComponent implements OnInit { localField?: RbdConfigurationSourceField ) => RbdConfigurationEntry[]; + namespaces: Array = []; + namespacesByPoolCache = {}; pools: Array = null; allPools: Array = null; dataPools: Array = null; @@ -85,6 +89,8 @@ export class RbdFormComponent implements OnInit { resource: string; private rbdImage = new AsyncSubject(); + icons = Icons; + constructor( private authStorageService: AuthStorageService, private route: ActivatedRoute, @@ -159,6 +165,7 @@ export class RbdFormComponent implements OnInit { pool: new FormControl(null, { validators: [Validators.required] }), + namespace: new FormControl(null), useDataPool: new FormControl(false), dataPool: new FormControl(null), size: new FormControl(null, { @@ -183,6 +190,7 @@ export class RbdFormComponent implements OnInit { disableForEdit() { this.rbdForm.get('parent').disable(); this.rbdForm.get('pool').disable(); + this.rbdForm.get('namespace').disable(); this.rbdForm.get('useDataPool').disable(); this.rbdForm.get('dataPool').disable(); this.rbdForm.get('obj_size').disable(); @@ -216,21 +224,24 @@ export class RbdFormComponent implements OnInit { } 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 ) { - this.route.params.subscribe((params: { pool: string; name: string; snap: string }) => { - const poolName = decodeURIComponent(params.pool); - const rbdName = decodeURIComponent(params.name); + this.route.params.subscribe((params: { image_spec: string; snap: string }) => { + const [poolName, namespace, rbdName] = this.rbdService.parseImageSpec( + decodeURIComponent(params.image_spec) + ); if (params.snap) { this.snapName = decodeURIComponent(params.snap); } - this.rbdService.get(poolName, rbdName).subscribe((resp: RbdFormResponseModel) => { - this.setResponse(resp, this.snapName); - this.rbdImage.next(resp); - }); + promisses[Promisse.RbdServiceGet] = this.rbdService.get(poolName, namespace, rbdName); }); } else { // New image @@ -239,37 +250,51 @@ export class RbdFormComponent implements OnInit { }); } if (this.mode !== this.rbdFormMode.editing && this.poolPermission.read) { - this.poolService - .list(['pool_name', 'type', 'flags_names', 'application_metadata']) - .then((resp) => { - const pools = []; - const dataPools = []; - for (const pool of resp) { - if (_.indexOf(pool.application_metadata, 'rbd') !== -1) { - if (!pool.pool_name.includes('/')) { - 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); - } - } + promisses[Promisse.PoolServiceList] = this.poolService.list([ + 'pool_name', + 'type', + 'flags_names', + 'application_metadata' + ]); + } + + forkJoin(promisses).subscribe((data: object) => { + // poolService.list + if (data[Promisse.PoolServiceList]) { + const pools = []; + 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); - } - }); - } + } + 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); + } + } + + // rbdService.get + if (data[Promisse.RbdServiceGet]) { + const resp: RbdFormResponseModel = data[Promisse.RbdServiceGet]; + this.setResponse(resp, this.snapName); + this.rbdImage.next(resp); + } + }); + _.each(this.features, (feature) => { this.rbdForm .get('features') @@ -279,13 +304,26 @@ export class RbdFormComponent implements OnInit { } onPoolChange(selectedPoolName) { - const newDataPools = this.allDataPools.filter((dataPool: any) => { - return dataPool.pool_name !== selectedPoolName; - }); + const newDataPools = this.allDataPools + ? this.allDataPools.filter((dataPool: any) => { + return dataPool.pool_name !== selectedPoolName; + }) + : []; if (this.rbdForm.getValue('dataPool') === selectedPoolName) { this.rbdForm.get('dataPool').setValue(null); } this.dataPools = newDataPools; + this.namespaces = null; + if (selectedPoolName in this.namespacesByPoolCache) { + this.namespaces = this.namespacesByPoolCache[selectedPoolName]; + } else { + this.rbdService.listNamespaces(selectedPoolName).subscribe((namespaces: any[]) => { + namespaces = namespaces.map((namespace) => namespace.namespace); + this.namespacesByPoolCache[selectedPoolName] = namespaces; + this.namespaces = namespaces; + }); + } + this.rbdForm.get('namespace').setValue(null); } onUseDataPoolChange() { @@ -458,13 +496,18 @@ export class RbdFormComponent implements OnInit { setResponse(response: RbdFormResponseModel, snapName: string) { this.response = response; + const imageSpec = this.rbdService.getImageSpec( + response.pool_name, + response.namespace, + response.name + ); if (this.mode === this.rbdFormMode.cloning) { - this.rbdForm.get('parent').setValue(`${response.pool_name}/${response.name}@${snapName}`); + this.rbdForm.get('parent').setValue(`${imageSpec}@${snapName}`); } else if (this.mode === this.rbdFormMode.copying) { if (snapName) { - this.rbdForm.get('parent').setValue(`${response.pool_name}/${response.name}@${snapName}`); + this.rbdForm.get('parent').setValue(`${imageSpec}@${snapName}`); } else { - this.rbdForm.get('parent').setValue(`${response.pool_name}/${response.name}`); + this.rbdForm.get('parent').setValue(`${imageSpec}`); } } else if (response.parent) { const parent = response.parent; @@ -476,6 +519,8 @@ export class RbdFormComponent implements OnInit { this.rbdForm.get('name').setValue(response.name); } this.rbdForm.get('pool').setValue(response.pool_name); + this.onPoolChange(response.pool_name); + this.rbdForm.get('namespace').setValue(response.namespace); if (response.data_pool) { this.rbdForm.get('useDataPool').setValue(true); this.rbdForm.get('dataPool').setValue(response.data_pool); @@ -498,6 +543,7 @@ export class RbdFormComponent implements OnInit { createRequest() { const request = new RbdFormCreateRequestModel(); request.pool_name = this.rbdForm.getValue('pool'); + request.namespace = this.rbdForm.getValue('namespace'); request.name = this.rbdForm.getValue('name'); request.size = this.formatter.toBytes(this.rbdForm.getValue('size')); request.obj_size = this.formatter.toBytes(this.rbdForm.getValue('obj_size')); @@ -523,6 +569,7 @@ export class RbdFormComponent implements OnInit { return this.taskWrapper.wrapTaskAroundCall({ task: new FinishedTask('rbd/create', { pool_name: request.pool_name, + namespace: request.namespace, image_name: request.name }), call: this.rbdService.create(request) @@ -547,6 +594,7 @@ export class RbdFormComponent implements OnInit { cloneRequest(): RbdFormCloneRequestModel { const request = new RbdFormCloneRequestModel(); request.child_pool_name = this.rbdForm.getValue('pool'); + request.child_namespace = this.rbdForm.getValue('namespace'); request.child_image_name = this.rbdForm.getValue('name'); request.obj_size = this.formatter.toBytes(this.rbdForm.getValue('obj_size')); _.forIn(this.features, (feature) => { @@ -572,10 +620,18 @@ export class RbdFormComponent implements OnInit { editAction(): Observable { return this.taskWrapper.wrapTaskAroundCall({ task: new FinishedTask('rbd/edit', { - pool_name: this.response.pool_name, - image_name: this.response.name + image_spec: this.rbdService.getImageSpec( + this.response.pool_name, + this.response.namespace, + this.response.name + ) }), - call: this.rbdService.update(this.response.pool_name, this.response.name, this.editRequest()) + call: this.rbdService.update( + this.response.pool_name, + this.response.namespace, + this.response.name, + this.editRequest() + ) }); } @@ -583,14 +639,19 @@ export class RbdFormComponent implements OnInit { const request = this.cloneRequest(); return this.taskWrapper.wrapTaskAroundCall({ task: new FinishedTask('rbd/clone', { - parent_pool_name: this.response.pool_name, - parent_image_name: this.response.name, + parent_image_spec: this.rbdService.get( + this.response.pool_name, + this.response.namespace, + this.response.name + ), parent_snap_name: this.snapName, child_pool_name: request.child_pool_name, + child_namespace: request.child_namespace, child_image_name: request.child_image_name }), call: this.rbdService.cloneSnapshot( this.response.pool_name, + this.response.namespace, this.response.name, this.snapName, request @@ -604,6 +665,7 @@ export class RbdFormComponent implements OnInit { request.snapshot_name = this.snapName; } request.dest_pool_name = this.rbdForm.getValue('pool'); + request.dest_namespace = this.rbdForm.getValue('namespace'); request.dest_image_name = this.rbdForm.getValue('name'); request.obj_size = this.formatter.toBytes(this.rbdForm.getValue('obj_size')); _.forIn(this.features, (feature) => { @@ -631,12 +693,21 @@ export class RbdFormComponent implements OnInit { return this.taskWrapper.wrapTaskAroundCall({ task: new FinishedTask('rbd/copy', { - src_pool_name: this.response.pool_name, - src_image_name: this.response.name, + src_image_spec: this.rbdService.getImageSpec( + this.response.pool_name, + this.response.namespace, + this.response.name + ), dest_pool_name: request.dest_pool_name, + dest_namespace: request.dest_namespace, dest_image_name: request.dest_image_name }), - call: this.rbdService.copy(this.response.pool_name, this.response.name, request) + call: this.rbdService.copy( + this.response.pool_name, + this.response.namespace, + this.response.name, + request + ) }); } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-form/rbd-form.model.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-form/rbd-form.model.ts index 9511a00d00302..6a4999d0fc57d 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-form/rbd-form.model.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-form/rbd-form.model.ts @@ -3,6 +3,7 @@ import { RbdConfigurationEntry } from '../../../shared/models/configuration'; export class RbdFormModel { name: string; pool_name: string; + namespace: string; data_pool: string; size: number; diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-form/rbd-parent.model.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-form/rbd-parent.model.ts index a10f8a3715ef7..000717b0a0e7b 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-form/rbd-parent.model.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-form/rbd-parent.model.ts @@ -1,5 +1,6 @@ export class RbdParentModel { image_name: string; pool_name: string; + pool_namespace: string; snap_name: string; } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-images/rbd-images.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-images/rbd-images.component.html index 0827a61dc8306..1b7f59c48ada1 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-images/rbd-images.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-images/rbd-images.component.html @@ -5,6 +5,10 @@ id="tab1"> + + + diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-images/rbd-images.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-images/rbd-images.component.spec.ts index e1fda888a4697..1a5c035620a9b 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-images/rbd-images.component.spec.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-images/rbd-images.component.spec.ts @@ -12,6 +12,7 @@ import { SharedModule } from '../../../shared/shared.module'; import { RbdConfigurationListComponent } from '../rbd-configuration-list/rbd-configuration-list.component'; import { RbdDetailsComponent } from '../rbd-details/rbd-details.component'; import { RbdListComponent } from '../rbd-list/rbd-list.component'; +import { RbdNamespaceListComponent } from '../rbd-namespace-list/rbd-namespace-list.component'; import { RbdSnapshotListComponent } from '../rbd-snapshot-list/rbd-snapshot-list.component'; import { RbdTrashListComponent } from '../rbd-trash-list/rbd-trash-list.component'; import { RbdImagesComponent } from './rbd-images.component'; @@ -25,6 +26,7 @@ describe('RbdImagesComponent', () => { RbdDetailsComponent, RbdImagesComponent, RbdListComponent, + RbdNamespaceListComponent, RbdSnapshotListComponent, RbdTrashListComponent, RbdConfigurationListComponent diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-list/rbd-list.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-list/rbd-list.component.html index ccd9592535357..0058b10df17a3 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-list/rbd-list.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-list/rbd-list.component.html @@ -28,7 +28,7 @@ - {{ value.pool_name }}/{{ value.image_name }}@{{ value.snap_name }} + {{ value.pool_name }}/{{ value.pool_namespace }}/{{ value.image_name }}@{{ value.snap_name }} - diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-list/rbd-list.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-list/rbd-list.component.spec.ts index 2236c63b986a7..8a8b6a5c94c42 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-list/rbd-list.component.spec.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-list/rbd-list.component.spec.ts @@ -117,21 +117,29 @@ describe('RbdListComponent', () => { case 'rbd/copy': task.metadata = { dest_pool_name: 'rbd', + dest_namespace: null, dest_image_name: 'd' }; break; case 'rbd/clone': task.metadata = { child_pool_name: 'rbd', + child_namespace: null, child_image_name: 'd' }; break; - default: + case 'rbd/create': task.metadata = { pool_name: 'rbd', + namespace: null, image_name: image_name }; break; + default: + task.metadata = { + image_spec: `rbd/${image_name}` + }; + break; } summaryService.addRunningTask(task); }; diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-list/rbd-list.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-list/rbd-list.component.ts index b15d140b85a86..28a3a6e668825 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-list/rbd-list.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-list/rbd-list.component.ts @@ -62,19 +62,32 @@ export class RbdListComponent implements OnInit { builders = { 'rbd/create': (metadata) => - this.createRbdFromTask(metadata['pool_name'], metadata['image_name']), - 'rbd/delete': (metadata) => - this.createRbdFromTask(metadata['pool_name'], metadata['image_name']), + this.createRbdFromTask(metadata['pool_name'], metadata['namespace'], metadata['image_name']), + 'rbd/delete': (metadata) => this.createRbdFromTaskImageSpec(metadata['image_spec']), 'rbd/clone': (metadata) => - this.createRbdFromTask(metadata['child_pool_name'], metadata['child_image_name']), + this.createRbdFromTask( + metadata['child_pool_name'], + metadata['child_namespace'], + metadata['child_image_name'] + ), 'rbd/copy': (metadata) => - this.createRbdFromTask(metadata['dest_pool_name'], metadata['dest_image_name']) + this.createRbdFromTask( + metadata['dest_pool_name'], + metadata['dest_namespace'], + metadata['dest_image_name'] + ) }; - private createRbdFromTask(pool: string, name: string): RbdModel { + private createRbdFromTaskImageSpec(imageSpec: string): RbdModel { + const [poolName, namespace, rbdName] = this.rbdService.parseImageSpec(imageSpec); + return this.createRbdFromTask(poolName, namespace, rbdName); + } + + private createRbdFromTask(pool: string, namespace: string, name: string): RbdModel { const model = new RbdModel(); model.id = '-1'; model.name = name; + model.namespace = namespace; model.pool_name = pool; return model; } @@ -94,8 +107,12 @@ export class RbdListComponent implements OnInit { this.permission = this.authStorageService.getPermissions().rbdImage; const getImageUri = () => this.selection.first() && - `${encodeURIComponent(this.selection.first().pool_name)}/${encodeURIComponent( - this.selection.first().name + `${encodeURIComponent( + this.rbdService.getImageSpec( + this.selection.first().pool_name, + this.selection.first().namespace, + this.selection.first().name + ) )}`; const addAction: CdTableAction = { permission: 'create', @@ -162,6 +179,11 @@ export class RbdListComponent implements OnInit { prop: 'pool_name', flexGrow: 2 }, + { + name: this.i18n('Namespace'), + prop: 'namespace', + flexGrow: 2 + }, { name: this.i18n('Size'), prop: 'size', @@ -205,13 +227,58 @@ export class RbdListComponent implements OnInit { } ]; + const itemFilter = (entry, task) => { + let taskImageSpec: string; + switch (task.name) { + case 'rbd/copy': + taskImageSpec = this.rbdService.getImageSpec( + task.metadata['dest_pool_name'], + task.metadata['dest_namespace'], + task.metadata['dest_image_name'] + ); + break; + case 'rbd/clone': + taskImageSpec = this.rbdService.getImageSpec( + task.metadata['child_pool_name'], + task.metadata['child_namespace'], + task.metadata['child_image_name'] + ); + break; + case 'rbd/create': + taskImageSpec = this.rbdService.getImageSpec( + task.metadata['pool_name'], + task.metadata['namespace'], + task.metadata['image_name'] + ); + break; + default: + taskImageSpec = task.metadata['image_spec']; + break; + } + return ( + taskImageSpec === this.rbdService.getImageSpec(entry.pool_name, entry.namespace, entry.name) + ); + }; + + const taskFilter = (task) => { + return [ + 'rbd/clone', + 'rbd/copy', + 'rbd/create', + 'rbd/delete', + 'rbd/edit', + 'rbd/flatten', + 'rbd/trash/move' + ].includes(task.name); + }; + this.taskListService.init( () => this.rbdService.list(), (resp) => this.prepareResponse(resp), (images) => (this.images = images), () => this.onFetchError(), - this.taskFilter, - this.itemFilter, + taskFilter, + itemFilter, this.builders ); } @@ -246,59 +313,26 @@ export class RbdListComponent implements OnInit { return images; } - itemFilter(entry, task) { - let pool_name_k: string; - let image_name_k: string; - switch (task.name) { - case 'rbd/copy': - pool_name_k = 'dest_pool_name'; - image_name_k = 'dest_image_name'; - break; - case 'rbd/clone': - pool_name_k = 'child_pool_name'; - image_name_k = 'child_image_name'; - break; - default: - pool_name_k = 'pool_name'; - image_name_k = 'image_name'; - break; - } - return ( - entry.pool_name === task.metadata[pool_name_k] && entry.name === task.metadata[image_name_k] - ); - } - - taskFilter(task) { - return [ - 'rbd/clone', - 'rbd/copy', - 'rbd/create', - 'rbd/delete', - 'rbd/edit', - 'rbd/flatten', - 'rbd/trash/move' - ].includes(task.name); - } - updateSelection(selection: CdTableSelection) { this.selection = selection; } deleteRbdModal() { const poolName = this.selection.first().pool_name; + const namespace = this.selection.first().namespace; const imageName = this.selection.first().name; + const imageSpec = this.rbdService.getImageSpec(poolName, namespace, imageName); this.modalRef = this.modalService.show(CriticalConfirmationModalComponent, { initialState: { itemDescription: 'RBD', - itemNames: [`${poolName}/${imageName}`], + itemNames: [imageSpec], submitActionObservable: () => this.taskWrapper.wrapTaskAroundCall({ task: new FinishedTask('rbd/delete', { - pool_name: poolName, - image_name: imageName + image_spec: imageSpec }), - call: this.rbdService.delete(poolName, imageName) + call: this.rbdService.delete(poolName, namespace, imageName) }) } }); @@ -308,19 +342,19 @@ export class RbdListComponent implements OnInit { const initialState = { metaType: 'RBD', poolName: this.selection.first().pool_name, + namespace: this.selection.first().namespace, imageName: this.selection.first().name }; this.modalRef = this.modalService.show(RbdTrashMoveModalComponent, { initialState }); } - flattenRbd(poolName, imageName) { + flattenRbd(poolName, namespace, imageName) { this.taskWrapper .wrapTaskAroundCall({ task: new FinishedTask('rbd/flatten', { - pool_name: poolName, - image_name: imageName + image_spec: this.rbdService.getImageSpec(poolName, namespace, imageName) }), - call: this.rbdService.flatten(poolName, imageName) + call: this.rbdService.flatten(poolName, namespace, imageName) }) .subscribe(undefined, undefined, () => { this.modalRef.hide(); @@ -329,19 +363,25 @@ export class RbdListComponent implements OnInit { flattenRbdModal() { const poolName = this.selection.first().pool_name; + const namespace = this.selection.first().namespace; const imageName = this.selection.first().name; const parent: RbdParentModel = this.selection.first().parent; + const parentImageSpec = this.rbdService.getImageSpec( + parent.pool_name, + parent.pool_namespace, + parent.image_name + ); const initialState = { titleText: 'RBD flatten', buttonText: 'Flatten', bodyTpl: this.flattenTpl, bodyData: { - parent: `${parent.pool_name}/${parent.image_name}@${parent.snap_name}`, - child: `${poolName}/${imageName}` + parent: `${parentImageSpec}@${parent.snap_name}`, + child: this.rbdService.getImageSpec(poolName, namespace, imageName) }, onSubmit: () => { - this.flattenRbd(poolName, imageName); + this.flattenRbd(poolName, namespace, imageName); } }; diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-list/rbd-model.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-list/rbd-model.ts index 92a77bdd6eaa4..625a2f251d06d 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-list/rbd-model.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-list/rbd-model.ts @@ -2,6 +2,7 @@ export class RbdModel { id: string; name: string; pool_name: string; + namespace: string; cdExecuting: string; } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-namespace-form/rbd-namespace-form.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-namespace-form/rbd-namespace-form.component.html new file mode 100644 index 0000000000000..13454fea4623e --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-namespace-form/rbd-namespace-form.component.html @@ -0,0 +1,92 @@ + +
+ + + +
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-namespace-form/rbd-namespace-form.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-namespace-form/rbd-namespace-form.component.scss new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-namespace-form/rbd-namespace-form.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-namespace-form/rbd-namespace-form.component.spec.ts new file mode 100644 index 0000000000000..c24381017c00e --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-namespace-form/rbd-namespace-form.component.spec.ts @@ -0,0 +1,41 @@ +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ReactiveFormsModule } from '@angular/forms'; +import { RouterTestingModule } from '@angular/router/testing'; + +import { BsModalRef, BsModalService } from 'ngx-bootstrap/modal'; +import { ToastrModule } from 'ngx-toastr'; + +import { configureTestBed, i18nProviders } from '../../../../testing/unit-test-helper'; +import { ApiModule } from '../../../shared/api/api.module'; +import { ComponentsModule } from '../../../shared/components/components.module'; +import { AuthStorageService } from '../../../shared/services/auth-storage.service'; +import { RbdNamespaceFormComponent } from './rbd-namespace-form.component'; + +describe('RbdNamespaceFormComponent', () => { + let component: RbdNamespaceFormComponent; + let fixture: ComponentFixture; + + configureTestBed({ + imports: [ + ReactiveFormsModule, + ComponentsModule, + HttpClientTestingModule, + ApiModule, + ToastrModule.forRoot(), + RouterTestingModule + ], + declarations: [RbdNamespaceFormComponent], + providers: [BsModalRef, BsModalService, AuthStorageService, i18nProviders] + }); + + beforeEach(() => { + fixture = TestBed.createComponent(RbdNamespaceFormComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-namespace-form/rbd-namespace-form.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-namespace-form/rbd-namespace-form.component.ts new file mode 100644 index 0000000000000..023dc7faa1eea --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-namespace-form/rbd-namespace-form.component.ts @@ -0,0 +1,146 @@ +import { Component, OnInit } from '@angular/core'; +import { + AbstractControl, + AsyncValidatorFn, + FormControl, + ValidationErrors, + ValidatorFn +} from '@angular/forms'; + +import { I18n } from '@ngx-translate/i18n-polyfill'; +import { BsModalRef } from 'ngx-bootstrap/modal'; +import { Subject } from 'rxjs'; + +import { PoolService } from '../../../shared/api/pool.service'; +import { RbdService } from '../../../shared/api/rbd.service'; +import { NotificationType } from '../../../shared/enum/notification-type.enum'; +import { CdFormGroup } from '../../../shared/forms/cd-form-group'; +import { FinishedTask } from '../../../shared/models/finished-task'; +import { Permission } from '../../../shared/models/permissions'; +import { AuthStorageService } from '../../../shared/services/auth-storage.service'; +import { NotificationService } from '../../../shared/services/notification.service'; + +@Component({ + selector: 'cd-rbd-namespace-form', + templateUrl: './rbd-namespace-form.component.html', + styleUrls: ['./rbd-namespace-form.component.scss'] +}) +export class RbdNamespaceFormComponent implements OnInit { + poolPermission: Permission; + pools: Array = null; + pool: string; + namespace: string; + + namespaceForm: CdFormGroup; + + editing = false; + + public onSubmit: Subject; + + constructor( + public modalRef: BsModalRef, + private authStorageService: AuthStorageService, + private notificationService: NotificationService, + private poolService: PoolService, + private rbdService: RbdService, + private i18n: I18n + ) { + this.poolPermission = this.authStorageService.getPermissions().pool; + this.createForm(); + } + + createForm() { + this.namespaceForm = new CdFormGroup( + { + pool: new FormControl(''), + namespace: new FormControl('') + }, + this.validator(), + this.asyncValidator() + ); + } + + validator(): ValidatorFn { + return (control: AbstractControl) => { + const poolCtrl = control.get('pool'); + const namespaceCtrl = control.get('namespace'); + let poolErrors = null; + if (!poolCtrl.value) { + poolErrors = { required: true }; + } + poolCtrl.setErrors(poolErrors); + let namespaceErrors = null; + if (!namespaceCtrl.value) { + namespaceErrors = { required: true }; + } + namespaceCtrl.setErrors(namespaceErrors); + return null; + }; + } + + asyncValidator(): AsyncValidatorFn { + return (control: AbstractControl): Promise => { + return new Promise((resolve) => { + const poolCtrl = control.get('pool'); + const namespaceCtrl = control.get('namespace'); + this.rbdService.listNamespaces(poolCtrl.value).subscribe((namespaces: any[]) => { + if (namespaces.some((ns) => ns.namespace === namespaceCtrl.value)) { + const error = { namespaceExists: true }; + namespaceCtrl.setErrors(error); + resolve(error); + } else { + resolve(null); + } + }); + }); + }; + } + + ngOnInit() { + this.onSubmit = new Subject(); + + if (this.poolPermission.read) { + this.poolService.list(['pool_name', 'type', 'application_metadata']).then((resp) => { + const pools = []; + for (const pool of resp) { + if (this.rbdService.isRBDPool(pool) && pool.type === 'replicated') { + pools.push(pool); + } + } + this.pools = pools; + if (this.pools.length === 1) { + const poolName = this.pools[0]['pool_name']; + this.namespaceForm.get('pool').setValue(poolName); + } + }); + } + } + + submit() { + const pool = this.namespaceForm.getValue('pool'); + const namespace = this.namespaceForm.getValue('namespace'); + const finishedTask = new FinishedTask(); + finishedTask.name = 'rbd/namespace/create'; + finishedTask.metadata = { + pool: pool, + namespace: namespace + }; + this.rbdService + .createNamespace(pool, namespace) + .toPromise() + .then(() => { + this.notificationService.show( + NotificationType.success, + this.i18n(`Created namespace '{{pool}}/{{namespace}}'`, { + pool: pool, + namespace: namespace + }) + ); + this.modalRef.hide(); + this.onSubmit.next(); + }) + .catch(() => { + this.namespaceForm.setErrors({ cdSubmitButton: true }); + }); + } +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-namespace-list/rbd-namespace-list.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-namespace-list/rbd-namespace-list.component.html new file mode 100644 index 0000000000000..e8b09382fa31e --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-namespace-list/rbd-namespace-list.component.html @@ -0,0 +1,16 @@ + +
+ + +
+
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-namespace-list/rbd-namespace-list.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-namespace-list/rbd-namespace-list.component.scss new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-namespace-list/rbd-namespace-list.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-namespace-list/rbd-namespace-list.component.spec.ts new file mode 100644 index 0000000000000..ac45d9f5f20c7 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-namespace-list/rbd-namespace-list.component.spec.ts @@ -0,0 +1,31 @@ +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; + +import { ToastrModule } from 'ngx-toastr'; + +import { configureTestBed, i18nProviders } from '../../../../testing/unit-test-helper'; +import { TaskListService } from '../../../shared/services/task-list.service'; +import { SharedModule } from '../../../shared/shared.module'; +import { RbdNamespaceListComponent } from './rbd-namespace-list.component'; + +describe('RbdNamespaceListComponent', () => { + let component: RbdNamespaceListComponent; + let fixture: ComponentFixture; + + configureTestBed({ + declarations: [RbdNamespaceListComponent], + imports: [SharedModule, HttpClientTestingModule, RouterTestingModule, ToastrModule.forRoot()], + providers: [TaskListService, i18nProviders] + }); + + beforeEach(() => { + fixture = TestBed.createComponent(RbdNamespaceListComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-namespace-list/rbd-namespace-list.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-namespace-list/rbd-namespace-list.component.ts new file mode 100644 index 0000000000000..ca6bb9d137bf4 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-namespace-list/rbd-namespace-list.component.ts @@ -0,0 +1,162 @@ +import { Component, OnInit } from '@angular/core'; + +import { I18n } from '@ngx-translate/i18n-polyfill'; +import { BsModalRef, BsModalService } from 'ngx-bootstrap/modal'; + +import * as _ from 'lodash'; +import { forkJoin } from 'rxjs'; +import { PoolService } from '../../../shared/api/pool.service'; +import { RbdService } from '../../../shared/api/rbd.service'; +import { CriticalConfirmationModalComponent } from '../../../shared/components/critical-confirmation-modal/critical-confirmation-modal.component'; +import { ActionLabelsI18n } from '../../../shared/constants/app.constants'; +import { Icons } from '../../../shared/enum/icons.enum'; +import { NotificationType } from '../../../shared/enum/notification-type.enum'; +import { CdTableAction } from '../../../shared/models/cd-table-action'; +import { CdTableColumn } from '../../../shared/models/cd-table-column'; +import { CdTableSelection } from '../../../shared/models/cd-table-selection'; +import { Permission } from '../../../shared/models/permissions'; +import { AuthStorageService } from '../../../shared/services/auth-storage.service'; +import { NotificationService } from '../../../shared/services/notification.service'; +import { TaskListService } from '../../../shared/services/task-list.service'; +import { RbdNamespaceFormComponent } from '../rbd-namespace-form/rbd-namespace-form.component'; + +@Component({ + selector: 'cd-rbd-namespace-list', + templateUrl: './rbd-namespace-list.component.html', + styleUrls: ['./rbd-namespace-list.component.scss'], + providers: [TaskListService] +}) +export class RbdNamespaceListComponent implements OnInit { + columns: CdTableColumn[]; + namespaces: any; + modalRef: BsModalRef; + permission: Permission; + selection = new CdTableSelection(); + tableActions: CdTableAction[]; + + constructor( + private authStorageService: AuthStorageService, + private rbdService: RbdService, + private poolService: PoolService, + private modalService: BsModalService, + private notificationService: NotificationService, + private i18n: I18n, + public actionLabels: ActionLabelsI18n + ) { + this.permission = this.authStorageService.getPermissions().rbdImage; + const createAction: CdTableAction = { + permission: 'create', + icon: Icons.add, + click: () => this.createModal(), + name: this.actionLabels.CREATE + }; + const deleteAction: CdTableAction = { + permission: 'delete', + icon: Icons.destroy, + click: () => this.deleteModal(), + name: this.actionLabels.DELETE, + disable: () => !this.selection.first() || !_.isUndefined(this.getDeleteDisableDesc()), + disableDesc: () => this.getDeleteDisableDesc() + }; + this.tableActions = [createAction, deleteAction]; + } + + ngOnInit() { + this.columns = [ + { + name: this.i18n('Namespace'), + prop: 'namespace', + flexGrow: 1 + }, + { + name: this.i18n('Pool'), + prop: 'pool', + flexGrow: 1 + }, + { + name: this.i18n('Total images'), + prop: 'num_images', + flexGrow: 1 + } + ]; + this.refresh(); + } + + refresh() { + this.poolService.list(['pool_name', 'type', 'application_metadata']).then((pools: any) => { + pools = pools.filter((pool) => this.rbdService.isRBDPool(pool) && pool.type === 'replicated'); + const promisses = []; + pools.forEach((pool) => { + promisses.push(this.rbdService.listNamespaces(pool['pool_name'])); + }); + if (promisses.length > 0) { + forkJoin(promisses).subscribe((data: Array>) => { + const result = []; + for (let i = 0; i < data.length; i++) { + const namespaces = data[i]; + const pool_name = pools[i]['pool_name']; + namespaces.forEach((namespace: any) => { + result.push({ + id: `${pool_name}/${namespace.namespace}`, + pool: pool_name, + namespace: namespace.namespace, + num_images: namespace.num_images + }); + }); + } + this.namespaces = result; + }); + } else { + this.namespaces = []; + } + }); + } + + updateSelection(selection: CdTableSelection) { + this.selection = selection; + } + + createModal() { + this.modalRef = this.modalService.show(RbdNamespaceFormComponent); + this.modalRef.content.onSubmit.subscribe(() => { + this.refresh(); + }); + } + + deleteModal() { + const pool = this.selection.first().pool; + const namespace = this.selection.first().namespace; + this.modalRef = this.modalService.show(CriticalConfirmationModalComponent, { + initialState: { + itemDescription: 'Namespace', + itemNames: [`${pool}/${namespace}`], + submitAction: () => + this.rbdService.deleteNamespace(pool, namespace).subscribe( + () => { + this.notificationService.show( + NotificationType.success, + this.i18n(`Deleted namespace '{{pool}}/{{namespace}}'`, { + pool: pool, + namespace: namespace + }) + ); + this.modalRef.hide(); + this.refresh(); + }, + () => { + this.modalRef.content.stopLoadingSpinner(); + } + ) + } + }); + } + + getDeleteDisableDesc(): string | undefined { + const first = this.selection.first(); + if (first) { + if (first.num_images > 0) { + return this.i18n('Namespace contains images'); + } + } + } +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-snapshot-form/rbd-snapshot-form.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-snapshot-form/rbd-snapshot-form.component.ts index b5d2a080bc507..c04bd21459ef7 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-snapshot-form/rbd-snapshot-form.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-snapshot-form/rbd-snapshot-form.component.ts @@ -17,6 +17,7 @@ import { TaskManagerService } from '../../../shared/services/task-manager.servic }) export class RbdSnapshotFormComponent implements OnInit { poolName: string; + namespace: string; imageName: string; snapName: string; @@ -66,12 +67,11 @@ export class RbdSnapshotFormComponent implements OnInit { const finishedTask = new FinishedTask(); finishedTask.name = 'rbd/snap/edit'; finishedTask.metadata = { - pool_name: this.poolName, - image_name: this.imageName, + image_spec: this.rbdService.getImageSpec(this.poolName, this.namespace, this.imageName), snapshot_name: snapshotName }; this.rbdService - .renameSnapshot(this.poolName, this.imageName, this.snapName, snapshotName) + .renameSnapshot(this.poolName, this.namespace, this.imageName, this.snapName, snapshotName) .toPromise() .then(() => { this.taskManagerService.subscribe( @@ -94,12 +94,11 @@ export class RbdSnapshotFormComponent implements OnInit { const finishedTask = new FinishedTask(); finishedTask.name = 'rbd/snap/create'; finishedTask.metadata = { - pool_name: this.poolName, - image_name: this.imageName, + image_spec: this.rbdService.getImageSpec(this.poolName, this.namespace, this.imageName), snapshot_name: snapshotName }; this.rbdService - .createSnapshot(this.poolName, this.imageName, snapshotName) + .createSnapshot(this.poolName, this.namespace, this.imageName, snapshotName) .toPromise() .then(() => { this.taskManagerService.subscribe( diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-snapshot-list/rbd-snapshot-list.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-snapshot-list/rbd-snapshot-list.component.html index 29cf4044a7319..b0a5abbbeb729 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-snapshot-list/rbd-snapshot-list.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-snapshot-list/rbd-snapshot-list.component.html @@ -23,5 +23,5 @@ You are about to rollback - {{ value.snapName }}. + {{ value.snapName }}. diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-snapshot-list/rbd-snapshot-list.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-snapshot-list/rbd-snapshot-list.component.spec.ts index 2044a784f2625..c752873828ca1 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-snapshot-list/rbd-snapshot-list.component.spec.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-snapshot-list/rbd-snapshot-list.component.spec.ts @@ -131,8 +131,7 @@ describe('RbdSnapshotListComponent', () => { const task = new ExecutingTask(); task.name = task_name; task.metadata = { - pool_name: 'rbd', - image_name: 'foo', + image_spec: 'rbd/foo', snapshot_name: snapshot_name }; summaryService.addRunningTask(task); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-snapshot-list/rbd-snapshot-list.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-snapshot-list/rbd-snapshot-list.component.ts index 1f2a9b15a9b8b..96a14ee2f6b25 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-snapshot-list/rbd-snapshot-list.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-snapshot-list/rbd-snapshot-list.component.ts @@ -41,6 +41,8 @@ export class RbdSnapshotListComponent implements OnInit, OnChanges { @Input() poolName: string; @Input() + namespace: string; + @Input() rbdName: string; @ViewChild('nameTpl', { static: false }) nameTpl: TemplateRef; @@ -129,8 +131,8 @@ export class RbdSnapshotListComponent implements OnInit, OnChanges { actions.unprotect.click = () => this.toggleProtection(); const getImageUri = () => this.selection.first() && - `${encodeURIComponent(this.poolName)}/${encodeURIComponent( - this.rbdName + `${encodeURIComponent( + this.rbdService.getImageSpec(this.poolName, this.namespace, this.rbdName) )}/${encodeURIComponent(this.selection.first().name)}`; actions.clone.routerLink = () => `/block/rbd/clone/${getImageUri()}`; actions.copy.routerLink = () => `/block/rbd/copy/${getImageUri()}`; @@ -147,8 +149,8 @@ export class RbdSnapshotListComponent implements OnInit, OnChanges { ['rbd/snap/create', 'rbd/snap/delete', 'rbd/snap/edit', 'rbd/snap/rollback'].includes( task.name ) && - this.poolName === task.metadata['pool_name'] && - this.rbdName === task.metadata['image_name'] + this.rbdService.getImageSpec(this.poolName, this.namespace, this.rbdName) === + task.metadata['image_spec'] ); }; @@ -167,6 +169,7 @@ export class RbdSnapshotListComponent implements OnInit, OnChanges { this.modalRef = this.modalService.show(RbdSnapshotFormComponent); this.modalRef.content.poolName = this.poolName; this.modalRef.content.imageName = this.rbdName; + this.modalRef.content.namespace = this.namespace; if (snapName) { this.modalRef.content.setEditing(); } else { @@ -202,12 +205,11 @@ export class RbdSnapshotListComponent implements OnInit, OnChanges { const finishedTask = new FinishedTask(); finishedTask.name = 'rbd/snap/edit'; finishedTask.metadata = { - pool_name: this.poolName, - image_name: this.rbdName, + image_spec: this.rbdService.getImageSpec(this.poolName, this.namespace, this.rbdName), snapshot_name: snapshotName }; this.rbdService - .protectSnapshot(this.poolName, this.rbdName, snapshotName, !isProtected) + .protectSnapshot(this.poolName, this.namespace, this.rbdName, snapshotName, !isProtected) .toPromise() .then(() => { const executingTask = new ExecutingTask(); @@ -229,11 +231,10 @@ export class RbdSnapshotListComponent implements OnInit, OnChanges { const finishedTask = new FinishedTask(); finishedTask.name = taskName; finishedTask.metadata = { - pool_name: this.poolName, - image_name: this.rbdName, + image_spec: this.rbdService.getImageSpec(this.poolName, this.namespace, this.rbdName), snapshot_name: snapshotName }; - this.rbdService[task](this.poolName, this.rbdName, snapshotName) + this.rbdService[task](this.poolName, this.namespace, this.rbdName, snapshotName) .toPromise() .then(() => { const executingTask = new ExecutingTask(); @@ -257,12 +258,13 @@ export class RbdSnapshotListComponent implements OnInit, OnChanges { rollbackModal() { const snapshotName = this.selection.selected[0].name; + const imageSpec = this.rbdService.getImageSpec(this.poolName, this.namespace, this.rbdName); const initialState = { titleText: this.i18n('RBD snapshot rollback'), buttonText: this.i18n('Rollback'), bodyTpl: this.rollbackTpl, bodyData: { - snapName: `${this.poolName}/${this.rbdName}@${snapshotName}` + snapName: `${imageSpec}@${snapshotName}` }, onSubmit: () => { this._asyncTask('rollbackSnapshot', 'rbd/snap/rollback', snapshotName); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-trash-list/rbd-trash-list.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-trash-list/rbd-trash-list.component.spec.ts index dc84eaffdc5ac..925d5fcfa7101 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-trash-list/rbd-trash-list.component.spec.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-trash-list/rbd-trash-list.component.spec.ts @@ -65,7 +65,8 @@ describe('RbdTrashListComponent', () => { const addImage = (id) => { images.push({ - id: id + id: id, + pool_name: 'pl' }); }; @@ -73,7 +74,7 @@ describe('RbdTrashListComponent', () => { const task = new ExecutingTask(); task.name = name; task.metadata = { - image_id: image_id + image_id_spec: `pl/${image_id}` }; summaryService.addRunningTask(task); }; diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-trash-list/rbd-trash-list.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-trash-list/rbd-trash-list.component.ts index 38b2d478c9231..7f3d9ea705c93 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-trash-list/rbd-trash-list.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-trash-list/rbd-trash-list.component.ts @@ -95,6 +95,11 @@ export class RbdTrashListComponent implements OnInit { prop: 'pool_name', flexGrow: 1 }, + { + name: this.i18n('Namespace'), + prop: 'namespace', + flexGrow: 1 + }, { name: this.i18n('Status'), prop: 'deferment_end_time', @@ -109,13 +114,24 @@ export class RbdTrashListComponent implements OnInit { } ]; + const itemFilter = (entry, task) => { + return ( + this.rbdService.getImageSpec(entry.pool_name, entry.namespace, entry.id) === + task.metadata['image_id_spec'] + ); + }; + + const taskFilter = (task) => { + return ['rbd/trash/remove', 'rbd/trash/restore'].includes(task.name); + }; + this.taskListService.init( () => this.rbdService.listTrash(), (resp) => this.prepareResponse(resp), (images) => (this.images = images), () => this.onFetchError(), - this.taskFilter, - this.itemFilter, + taskFilter, + itemFilter, undefined ); } @@ -154,14 +170,6 @@ export class RbdTrashListComponent implements OnInit { this.viewCacheStatusList = [{ status: ViewCacheStatus.ValueException }]; } - itemFilter(entry, task) { - return entry.id === task.metadata['image_id']; - } - - taskFilter(task) { - return ['rbd/trash/remove', 'rbd/trash/restore'].includes(task.name); - } - updateSelection(selection: CdTableSelection) { this.selection = selection; } @@ -170,6 +178,7 @@ export class RbdTrashListComponent implements OnInit { const initialState = { metaType: 'RBD', poolName: this.selection.first().pool_name, + namespace: this.selection.first().namespace, imageName: this.selection.first().name, imageId: this.selection.first().id }; @@ -179,24 +188,23 @@ export class RbdTrashListComponent implements OnInit { deleteModal() { const poolName = this.selection.first().pool_name; - const imageName = this.selection.first().name; + const namespace = this.selection.first().namespace; const imageId = this.selection.first().id; const expiresAt = this.selection.first().deferment_end_time; + const imageIdSpec = this.rbdService.getImageSpec(poolName, namespace, imageId); this.modalRef = this.modalService.show(CriticalConfirmationModalComponent, { initialState: { itemDescription: 'RBD', - itemNames: [`${poolName}/${imageName}`], + itemNames: [imageIdSpec], bodyTemplate: this.deleteTpl, bodyContext: { $implicit: expiresAt }, submitActionObservable: () => this.taskWrapper.wrapTaskAroundCall({ task: new FinishedTask('rbd/trash/remove', { - pool_name: poolName, - image_id: imageId, - image_name: imageName + image_id_spec: imageIdSpec }), - call: this.rbdService.removeTrash(poolName, imageId, imageName, true) + call: this.rbdService.removeTrash(poolName, namespace, imageId, true) }) } }); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-trash-move-modal/rbd-trash-move-modal.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-trash-move-modal/rbd-trash-move-modal.component.html index e6633bcae193b..06d913bc3c64b 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-trash-move-modal/rbd-trash-move-modal.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-trash-move-modal/rbd-trash-move-modal.component.html @@ -9,7 +9,7 @@ [formGroup]="moveForm" novalidate>