In order to clone VMs, we will need to be able to copy storage volumes.
Add a new method to create a storage volume. This method requires the
parameter 'volume_path' which specifies the base storage volume. The
new volume will be created with the same content as the original one.
Signed-off-by: Crístian Viana <vianac(a)linux.vnet.ibm.com>
---
docs/API.md | 1 +
src/kimchi/API.json | 5 +++
src/kimchi/i18n.py | 1 +
src/kimchi/mockmodel.py | 44 ++++++++++++++++++++++-
src/kimchi/model/storagevolumes.py | 73 +++++++++++++++++++++++++++++++++++---
tests/test_model.py | 19 ++++++++++
tests/test_rest.py | 23 ++++++++++++
7 files changed, 161 insertions(+), 5 deletions(-)
diff --git a/docs/API.md b/docs/API.md
index 9c06f85..29ae4e1 100644
--- a/docs/API.md
+++ b/docs/API.md
@@ -445,6 +445,7 @@ A interface represents available network interface on VM.
* format: The format of the defined Storage Volume
* file: File to be uploaded, passed through form data
* url: URL to be downloaded
+ * volume_path: The path of an existing storage volume to be copied.
### Resource: Storage Volume
diff --git a/src/kimchi/API.json b/src/kimchi/API.json
index 0ad36ab..81492f5 100644
--- a/src/kimchi/API.json
+++ b/src/kimchi/API.json
@@ -216,6 +216,11 @@
"type": "string",
"pattern": "^(http|ftp)[s]?://",
"error": "KCHVOL0021E"
+ },
+ "volume_path": {
+ "description": "The path of an existing storage volume
to be copied",
+ "type": "string",
+ "error": "KCHVOL0023E"
}
}
},
diff --git a/src/kimchi/i18n.py b/src/kimchi/i18n.py
index 10408bf..712e5a6 100644
--- a/src/kimchi/i18n.py
+++ b/src/kimchi/i18n.py
@@ -207,6 +207,7 @@ messages = {
"KCHVOL0020E": _("Storage volume capacity must be an integer
number."),
"KCHVOL0021E": _("Storage volume URL must be http://, https://, ftp://
or ftps://."),
"KCHVOL0022E": _("Unable to access file %(url)s. Please, check
it."),
+ "KCHVOL0023E": _("Storage volume path must be a string"),
"KCHIFACE0001E": _("Interface %(name)s does not exist"),
diff --git a/src/kimchi/mockmodel.py b/src/kimchi/mockmodel.py
index 7163f8d..baee0b6 100644
--- a/src/kimchi/mockmodel.py
+++ b/src/kimchi/mockmodel.py
@@ -485,7 +485,7 @@ class MockModel(object):
raise NotFoundError("KCHPOOL0002E", {'name': name})
def storagevolumes_create(self, pool_name, params):
- vol_source = ['file', 'url', 'capacity']
+ vol_source = ['file', 'url', 'capacity',
'volume_path']
require_name_params = ['capacity']
name = params.get('name')
@@ -506,6 +506,10 @@ class MockModel(object):
name = os.path.basename(params['file'].filename)
elif create_param == 'url':
name = os.path.basename(params['url'])
+ elif create_param == 'volume_path':
+ basename = os.path.basename(params['volume_path'])
+ split = os.path.splitext(basename)
+ name = u'%s-clone%s' % (split[0], split[1])
else:
name = 'upload-%s' % int(time.time())
params['name'] = name
@@ -588,6 +592,44 @@ class MockModel(object):
cb('OK', True)
+ def _create_volume_with_volume_path(self, cb, params):
+ try:
+ new_vol_name = params['name'].decode('utf-8')
+ new_pool_name = params['pool'].decode('utf-8')
+ orig_vol_path = params['volume_path'].decode('utf-8')
+
+ orig_pool = orig_vol = None
+ for pn, p in self._mock_storagepools.items():
+ for vn, v in p._volumes.items():
+ if v.info['path'] == orig_vol_path:
+ orig_pool = self._get_storagepool(pn)
+ orig_vol = self._get_storagevolume(pn, vn)
+ break
+
+ if orig_vol is not None:
+ break
+
+ if orig_vol is None:
+ raise OperationFailed('KCHVOL0007E',
+ {'name': new_vol_name,
+ 'pool': new_pool_name,
+ 'err': 'could not find volume
\'%s\'' %
+ orig_vol_path})
+
+ new_vol = copy.deepcopy(orig_vol)
+ new_vol.info['name'] = new_vol_name
+ new_vol.info['path'] = os.path.join(orig_pool.info['path'],
+ new_vol_name)
+
+ new_pool = self._get_storagepool(new_pool_name)
+ new_pool._volumes[new_vol_name] = new_vol
+ except (KeyError, NotFoundError), e:
+ raise OperationFailed('KCHVOL0007E',
+ {'name': new_vol_name, 'pool':
new_pool_name,
+ 'err': e.message})
+
+ cb('OK', True)
+
def storagevolume_lookup(self, pool, name):
if self._get_storagepool(pool).info['state'] != 'active':
raise InvalidOperation("KCHVOL0005E", {'pool': pool,
diff --git a/src/kimchi/model/storagevolumes.py b/src/kimchi/model/storagevolumes.py
index 9ff43e6..d7325e4 100644
--- a/src/kimchi/model/storagevolumes.py
+++ b/src/kimchi/model/storagevolumes.py
@@ -18,9 +18,11 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
import contextlib
+import lxml.etree as ET
import os
import time
import urllib2
+from lxml.builder import E
import libvirt
@@ -53,9 +55,10 @@ class StorageVolumesModel(object):
self.conn = kargs['conn']
self.objstore = kargs['objstore']
self.task = TaskModel(**kargs)
+ self.storagevolume = StorageVolumeModel(**kargs)
def create(self, pool_name, params):
- vol_source = ['file', 'url', 'capacity']
+ vol_source = ['file', 'url', 'capacity',
'volume_path']
name = params.get('name')
@@ -81,13 +84,17 @@ class StorageVolumesModel(object):
if create_param in REQUIRE_NAME_PARAMS:
raise InvalidParameter('KCHVOL0016E')
- # if 'name' is omitted - except for the methods listed in
- # 'REQUIRE_NAME_PARAMS' - the default volume name will be the
- # file/URL basename.
+ # if 'name' is omitted,the default volume name for 'file' and
'url'
+ # will be the file/URL basename, and for 'volume' it will be
+ # '<volume-name>-clone.<volume-extension>'.
if create_param == 'file':
name = os.path.basename(params['file'].filename)
elif create_param == 'url':
name = os.path.basename(params['url'])
+ elif create_param == 'volume_path':
+ basename = os.path.basename(params['volume_path'])
+ split = os.path.splitext(basename)
+ name = u'%s-clone%s' % (split[0], split[1])
else:
name = 'upload-%s' % int(time.time())
params['name'] = name
@@ -221,6 +228,64 @@ class StorageVolumesModel(object):
StoragePoolModel.get_storagepool(pool_name, self.conn).refresh(0)
cb('OK', True)
+ def _create_volume_with_volume_path(self, cb, params):
+ """Create a storage volume based on an existing volume.
+
+ The existing volume is referenced by its path. This function copies all
+ the data inside the original volume into the new one.
+
+ Arguments:
+ cb -- A callback function to signal the Task's progress.
+ params -- A dict with the following values:
+ "pool": the name of the new pool.
+ "name": the name of the new volume.
+ "volume_path": the file path of the original storage volume.
+ """
+ try:
+ new_vol_name = params['name'].decode('utf-8')
+ new_pool_name = params['pool'].decode('utf-8')
+ orig_vol_path = params['volume_path'].decode('utf-8')
+ except KeyError, e:
+ raise MissingParameter('KCHVOL0004E',
+ {'item': str(e), 'volume':
new_vol_name})
+
+ try:
+ cb('setting up volume creation')
+ vir_conn = self.conn.get()
+ orig_vir_vol = vir_conn.storageVolLookupByPath(orig_vol_path)
+ orig_vol_name = orig_vir_vol.name().decode('utf-8')
+ orig_vir_pool = orig_vir_vol.storagePoolLookupByVolume()
+ orig_pool_name = orig_vir_pool.name().decode('utf-8')
+ orig_vol = self.storagevolume.lookup(orig_pool_name, orig_vol_name)
+
+ new_vir_pool = StoragePoolModel.get_storagepool(new_pool_name,
+ self.conn)
+
+ cb('building volume XML')
+ root_elem = E.volume()
+ root_elem.append(E.name(new_vol_name))
+ root_elem.append(E.capacity(unicode(orig_vol['capacity']),
+ unit='bytes'))
+ target_elem = E.target()
+ target_elem.append(E.format(type=orig_vol['format']))
+ root_elem.append(target_elem)
+ new_vol_xml = ET.tostring(root_elem, encoding='utf-8',
+ pretty_print=True)
+
+ cb('creating volume')
+ new_vir_pool.createXMLFrom(new_vol_xml, orig_vir_vol, 0)
+ except (InvalidOperation, NotFoundError, libvirt.libvirtError), e:
+ raise OperationFailed("KCHVOL0007E",
+ {'name': new_vol_name, 'pool':
new_pool_name,
+ 'err': e.get_error_message()})
+
+ cb('adding volume to the object store')
+ new_vol_id = '%s:%s' % (new_pool_name, new_vol_name)
+ with self.objstore as session:
+ session.store('storagevolume', new_vol_id, {'ref_cnt': 0})
+
+ cb('OK', True)
+
def get_list(self, pool_name):
pool = StoragePoolModel.get_storagepool(pool_name, self.conn)
if not pool.isActive():
diff --git a/tests/test_model.py b/tests/test_model.py
index 21e1b6b..b165731 100644
--- a/tests/test_model.py
+++ b/tests/test_model.py
@@ -623,6 +623,25 @@ class ModelTests(unittest.TestCase):
cp_content = cp_file.read()
self.assertEquals(vol_content, cp_content)
+ # clone the volume created above
+ params = {'volume_path': vol_path}
+ task = inst.storagevolumes_create(pool, params)
+ taskid = task['id']
+ cloned_vol_name = task['target_uri'].split('/')[-1]
+ inst.task_wait(taskid)
+ self.assertEquals('finished',
inst.task_lookup(taskid)['status'])
+ rollback.prependDefer(inst.storagevolume_delete, pool,
+ cloned_vol_name)
+
+ orig_vol = inst.storagevolume_lookup(pool, vol_name)
+ cloned_vol = inst.storagevolume_lookup(pool, cloned_vol_name)
+
+ self.assertNotEquals(orig_vol['path'], cloned_vol['path'])
+ del orig_vol['path']
+ del cloned_vol['path']
+
+ self.assertEquals(orig_vol, cloned_vol)
+
@unittest.skipUnless(utils.running_as_root(), 'Must be run as root')
def test_template_storage_customise(self):
inst = model.Model(objstore_loc=self.tmp_store)
diff --git a/tests/test_rest.py b/tests/test_rest.py
index 9bc930f..66072bc 100644
--- a/tests/test_rest.py
+++ b/tests/test_rest.py
@@ -1047,6 +1047,29 @@ class RestTests(unittest.TestCase):
resp = self.request('/storagepools/pool-1/storagevolumes/%s' %
vol_name, '{}', 'GET')
self.assertEquals(200, resp.status)
+ vol = json.loads(resp.read())
+
+ # clone the volume created above
+ req = json.dumps({'volume_path': vol['path']})
+ resp = self.request('/storagepools/pool-1/storagevolumes', req,
'POST')
+ self.assertEquals(202, resp.status)
+ task = json.loads(resp.read())
+ cloned_vol_name = task['target_uri'].split('/')[-1]
+ wait_task(self._task_lookup, task['id'])
+ task = json.loads(self.request('/tasks/%s' % task['id']).read())
+ self.assertEquals('finished', task['status'])
+ resp = self.request('/storagepools/pool-1/storagevolumes/%s' %
+ cloned_vol_name, '{}', 'GET')
+ self.assertEquals(200, resp.status)
+ cloned_vol = json.loads(resp.read())
+
+ self.assertNotEquals(vol['name'], cloned_vol['name'])
+ del vol['name']
+ del cloned_vol['name']
+ self.assertNotEquals(vol['path'], cloned_vol['path'])
+ del vol['path']
+ del cloned_vol['path']
+ self.assertEquals(vol, cloned_vol)
# Now remove the StoragePool from mock model
self._delete_pool('pool-1')
--
1.9.3