]> review.fuel-infra Code Review - openstack-build/cinder-build.git/commitdiff
XenAPINFS: Copy image from glance
authorMate Lakat <mate.lakat@citrix.com>
Fri, 25 Jan 2013 14:44:08 +0000 (14:44 +0000)
committerMate Lakat <mate.lakat@citrix.com>
Tue, 5 Feb 2013 18:34:15 +0000 (18:34 +0000)
related to blueprint xenapinfs-glance-integration

This change is using the glance xenapi plugin to copy a glance image to
a xenapinfs backed volume. It slightly differs from the reference
implementation, as this implementation is composed of three steps:
- create volume
- overwrite the created volume with the one downloaded from glance
- resize the volume
As opposed to fill the bytes from glance to the created image. This
approach works more effective with XenServer type semi ovf tgz -ed vhds.

Change-Id: I2345f6bb355aa285eee0455380c580e4724f004e

cinder/exception.py
cinder/tests/test_xenapi_sm.py
cinder/volume/drivers/xenapi/lib.py
cinder/volume/drivers/xenapi/sm.py
cinder/volume/manager.py

index daf003f946e363565030d4d365cc18e757f973b4..39ac7098e15bc5bee24598948be261fdaa125fc4 100644 (file)
@@ -525,3 +525,7 @@ class NfsNoSuitableShareFound(NotFound):
 class GlanceMetadataExists(Invalid):
     message = _("Glance metadata cannot be updated, key %(key)s"
                 " exists for volume id %(volume_id)s")
+
+
+class ImageCopyFailure(Invalid):
+    message = _("Failed to copy image to volume")
index bc81b76f9946cc3c7b4918e5cd03419dd00eab02..71c4fe0363ca10f71b2fefd3afdf3964b1427a8e 100644 (file)
 #    under the License.
 
 from cinder.db import api as db_api
+from cinder import exception
 from cinder.volume.drivers.xenapi import lib
 from cinder.volume.drivers.xenapi import sm as driver
 import mox
 import unittest
 
 
+class MockContext(object):
+    def __init__(ctxt, auth_token):
+        ctxt.auth_token = auth_token
+
+
 class DriverTestCase(unittest.TestCase):
 
     def assert_flag(self, flagname):
@@ -34,6 +40,7 @@ class DriverTestCase(unittest.TestCase):
         self.assert_flag('xenapi_connection_password')
         self.assert_flag('xenapi_nfs_server')
         self.assert_flag('xenapi_nfs_serverpath')
+        self.assert_flag('xenapi_sr_base_path')
 
     def test_do_setup(self):
         mock = mox.Mox()
@@ -184,7 +191,7 @@ class DriverTestCase(unittest.TestCase):
             result
         )
 
-    def _setup_for_snapshots(self, server, serverpath):
+    def _setup_mock_driver(self, server, serverpath, sr_base_path="_srbp"):
         mock = mox.Mox()
 
         drv = driver.XenAPINFSDriver()
@@ -196,11 +203,12 @@ class DriverTestCase(unittest.TestCase):
         mock.StubOutWithMock(driver, 'FLAGS')
         driver.FLAGS.xenapi_nfs_server = server
         driver.FLAGS.xenapi_nfs_serverpath = serverpath
+        driver.FLAGS.xenapi_sr_base_path = sr_base_path
 
         return mock, drv
 
     def test_create_snapshot(self):
-        mock, drv = self._setup_for_snapshots('server', 'serverpath')
+        mock, drv = self._setup_mock_driver('server', 'serverpath')
 
         snapshot = dict(
             volume_id="volume-id",
@@ -221,7 +229,7 @@ class DriverTestCase(unittest.TestCase):
             result)
 
     def test_create_volume_from_snapshot(self):
-        mock, drv = self._setup_for_snapshots('server', 'serverpath')
+        mock, drv = self._setup_mock_driver('server', 'serverpath')
 
         snapshot = dict(
             provider_location='src-sr-uuid/src-vdi-uuid')
@@ -241,7 +249,7 @@ class DriverTestCase(unittest.TestCase):
             dict(provider_location='copied-sr/copied-vdi'), result)
 
     def test_delete_snapshot(self):
-        mock, drv = self._setup_for_snapshots('server', 'serverpath')
+        mock, drv = self._setup_mock_driver('server', 'serverpath')
 
         snapshot = dict(
             provider_location='src-sr-uuid/src-vdi-uuid')
@@ -252,3 +260,52 @@ class DriverTestCase(unittest.TestCase):
         mock.ReplayAll()
         drv.delete_snapshot(snapshot)
         mock.VerifyAll()
