Added unit tests for the xenapi-glance integration. This adds a glance
simulator that can stub in place of glance.client.Client, and enhances the xapi simulator to add the additional calls that the Glance-specific path requires. The test itself is just the spawn test, but now we run through with xenapi_image_service set to "objectstore", and then again set to "glance".
This commit is contained in:
parent
d47183b268
commit
6f9408d7ac
|
@ -0,0 +1,20 @@
|
|||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright (c) 2011 Citrix Systems, Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
"""
|
||||
:mod:`glance` -- Stubs for Glance
|
||||
=================================
|
||||
"""
|
|
@ -0,0 +1,37 @@
|
|||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright (c) 2011 Citrix Systems, Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import StringIO
|
||||
|
||||
import glance.client
|
||||
|
||||
|
||||
def stubout_glance_client(stubs, cls):
|
||||
"""Stubs out glance.client.Client"""
|
||||
stubs.Set(glance.client, 'Client',
|
||||
lambda *args, **kwargs: cls(*args, **kwargs))
|
||||
|
||||
|
||||
class FakeGlance(object):
|
||||
def __init__(self, host, port=None, use_ssl=False):
|
||||
pass
|
||||
|
||||
def get_image(self, image):
|
||||
meta = {
|
||||
'size': 0,
|
||||
}
|
||||
image_file = StringIO.StringIO('')
|
||||
return meta, image_file
|
|
@ -33,6 +33,7 @@ from nova.virt.xenapi import fake as xenapi_fake
|
|||
from nova.virt.xenapi import volume_utils
|
||||
from nova.tests.db import fakes as db_fakes
|
||||
from nova.tests.xenapi import stubs
|
||||
from nova.tests.glance import stubs as glance_stubs
|
||||
|
||||
FLAGS = flags.FLAGS
|
||||
|
||||
|
@ -107,18 +108,16 @@ class XenAPIVolumeTestCase(test.TestCase):
|
|||
conn = xenapi_conn.get_connection(False)
|
||||
volume = self._create_volume()
|
||||
instance = db.instance_create(self.values)
|
||||
xenapi_fake.create_vm(instance.name, 'Running')
|
||||
vm = xenapi_fake.create_vm(instance.name, 'Running')
|
||||
result = conn.attach_volume(instance.name, volume['id'], '/dev/sdc')
|
||||
|
||||
def check():
|
||||
# check that the VM has a VBD attached to it
|
||||
# Get XenAPI reference for the VM
|
||||
vms = xenapi_fake.get_all('VM')
|
||||
# Get XenAPI record for VBD
|
||||
vbds = xenapi_fake.get_all('VBD')
|
||||
vbd = xenapi_fake.get_record('VBD', vbds[0])
|
||||
vm_ref = vbd['VM']
|
||||
self.assertEqual(vm_ref, vms[0])
|
||||
self.assertEqual(vm_ref, vm)
|
||||
|
||||
check()
|
||||
|
||||
|
@ -156,9 +155,14 @@ class XenAPIVMTestCase(test.TestCase):
|
|||
FLAGS.xenapi_connection_url = 'test_url'
|
||||
FLAGS.xenapi_connection_password = 'test_pass'
|
||||
xenapi_fake.reset()
|
||||
xenapi_fake.create_local_srs()
|
||||
db_fakes.stub_out_db_instance_api(self.stubs)
|
||||
xenapi_fake.create_network('fake', FLAGS.flat_network_bridge)
|
||||
stubs.stubout_session(self.stubs, stubs.FakeSessionForVMTests)
|
||||
stubs.stubout_get_this_vm_uuid(self.stubs)
|
||||
stubs.stubout_stream_disk(self.stubs)
|
||||
glance_stubs.stubout_glance_client(self.stubs,
|
||||
glance_stubs.FakeGlance)
|
||||
self.conn = xenapi_conn.get_connection(False)
|
||||
|
||||
def test_list_instances_0(self):
|
||||
|
@ -206,7 +210,15 @@ class XenAPIVMTestCase(test.TestCase):
|
|||
|
||||
check()
|
||||
|
||||
def test_spawn(self):
|
||||
def test_spawn_glance(self):
|
||||
FLAGS.xenapi_image_service = 'glance'
|
||||
self._test_spawn()
|
||||
|
||||
def test_spawn_objectstore(self):
|
||||
FLAGS.xenapi_image_service = 'objectstore'
|
||||
self._test_spawn()
|
||||
|
||||
def _test_spawn(self):
|
||||
instance = self._create_instance()
|
||||
|
||||
def check():
|
||||
|
@ -217,8 +229,10 @@ class XenAPIVMTestCase(test.TestCase):
|
|||
vm_info = self.conn.get_info(1)
|
||||
|
||||
# Get XenAPI record for VM
|
||||
vms = xenapi_fake.get_all('VM')
|
||||
vm = xenapi_fake.get_record('VM', vms[0])
|
||||
vms = [rec for ref, rec
|
||||
in xenapi_fake.get_all_records('VM').iteritems()
|
||||
if not rec['is_control_domain']]
|
||||
vm = vms[0]
|
||||
|
||||
# Check that m1.large above turned into the right thing.
|
||||
instance_type = instance_types.INSTANCE_TYPES['m1.large']
|
||||
|
|
|
@ -91,6 +91,21 @@ def stub_out_get_target(stubs):
|
|||
stubs.Set(volume_utils, '_get_target', fake_get_target)
|
||||
|
||||
|
||||
def stubout_get_this_vm_uuid(stubs):
|
||||
def f():
|
||||
vms = [rec['uuid'] for ref, rec
|
||||
in fake.get_all_records('VM').iteritems()
|
||||
if rec['is_control_domain']]
|
||||
return vms[0]
|
||||
stubs.Set(vm_utils, 'get_this_vm_uuid', f)
|
||||
|
||||
|
||||
def stubout_stream_disk(stubs):
|
||||
def f(_):
|
||||
pass
|
||||
stubs.Set(vm_utils, '_stream_disk', f)
|
||||
|
||||
|
||||
class FakeSessionForVMTests(fake.SessionBase):
|
||||
""" Stubs out a XenAPISession for VM tests """
|
||||
def __init__(self, uri):
|
||||
|
@ -100,7 +115,10 @@ class FakeSessionForVMTests(fake.SessionBase):
|
|||
return self.xenapi.network.get_all_records()
|
||||
|
||||
def host_call_plugin(self, _1, _2, _3, _4, _5):
|
||||
return ''
|
||||
sr_ref = fake.get_all('SR')[0]
|
||||
vdi_ref = fake.create_vdi('', False, sr_ref, False)
|
||||
vdi_rec = fake.get_record('VDI', vdi_ref)
|
||||
return '<string>%s</string>' % vdi_rec['uuid']
|
||||
|
||||
def VM_start(self, _1, ref, _2, _3):
|
||||
vm = fake.get_record('VM', ref)
|
||||
|
@ -135,10 +153,6 @@ class FakeSessionForVolumeTests(fake.SessionBase):
|
|||
def __init__(self, uri):
|
||||
super(FakeSessionForVolumeTests, self).__init__(uri)
|
||||
|
||||
def VBD_plug(self, _1, ref):
|
||||
rec = fake.get_record('VBD', ref)
|
||||
rec['currently-attached'] = True
|
||||
|
||||
def VDI_introduce(self, _1, uuid, _2, _3, _4, _5,
|
||||
_6, _7, _8, _9, _10, _11):
|
||||
valid_vdi = False
|
||||
|
|
|
@ -74,6 +74,7 @@ def reset():
|
|||
for c in _CLASSES:
|
||||
_db_content[c] = {}
|
||||
create_host('fake')
|
||||
create_vm('fake', 'Running', is_a_template=False, is_control_domain=True)
|
||||
|
||||
|
||||
def create_host(name_label):
|
||||
|
@ -134,14 +135,20 @@ def create_vdi(name_label, read_only, sr_ref, sharable):
|
|||
|
||||
|
||||
def create_vbd(vm_ref, vdi_ref):
|
||||
vbd_rec = {'VM': vm_ref, 'VDI': vdi_ref}
|
||||
vbd_rec = {
|
||||
'VM': vm_ref,
|
||||
'VDI': vdi_ref,
|
||||
'currently_attached': False,
|
||||
}
|
||||
vbd_ref = _create_object('VBD', vbd_rec)
|
||||
after_VBD_create(vbd_ref, vbd_rec)
|
||||
return vbd_ref
|
||||
|
||||
|
||||
def after_VBD_create(vbd_ref, vbd_rec):
|
||||
"""Create backref from VM to VBD when VBD is created"""
|
||||
"""Create read-only fields and backref from VM to VBD when VBD is created"""
|
||||
vbd_rec['currently_attached'] = False
|
||||
vbd_rec['device'] = ''
|
||||
vm_ref = vbd_rec['VM']
|
||||
vm_rec = _db_content['VM'][vm_ref]
|
||||
vm_rec['VBDs'] = [vbd_ref]
|
||||
|
@ -150,9 +157,10 @@ def after_VBD_create(vbd_ref, vbd_rec):
|
|||
vbd_rec['vm_name_label'] = vm_name_label
|
||||
|
||||
|
||||
def create_pbd(config, sr_ref, attached):
|
||||
def create_pbd(config, host_ref, sr_ref, attached):
|
||||
return _create_object('PBD', {
|
||||
'device-config': config,
|
||||
'host': host_ref,
|
||||
'SR': sr_ref,
|
||||
'currently-attached': attached,
|
||||
})
|
||||
|
@ -165,6 +173,33 @@ def create_task(name_label):
|
|||
})
|
||||
|
||||
|
||||
def create_local_srs():
|
||||
"""Create an SR that looks like the one created on the local disk by
|
||||
default by the XenServer installer. Do this one per host."""
|
||||
for host_ref in _db_content['host'].keys():
|
||||
_create_local_sr(host_ref)
|
||||
|
||||
|
||||
def _create_local_sr(host_ref):
|
||||
sr_ref = _create_object('SR', {
|
||||
'name_label': 'Local storage',
|
||||
'type': 'lvm',
|
||||
'content_type': 'user',
|
||||
'shared': False,
|
||||
'physical_size': str(1 << 30),
|
||||
'physical_utilisation': str(0),
|
||||
'virtual_allocation': str(0),
|
||||
'other_config': {
|
||||
'i18n-original-value-name_label': 'Local storage',
|
||||
'i18n-key': 'local-storage',
|
||||
},
|
||||
'VDIs': []
|
||||
})
|
||||
pbd_ref = create_pbd('', host_ref, sr_ref, True)
|
||||
_db_content['SR'][sr_ref]['PBDs'] = [pbd_ref]
|
||||
return sr_ref
|
||||
|
||||
|
||||
def _create_object(table, obj):
|
||||
ref = str(uuid.uuid4())
|
||||
obj['uuid'] = str(uuid.uuid4())
|
||||
|
@ -177,9 +212,10 @@ def _create_sr(table, obj):
|
|||
# Forces fake to support iscsi only
|
||||
if sr_type != 'iscsi':
|
||||
raise Failure(['SR_UNKNOWN_DRIVER', sr_type])
|
||||
host_ref = _db_content['host'].keys()[0]
|
||||
sr_ref = _create_object(table, obj[2])
|
||||
vdi_ref = create_vdi('', False, sr_ref, False)
|
||||
pbd_ref = create_pbd('', sr_ref, True)
|
||||
pbd_ref = create_pbd('', host_ref, sr_ref, True)
|
||||
_db_content['SR'][sr_ref]['VDIs'] = [vdi_ref]
|
||||
_db_content['SR'][sr_ref]['PBDs'] = [pbd_ref]
|
||||
_db_content['VDI'][vdi_ref]['SR'] = sr_ref
|
||||
|
@ -231,6 +267,20 @@ class SessionBase(object):
|
|||
def __init__(self, uri):
|
||||
self._session = None
|
||||
|
||||
def VBD_plug(self, _1, ref):
|
||||
rec = get_record('VBD', ref)
|
||||
if rec['currently_attached']:
|
||||
raise Failure(['DEVICE_ALREADY_ATTACHED', ref])
|
||||
rec['currently_attached'] = True
|
||||
rec['device'] = rec['userdevice']
|
||||
|
||||
def VBD_unplug(self, _1, ref):
|
||||
rec = get_record('VBD', ref)
|
||||
if not rec['currently_attached']:
|
||||
raise Failure(['DEVICE_ALREADY_DETACHED', ref])
|
||||
rec['currently_attached'] = False
|
||||
rec['device'] = ''
|
||||
|
||||
def xenapi_request(self, methodname, params):
|
||||
if methodname.startswith('login'):
|
||||
self._login(methodname, params)
|
||||
|
@ -287,6 +337,8 @@ class SessionBase(object):
|
|||
return lambda *params: self._getter(name, params)
|
||||
elif self._is_create(name):
|
||||
return lambda *params: self._create(name, params)
|
||||
elif self._is_destroy(name):
|
||||
return lambda *params: self._destroy(name, params)
|
||||
else:
|
||||
return None
|
||||
|
||||
|
@ -297,10 +349,16 @@ class SessionBase(object):
|
|||
bits[1].startswith(getter and 'get_' or 'set_'))
|
||||
|
||||
def _is_create(self, name):
|
||||
return self._is_method(name, 'create')
|
||||
|
||||
def _is_destroy(self, name):
|
||||
return self._is_method(name, 'destroy')
|
||||
|
||||
def _is_method(self, name, meth):
|
||||
bits = name.split('.')
|
||||
return (len(bits) == 2 and
|
||||
bits[0] in _CLASSES and
|
||||
bits[1] == 'create')
|
||||
bits[1] == meth)
|
||||
|
||||
def _getter(self, name, params):
|
||||
self._check_session(params)
|
||||
|
@ -368,10 +426,9 @@ class SessionBase(object):
|
|||
_create_sr(cls, params) or _create_object(cls, params[1])
|
||||
|
||||
# Call hook to provide any fixups needed (ex. creating backrefs)
|
||||
try:
|
||||
globals()["after_%s_create" % cls](ref, params[1])
|
||||
except KeyError:
|
||||
pass
|
||||
after_hook = 'after_%s_create' % cls
|
||||
if after_hook in globals():
|
||||
globals()[after_hook](ref, params[1])
|
||||
|
||||
obj = get_record(cls, ref)
|
||||
|
||||
|
@ -381,6 +438,15 @@ class SessionBase(object):
|
|||
|
||||
return ref
|
||||
|
||||
def _destroy(self, name, params):
|
||||
self._check_session(params)
|
||||
self._check_arg_count(params, 2)
|
||||
table, _ = name.split('.')
|
||||
ref = params[1]
|
||||
if ref not in _db_content[table]:
|
||||
raise Failure(['HANDLE_INVALID', table, ref])
|
||||
del _db_content[table][ref]
|
||||
|
||||
def _async(self, name, params):
|
||||
task_ref = create_task(name)
|
||||
task = _db_content['task'][task_ref]
|
||||
|
@ -418,7 +484,7 @@ class SessionBase(object):
|
|||
try:
|
||||
return result[0]
|
||||
except IndexError:
|
||||
return None
|
||||
raise Failure(['UUID_INVALID', v, result, recs, k])
|
||||
|
||||
return result
|
||||
|
||||
|
|
|
@ -296,7 +296,7 @@ class VMHelper(HelperBase):
|
|||
access, type)
|
||||
else:
|
||||
return cls._fetch_image_objectstore(session, instance_id, image,
|
||||
access, type)
|
||||
access, user.secret, type)
|
||||
|
||||
@classmethod
|
||||
def _fetch_image_glance(cls, session, instance_id, image, access, type):
|
||||
|
@ -318,18 +318,7 @@ class VMHelper(HelperBase):
|
|||
vdi = cls.create_vdi(session, sr, _('Glance image %s') % image,
|
||||
vdi_size, False)
|
||||
|
||||
def stream(dev):
|
||||
offset = 0
|
||||
if type == ImageType.DISK:
|
||||
offset = MBR_SIZE_BYTES
|
||||
_write_partition(virtual_size, dev)
|
||||
|
||||
with open('/dev/%s' % dev, 'wb') as f:
|
||||
f.seek(offset)
|
||||
for chunk in image_file:
|
||||
f.write(chunk)
|
||||
|
||||
with_vdi_attached_here(session, vdi, False, stream)
|
||||
with_vdi_attached_here(session, vdi, False, _stream_disk)
|
||||
if (type==ImageType.KERNEL_RAMDISK):
|
||||
#we need to invoke a plugin for copying VDI's content into proper path
|
||||
fn = "copy_kernel_vdi"
|
||||
|
@ -345,14 +334,14 @@ class VMHelper(HelperBase):
|
|||
|
||||
@classmethod
|
||||
def _fetch_image_objectstore(cls, session, instance_id, image, access,
|
||||
type):
|
||||
secret, type):
|
||||
url = images.image_url(image)
|
||||
logging.debug("Asking xapi to fetch %s as %s", url, access)
|
||||
fn = (type != ImageType.KERNEL_RAMDISK) and 'get_vdi' or 'get_kernel'
|
||||
args = {}
|
||||
args['src_url'] = url
|
||||
args['username'] = access
|
||||
args['password'] = user.secret
|
||||
args['password'] = secret
|
||||
args['add_partition'] = 'false'
|
||||
args['raw'] = 'false'
|
||||
if type != ImageType.KERNEL_RAMDISK:
|
||||
|
@ -629,7 +618,7 @@ def vbd_unplug_with_retry(session, vbd):
|
|||
session.get_xenapi().VBD.unplug(vbd)
|
||||
logging.debug(_('VBD.unplug successful first time.'))
|
||||
return
|
||||
except XenAPI.Failure, e:
|
||||
except VMHelper.XenAPI.Failure, e:
|
||||
if (len(e.details) > 0 and
|
||||
e.details[0] == 'DEVICE_DETACH_REJECTED'):
|
||||
logging.debug(_('VBD.unplug rejected: retrying...'))
|
||||
|
@ -647,7 +636,7 @@ def vbd_unplug_with_retry(session, vbd):
|
|||
def ignore_failure(func, *args, **kwargs):
|
||||
try:
|
||||
return func(*args, **kwargs)
|
||||
except XenAPI.Failure, e:
|
||||
except VMHelper.XenAPI.Failure, e:
|
||||
logging.error(_('Ignoring XenAPI.Failure %s'), e)
|
||||
return None
|
||||
|
||||
|
@ -661,6 +650,18 @@ def get_this_vm_ref(session):
|
|||
return session.get_xenapi().VM.get_by_uuid(get_this_vm_uuid())
|
||||
|
||||
|
||||
def _stream_disk(dev):
|
||||
offset = 0
|
||||
if type == ImageType.DISK:
|
||||
offset = MBR_SIZE_BYTES
|
||||
_write_partition(virtual_size, dev)
|
||||
|
||||
with open('/dev/%s' % dev, 'wb') as f:
|
||||
f.seek(offset)
|
||||
for chunk in image_file:
|
||||
f.write(chunk)
|
||||
|
||||
|
||||
def _write_partition(virtual_size, dev):
|
||||
dest = '/dev/%s' % dev
|
||||
mbr_last = MBR_SIZE_SECTORS - 1
|
||||
|
|
Loading…
Reference in New Issue