[Kimchi-devel] [PATCH v3 1/4] storagevolume: Download remote images to a storage pool

Crístian Viana vianac at linux.vnet.ibm.com
Thu Sep 4 20:54:21 UTC 2014


In order to simplify the creation of a remote image in Kimchi, the user
will be able to provide a remote URL and the Kimchi server will download
it and put it on a storage pool.

Download a remote image to a storage pool

Signed-off-by: Crístian Viana <vianac at linux.vnet.ibm.com>
---
 docs/API.md                        |  1 +
 src/kimchi/API.json                |  6 ++++++
 src/kimchi/i18n.py                 |  1 +
 src/kimchi/mockmodel.py            | 20 ++++++++++++++++++++
 src/kimchi/model/storagevolumes.py | 32 ++++++++++++++++++++++++++++++++
 tests/test_model.py                | 24 ++++++++++++++++++++++++
 tests/test_rest.py                 | 12 ++++++++++++
 7 files changed, 96 insertions(+)

diff --git a/docs/API.md b/docs/API.md
index 0c4a641..9b1bff3 100644
--- a/docs/API.md
+++ b/docs/API.md
@@ -418,6 +418,7 @@ A interface represents available network interface on VM.
     * capacity: The total space which can be used to store volumes
                 The unit is MBytes
     * format: The format of the defined Storage Volume
+    * url: URL to be downloaded
 
 ### Resource: Storage Volume
 
diff --git a/src/kimchi/API.json b/src/kimchi/API.json
index 2e2f9b0..8a95804 100644
--- a/src/kimchi/API.json
+++ b/src/kimchi/API.json
@@ -182,6 +182,12 @@
                     "type": "string",
                     "pattern": "^qcow2|raw$",
                     "error": "KCHVOL0015E"
+                },
+                "url": {
+                    "description": "The remote URL of the storage volume",
+                    "type": "string",
+                    "pattern": "^(http|ftp)[s]?://",
+                    "error": "KCHVOL0021E"
                 }
             }
         },