+
+    def test_copy_image_to_volume_success(self):
+        mock, drv = self._setup_mock_driver(
+            'server', 'serverpath', '/var/run/sr-mount')
+
+        volume = dict(
+            provider_location='sr-uuid/vdi-uuid',
+            size=2)
+
+        mock.StubOutWithMock(driver.glance, 'get_api_servers')
+
+        driver.glance.get_api_servers().AndReturn((x for x in ['glancesrv']))
+
+        drv.nfs_ops.use_glance_plugin_to_overwrite_volume(
+            'server', 'serverpath', 'sr-uuid', 'vdi-uuid', 'glancesrv',
+            'image_id', 'token', '/var/run/sr-mount').AndReturn(True)
+
+        drv.nfs_ops.resize_volume(
+            'server', 'serverpath', 'sr-uuid', 'vdi-uuid', 2)
+
+        mock.ReplayAll()
+        drv.copy_image_to_volume(
+            MockContext('token'), volume, "ignore", "image_id")
+        mock.VerifyAll()
+
+    def test_copy_image_to_volume_fail(self):
+        mock, drv = self._setup_mock_driver(
+            'server', 'serverpath', '/var/run/sr-mount')
+
+        volume = dict(
+            provider_location='sr-uuid/vdi-uuid',
+            size=2)
+
+        mock.StubOutWithMock(driver.glance, 'get_api_servers')
+
+        driver.glance.get_api_servers().AndReturn((x for x in ['glancesrv']))
+
+        drv.nfs_ops.use_glance_plugin_to_overwrite_volume(
+            'server', 'serverpath', 'sr-uuid', 'vdi-uuid', 'glancesrv',
+            'image_id', 'token', '/var/run/sr-mount').AndReturn(False)
+
+        mock.ReplayAll()
+
+        self.assertRaises(
+            exception.ImageCopyFailure,
+            lambda: drv.copy_image_to_volume(
+                MockContext('token'), volume, "ignore", "image_id"))
+
+        mock.VerifyAll()
index dc17682e6cc38049670ad2c8354066023e9e9c9d..8620d33a628be42389f5b638f56fa105b69165b8 100644 (file)
@@ -17,6 +17,8 @@
 #    under the License.
 
 import contextlib
+import os
+import pickle
 
 
 class XenAPIException(Exception):
@@ -138,6 +140,9 @@ class VdiOperations(OperationsBase):
     def copy(self, vdi_ref, sr_ref):
         return self.call_xenapi('VDI.copy', vdi_ref, sr_ref)
 
+    def resize(self, vdi_ref, size):
+        return self.call_xenapi('VDI.resize', vdi_ref, str(size))
+
 
 class HostOperations(OperationsBase):
     def get_record(self, host_ref):
@@ -160,12 +165,22 @@ class XenAPISession(object):
     def close(self):
         return self.call_xenapi('logout')
 
-    def call_xenapi(self, method, *args):
+    @contextlib.contextmanager
+    def exception_converter(self):
         try:
-            return self._session.xenapi_request(method, args)
+            yield None
         except self._exception_to_convert as e:
             raise XenAPIException(e)
 
+    def call_xenapi(self, method, *args):
+        with self.exception_converter():
+            return self._session.xenapi_request(method, args)
+
+    def call_plugin(self, host_ref, plugin, function, args):
+        with self.exception_converter():
+            return self._session.xenapi.host.call_plugin(
+                host_ref, plugin, function, args)
+
     def get_pool(self):
         return self.call_xenapi('session.get_pool', self.handle)
 
@@ -292,9 +307,44 @@ class SessionFactory(object):
         return connect(self.url, self.user, self.password)
 
 
+class XapiPluginProxy(object):
+    def __init__(self, session_factory, plugin_name):
+        self._session_factory = session_factory
+        self._plugin_name = plugin_name
+
+    def call(self, function, *plugin_args, **plugin_kwargs):
+        plugin_params = dict(args=plugin_args, kwargs=plugin_kwargs)
+        args = dict(params=pickle.dumps(plugin_params))
+
+        with self._session_factory.get_session() as session:
+            host_ref = session.get_this_host()
+            result = session.call_plugin(
+                host_ref, self._plugin_name, function, args)
+
+        return pickle.loads(result)
+
+
+class GlancePluginProxy(XapiPluginProxy):
+    def __init__(self, session_factory):
+        super(GlancePluginProxy, self).__init__(session_factory, 'glance')
+
+    def download_vhd(self, image_id, glance_host, glance_port, glance_use_ssl,
+                     uuid_stack, sr_path, auth_token):
+        return self.call(
+            'download_vhd',
+            image_id=image_id,
+            glance_host=glance_host,
+            glance_port=glance_port,
+            glance_use_ssl=glance_use_ssl,
+            uuid_stack=uuid_stack,
+            sr_path=sr_path,
+            auth_token=auth_token)
+
+
 class NFSBasedVolumeOperations(object):
     def __init__(self, session_factory):
         self._session_factory = session_factory
