]> git-server-git.apps.pok.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard: Support RBD namespaces 30935/head
authorRicardo Marques <rimarques@suse.com>
Wed, 31 Jul 2019 09:34:57 +0000 (10:34 +0100)
committerRicardo Marques <rimarques@suse.com>
Mon, 11 Nov 2019 15:08:07 +0000 (15:08 +0000)
Fixes: https://tracker.ceph.com/issues/25125
Signed-off-by: Ricardo Marques <rimarques@suse.com>
53 files changed:
qa/tasks/mgr/dashboard/test_rbd.py
src/pybind/mgr/dashboard/controllers/rbd.py
src/pybind/mgr/dashboard/controllers/rbd_mirroring.py
src/pybind/mgr/dashboard/frontend/e2e/block/images.e2e-spec.ts
src/pybind/mgr/dashboard/frontend/e2e/block/images.po.ts
src/pybind/mgr/dashboard/frontend/src/app/app.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/block.module.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi-target-form/iscsi-target-form.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-details/rbd-details.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-form/rbd-form-clone-request.model.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-form/rbd-form-copy-request.model.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-form/rbd-form.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-form/rbd-form.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-form/rbd-form.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-form/rbd-form.model.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-form/rbd-parent.model.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-images/rbd-images.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-images/rbd-images.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-list/rbd-list.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-list/rbd-list.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-list/rbd-list.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-list/rbd-model.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-namespace-form/rbd-namespace-form.component.html [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-namespace-form/rbd-namespace-form.component.scss [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-namespace-form/rbd-namespace-form.component.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-namespace-form/rbd-namespace-form.component.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-namespace-list/rbd-namespace-list.component.html [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-namespace-list/rbd-namespace-list.component.scss [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-namespace-list/rbd-namespace-list.component.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-namespace-list/rbd-namespace-list.component.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-snapshot-form/rbd-snapshot-form.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-snapshot-list/rbd-snapshot-list.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-snapshot-list/rbd-snapshot-list.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-snapshot-list/rbd-snapshot-list.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-trash-list/rbd-trash-list.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-trash-list/rbd-trash-list.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-trash-move-modal/rbd-trash-move-modal.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-trash-move-modal/rbd-trash-move-modal.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-trash-move-modal/rbd-trash-move-modal.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-trash-restore-modal/rbd-trash-restore-modal.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-trash-restore-modal/rbd-trash-restore-modal.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-trash-restore-modal/rbd-trash-restore-modal.component.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/api/rbd.service.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/api/rbd.service.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/components/notifications-sidebar/notifications-sidebar.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/services/notification.service.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/services/prometheus-alert-formatter.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/services/task-list.service.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/services/task-message.service.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/services/task-message.service.ts
src/pybind/mgr/dashboard/services/progress.py
src/pybind/mgr/dashboard/services/rbd.py
src/pybind/mgr/dashboard/tests/test_rbd_service.py [new file with mode: 0644]

index 570c86468291676c577bcaeaa98f9c83d39283b3..7ac313a565008eebeb09a535e619aba1ad574e43 100644 (file)
@@ -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')
index 133a359b71395f28f59f7e180f843c2261b74b2d..7ea98360240bd6836d215409b0e08d3d8d4d2096 100644 (file)
@@ -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
index 5c5d2c592cda9cc30e56b53cc4abd4fed4a2b6b6..1d0a360b497f7487f8eb5e4c803a9bc6fc050569 100644 (file)
@@ -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)
index 943c7877d8fdb75055c71024013f5e9a2d528e9b..f031a3ea72f7570c8a77f4f0976c7c62d24b3101 100644 (file)
@@ -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');
     });
   });
 
index c9878c08998bb1e32eec4d425ec3764f2fbc21f7..33b3661d49e07c7e7edab4ee2b6318a22439d812 100644 (file)
@@ -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);
 
index cdef9faf56cd19cbc628384a7932f210648660e2..c3ef68849288855f367ab0506bd4aef8172c04d6 100644 (file)
@@ -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(() => {
index a2ba84cacfe97101bc752abb5b0f8ce40a2b1b7f..217bd98e5575f0f57bdcf676f4612b439d2ec045 100644 (file)
@@ -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 }
       }
index 1f0541f3838045181b49a5905877f357993e9570..ea0eaf030ac0eb224956f0a513dcbd5ff61e2d70 100644 (file)
@@ -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;
index aeed1654a9578f32479ec2fca8216b65b9749ee0..c5c776613a3ce6cbbab08882aea61062a4567f37 100644 (file)
@@ -2,7 +2,6 @@
   <ng-container i18n>Only available for RBD images with <strong>fast-diff</strong> enabled</ng-container>
 </ng-template>
 
-
 <tabset *ngIf="selection?.hasSingleSelection">
   <tab i18n-heading
        heading="Details">
@@ -96,9 +95,7 @@
           <td i18n
               class="bold">Parent</td>
           <td>
-            <span *ngIf="selectedItem.parent">{{ selectedItem.parent.pool_name }}
-              /{{ selectedItem.parent.image_name }}
-              @{{ selectedItem.parent.snap_name }}</span>
+            <span *ngIf="selectedItem.parent">{{ selectedItem.parent.pool_name }}<span *ngIf="selectedItem.parent.pool_namespace">/{{ selectedItem.parent.pool_namespace }}</span>/{{ selectedItem.parent.image_name }}@{{ selectedItem.parent.snap_name }}</span>
             <span *ngIf="!selectedItem.parent">-</span>
           </td>
         </tr>
     <cd-rbd-snapshot-list [snapshots]="selectedItem.snapshots"
                           [featuresName]="selectedItem.features_name"
                           [poolName]="selectedItem.pool_name"
+                          [namespace]="selectedItem.namespace"
                           [rbdName]="selectedItem.name"></cd-rbd-snapshot-list>
   </tab>
   <tab i18n-heading
index 0c18352d6574751a979a9e0ed5ce8be9a60a7c9b..0666d8c5d405f286af222dee303b20b88a23733d 100644 (file)
@@ -2,6 +2,7 @@ import { RbdConfigurationEntry } from '../../../shared/models/configuration';
 
 export class RbdFormCloneRequestModel {
   child_pool_name: string;
+  child_namespace: string;
   child_image_name: string;
   obj_size: number;
   features: Array<string> = [];
index c1b290dca283c22ac7ad3bbfa23c67ed7644caf5..6642237f144bf72179e7fb48bb10551880843288 100644 (file)
@@ -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;
index c14ddb2bebc9b787799f6b14759222e5ebd53bfc..8e19837e72a68ceb0ea6a22c0ca78f973d996452 100644 (file)
           </div>
         </div>
 
+        <!-- Namespace -->
+        <div class="form-group row"
+             *ngIf="mode !== 'editing' && rbdForm.getValue('pool') && namespaces === null">
+          <div class="col-sm-9 offset-sm-3">
+            <i [ngClass]="[icons.spinner, icons.spin]"></i>
+          </div>
+        </div>
+        <div class="form-group row"
+             *ngIf="(mode === 'editing' && rbdForm.getValue('namespace')) || mode !== 'editing' && (namespaces && namespaces.length > 0 || !poolPermission.read)">
+          <label class="col-form-label col-sm-3"
+                 for="pool">
+            Namespace
+          </label>
+          <div class="col-sm-9">
+            <input class="form-control"
+                   type="text"
+                   placeholder="Namespace..."
+                   id="namespace"
+                   name="namespace"
+                   formControlName="namespace"
+                   *ngIf="mode === 'editing' || !poolPermission.read">
+            <select id="namespace"
+                    name="namespace"
+                    class="form-control custom-select"
+                    formControlName="namespace"
+                    *ngIf="mode !== 'editing' && poolPermission.read">
+              <option *ngIf="pools === null"
+                      [ngValue]="null"
+                      i18n>Loading...</option>
+              <option *ngIf="pools !== null && pools.length === 0"
+                      [ngValue]="null"
+                      i18n>-- No namespaces available --</option>
+              <option *ngIf="pools !== null && pools.length > 0"
+                      [ngValue]="null"
+                      i18n>-- Select a namespace --</option>
+              <option *ngFor="let namespace of namespaces"
+                      [value]="namespace">{{ namespace }}</option>
+            </select>
+          </div>
+        </div>
+
         <!-- Use a dedicated pool -->
         <div class="form-group row">
           <div class="offset-sm-3 col-sm-9">
index 78926ba7521bae128f57595560e1d97b73a800f4..04635367646aef1e4d89d278a1600867ddb18a37 100644 (file)
@@ -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');
     });
   });
