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
@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):
'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),
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,
'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'
'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:
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,
'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,
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):
'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)
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):
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)
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)
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)
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)
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)
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)
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')
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
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):
@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"}
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]
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
@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)
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
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):
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:
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
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):
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
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)
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');
});
});
const base_url = '/#/block/rbd/edit/';
const editURL = base_url
.concat(pool)
- .concat('/')
+ .concat('%2F')
.concat(name);
await browser.get(editURL);
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';
],
declarations: [AppComponent],
schemas: [NO_ERRORS_SCHEMA],
- providers: [AuthStorageService, i18nProviders]
+ providers: [AuthStorageService, i18nProviders, RbdService]
});
beforeEach(() => {
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';
@NgModule({
entryComponents: [
RbdDetailsComponent,
+ RbdNamespaceFormComponent,
RbdSnapshotFormComponent,
RbdTrashMoveModalComponent,
RbdTrashRestoreModalComponent,
IscsiTargetListComponent,
RbdDetailsComponent,
RbdFormComponent,
+ RbdNamespaceFormComponent,
+ RbdNamespaceListComponent,
RbdSnapshotListComponent,
RbdSnapshotFormComponent,
RbdTrashListComponent,
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 }
}
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;
<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">
<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
export class RbdFormCloneRequestModel {
child_pool_name: string;
+ child_namespace: string;
child_image_name: string;
obj_size: number;
features: Array<string> = [];
export class RbdFormCopyRequestModel {
dest_pool_name: string;
+ dest_namespace: string;
dest_image_name: string;
snapshot_name: string;
obj_size: number;
</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">
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');
});
});
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,
localField?: RbdConfigurationSourceField
) => RbdConfigurationEntry[];
+ namespaces: Array<string> = [];
+ namespacesByPoolCache = {};
pools: Array<string> = null;
allPools: Array<string> = null;
dataPools: Array<string> = null;
resource: string;
private rbdImage = new AsyncSubject();
+ icons = Icons;
+
constructor(
private authStorageService: AuthStorageService,
private route: ActivatedRoute,
pool: new FormControl(null, {
validators: [Validators.required]
}),
+ namespace: new FormControl(null),
useDataPool: new FormControl(false),
dataPool: new FormControl(null),
size: new FormControl(null, {
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();
} 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
});
}
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')
}
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() {
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;
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);
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'));
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)
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) => {
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()
+ )
});
}
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
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) => {
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
+ )
});
}
export class RbdFormModel {
name: string;
pool_name: string;
+ namespace: string;
data_pool: string;
size: number;
export class RbdParentModel {
image_name: string;
pool_name: string;
+ pool_namespace: string;
snap_name: string;
}
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>
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';
RbdDetailsComponent,
RbdImagesComponent,
RbdListComponent,
+ RbdNamespaceListComponent,
RbdSnapshotListComponent,
RbdTrashListComponent,
RbdConfigurationListComponent
<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>
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);
};
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;
}
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',
prop: 'pool_name',
flexGrow: 2
},
+ {
+ name: this.i18n('Namespace'),
+ prop: 'namespace',
+ flexGrow: 2
+ },
{
name: this.i18n('Size'),
prop: 'size',
}
];
+ 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
);
}
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)
})
}
});
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();
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);
}
};
id: string;
name: string;
pool_name: string;
+ namespace: string;
cdExecuting: string;
}
--- /dev/null
+<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">×</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>
--- /dev/null
+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();
+ });
+});
--- /dev/null
+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 });
+ });
+ }
+}
--- /dev/null
+<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>
--- /dev/null
+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();
+ });
+});
--- /dev/null
+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');
+ }
+ }
+ }
+}
})
export class RbdSnapshotFormComponent implements OnInit {
poolName: string;
+ namespace: string;
imageName: string;
snapName: string;
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(
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(
<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>
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);
@Input()
poolName: string;
@Input()
+ namespace: string;
+ @Input()
rbdName: string;
@ViewChild('nameTpl', { static: false })
nameTpl: TemplateRef<any>;
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()}`;
['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']
);
};
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 {
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();
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();
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);
const addImage = (id) => {
images.push({
- id: id
+ id: id,
+ pool_name: 'pl'
});
};
const task = new ExecutingTask();
task.name = name;
task.metadata = {
- image_id: image_id
+ image_id_spec: `pl/${image_id}`
};
summaryService.addRunningTask(task);
};
prop: 'pool_name',
flexGrow: 1
},
+ {
+ name: this.i18n('Namespace'),
+ prop: 'namespace',
+ flexGrow: 1
+ },
{
name: this.i18n('Status'),
prop: 'deferment_end_time',
}
];
+ 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
);
}
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;
}
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
};
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)
})
}
});
[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">
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 });
});
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 });
});
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);
});
export class RbdTrashMoveModalComponent implements OnInit {
metaType: string;
poolName: string;
+ namespace: string;
imageName: string;
+ imageSpec: string;
executingTasks: ExecutingTask[];
moveForm: CdFormGroup;
}
ngOnInit() {
+ this.imageSpec = this.rbdService.getImageSpec(this.poolName, this.namespace, this.imageName);
this.pattern = `${this.poolName}/${this.imageName}`;
}
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();
<div class="modal-body">
<p>
<ng-container i18n>To restore</ng-container>
- <kbd>{{ poolName }}/{{ imageName }}@{{ imageId }}</kbd>,
+ <kbd>{{ imageSpec }}@{{ imageId }}</kbd>,
<ng-container i18n>type the image's new name and click</ng-container>
<kbd i18n>Restore Image</kbd>.
</p>
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();
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', () => {
export class RbdTrashRestoreModalComponent implements OnInit {
metaType: string;
poolName: string;
+ namespace: string;
imageName: string;
+ imageSpec: string;
imageId: string;
executingTasks: ExecutingTask[];
) {}
ngOnInit() {
+ this.imageSpec = this.rbdService.getImageSpec(this.poolName, this.namespace, this.imageName);
this.restoreForm = this.fb.group({
name: this.imageName
});
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,
});
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');
});
});
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');
});
});
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'
});
});
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'
});
});
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
});
});
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');
+ });
+ });
});
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';
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() {
);
}
- 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'
});
}
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' }
);
});
}
- 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' }
);
}
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';
PrometheusService,
SettingsService,
SummaryService,
- NotificationService
+ NotificationService,
+ RbdService
]
});
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] });
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';
TaskMessageService,
{ provide: ToastrService, useValue: toastFakeService },
{ provide: CdDatePipe, useValue: { transform: (d) => d } },
- i18nProviders
- ]
+ i18nProviders,
+ RbdService
+ ],
+ imports: [HttpClientTestingModule]
});
beforeEach(() => {
import { ToastrModule } from 'ngx-toastr';
+import { HttpClientTestingModule } from '@angular/common/http/testing';
import {
configureTestBed,
i18nProviders,
let prometheus: PrometheusHelper;
configureTestBed({
- imports: [ToastrModule.forRoot(), SharedModule],
+ imports: [ToastrModule.forRoot(), SharedModule, HttpClientTestingModule],
providers: [PrometheusAlertFormatter, i18nProviders]
});
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';
};
configureTestBed({
- providers: [TaskListService, TaskMessageService, SummaryService, i18nProviders],
+ providers: [TaskListService, TaskMessageService, SummaryService, i18nProviders, RbdService],
imports: [HttpClientTestingModule, RouterTestingModule]
});
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';
let finishedTask: FinishedTask;
configureTestBed({
- providers: [TaskMessageService, i18nProviders]
+ providers: [TaskMessageService, i18nProviders, RbdService],
+ imports: [HttpClientTestingModule]
});
beforeEach(() => {
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}'`;
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.`);
});
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', () => {
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';
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')),
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}`
})
};
// 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)
})
})
),
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.')
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
})
})
),
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(
from datetime import datetime
from .. import mgr, logger
+from . import rbd
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()),
})
import rbd
from .. import mgr
+from ..tools import ViewCache
+from .ceph_service import CephService
RBD_FEATURES_NAME_MAPPING = {
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
if self._pool_name:
ioctx = mgr.rados.open_ioctx(self._pool_name)
+ ioctx.set_namespace(self._namespace)
else:
ioctx = self._pool_ioctx
# 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)
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:
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)
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
--- /dev/null
+# -*- 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'))