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")
# 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):
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()
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()
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",
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')
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')
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()
# under the License.
import contextlib
+import os
+import pickle
class XenAPIException(Exception):
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):
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)
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):
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
# 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,
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 = [
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()
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)