diff --git a/src/kimchi/i18n.py b/src/kimchi/i18n.py
index 1e8b47a..bbe4b02 100644
--- a/src/kimchi/i18n.py
+++ b/src/kimchi/i18n.py
@@ -188,6 +188,7 @@ messages = {
     "KCHVOL0018E": _("Only one of %(param)s can be specified"),
     "KCHVOL0019E": _("Creating volume from %(param)s is not supported"),
     "KCHVOL0020E": _("Storage volume capacity must be an integer number."),
+    "KCHVOL0021E": _("Storage volume URL must be http://, https://, ftp:// or ftps://."),
 
     "KCHIFACE0001E": _("Interface %(name)s does not exist"),
 
diff --git a/src/kimchi/mockmodel.py b/src/kimchi/mockmodel.py
index b94c3fe..5241696 100644
--- a/src/kimchi/mockmodel.py
+++ b/src/kimchi/mockmodel.py
@@ -524,6 +524,26 @@ class MockModel(object):
         pool._volumes[name] = volume
         cb('OK', True)
 
+    def _create_volume_with_url(self, cb, params):
+        pool_name = params['pool']
+        name = params['name']
+        url = params['url']
+
+        pool = self._get_storagepool(pool_name)
+
+        file_path = os.path.join(pool.info['path'], name)
+
+        with open(file_path, 'w') as file:
+            file.write(url)
+
+        params['path'] = file_path
+        params['type'] = 'file'
+
+        volume = MockStorageVolume(pool, name, params)
+        pool._volumes[name] = volume
+
+        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 6b001f7..3062e78 100644
--- a/src/kimchi/model/storagevolumes.py
+++ b/src/kimchi/model/storagevolumes.py
@@ -17,7 +17,9 @@
 # License along with this library; if not, write to the Free Software
 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301 USA
 
+import contextlib
 import os
+import urllib2
 
 import libvirt
 
@@ -39,6 +41,9 @@ VOLUME_TYPE_MAP = {0: 'file',
                    3: 'network'}
 
 
+DOWNLOAD_CHUNK_SIZE = 1048576  # 1 MiB
+
+
 class StorageVolumesModel(object):
     def __init__(self, **kargs):
         self.conn = kargs['conn']
@@ -112,6 +117,33 @@ class StorageVolumesModel(object):
 
         cb('', True)
 
+    def _create_volume_with_url(self, cb, params):
+        pool_name = params['pool']
+        name = params['name']
+        url = params['url']
+
+        pool_model = StoragePoolModel(conn=self.conn,
+                                      objstore=self.objstore)
+        pool = pool_model.lookup(pool_name)
+        file_path = os.path.join(pool['path'], name)
+
+        with contextlib.closing(urllib2.urlopen(url)) as response,\
+                open(file_path, 'w') as volume_file:
+            try:
+                while True:
+                    chunk_data = response.read(DOWNLOAD_CHUNK_SIZE)
+                    if not chunk_data:
+                        break
+
+                    volume_file.write(chunk_data)
+            except Exception, e:
+                raise OperationFailed('KCHVOL0007E', {'name': name,
+                                                      'pool': pool_name,
+                                                      'err': e.message})
+
+        StoragePoolModel.get_storagepool(pool_name, self.conn).refresh()
+        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 5ee824d..4e9ba97 100644
--- a/tests/test_model.py
+++ b/tests/test_model.py
@@ -36,6 +36,7 @@ import iso_gen
 import kimchi.objectstore
 import utils
 from kimchi import netinfo
+from kimchi.config import paths
 from kimchi.exception import InvalidOperation, InvalidParameter
 from kimchi.exception import NotFoundError, OperationFailed
 from kimchi.iscsi import TargetClient
@@ -557,6 +558,29 @@ class ModelTests(unittest.TestCase):
             poolinfo = inst.storagepool_lookup(pool)
             self.assertEquals(len(vols), poolinfo['nr_volumes'])
 
+            # download remote volume
+            # 1) try an invalid URL
+            params = {'name': 'foo', 'url': 'http://www.invalid.url'}
+            taskid = inst.storagevolumes_create(pool, params)['id']
+            self._wait_task(inst, taskid)
+            self.assertEquals('failed', inst.task_lookup(taskid)['status'])
+            # 2) download Kimchi's "COPYING" from Github and compare its
+            #    content to the corresponding local file's
+            url = 'https://github.com/kimchi-project/kimchi/raw/master/COPYING'
+            params = {'name': 'copying', 'url': url}
+            taskid = inst.storagevolumes_create(pool, params)['id']
+            self._wait_task(inst, taskid)
+            self.assertEquals('finished', inst.task_lookup(taskid)['status'])
+            rollback.prependDefer(inst.storagevolume_delete, pool,
+                                  params['name'])
+            vol_path = os.path.join(args['path'], params['name'])
+            self.assertTrue(os.path.isfile(vol_path))
+            with open(vol_path) as vol_file:
+                vol_content = vol_file.read()
+            with open(os.path.join(paths.get_prefix(), 'COPYING')) as cp_file:
+                cp_content = cp_file.read()
+            self.assertEquals(vol_content, cp_content)
+
     @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 7b8dfc2..0df435d 100644
--- a/tests/test_rest.py
+++ b/tests/test_rest.py
@@ -1026,6 +1026,18 @@ class RestTests(unittest.TestCase):
         self.assertEquals('/var/lib/libvirt/images/volume-1',
                           storagevolume['path'])
 
+        req = json.dumps({'name': 'downloaded',
+                          'url': 'https://anyurl.wor.kz'})
+        resp = self.request('/storagepools/pool-1/storagevolumes', req, 'POST')
+        self.assertEquals(202, resp.status)
+        task = json.loads(resp.read())
+        self._wait_task(task['id'])
+        task = json.loads(self.request('/tasks/%s' % task['id']).read())
+        self.assertEquals('finished', task['status'])
+        resp = self.request('/storagepools/pool-1/storagevolumes/downloaded',
+                            '{}', 'GET')
+        self.assertEquals(200, resp.status)
+
         # Now remove the StoragePool from mock model
         self._delete_pool('pool-1')
 
-- 
1.9.3




More information about the Kimchi-devel mailing list