From: Mate Lakat Date: Fri, 25 Jan 2013 14:44:08 +0000 (+0000) Subject: XenAPINFS: Copy image from glance X-Git-Url: https://review.fuel-infra.org/gitweb?a=commitdiff_plain;h=a84c910c4e4fe3e60c5fc0e8d82658e8ec8bfd1a;p=openstack-build%2Fcinder-build.git XenAPINFS: Copy image from glance 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 --- diff --git a/cinder/exception.py b/cinder/exception.py index daf003f94..39ac7098e 100644 --- a/cinder/exception.py +++ b/cinder/exception.py @@ -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") diff --git a/cinder/tests/test_xenapi_sm.py b/cinder/tests/test_xenapi_sm.py index bc81b76f9..71c4fe036 100644 --- a/cinder/tests/test_xenapi_sm.py +++ b/cinder/tests/test_xenapi_sm.py @@ -17,12 +17,18 @@ # 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() diff --git a/cinder/volume/drivers/xenapi/lib.py b/cinder/volume/drivers/xenapi/lib.py index dc17682e6..8620d33a6 100644 --- a/cinder/volume/drivers/xenapi/lib.py +++ b/cinder/volume/drivers/xenapi/lib.py @@ -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 diff --git a/cinder/volume/drivers/xenapi/sm.py b/cinder/volume/drivers/xenapi/sm.py index a0bf1164d..042a62717 100644 --- a/cinder/volume/drivers/xenapi/sm.py +++ b/cinder/volume/drivers/xenapi/sm.py @@ -16,12 +16,18 @@ # 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() diff --git a/cinder/volume/manager.py b/cinder/volume/manager.py index 40dfafe03..57f4fb6d6 100644 --- a/cinder/volume/manager.py +++ b/cinder/volume/manager.py @@ -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)