index 6e9bb294ce1d4ee6bc301134f38d8aa572047b9a..78a0a9f36b2aa16473372789d274e5f7f4fe20cd 100644 (file)
@@ -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<string> = [];
+  namespacesByPoolCache = {};
   pools: Array<string> = null;
   allPools: Array<string> = null;
   dataPools: Array<string> = 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<any> {
     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
+      )
     });
   }
 
index 9511a00d003022dcb2ca2d502f7fb12dc19ec6da..6a4999d0fc57de47cfa59bcc1927b821a01c7209 100644 (file)
@@ -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;
 
index a10f8a3715ef7df17dc6bfb69134f940c6b46069..000717b0a0e7be4e969b87101640e98ca797132c 100644 (file)
@@ -1,5 +1,6 @@
 export class RbdParentModel {
   image_name: string;
   pool_name: string;
+  pool_namespace: string;
   snap_name: string;
 }
index 0827a61dc830634b828840ec1784d2185f725bfa..1b7f59c48ada1f80ccd440f299463667803749e9 100644 (file)
@@ -5,6 +5,10 @@
          id="tab1">
       <cd-rbd-list></cd-rbd-list>
     </tab>
+    <tab heading="Namespaces"
+         i18n-heading>
+      <cd-rbd-namespace-list></cd-rbd-namespace-list>
+    </tab>
     <tab heading="Trash"
          i18n-heading>
       <cd-rbd-trash-list></cd-rbd-trash-list>
index e1fda888a4697761dffd794a00502987b41dc04e..1a5c035620a9be442a8df1757695af195d6d48f5 100644 (file)
@@ -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
index ccd95925353571c5afdbcada2ef6ad2bd956c43a..0058b10df17a317e79cd745238e9240337b7f61c 100644 (file)
@@ -28,7 +28,7 @@
 
 <ng-template #parentTpl
              let-value="value">
-  <span *ngIf="value">{{ value.pool_name }}/{{ value.image_name }}@{{ value.snap_name }}</span>
+  <span *ngIf="value">{{ value.pool_name }}<span *ngIf="value.pool_namespace">/{{ value.pool_namespace }}</span>/{{ value.image_name }}@{{ value.snap_name }}</span>
   <span *ngIf="!value">-</span>
 </ng-template>
 
index 2236c63b986a7d1c8c5acc77dd0e3fa35194d287..8a8b6a5c94c424d66af3a0a193647bd0b97a5d2e 100644 (file)
@@ -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);
     };
index b15d140b85a86f527b42d517c5522eb56edc378f..28a3a6e66882545c897aa595a9d97aeeb1724fce 100644 (file)
@@ -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);
       }
     };
 
index 92a77bdd6eaa4cf3e50d9dca726120cd2505c3bb..625a2f251d06dbb713d57ce7b6c573264ef6102f 100644 (file)
@@ -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 (file)
index 0000000..13454fe
--- /dev/null
@@ -0,0 +1,92 @@
+<div class="modal-header">
+  <h4 class="modal-title float-left"
+      i18n>Create Namespace</h4>
+  <button type="button"
+          class="close float-right"
+          aria-label="Close"
+          (click)="modalRef.hide()">
+    <span aria-hidden="true">&times;</span>
+  </button>
+</div>
+<form name="namespaceForm"
+      #formDir="ngForm"
+      [formGroup]="namespaceForm"
+      novalidate>
+  <div class="modal-body">
+
+    <!-- Pool -->
+    <div class="form-group row">
+      <label class="col-form-label col-sm-3"
+             for="pool">
+        Pool
+        <span class="required"></span>
+      </label>
+      <div class="col-sm-9">
+        <input class="form-control"
+               type="text"
+               placeholder="Pool name..."
+               id="pool"
+               name="pool"
+               formControlName="pool"
+               *ngIf="!poolPermission.read">
+        <select id="pool"
+                name="pool"
+                class="form-control custom-select"
+                formControlName="pool"
+                *ngIf="poolPermission.read">
+          <option *ngIf="pools === null"
+                  [ngValue]="null"
+                  i18n>Loading...</option>
+          <option *ngIf="pools !== null && pools.length === 0"
+                  [ngValue]="null"
+                  i18n>-- No rbd pools available --</option>
+          <option *ngIf="pools !== null && pools.length > 0"
+                  [ngValue]="null"
+                  i18n>-- Select a pool --</option>
+          <option *ngFor="let pool of pools"
+                  [value]="pool.pool_name">{{ pool.pool_name }}</option>
+        </select>
+        <span *ngIf="namespaceForm.showError('pool', formDir, 'required')"
+              class="invalid-feedback"
+              i18n>This field is required.</span>
+      </div>
+    </div>
+
+    <!-- Name -->
+    <div class="form-group row">
+      <label class="col-form-label col-sm-3"
+             for="namespace">
+        <ng-container i18n>Name</ng-container>
+        <span class="required"></span>
+      </label>
+      <div class="col-sm-9">
+        <input class="form-control"
+               type="text"
+               placeholder="Namespace name..."
+               id="namespace"
+               name="namespace"
+               formControlName="namespace"
+               autofocus>
+        <span class="invalid-feedback"
+              *ngIf="namespaceForm.showError('namespace', formDir, 'required')"
+              i18n>This field is required.</span>
+        <span class="invalid-feedback"
+              *ngIf="namespaceForm.showError('namespace', formDir, 'namespaceExists')"
+              i18n>Namespace already exists.</span>
+      </div>
+    </div>
+
+  </div>
+
+  <div class="modal-footer">
+    <div class="button-group text-right">
+      <cd-submit-button [form]="namespaceForm"
+                        (submitAction)="submit()"
+                        i18n>Create Namespace</cd-submit-button>
+      <cd-back-button [back]="modalRef.hide"
+                      name="Close"
+                      i18n-name>
+      </cd-back-button>
+    </div>
+  </div>
+</form>
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 (file)
index 0000000..e69de29
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 (file)
index 0000000..c243810
--- /dev/null
@@ -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<RbdNamespaceFormComponent>;
+
+  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 (file)
index 0000000..023dc7f
--- /dev/null
@@ -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<string> = null;
+  pool: string;
+  namespace: string;
+
+  namespaceForm: CdFormGroup;
+
+  editing = false;
+
+  public onSubmit: Subject<void>;
+
+  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<ValidationErrors | null> => {
+      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 (file)
index 0000000..e8b0938
--- /dev/null
@@ -0,0 +1,16 @@
+<cd-table [data]="namespaces"
+          (fetchData)="refresh()"
+          columnMode="flex"
+          [columns]="columns"
+          identifier="id"
+          forceIdentifier="true"
+          selectionType="single"
+          (updateSelection)="updateSelection($event)">
+  <div class="table-actions btn-toolbar">
+    <cd-table-actions class="btn-group"
+                      [permission]="permission"
+                      [selection]="selection"
+                      [tableActions]="tableActions">
+    </cd-table-actions>
+  </div>
+</cd-table>
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 (file)
index 0000000..e69de29
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 (file)
index 0000000..ac45d9f
--- /dev/null
@@ -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<RbdNamespaceListComponent>;
+
+  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 (file)
index 0000000..ca6bb9d
--- /dev/null
@@ -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<Array<string>>) => {
+          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');
+      }
+    }
+  }
+}
index b5d2a080bc507b7e5ddaeb5a7e010af1ef4da158..c04bd21459ef7cf92f2bd2cdc2f080b442e36972 100644 (file)
@@ -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(
index 29cf4044a731928416e4c39fdfaed3ee3b9a4a83..b0a5abbbeb7296f56e697c8d06f15b7a1b0aa47d 100644 (file)
@@ -23,5 +23,5 @@
 <ng-template #rollbackTpl
              let-value>
   <ng-container i18n>You are about to rollback</ng-container>
-  <strong>{{ value.snapName }}</strong>.
+  <strong> {{ value.snapName }}</strong>.
 </ng-template>
index 2044a784f26250bd6c79d716efa42e485e2828ea..c752873828ca12bd4bb60702f261bbaf3cbf460d 100644 (file)
@@ -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);
index 1f2a9b15a9b8b1cd78dc465d9653663b458b5dd7..96a14ee2f6b253e868e360a192891ea12271c425 100644 (file)
@@ -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<any>;
@@ -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);
index dc84eaffdc5ac5b768bfc2f6c3703063ee66c670..925d5fcfa7101b14e1f647708b2dcb929502a7c0 100644 (file)
@@ -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);
     };
