[Kimchi-devel] [PATCH 6/8] Create storage volume based on an existing volume

Crístian Viana vianac at linux.vnet.ibm.com
Mon Nov 3 01:05:27 UTC 2014


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 at 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




More information about the Kimchi-devel mailing list