+        self.glance_plugin = GlancePluginProxy(session_factory)
 
     def create_volume(self, server, serverpath, size,
                       name=None, description=None):
@@ -355,3 +405,35 @@ class NFSBasedVolumeOperations(object):
                 session.unplug_pbds_and_forget_sr(src_refs['sr_ref'])
 
             return dst_refs
+
+    def resize_volume(self, server, serverpath, sr_uuid, vdi_uuid,
+                      size_in_gigabytes):
+        self.connect_volume(server, serverpath, sr_uuid, vdi_uuid)
+
+        try:
+            with self._session_factory.get_session() as session:
+                vdi_ref = session.VDI.get_by_uuid(vdi_uuid)
+                session.VDI.resize(vdi_ref, to_bytes(size_in_gigabytes))
+        finally:
+            self.disconnect_volume(vdi_uuid)
+
+    def use_glance_plugin_to_overwrite_volume(self, server, serverpath,
+                                              sr_uuid, vdi_uuid, glance_server,
+                                              image_id, auth_token,
+                                              sr_base_path):
+        self.connect_volume(server, serverpath, sr_uuid, vdi_uuid)
+
+        uuid_stack = [vdi_uuid]
+        glance_host, glance_port, glance_use_ssl = glance_server
+
+        try:
+            result = self.glance_plugin.download_vhd(
+                image_id, glance_host, glance_port, glance_use_ssl, uuid_stack,
+                os.path.join(sr_base_path, sr_uuid), auth_token)
+        finally:
+            self.disconnect_volume(vdi_uuid)
+
+        if len(result) != 1 or result['root']['uuid'] != vdi_uuid:
+            return False
+
+        return True
index a0bf1164d2fc7ff52177a87242b21cf6fa31f575..042a6271707ae981993c9f7318376ca112ae5765 100644 (file)
 #    License for the specific language governing permissions and limitations
 #    under the License.
 
+
+from cinder import exception
 from cinder import flags
+from cinder.image import glance
 from cinder.openstack.common import cfg
+from cinder.openstack.common import log as logging
 from cinder.volume import driver
 from cinder.volume.drivers.xenapi import lib as xenapi_lib
 
 
+LOG = logging.getLogger(__name__)
+
 xenapi_opts = [
     cfg.StrOpt('xenapi_connection_url',
                default=None,
@@ -33,6 +39,9 @@ xenapi_opts = [
                default=None,
                help='Password for XenAPI connection',
                secret=True),
+    cfg.StrOpt('xenapi_sr_base_path',
+               default='/var/run/sr-mount',
+               help='Base path to the storage repository'),
 ]
 
 xenapi_nfs_opts = [
@@ -143,7 +152,31 @@ class XenAPINFSDriver(driver.VolumeDriver):
         pass
 
     def copy_image_to_volume(self, context, volume, image_service, image_id):
-        raise NotImplementedError()
+        sr_uuid, vdi_uuid = volume['provider_location'].split('/')
+
+        api_servers = glance.get_api_servers()
+        glance_server = api_servers.next()
+        auth_token = context.auth_token
+
+        overwrite_result = self.nfs_ops.use_glance_plugin_to_overwrite_volume(
+            FLAGS.xenapi_nfs_server,
+            FLAGS.xenapi_nfs_serverpath,
+            sr_uuid,
+            vdi_uuid,
+            glance_server,
+            image_id,
+            auth_token,
+            FLAGS.xenapi_sr_base_path)
+
+        if overwrite_result is False:
+            raise exception.ImageCopyFailure()
+
+        self.nfs_ops.resize_volume(
+            FLAGS.xenapi_nfs_server,
+            FLAGS.xenapi_nfs_serverpath,
+            sr_uuid,
+            vdi_uuid,
+            volume['size'])
 
     def copy_volume_to_image(self, context, volume, image_service, image_meta):
         raise NotImplementedError()
index 40dfafe0393184310536f22d47638092a6e4d1bc..57f4fb6d674ee0ad5f421ed3604c570557e49423 100644 (file)
@@ -195,7 +195,8 @@ class VolumeManager(manager.SchedulerDependentManager):
                     status = 'downloading'
 
             if model_update:
-                self.db.volume_update(context, volume_ref['id'], model_update)
+                volume_ref = self.db.volume_update(
+                    context, volume_ref['id'], model_update)
 
             LOG.debug(_("volume %s: creating export"), volume_ref['name'])
             model_update = self.driver.create_export(context, volume_ref)