index 38b2d478c923130ae847e0a904a9c291466ccac1..7f3d9ea705c93384020f1d3e61d3185715866a96 100644 (file)
@@ -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)
           })
       }
     });
index e6633bcae193ba01e82b8b6384d2636c12d8117d..06d913bc3c64b91a8c373be96d33d029e22235a0 100644 (file)
@@ -9,7 +9,7 @@
           [formGroup]="moveForm"
           novalidate>
       <div class="modal-body">
-        <p i18n>To move <kbd>{{ poolName }}/{{ imageName }}</kbd> to trash,
+        <p i18n>To move <kbd>{{ imageSpec }}</kbd> to trash,
           click <kbd>Move Image</kbd>. Optionally, you can pick an expiration date.</p>
 
         <div class="form-group">
index 1c77198e8d969698d3c5c7e2352e23b984069596..bf90ee7c8e4d040dd523b23a1bbca2fd9386d0b3 100644 (file)
@@ -67,7 +67,7 @@ describe('RbdTrashMoveModalComponent', () => {
 
     it('with normal delay', () => {
       component.moveImage();
-      const req = httpTesting.expectOne('api/block/image/foo/bar/move_trash');
+      const req = httpTesting.expectOne('api/block/image/foo%2Fbar/move_trash');
       req.flush(null);
       expect(req.request.body).toEqual({ delay: 0 });
     });
@@ -79,7 +79,7 @@ describe('RbdTrashMoveModalComponent', () => {
       component.moveForm.patchValue({ expiresAt: oldDate });
 
       component.moveImage();
-      const req = httpTesting.expectOne('api/block/image/foo/bar/move_trash');
+      const req = httpTesting.expectOne('api/block/image/foo%2Fbar/move_trash');
       req.flush(null);
       expect(req.request.body).toEqual({ delay: 0 });
     });
@@ -92,7 +92,7 @@ describe('RbdTrashMoveModalComponent', () => {
       component.moveForm.patchValue({ expiresAt: oldDate });
 
       component.moveImage();
-      const req = httpTesting.expectOne('api/block/image/foo/bar/move_trash');
+      const req = httpTesting.expectOne('api/block/image/foo%2Fbar/move_trash');
       req.flush(null);
       expect(req.request.body.delay).toBeGreaterThan(86390);
     });
index 84be13dfb020efd6960bc3630d2b415270656b3c..14a7543fbdcfe8476a3cefc9c5a69b6c32965cc3 100644 (file)
@@ -19,7 +19,9 @@ import { TaskWrapperService } from '../../../shared/services/task-wrapper.servic
 export class RbdTrashMoveModalComponent implements OnInit {
   metaType: string;
   poolName: string;
+  namespace: string;
   imageName: string;
+  imageSpec: string;
   executingTasks: ExecutingTask[];
 
   moveForm: CdFormGroup;
@@ -58,6 +60,7 @@ export class RbdTrashMoveModalComponent implements OnInit {
   }
 
   ngOnInit() {
+    this.imageSpec = this.rbdService.getImageSpec(this.poolName, this.namespace, this.imageName);
     this.pattern = `${this.poolName}/${this.imageName}`;
   }
 
@@ -76,10 +79,9 @@ export class RbdTrashMoveModalComponent implements OnInit {
     this.taskWrapper
       .wrapTaskAroundCall({
         task: new FinishedTask('rbd/trash/move', {
-          pool_name: this.poolName,
-          image_name: this.imageName
+          image_spec: this.imageSpec
         }),
-        call: this.rbdService.moveTrash(this.poolName, this.imageName, delay)
+        call: this.rbdService.moveTrash(this.poolName, this.namespace, this.imageName, delay)
       })
       .subscribe(undefined, undefined, () => {
         this.modalRef.hide();
index 6ed23bc4190becc259b683226b0cbb8d96f32ca8..522ce00a86cc4cf5fae85175db6d9099b1e8b5e6 100644 (file)
@@ -11,7 +11,7 @@
       <div class="modal-body">
         <p>
           <ng-container i18n>To restore</ng-container>&nbsp;
-          <kbd>{{ poolName }}/{{ imageName }}@{{ imageId }}</kbd>,&nbsp;
+          <kbd>{{ imageSpec }}@{{ imageId }}</kbd>,&nbsp;
           <ng-container i18n>type the image's new name and click</ng-container>&nbsp;
           <kbd i18n>Restore Image</kbd>.
         </p>
index caa6cf945eb3e32ac4b73bd155851593a7a4f36d..9dbab72e98cdc4f078f23b9627357af664281f28 100644 (file)
@@ -49,7 +49,9 @@ describe('RbdTrashRestoreModalComponent', () => {
       modalRef = TestBed.get(BsModalRef);
 
       component.poolName = 'foo';
-      component.imageId = 'bar';
+      component.imageName = 'bar';
+      component.imageId = '113cb6963793';
+      component.ngOnInit();
 
       spyOn(modalRef, 'hide').and.stub();
       spyOn(component.restoreForm, 'setErrors').and.stub();
@@ -57,7 +59,7 @@ describe('RbdTrashRestoreModalComponent', () => {
 
       component.restore();
 
-      req = httpTesting.expectOne('api/block/image/trash/foo/bar/restore');
+      req = httpTesting.expectOne('api/block/image/trash/foo%2F113cb6963793/restore');
     });
 
     it('with success', () => {
index 42794419821324fd20af575cec626ceae4a2ba8d..b45ef7bf8fe3dcf5ec98d087c7631d4bdc3bd492 100644 (file)
@@ -17,7 +17,9 @@ import { TaskWrapperService } from '../../../shared/services/task-wrapper.servic
 export class RbdTrashRestoreModalComponent implements OnInit {
   metaType: string;
   poolName: string;
+  namespace: string;
   imageName: string;
+  imageSpec: string;
   imageId: string;
   executingTasks: ExecutingTask[];
 
@@ -31,6 +33,7 @@ export class RbdTrashRestoreModalComponent implements OnInit {
   ) {}
 
   ngOnInit() {
+    this.imageSpec = this.rbdService.getImageSpec(this.poolName, this.namespace, this.imageName);
     this.restoreForm = this.fb.group({
       name: this.imageName
     });
@@ -42,11 +45,10 @@ export class RbdTrashRestoreModalComponent implements OnInit {
     this.taskWrapper
       .wrapTaskAroundCall({
         task: new FinishedTask('rbd/trash/restore', {
-          pool_name: this.poolName,
-          image_id: this.imageId,
+          image_id_spec: this.rbdService.getImageSpec(this.poolName, this.namespace, this.imageId),
           new_image_name: name
         }),
-        call: this.rbdService.restoreTrash(this.poolName, this.imageId, name)
+        call: this.rbdService.restoreTrash(this.poolName, this.namespace, this.imageId, name)
       })
       .subscribe(
         undefined,
index b09fec663a166b9d0d9a50e75155f49a01f86829..e636495bbd292e8e2f0359a4ceaed326d729fbcd 100644 (file)
@@ -35,21 +35,21 @@ describe('RbdService', () => {
   });
 
   it('should call delete', () => {
-    service.delete('poolName', 'rbdName').subscribe();
-    const req = httpTesting.expectOne('api/block/image/poolName/rbdName');
+    service.delete('poolName', null, 'rbdName').subscribe();
+    const req = httpTesting.expectOne('api/block/image/poolName%2FrbdName');
     expect(req.request.method).toBe('DELETE');
   });
 
   it('should call update', () => {
-    service.update('poolName', 'rbdName', 'foo').subscribe();
-    const req = httpTesting.expectOne('api/block/image/poolName/rbdName');
+    service.update('poolName', null, 'rbdName', 'foo').subscribe();
+    const req = httpTesting.expectOne('api/block/image/poolName%2FrbdName');
     expect(req.request.body).toEqual('foo');
     expect(req.request.method).toBe('PUT');
   });
 
   it('should call get', () => {
-    service.get('poolName', 'rbdName').subscribe();
-    const req = httpTesting.expectOne('api/block/image/poolName/rbdName');
+    service.get('poolName', null, 'rbdName').subscribe();
+    const req = httpTesting.expectOne('api/block/image/poolName%2FrbdName');
     expect(req.request.method).toBe('GET');
   });
 
@@ -60,15 +60,15 @@ describe('RbdService', () => {
   });
 
   it('should call copy', () => {
-    service.copy('poolName', 'rbdName', 'foo').subscribe();
-    const req = httpTesting.expectOne('api/block/image/poolName/rbdName/copy');
+    service.copy('poolName', null, 'rbdName', 'foo').subscribe();
+    const req = httpTesting.expectOne('api/block/image/poolName%2FrbdName/copy');
     expect(req.request.body).toEqual('foo');
     expect(req.request.method).toBe('POST');
   });
 
   it('should call flatten', () => {
-    service.flatten('poolName', 'rbdName').subscribe();
-    const req = httpTesting.expectOne('api/block/image/poolName/rbdName/flatten');
+    service.flatten('poolName', null, 'rbdName').subscribe();
+    const req = httpTesting.expectOne('api/block/image/poolName%2FrbdName/flatten');
     expect(req.request.body).toEqual(null);
     expect(req.request.method).toBe('POST');
   });
@@ -80,8 +80,8 @@ describe('RbdService', () => {
   });
 
   it('should call createSnapshot', () => {
-    service.createSnapshot('poolName', 'rbdName', 'snapshotName').subscribe();
-    const req = httpTesting.expectOne('api/block/image/poolName/rbdName/snap');
+    service.createSnapshot('poolName', null, 'rbdName', 'snapshotName').subscribe();
+    const req = httpTesting.expectOne('api/block/image/poolName%2FrbdName/snap');
     expect(req.request.body).toEqual({
       snapshot_name: 'snapshotName'
     });
@@ -89,8 +89,8 @@ describe('RbdService', () => {
   });
 
   it('should call renameSnapshot', () => {
-    service.renameSnapshot('poolName', 'rbdName', 'snapshotName', 'foo').subscribe();
-    const req = httpTesting.expectOne('api/block/image/poolName/rbdName/snap/snapshotName');
+    service.renameSnapshot('poolName', null, 'rbdName', 'snapshotName', 'foo').subscribe();
+    const req = httpTesting.expectOne('api/block/image/poolName%2FrbdName/snap/snapshotName');
     expect(req.request.body).toEqual({
       new_snap_name: 'foo'
     });
@@ -98,8 +98,8 @@ describe('RbdService', () => {
   });
 
   it('should call protectSnapshot', () => {
-    service.protectSnapshot('poolName', 'rbdName', 'snapshotName', true).subscribe();
-    const req = httpTesting.expectOne('api/block/image/poolName/rbdName/snap/snapshotName');
+    service.protectSnapshot('poolName', null, 'rbdName', 'snapshotName', true).subscribe();
+    const req = httpTesting.expectOne('api/block/image/poolName%2FrbdName/snap/snapshotName');
     expect(req.request.body).toEqual({
       is_protected: true
     });
@@ -107,31 +107,57 @@ describe('RbdService', () => {
   });
 
   it('should call rollbackSnapshot', () => {
-    service.rollbackSnapshot('poolName', 'rbdName', 'snapshotName').subscribe();
+    service.rollbackSnapshot('poolName', null, 'rbdName', 'snapshotName').subscribe();
     const req = httpTesting.expectOne(
-      'api/block/image/poolName/rbdName/snap/snapshotName/rollback'
+      'api/block/image/poolName%2FrbdName/snap/snapshotName/rollback'
     );
     expect(req.request.body).toEqual(null);
     expect(req.request.method).toBe('POST');
   });
 
   it('should call cloneSnapshot', () => {
-    service.cloneSnapshot('poolName', 'rbdName', 'snapshotName', null).subscribe();
-    const req = httpTesting.expectOne('api/block/image/poolName/rbdName/snap/snapshotName/clone');
+    service.cloneSnapshot('poolName', null, 'rbdName', 'snapshotName', null).subscribe();
+    const req = httpTesting.expectOne('api/block/image/poolName%2FrbdName/snap/snapshotName/clone');
     expect(req.request.body).toEqual(null);
     expect(req.request.method).toBe('POST');
   });
 
   it('should call deleteSnapshot', () => {
-    service.deleteSnapshot('poolName', 'rbdName', 'snapshotName').subscribe();
-    const req = httpTesting.expectOne('api/block/image/poolName/rbdName/snap/snapshotName');
+    service.deleteSnapshot('poolName', null, 'rbdName', 'snapshotName').subscribe();
+    const req = httpTesting.expectOne('api/block/image/poolName%2FrbdName/snap/snapshotName');
     expect(req.request.method).toBe('DELETE');
   });
 
   it('should call moveTrash', () => {
-    service.moveTrash('poolName', 'rbdName', 1).subscribe();
-    const req = httpTesting.expectOne('api/block/image/poolName/rbdName/move_trash');
+    service.moveTrash('poolName', null, 'rbdName', 1).subscribe();
+    const req = httpTesting.expectOne('api/block/image/poolName%2FrbdName/move_trash');
     expect(req.request.method).toBe('POST');
     expect(req.request.body).toEqual({ delay: 1 });
   });
+
+  describe('should compose image spec', () => {
+    it('with namespace', () => {
+      expect(service.getImageSpec('mypool', 'myns', 'myimage')).toBe('mypool/myns/myimage');
+    });
+
+    it('without namespace', () => {
+      expect(service.getImageSpec('mypool', null, 'myimage')).toBe('mypool/myimage');
+    });
+  });
+
+  describe('should parse image spec', () => {
+    it('with namespace', () => {
+      const [poolName, namespace, rbdName] = service.parseImageSpec('mypool/myns/myimage');
+      expect(poolName).toBe('mypool');
+      expect(namespace).toBe('myns');
+      expect(rbdName).toBe('myimage');
+    });
+
+    it('without namespace', () => {
+      const [poolName, namespace, rbdName] = service.parseImageSpec('mypool/myimage');
+      expect(poolName).toBe('mypool');
+      expect(namespace).toBeNull();
+      expect(rbdName).toBe('myimage');
+    });
+  });
 });
index 3ccf6100254ece192b67da4d725b1cc1710ae072..2b2459af8bef3cb1ec223a35fcd6d451c7f0e453 100644 (file)
@@ -1,6 +1,7 @@
 import { HttpClient } from '@angular/common/http';
 import { Injectable } from '@angular/core';
 
+import * as _ from 'lodash';
 import { map } from 'rxjs/operators';
 
 import { cdEncode, cdEncodeNot } from '../decorators/cd-encode';
@@ -15,20 +16,44 @@ import { RbdPool } from './rbd.model';
 export class RbdService {
   constructor(private http: HttpClient, private rbdConfigurationService: RbdConfigurationService) {}
 
+  getImageSpec(poolName: string, namespace: string, rbdName: string) {
+    namespace = namespace ? `${namespace}/` : '';
+    return `${poolName}/${namespace}${rbdName}`;
+  }
+
+  parseImageSpec(@cdEncodeNot imageSpec: string) {
+    const imageSpecSplited = imageSpec.split('/');
+    const poolName = imageSpecSplited[0];
+    const namespace = imageSpecSplited.length >= 3 ? imageSpecSplited[1] : null;
+    const imageName = imageSpecSplited.length >= 3 ? imageSpecSplited[2] : imageSpecSplited[1];
+    return [poolName, namespace, imageName];
+  }
+
+  isRBDPool(pool) {
+    return _.indexOf(pool.application_metadata, 'rbd') !== -1 && !pool.pool_name.includes('/');
+  }
+
   create(rbd) {
     return this.http.post('api/block/image', rbd, { observe: 'response' });
   }
 
-  delete(poolName, rbdName) {
-    return this.http.delete(`api/block/image/${poolName}/${rbdName}`, { observe: 'response' });
+  delete(poolName, namespace, rbdName) {
+    const imageSpec = this.getImageSpec(poolName, namespace, rbdName);
+    return this.http.delete(`api/block/image/${encodeURIComponent(imageSpec)}`, {
+      observe: 'response'
+    });
   }
 
-  update(poolName, rbdName, rbd) {
-    return this.http.put(`api/block/image/${poolName}/${rbdName}`, rbd, { observe: 'response' });
+  update(poolName, namespace, rbdName, rbd) {
+    const imageSpec = this.getImageSpec(poolName, namespace, rbdName);
+    return this.http.put(`api/block/image/${encodeURIComponent(imageSpec)}`, rbd, {
+      observe: 'response'
+    });
   }
 
-  get(poolName, rbdName) {
-    return this.http.get(`api/block/image/${poolName}/${rbdName}`);
+  get(poolName, namespace, rbdName) {
+    const imageSpec = this.getImageSpec(poolName, namespace, rbdName);
+    return this.http.get(`api/block/image/${encodeURIComponent(imageSpec)}`);
   }
 
   list() {
@@ -50,14 +75,16 @@ export class RbdService {
     );
   }
 
-  copy(poolName, rbdName, rbd) {
-    return this.http.post(`api/block/image/${poolName}/${rbdName}/copy`, rbd, {
+  copy(poolName, namespace, rbdName, rbd) {
+    const imageSpec = this.getImageSpec(poolName, namespace, rbdName);
+    return this.http.post(`api/block/image/${encodeURIComponent(imageSpec)}/copy`, rbd, {
       observe: 'response'
     });
   }
 
-  flatten(poolName, rbdName) {
-    return this.http.post(`api/block/image/${poolName}/${rbdName}/flatten`, null, {
+  flatten(poolName, namespace, rbdName) {
+    const imageSpec = this.getImageSpec(poolName, namespace, rbdName);
+    return this.http.post(`api/block/image/${encodeURIComponent(imageSpec)}/flatten`, null, {
       observe: 'response'
     });
   }
@@ -66,62 +93,97 @@ export class RbdService {
     return this.http.get('api/block/image/default_features');
   }
 
-  createSnapshot(poolName, rbdName, @cdEncodeNot snapshotName) {
+  createSnapshot(poolName, namespace, rbdName, @cdEncodeNot snapshotName) {
+    const imageSpec = this.getImageSpec(poolName, namespace, rbdName);
     const request = {
       snapshot_name: snapshotName
     };
-    return this.http.post(`api/block/image/${poolName}/${rbdName}/snap`, request, {
+    return this.http.post(`api/block/image/${encodeURIComponent(imageSpec)}/snap`, request, {
       observe: 'response'
     });
   }
 
-  renameSnapshot(poolName, rbdName, snapshotName, @cdEncodeNot newSnapshotName) {
+  renameSnapshot(poolName, namespace, rbdName, snapshotName, @cdEncodeNot newSnapshotName) {
+    const imageSpec = this.getImageSpec(poolName, namespace, rbdName);
     const request = {
       new_snap_name: newSnapshotName
     };
-    return this.http.put(`api/block/image/${poolName}/${rbdName}/snap/${snapshotName}`, request, {
-      observe: 'response'
-    });
+    return this.http.put(
+      `api/block/image/${encodeURIComponent(imageSpec)}/snap/${snapshotName}`,
+      request,
+      {
+        observe: 'response'
+      }
+    );
   }
 
-  protectSnapshot(poolName, rbdName, snapshotName, @cdEncodeNot isProtected) {
+  protectSnapshot(poolName, namespace, rbdName, snapshotName, @cdEncodeNot isProtected) {
+    const imageSpec = this.getImageSpec(poolName, namespace, rbdName);
     const request = {
       is_protected: isProtected
     };
-    return this.http.put(`api/block/image/${poolName}/${rbdName}/snap/${snapshotName}`, request, {
-      observe: 'response'
-    });
+    return this.http.put(
+      `api/block/image/${encodeURIComponent(imageSpec)}/snap/${snapshotName}`,
+      request,
+      {
+        observe: 'response'
+      }
+    );
   }
 
-  rollbackSnapshot(poolName, rbdName, snapshotName) {
+  rollbackSnapshot(poolName, namespace, rbdName, snapshotName) {
+    const imageSpec = this.getImageSpec(poolName, namespace, rbdName);
     return this.http.post(
-      `api/block/image/${poolName}/${rbdName}/snap/${snapshotName}/rollback`,
+      `api/block/image/${encodeURIComponent(imageSpec)}/snap/${snapshotName}/rollback`,
       null,
       { observe: 'response' }
     );
   }
 
-  cloneSnapshot(poolName, rbdName, snapshotName, request) {
+  cloneSnapshot(poolName, namespace, rbdName, snapshotName, request) {
+    const imageSpec = this.getImageSpec(poolName, namespace, rbdName);
     return this.http.post(
-      `api/block/image/${poolName}/${rbdName}/snap/${snapshotName}/clone`,
+      `api/block/image/${encodeURIComponent(imageSpec)}/snap/${snapshotName}/clone`,
       request,
       { observe: 'response' }
     );
   }
 
-  deleteSnapshot(poolName, rbdName, snapshotName) {
-    return this.http.delete(`api/block/image/${poolName}/${rbdName}/snap/${snapshotName}`, {
-      observe: 'response'
-    });
+  deleteSnapshot(poolName, namespace, rbdName, snapshotName) {
+    const imageSpec = this.getImageSpec(poolName, namespace, rbdName);
+    return this.http.delete(
+      `api/block/image/${encodeURIComponent(imageSpec)}/snap/${snapshotName}`,
+      {
+        observe: 'response'
+      }
+    );
   }
 
   listTrash() {
     return this.http.get(`api/block/image/trash/`);
   }
 
-  moveTrash(poolName, rbdName, delay) {
+  createNamespace(pool, namespace) {
+    const request = {
+      namespace: namespace
+    };
+    return this.http.post(`api/block/pool/${pool}/namespace`, request, { observe: 'response' });
+  }
+
+  listNamespaces(pool) {
+    return this.http.get(`api/block/pool/${pool}/namespace/`);
+  }
+
+  deleteNamespace(pool, namespace) {
+    return this.http.delete(`api/block/pool/${pool}/namespace/${namespace}`, {
+      observe: 'response'
+    });
+  }
+
+  moveTrash(poolName, namespace, rbdName, delay) {
+    const imageSpec = this.getImageSpec(poolName, namespace, rbdName);
     return this.http.post(
-      `api/block/image/${poolName}/${rbdName}/move_trash`,
+      `api/block/image/${encodeURIComponent(imageSpec)}/move_trash`,
       { delay: delay },
       { observe: 'response' }
     );
@@ -133,17 +195,19 @@ export class RbdService {
     });
   }
 
-  restoreTrash(poolName, imageId, newImageName) {
+  restoreTrash(poolName, namespace, imageId, newImageName) {
+    const imageSpec = this.getImageSpec(poolName, namespace, imageId);
     return this.http.post(
-      `api/block/image/trash/${poolName}/${imageId}/restore`,
+      `api/block/image/trash/${encodeURIComponent(imageSpec)}/restore`,
       { new_image_name: newImageName },
       { observe: 'response' }
     );
   }
 
-  removeTrash(poolName, imageId, imageName, force = false) {
+  removeTrash(poolName, namespace, imageId, force = false) {
+    const imageSpec = this.getImageSpec(poolName, namespace, imageId);
     return this.http.delete(
-      `api/block/image/trash/${poolName}/${imageId}/?image_name=${imageName}&force=${force}`,
+      `api/block/image/trash/${encodeURIComponent(imageSpec)}/?force=${force}`,
       { observe: 'response' }
     );
   }
index 7c9e0f3188b2a4a016e4c1b4535e5f68dc5f7eb4..d379a9ccdf9f1108948ed92a0f4a577560cb4ade 100644 (file)
@@ -9,6 +9,7 @@ import { ToastrModule } from 'ngx-toastr';
 import { NoopAnimationsModule } from '@angular/platform-browser/animations';
 import { configureTestBed, i18nProviders } from '../../../../testing/unit-test-helper';
 import { PrometheusService } from '../../api/prometheus.service';
+import { RbdService } from '../../api/rbd.service';
 import { SettingsService } from '../../api/settings.service';
 import { NotificationType } from '../../enum/notification-type.enum';
 import { ExecutingTask } from '../../models/executing-task';
@@ -40,7 +41,8 @@ describe('NotificationsSidebarComponent', () => {
       PrometheusService,
       SettingsService,
       SummaryService,
-      NotificationService
+      NotificationService,
+      RbdService
     ]
   });
 
@@ -116,8 +118,7 @@ describe('NotificationsSidebarComponent', () => {
 
     it('should handle executing tasks', () => {
       const running_tasks = new ExecutingTask('rbd/delete', {
-        pool_name: 'somePool',
-        image_name: 'someImage'
+        image_spec: 'somePool/someImage'
       });
 
       summaryService['summaryDataSource'].next({ executing_tasks: [running_tasks] });
index 2767f3f69cf2d742c4bb1cc3a0f5217e8bd6d21a..b1cdf8f17751279f5bfb367ab99b3c286ff3a8ec 100644 (file)
@@ -3,7 +3,9 @@ import { fakeAsync, TestBed, tick } from '@angular/core/testing';
 import * as _ from 'lodash';
 import { ToastrService } from 'ngx-toastr';
 
+import { HttpClientTestingModule } from '@angular/common/http/testing';
 import { configureTestBed, i18nProviders } from '../../../testing/unit-test-helper';
+import { RbdService } from '../api/rbd.service';
 import { NotificationType } from '../enum/notification-type.enum';
 import { CdNotificationConfig } from '../models/cd-notification';
 import { FinishedTask } from '../models/finished-task';
@@ -25,8 +27,10 @@ describe('NotificationService', () => {
       TaskMessageService,
       { provide: ToastrService, useValue: toastFakeService },
       { provide: CdDatePipe, useValue: { transform: (d) => d } },
-      i18nProviders
-    ]
+      i18nProviders,
+      RbdService
+    ],
+    imports: [HttpClientTestingModule]
   });
 
   beforeEach(() => {
index 0b0190553aaf522b20582fdf7b3ea4cbea4a3e4e..dfd43778f73d54df4006c8978d18708be17c2bb1 100644 (file)
@@ -2,6 +2,7 @@ import { TestBed } from '@angular/core/testing';
 
 import { ToastrModule } from 'ngx-toastr';
 
+import { HttpClientTestingModule } from '@angular/common/http/testing';
 import {
   configureTestBed,
   i18nProviders,
@@ -20,7 +21,7 @@ describe('PrometheusAlertFormatter', () => {
   let prometheus: PrometheusHelper;
 
   configureTestBed({
-    imports: [ToastrModule.forRoot(), SharedModule],
+    imports: [ToastrModule.forRoot(), SharedModule, HttpClientTestingModule],
     providers: [PrometheusAlertFormatter, i18nProviders]
   });
 
index 91d2252af0f709f0d325aa919644bfbc225adf14..f18d9b6096ddcbb598f0c9edb3768b953dbbf92d 100644 (file)
@@ -9,6 +9,7 @@ import {
   expectItemTasks,
   i18nProviders
 } from '../../../testing/unit-test-helper';
+import { RbdService } from '../api/rbd.service';
 import { ExecutingTask } from '../models/executing-task';
 import { SummaryService } from './summary.service';
 import { TaskListService } from './task-list.service';
@@ -28,7 +29,7 @@ describe('TaskListService', () => {
   };
 
   configureTestBed({
-    providers: [TaskListService, TaskMessageService, SummaryService, i18nProviders],
+    providers: [TaskListService, TaskMessageService, SummaryService, i18nProviders, RbdService],
     imports: [HttpClientTestingModule, RouterTestingModule]
   });
 
index d8ddab699d30923b9a97afeee46a011a6d18a489..bff7cff540ce4393b07e47d131a5f0a0cc044a39 100644 (file)
@@ -2,7 +2,9 @@ import { TestBed } from '@angular/core/testing';
 
 import * as _ from 'lodash';
 
+import { HttpClientTestingModule } from '@angular/common/http/testing';
 import { configureTestBed, i18nProviders } from '../../../testing/unit-test-helper';
+import { RbdService } from '../api/rbd.service';
 import { FinishedTask } from '../models/finished-task';
 import { TaskException } from '../models/task-exception';
 import { TaskMessageOperation, TaskMessageService } from './task-message.service';
@@ -12,7 +14,8 @@ describe('TaskManagerMessageService', () => {
   let finishedTask: FinishedTask;
 
   configureTestBed({
-    providers: [TaskMessageService, i18nProviders]
+    providers: [TaskMessageService, i18nProviders, RbdService],
+    imports: [HttpClientTestingModule]
   });
 
   beforeEach(() => {
@@ -129,13 +132,15 @@ describe('TaskManagerMessageService', () => {
         metadata = {
           pool_name: 'somePool',
           image_name: 'someImage',
+          image_id: '12345',
+          image_spec: 'somePool/someImage',
+          image_id_spec: 'somePool/12345',
           snapshot_name: 'someSnapShot',
           dest_pool_name: 'someDestinationPool',
           dest_image_name: 'someDestinationImage',
           child_pool_name: 'someChildPool',
           child_image_name: 'someChildImage',
-          new_image_name: 'newImage',
-          image_id: '12345'
+          new_image_name: 'someImage2'
         };
         defaultMsg = `RBD '${metadata.pool_name}/${metadata.image_name}'`;
         childMsg = `RBD '${metadata.child_pool_name}/${metadata.child_image_name}'`;
@@ -207,7 +212,7 @@ describe('TaskManagerMessageService', () => {
         finishedTask.name = 'rbd/trash/move';
         testMessages(
           new TaskMessageOperation('Moving', 'move', 'Moved'),
-          `image '${metadata.pool_name}/${metadata.image_name}' to trash`
+          `image '${metadata.image_spec}' to trash`
         );
         testErrorCode(2, `Could not find image.`);
       });
@@ -216,18 +221,14 @@ describe('TaskManagerMessageService', () => {
         finishedTask.name = 'rbd/trash/restore';
         testMessages(
           new TaskMessageOperation('Restoring', 'restore', 'Restored'),
-          `image '${metadata.pool_name}@${metadata.image_id}' ` +
-            `into '${metadata.pool_name}/${metadata.new_image_name}'`
-        );
-        testErrorCode(
-          17,
-          `Image name '${metadata.pool_name}/${metadata.new_image_name}' is already in use.`
+          `image '${metadata.image_id_spec}' ` + `into '${metadata.new_image_name}'`
         );
+        testErrorCode(17, `Image name '${metadata.new_image_name}' is already in use.`);
       });
 
       it('tests rbd/trash/remove messages', () => {
         finishedTask.name = 'rbd/trash/remove';
-        testDelete(`image '${metadata.pool_name}/${metadata.image_name}@${metadata.image_id}'`);
+        testDelete(`image '${metadata.image_id_spec}'`);
       });
 
       it('tests rbd/trash/purge messages', () => {
index e8e5b6d4e3a8b7e57e6ff260df6e5e6a6acb4adf..9aa7073496f3ee0fbfa09a188fd1fc213a8a0339 100644 (file)
@@ -2,6 +2,7 @@ import { Injectable } from '@angular/core';
 
 import { I18n } from '@ngx-translate/i18n-polyfill';
 
+import { RbdService } from '../api/rbd.service';
 import { Components } from '../enum/components.enum';
 import { FinishedTask } from '../models/finished-task';
 import { Task } from '../models/task';
@@ -57,7 +58,7 @@ class TaskMessage {
   providedIn: 'root'
 })
 export class TaskMessageService {
-  constructor(private i18n: I18n) {}
+  constructor(private i18n: I18n, private rbdService: RbdService) {}
 
   defaultMessage = this.newTaskMessage(
     new TaskMessageOperation(this.i18n('Executing'), this.i18n('execute'), this.i18n('Executed')),
@@ -99,19 +100,41 @@ export class TaskMessageService {
   rbd = {
     default: (metadata) =>
       this.i18n(`RBD '{{id}}'`, {
-        id: `${metadata.pool_name}/${metadata.image_name}`
-      }),
-    child: (metadata) =>
-      this.i18n(`RBD '{{id}}'`, {
-        id: `${metadata.child_pool_name}/${metadata.child_image_name}`
-      }),
-    destination: (metadata) =>
-      this.i18n(`RBD '{{id}}'`, {
-        id: `${metadata.dest_pool_name}/${metadata.dest_image_name}`
+        id: `${metadata.image_spec}`
       }),
+    create: (metadata) => {
+      const id = this.rbdService.getImageSpec(
+        metadata.pool_name,
+        metadata.namespace,
+        metadata.image_name
+      );
+      return this.i18n(`RBD '{{id}}'`, {
+        id: id
+      });
+    },
+    child: (metadata) => {
+      const id = this.rbdService.getImageSpec(
+        metadata.child_pool_name,
+        metadata.child_namespace,
+        metadata.child_image_name
+      );
+      return this.i18n(`RBD '{{id}}'`, {
+        id: id
+      });
+    },
+    destination: (metadata) => {
+      const id = this.rbdService.getImageSpec(
+        metadata.dest_pool_name,
+        metadata.dest_namespace,
+        metadata.dest_image_name
+      );
+      return this.i18n(`RBD '{{id}}'`, {
+        id: id
+      });
+    },
     snapshot: (metadata) =>
       this.i18n(`RBD snapshot '{{id}}'`, {
-        id: `${metadata.pool_name}/${metadata.image_name}@${metadata.snapshot_name}`
+        id: `${metadata.image_spec}@${metadata.snapshot_name}`
       })
   };
 
@@ -174,10 +197,10 @@ export class TaskMessageService {
     // RBD tasks
     'rbd/create': this.newTaskMessage(
       this.commonOperations.create,
-      this.rbd.default,
+      this.rbd.create,
       (metadata) => ({
         '17': this.i18n('Name is already used by {{rbd_name}}.', {
-          rbd_name: this.rbd.default(metadata)
+          rbd_name: this.rbd.create(metadata)
         })
       })
     ),
@@ -265,7 +288,7 @@ export class TaskMessageService {
       new TaskMessageOperation(this.i18n('Moving'), this.i18n('move'), this.i18n('Moved')),
       (metadata) =>
         this.i18n(`image '{{id}}' to trash`, {
-          id: `${metadata.pool_name}/${metadata.image_name}`
+          id: metadata.image_spec
         }),
       () => ({
         2: this.i18n('Could not find image.')
@@ -275,12 +298,12 @@ export class TaskMessageService {
       new TaskMessageOperation(this.i18n('Restoring'), this.i18n('restore'), this.i18n('Restored')),
       (metadata) =>
         this.i18n(`image '{{id}}' into '{{new_id}}'`, {
-          id: `${metadata.pool_name}@${metadata.image_id}`,
-          new_id: `${metadata.pool_name}/${metadata.new_image_name}`
+          id: metadata.image_id_spec,
+          new_id: metadata.new_image_name
         }),
       (metadata) => ({
         17: this.i18n(`Image name '{{id}}' is already in use.`, {
-          id: `${metadata.pool_name}/${metadata.new_image_name}`
+          id: metadata.new_image_name
         })
       })
     ),
@@ -288,7 +311,7 @@ export class TaskMessageService {
       new TaskMessageOperation(this.i18n('Deleting'), this.i18n('delete'), this.i18n('Deleted')),
       (metadata) =>
         this.i18n(`image '{{id}}'`, {
-          id: `${metadata.pool_name}/${metadata.image_name}@${metadata.image_id}`
+          id: `${metadata.image_id_spec}`
         })
     ),
     'rbd/trash/purge': this.newTaskMessage(
index 95a335828d412632b00c17164e181d77a207f1e2..65094d5a95b4f267f121d90b44d8b5cb5c209cd2 100644 (file)
@@ -12,6 +12,7 @@ from __future__ import absolute_import
 from datetime import datetime
 
 from .. import mgr, logger
+from . import rbd
 
 
 def _progress_event_to_dashboard_task_common(event, task):
@@ -25,9 +26,18 @@ def _progress_event_to_dashboard_task_common(event, task):
                 'trash remove': "trash/remove"
             }
             action = action_map.get(refs['action'], refs['action'])
+            metadata = {}
+            if 'image_name' in refs:
+                metadata['image_spec'] = rbd.get_image_spec(refs['pool_name'],
+                                                            refs['pool_namespace'],
+                                                            refs['image_name'])
+            else:
+                metadata['image_id_spec'] = rbd.get_image_spec(refs['pool_name'],
+                                                               refs['pool_namespace'],
+                                                               refs['image_id'])
             task.update({
                 'name': "rbd/{}".format(action),
-                'metadata': refs,
+                'metadata': metadata,
                 'begin_time': "{}Z".format(datetime.fromtimestamp(event["started_at"])
                                            .isoformat()),
             })
index 65df6e63b748cde96bbb3cf5a953adf8be1031c8..1f708eef3ecd75de66683a1cbd6a297574adc7d6 100644 (file)
@@ -6,6 +6,8 @@ import six
 import rbd
 
 from .. import mgr
+from ..tools import ViewCache
+from .ceph_service import CephService
 
 
 RBD_FEATURES_NAME_MAPPING = {
@@ -60,13 +62,29 @@ def format_features(features):
     return res
 
 
+def get_image_spec(pool_name, namespace, rbd_name):
+    namespace = '{}/'.format(namespace) if namespace else ''
+    return '{}/{}{}'.format(pool_name, namespace, rbd_name)
+
+
+def parse_image_spec(image_spec):
+    namespace_spec, image_name = image_spec.rsplit('/', 1)
+    if '/' in namespace_spec:
+        pool_name, namespace = namespace_spec.rsplit('/', 1)
+    else:
+        pool_name, namespace = namespace_spec, None
+    return pool_name, namespace, image_name
+
+
 class RbdConfiguration(object):
     _rbd = rbd.RBD()
 
-    def __init__(self, pool_name='', image_name='', pool_ioctx=None, image_ioctx=None):
+    def __init__(self, pool_name='', namespace='', image_name='', pool_ioctx=None,
+                 image_ioctx=None):
         # type: (str, str, object, object) -> None
         assert bool(pool_name) != bool(pool_ioctx)  # xor
         self._pool_name = pool_name
+        self._namespace = namespace if namespace is not None else ''
         self._image_name = image_name
         self._pool_ioctx = pool_ioctx
         self._image_ioctx = image_ioctx
@@ -88,6 +106,7 @@ class RbdConfiguration(object):
 
         if self._pool_name:
             ioctx = mgr.rados.open_ioctx(self._pool_name)
+            ioctx.set_namespace(self._namespace)
         else:
             ioctx = self._pool_ioctx
 
@@ -97,6 +116,7 @@ class RbdConfiguration(object):
         # type: (str) -> str
         option_name = self._ensure_prefix(option_name)
         with mgr.rados.open_ioctx(self._pool_name) as pool_ioctx:
+            pool_ioctx.set_namespace(self._namespace)
             if self._image_name:
                 with rbd.Image(pool_ioctx, self._image_name) as image:
                     return image.metadata_get(option_name)
@@ -112,6 +132,7 @@ class RbdConfiguration(object):
         if self._pool_name:  # open ioctx
             pool_ioctx = mgr.rados.open_ioctx(self._pool_name)
             pool_ioctx.__enter__()
+            pool_ioctx.set_namespace(self._namespace)
 
         image_ioctx = self._image_ioctx
         if self._image_name:
@@ -147,6 +168,7 @@ class RbdConfiguration(object):
 
         if self._pool_name:
             with mgr.rados.open_ioctx(self._pool_name) as pool_ioctx:
+                pool_ioctx.set_namespace(self._namespace)
                 _remove(pool_ioctx)
         else:
             _remove(self._pool_ioctx)
@@ -158,3 +180,141 @@ class RbdConfiguration(object):
                     self.set(option_name, option_value)
                 else:
                     self.remove(option_name)
+
+
+class RbdService(object):
+
+    @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, namespace, 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
+            stat['namespace'] = namespace
+            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:
+                stat['parent'] = img.get_parent_image_spec()
+            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
+    def _rbd_image_names(cls, ioctx):
+        rbd_inst = rbd.RBD()
+        return rbd_inst.list(ioctx)
+
+    @classmethod
+    def _rbd_image_stat(cls, ioctx, pool_name, namespace, image_name):
+        return cls.rbd_image(ioctx, pool_name, namespace, image_name)
+
+    @classmethod
+    @ViewCache()
+    def rbd_pool_list(cls, pool_name, namespace=None):
+        rbd_inst = rbd.RBD()
+        with mgr.rados.open_ioctx(pool_name) as ioctx:
+            result = []
+            if namespace:
+                namespaces = [namespace]
+            else:
+                namespaces = rbd_inst.namespace_list(ioctx)
+                # images without namespace
+                namespaces.append('')
+            for current_namespace in namespaces:
+                ioctx.set_namespace(current_namespace)
+                names = cls._rbd_image_names(ioctx)
+                for name in names:
+                    try:
+                        stat = cls._rbd_image_stat(ioctx, pool_name, current_namespace, name)
+                    except rbd.ImageNotFound:
+                        # may have been removed in the meanwhile
+                        continue
+                    result.append(stat)
+            return result
diff --git a/src/pybind/mgr/dashboard/tests/test_rbd_service.py b/src/pybind/mgr/dashboard/tests/test_rbd_service.py
new file mode 100644 (file)
index 0000000..d1a02ca
--- /dev/null
@@ -0,0 +1,18 @@
+# -*- coding: utf-8 -*-
+# pylint: disable=dangerous-default-value,too-many-public-methods
+from __future__ import absolute_import
+
+import unittest
+
+from ..services.rbd import get_image_spec, parse_image_spec
+
+
+class RbdServiceTest(unittest.TestCase):
+
+    def test_compose_image_spec(self):
+        self.assertEqual(get_image_spec('mypool', 'myns', 'myimage'), 'mypool/myns/myimage')
+        self.assertEqual(get_image_spec('mypool', None, 'myimage'), 'mypool/myimage')
+
+    def test_parse_image_spec(self):
+        self.assertEqual(parse_image_spec('mypool/myns/myimage'), ('mypool', 'myns', 'myimage'))
+        self.assertEqual(parse_image_spec('mypool/myimage'), ('mypool', None, 'myimage'))