diff options
| author | Salvatore Orlando <salvatore.orlando@eu.citrix.com> | 2011-03-25 23:43:21 +0000 |
|---|---|---|
| committer | Salvatore Orlando <salvatore.orlando@eu.citrix.com> | 2011-03-25 23:43:21 +0000 |
| commit | 82d6fb2144a5411e96dc71a482d33be4990f7cab (patch) | |
| tree | f1911f3b119d7221150f6bbdf41e29673b78c7ac | |
| parent | 129e020b3a0712404de28386b19a652d7db8fae8 (diff) | |
| parent | 9a1a2c174984ef873c80bf7aea307b393552f3a9 (diff) | |
| download | nova-82d6fb2144a5411e96dc71a482d33be4990f7cab.tar.gz nova-82d6fb2144a5411e96dc71a482d33be4990f7cab.tar.xz nova-82d6fb2144a5411e96dc71a482d33be4990f7cab.zip | |
merge trunk
addressing Trey's comments
| -rw-r--r-- | Authors | 1 | ||||
| -rw-r--r-- | nova/tests/db/fakes.py | 31 | ||||
| -rw-r--r-- | nova/tests/fake_utils.py | 16 | ||||
| -rw-r--r-- | nova/tests/fake_utils.py.moved | 106 | ||||
| -rw-r--r-- | nova/tests/test_xenapi.py | 116 | ||||
| -rw-r--r-- | nova/tests/xenapi/stubs.py | 4 | ||||
| -rw-r--r-- | nova/virt/disk.py | 26 | ||||
| -rw-r--r-- | nova/virt/libvirt_conn.py | 4 | ||||
| -rw-r--r-- | nova/virt/xenapi/fake.py | 22 | ||||
| -rw-r--r-- | nova/virt/xenapi/vm_utils.py | 133 | ||||
| -rw-r--r-- | nova/virt/xenapi/vmops.py | 28 | ||||
| -rw-r--r-- | nova/virt/xenapi_conn.py | 14 |
12 files changed, 448 insertions, 53 deletions
@@ -1,4 +1,5 @@ Andy Smith <code@term.ie> +Andy Southgate <andy.southgate@citrix.com> Anne Gentle <anne@openstack.org> Anthony Young <sleepsonthefloor@gmail.com> Antony Messerli <ant@openstack.org> diff --git a/nova/tests/db/fakes.py b/nova/tests/db/fakes.py index e94c7bfee..05a47d4c9 100644 --- a/nova/tests/db/fakes.py +++ b/nova/tests/db/fakes.py @@ -23,8 +23,6 @@ from nova import db from nova import test from nova import utils -from nova import log as LOG - def stub_out_db_instance_api(stubs, injected=True): """ Stubs out the db API for creating Instances """ @@ -87,6 +85,11 @@ def stub_out_db_instance_api(stubs, injected=True): 'vlan': 111, 'injected': False} + fixed_ip_fields = { + 'address': '10.0.0.3', + 'address_v6': 'fe80::a00:3', + 'network_id': 'fake_flat'} + class FakeModel(object): """ Stubs out for model """ def __init__(self, values): @@ -113,18 +116,34 @@ def stub_out_db_instance_api(stubs, injected=True): return FakeModel(vlan_network_fields) else: return FakeModel(flat_network_fields) + return FakeModel(network_fields) def fake_network_get_all_by_instance(context, instance_id): - l = [] #even instance numbers are on vlan networks if instance_id % 2 == 0: - l.append(FakeModel(vlan_network_fields)) + return [FakeModel(vlan_network_fields)] else: - l.append(FakeModel(flat_network_fields)) - return l + return [FakeModel(flat_network_fields)] + + def fake_instance_get_fixed_address(context, instance_id): + return FakeModel(fixed_ip_fields).address + + def fake_instance_get_fixed_address_v6(context, instance_id): + return FakeModel(fixed_ip_fields).address + + def fake_fixed_ip_get_all_by_instance(context, instance_id): + return [FakeModel(fixed_ip_fields)] stubs.Set(db, 'network_get_by_instance', fake_network_get_by_instance) stubs.Set(db, 'network_get_all_by_instance', fake_network_get_all_by_instance) stubs.Set(db, 'instance_type_get_all', fake_instance_type_get_all) stubs.Set(db, 'instance_type_get_by_name', fake_instance_type_get_by_name) + stubs.Set(db, 'instance_get_fixed_address', + fake_instance_get_fixed_address) + stubs.Set(db, 'instance_get_fixed_address_v6', + fake_instance_get_fixed_address_v6) + stubs.Set(db, 'network_get_all_by_instance', + fake_network_get_all_by_instance) + stubs.Set(db, 'fixed_ip_get_all_by_instance', + fake_fixed_ip_get_all_by_instance) diff --git a/nova/tests/fake_utils.py b/nova/tests/fake_utils.py index 8982f50be..823c775cb 100644 --- a/nova/tests/fake_utils.py +++ b/nova/tests/fake_utils.py @@ -33,7 +33,6 @@ _fake_execute_log = [] def fake_execute_get_log(): - global _fake_execute_log return _fake_execute_log @@ -55,7 +54,7 @@ def fake_execute_default_reply_handler(*ignore_args, **ignore_kwargs): return '', '' -def fake_execute(*cmd, **kwargs): +def fake_execute(*cmd_parts, **kwargs): """This function stubs out execute, optionally executing a preconfigued function to return expected data """ @@ -64,8 +63,7 @@ def fake_execute(*cmd, **kwargs): process_input = kwargs.get('process_input', None) addl_env = kwargs.get('addl_env', None) check_exit_code = kwargs.get('check_exit_code', 0) - cmd_map = map(str, cmd) - cmd_str = ' '.join(cmd_map) + cmd_str = ' '.join(str(part) for part in cmd_parts) LOG.debug(_("Faking execution of cmd (subprocess): %s"), cmd_str) _fake_execute_log.append(cmd_str) @@ -78,13 +76,13 @@ def fake_execute(*cmd, **kwargs): LOG.debug(_('Faked command matched %s') % fake_replier[0]) break - if isinstance(reply_handler, types.StringTypes): + if isinstance(reply_handler, basestring): # If the reply handler is a string, return it as stdout reply = reply_handler, '' else: try: # Alternative is a function, so call it - reply = reply_handler(cmd, + reply = reply_handler(cmd_parts, process_input=process_input, addl_env=addl_env, check_exit_code=check_exit_code) @@ -92,8 +90,10 @@ def fake_execute(*cmd, **kwargs): LOG.debug(_('Faked command raised an exception %s' % str(e))) raise - LOG.debug(_("Reply to faked command is stdout='%(0)s' stderr='%(1)s'") % - {'0': reply[0], '1': reply[1]}) + stdout = reply[0] + stderr = reply[1] + LOG.debug(_("Reply to faked command is stdout='%(stdout)s' " + "stderr='%(stderr)s'") % locals()) # Replicate the sleep call in the real function greenthread.sleep(0) diff --git a/nova/tests/fake_utils.py.moved b/nova/tests/fake_utils.py.moved new file mode 100644 index 000000000..8982f50be --- /dev/null +++ b/nova/tests/fake_utils.py.moved @@ -0,0 +1,106 @@ +# 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. + +"""This modules stubs out functions in nova.utils +""" + +import re +import types + +from eventlet import greenthread + +from nova import exception +from nova import log as logging +from nova import utils + +LOG = logging.getLogger('nova.tests.fake_utils') + +_fake_execute_repliers = [] +_fake_execute_log = [] + + +def fake_execute_get_log(): + global _fake_execute_log + return _fake_execute_log + + +def fake_execute_clear_log(): + global _fake_execute_log + _fake_execute_log = [] + + +def fake_execute_set_repliers(repliers): + """Allows the client to configure replies to commands""" + global _fake_execute_repliers + _fake_execute_repliers = repliers + + +def fake_execute_default_reply_handler(*ignore_args, **ignore_kwargs): + """A reply handler for commands that haven't been added to the reply + list. Returns empty strings for stdout and stderr + """ + return '', '' + + +def fake_execute(*cmd, **kwargs): + """This function stubs out execute, optionally executing + a preconfigued function to return expected data + """ + global _fake_execute_repliers + + process_input = kwargs.get('process_input', None) + addl_env = kwargs.get('addl_env', None) + check_exit_code = kwargs.get('check_exit_code', 0) + cmd_map = map(str, cmd) + cmd_str = ' '.join(cmd_map) + + LOG.debug(_("Faking execution of cmd (subprocess): %s"), cmd_str) + _fake_execute_log.append(cmd_str) + + reply_handler = fake_execute_default_reply_handler + + for fake_replier in _fake_execute_repliers: + if re.match(fake_replier[0], cmd_str): + reply_handler = fake_replier[1] + LOG.debug(_('Faked command matched %s') % fake_replier[0]) + break + + if isinstance(reply_handler, types.StringTypes): + # If the reply handler is a string, return it as stdout + reply = reply_handler, '' + else: + try: + # Alternative is a function, so call it + reply = reply_handler(cmd, + process_input=process_input, + addl_env=addl_env, + check_exit_code=check_exit_code) + except exception.ProcessExecutionError as e: + LOG.debug(_('Faked command raised an exception %s' % str(e))) + raise + + LOG.debug(_("Reply to faked command is stdout='%(0)s' stderr='%(1)s'") % + {'0': reply[0], '1': reply[1]}) + + # Replicate the sleep call in the real function + greenthread.sleep(0) + return reply + + +def stub_out_utils_execute(stubs): + fake_execute_set_repliers([]) + fake_execute_clear_log() + stubs.Set(utils, 'execute', fake_execute) diff --git a/nova/tests/test_xenapi.py b/nova/tests/test_xenapi.py index 2196257bd..f91f37d4b 100644 --- a/nova/tests/test_xenapi.py +++ b/nova/tests/test_xenapi.py @@ -19,11 +19,15 @@ Test suite for XenAPI """ import functools +import os +import re import stubout +import ast from nova import db from nova import context from nova import flags +from nova import log as logging from nova import test from nova import utils from nova.auth import manager @@ -40,7 +44,7 @@ from nova.tests.xenapi import stubs from nova.tests.glance import stubs as glance_stubs from nova.tests import fake_utils -from nova import log as LOG +LOG = logging.getLogger('nova.tests.test_xenapi') FLAGS = flags.FLAGS @@ -95,7 +99,7 @@ class XenAPIVolumeTestCase(test.TestCase): vol['availability_zone'] = FLAGS.storage_availability_zone vol['status'] = "creating" vol['attach_status'] = "detached" - return db.volume_create(context.get_admin_context(), vol) + return db.volume_create(self.context, vol) def test_create_iscsi_storage(self): """ This shows how to test helper classes' methods """ @@ -251,7 +255,6 @@ class XenAPIVMTestCase(test.TestCase): # Get Nova record for VM vm_info = conn.get_info(instance_id) - # Get XenAPI record for VM vms = [rec for ref, rec in xenapi_fake.get_all_records('VM').iteritems() @@ -260,7 +263,7 @@ class XenAPIVMTestCase(test.TestCase): self.vm_info = vm_info self.vm = vm - def check_vm_record(self, conn): + def check_vm_record(self, conn, check_injection=False): # Check that m1.large above turned into the right thing. instance_type = db.instance_type_get_by_name(conn, 'm1.large') mem_kib = long(instance_type['memory_mb']) << 10 @@ -280,6 +283,25 @@ class XenAPIVMTestCase(test.TestCase): # Check that the VM is running according to XenAPI. self.assertEquals(self.vm['power_state'], 'Running') + if check_injection: + xenstore_data = self.vm['xenstore_data'] + key = 'vm-data/networking/aabbccddeeff' + xenstore_value = xenstore_data[key] + tcpip_data = ast.literal_eval(xenstore_value) + self.assertEquals(tcpip_data, { + 'label': 'fake_flat_network', + 'broadcast': '10.0.0.255', + 'ips': [{'ip': '10.0.0.3', + 'netmask':'255.255.255.0', + 'enabled':'1'}], + 'ip6s': [{'ip': 'fe80::a8bb:ccff:fedd:eeff', + 'netmask': '120', + 'enabled': '1', + 'gateway': 'fe80::a00:1'}], + 'mac': 'aa:bb:cc:dd:ee:ff', + 'dns': ['10.0.0.2'], + 'gateway': '10.0.0.1'}) + def check_vm_params_for_windows(self): self.assertEquals(self.vm['platform']['nx'], 'true') self.assertEquals(self.vm['HVM_boot_params'], {'order': 'dc'}) @@ -313,7 +335,8 @@ class XenAPIVMTestCase(test.TestCase): self.assertEquals(self.vm['HVM_boot_policy'], '') def _test_spawn(self, image_id, kernel_id, ramdisk_id, - instance_type="m1.large", os_type="linux", instance_id=1): + instance_type="m1.large", os_type="linux", + instance_id=1, check_injection=False): stubs.stubout_loopingcall_start(self.stubs) values = {'id': instance_id, 'project_id': self.project.id, @@ -327,7 +350,7 @@ class XenAPIVMTestCase(test.TestCase): instance = db.instance_create(self.context, values) self.conn.spawn(instance) self.create_vm_record(self.conn, os_type, instance_id) - self.check_vm_record(self.conn) + self.check_vm_record(self.conn, check_injection) def test_spawn_not_enough_memory(self): FLAGS.xenapi_image_service = 'glance' @@ -368,6 +391,85 @@ class XenAPIVMTestCase(test.TestCase): glance_stubs.FakeGlance.IMAGE_RAMDISK) self.check_vm_params_for_linux_with_external_kernel() + def test_spawn_netinject_file(self): + FLAGS.xenapi_image_service = 'glance' + db_fakes.stub_out_db_instance_api(self.stubs, injected=True) + + self._tee_executed = False + + def _tee_handler(cmd, **kwargs): + input = kwargs.get('process_input', None) + self.assertNotEqual(input, None) + config = [line.strip() for line in input.split("\n")] + # Find the start of eth0 configuration and check it + index = config.index('auto eth0') + self.assertEquals(config[index + 1:index + 8], [ + 'iface eth0 inet static', + 'address 10.0.0.3', + 'netmask 255.255.255.0', + 'broadcast 10.0.0.255', + 'gateway 10.0.0.1', + 'dns-nameservers 10.0.0.2', + '']) + self._tee_executed = True + return '', '' + + fake_utils.fake_execute_set_repliers([ + # Capture the sudo tee .../etc/network/interfaces command + (r'(sudo\s+)?tee.*interfaces', _tee_handler), + ]) + FLAGS.xenapi_image_service = 'glance' + self._test_spawn(glance_stubs.FakeGlance.IMAGE_MACHINE, + glance_stubs.FakeGlance.IMAGE_KERNEL, + glance_stubs.FakeGlance.IMAGE_RAMDISK, + check_injection=True) + self.assertTrue(self._tee_executed) + + def test_spawn_netinject_xenstore(self): + FLAGS.xenapi_image_service = 'glance' + db_fakes.stub_out_db_instance_api(self.stubs, injected=True) + + self._tee_executed = False + + def _mount_handler(cmd, *ignore_args, **ignore_kwargs): + # When mounting, create real files under the mountpoint to simulate + # files in the mounted filesystem + + # mount point will be the last item of the command list + self._tmpdir = cmd[len(cmd) - 1] + LOG.debug(_('Creating files in %s to simulate guest agent' % + self._tmpdir)) + os.makedirs(os.path.join(self._tmpdir, 'usr', 'sbin')) + # Touch the file using open + open(os.path.join(self._tmpdir, 'usr', 'sbin', + 'xe-update-networking'), 'w').close() + return '', '' + + def _umount_handler(cmd, *ignore_args, **ignore_kwargs): + # Umount would normall make files in the m,ounted filesystem + # disappear, so do that here + LOG.debug(_('Removing simulated guest agent files in %s' % + self._tmpdir)) + os.remove(os.path.join(self._tmpdir, 'usr', 'sbin', + 'xe-update-networking')) + os.rmdir(os.path.join(self._tmpdir, 'usr', 'sbin')) + os.rmdir(os.path.join(self._tmpdir, 'usr')) + return '', '' + + def _tee_handler(cmd, *ignore_args, **ignore_kwargs): + self._tee_executed = True + return '', '' + + fake_utils.fake_execute_set_repliers([ + (r'(sudo\s+)?mount', _mount_handler), + (r'(sudo\s+)?umount', _umount_handler), + (r'(sudo\s+)?tee.*interfaces', _tee_handler)]) + self._test_spawn(1, 2, 3, check_injection=True) + + # tee must not run in this case, where an injection-capable + # guest agent is detected + self.assertFalse(self._tee_executed) + def test_spawn_vlanmanager(self): self.flags(xenapi_image_service='glance', network_manager='nova.network.manager.VlanManager', @@ -399,6 +501,7 @@ class XenAPIVMTestCase(test.TestCase): str(4 * 1024)) def test_rescue(self): + self.flags(xenapi_inject_image=False) instance = self._create_instance() conn = xenapi_conn.get_connection(False) conn.rescue(instance, None) @@ -492,6 +595,7 @@ class XenAPIMigrateInstance(test.TestCase): 'mac_address': 'aa:bb:cc:dd:ee:ff', 'os_type': 'linux'} + fake_utils.stub_out_utils_execute(self.stubs) stubs.stub_out_migration_methods(self.stubs) stubs.stubout_get_this_vm_uuid(self.stubs) glance_stubs.stubout_glance_client(self.stubs, diff --git a/nova/tests/xenapi/stubs.py b/nova/tests/xenapi/stubs.py index 7a2a2c3e2..205f6c902 100644 --- a/nova/tests/xenapi/stubs.py +++ b/nova/tests/xenapi/stubs.py @@ -139,9 +139,9 @@ def stubout_is_vdi_pv(stubs): def stubout_loopingcall_start(stubs): - def f_1(self, interval, now=True): + def fake_start(self, interval, now=True): self.f(*self.args, **self.kw) - stubs.Set(utils.LoopingCall, 'start', f_1) + stubs.Set(utils.LoopingCall, 'start', fake_start) class FakeSessionForVMTests(fake.SessionBase): diff --git a/nova/virt/disk.py b/nova/virt/disk.py index 9abe44cc3..25e4f54a9 100644 --- a/nova/virt/disk.py +++ b/nova/virt/disk.py @@ -26,6 +26,8 @@ import os import tempfile import time +from nova import context +from nova import db from nova import exception from nova import flags from nova import log as logging @@ -38,6 +40,9 @@ flags.DEFINE_integer('minimum_root_size', 1024 * 1024 * 1024 * 10, 'minimum size in bytes of root partition') flags.DEFINE_integer('block_size', 1024 * 1024 * 256, 'block_size to use for dd') +flags.DEFINE_string('injected_network_template', + utils.abspath('virt/interfaces.template'), + 'Template file for injected network') flags.DEFINE_integer('timeout_nbd', 10, 'time to wait for a NBD device coming up') flags.DEFINE_integer('max_nbd_devices', 16, @@ -97,11 +102,7 @@ def inject_data(image, key=None, net=None, partition=None, nbd=False): % err) try: - if key: - # inject key file - _inject_key_into_fs(key, tmpdir) - if net: - _inject_net_into_fs(net, tmpdir) + inject_data_into_fs(tmpdir, key, net, utils.execute) finally: # unmount device utils.execute('sudo', 'umount', mapped_device) @@ -164,7 +165,18 @@ def _free_device(device): _DEVICES.append(device) -def _inject_key_into_fs(key, fs): +def inject_data_into_fs(fs, key, net, execute): + """Injects data into a filesystem already mounted by the caller. + Virt connections can call this directly if they mount their fs + in a different way to inject_data + """ + if key: + _inject_key_into_fs(key, fs, execute=execute) + if net: + _inject_net_into_fs(net, fs, execute=execute) + + +def _inject_key_into_fs(key, fs, execute=None): """Add the given public ssh key to root's authorized_keys. key is an ssh key string. @@ -179,7 +191,7 @@ def _inject_key_into_fs(key, fs): process_input='\n' + key.strip() + '\n') -def _inject_net_into_fs(net, fs): +def _inject_net_into_fs(net, fs, execute=None): """Inject /etc/network/interfaces into the filesystem rooted at fs. net is the contents of /etc/network/interfaces. diff --git a/nova/virt/libvirt_conn.py b/nova/virt/libvirt_conn.py index 2cecb010d..5962507d7 100644 --- a/nova/virt/libvirt_conn.py +++ b/nova/virt/libvirt_conn.py @@ -76,9 +76,7 @@ flags.DECLARE('live_migration_retry_count', 'nova.compute.manager') flags.DEFINE_string('rescue_image_id', 'ami-rescue', 'Rescue ami image') flags.DEFINE_string('rescue_kernel_id', 'aki-rescue', 'Rescue aki image') flags.DEFINE_string('rescue_ramdisk_id', 'ari-rescue', 'Rescue ari image') -flags.DEFINE_string('injected_network_template', - utils.abspath('virt/interfaces.template'), - 'Template file for injected network') + flags.DEFINE_string('libvirt_xml_template', utils.abspath('virt/libvirt.xml.template'), 'Libvirt XML Template') diff --git a/nova/virt/xenapi/fake.py b/nova/virt/xenapi/fake.py index c1d7f5ae7..6d17a642e 100644 --- a/nova/virt/xenapi/fake.py +++ b/nova/virt/xenapi/fake.py @@ -168,6 +168,12 @@ def after_VBD_create(vbd_ref, vbd_rec): vbd_rec['vm_name_label'] = vm_name_label +def after_VM_create(vm_ref, vm_rec): + """Create read-only fields in the VM record.""" + if 'is_control_domain' not in vm_rec: + vm_rec['is_control_domain'] = False + + def create_pbd(config, host_ref, sr_ref, attached): return _create_object('PBD', { 'device-config': config, @@ -340,6 +346,10 @@ class SessionBase(object): return db_ref['xenstore_data'][key] = None + def network_get_all_records_where(self, _1, _2): + # TODO (salvatore-orlando):filter table on _2 + return _db_content['network'] + def VM_add_to_xenstore_data(self, _1, vm_ref, key, value): db_ref = _db_content['VM'][vm_ref] if not 'xenstore_data' in db_ref: @@ -439,7 +449,6 @@ class SessionBase(object): def _getter(self, name, params): self._check_session(params) (cls, func) = name.split('.') - if func == 'get_all': self._check_arg_count(params, 1) return get_all(cls) @@ -462,10 +471,11 @@ class SessionBase(object): if len(params) == 2: field = func[len('get_'):] ref = params[1] - - if (ref in _db_content[cls] and - field in _db_content[cls][ref]): - return _db_content[cls][ref][field] + if (ref in _db_content[cls]): + if (field in _db_content[cls][ref]): + return _db_content[cls][ref][field] + else: + raise Failure(['HANDLE_INVALID', cls, ref]) LOG.debug(_('Raising NotImplemented')) raise NotImplementedError( @@ -543,7 +553,7 @@ class SessionBase(object): def _check_session(self, params): if (self._session is None or self._session not in _db_content['session']): - raise Failure(['HANDLE_INVALID', 'session', self._session]) + raise Failure(['HANDLE_INVALID', 'session', self._session]) if len(params) == 0 or params[0] != self._session: LOG.debug(_('Raising NotImplemented')) raise NotImplementedError('Call to XenAPI without using .xenapi') diff --git a/nova/virt/xenapi/vm_utils.py b/nova/virt/xenapi/vm_utils.py index c30e4b2d1..2288ea8a5 100644 --- a/nova/virt/xenapi/vm_utils.py +++ b/nova/virt/xenapi/vm_utils.py @@ -22,6 +22,7 @@ their attributes like VDIs, VIFs, as well as their lookup functions. import os import pickle import re +import tempfile import time import urllib import uuid @@ -29,6 +30,8 @@ from xml.dom import minidom from eventlet import event import glance.client +from nova import context +from nova import db from nova import exception from nova import flags from nova import log as logging @@ -36,6 +39,7 @@ from nova import utils from nova.auth.manager import AuthManager from nova.compute import instance_types from nova.compute import power_state +from nova.virt import disk from nova.virt import images from nova.virt.xenapi import HelperBase from nova.virt.xenapi.volume_utils import StorageError @@ -670,6 +674,23 @@ class VMHelper(HelperBase): return None @classmethod + def preconfigure_instance(cls, session, instance, vdi_ref, network_info): + """Makes alterations to the image before launching as part of spawn. + """ + + # As mounting the image VDI is expensive, we only want do do it once, + # if at all, so determine whether it's required first, and then do + # everything + mount_required = False + key, net = _prepare_injectables(instance, network_info) + mount_required = key or net + if not mount_required: + return + + with_vdi_attached_here(session, vdi_ref, False, + lambda dev: _mounted_processing(dev, key, net)) + + @classmethod def lookup_kernel_ramdisk(cls, session, vm): vm_rec = session.get_xenapi().VM.get_record(vm) if 'PV_kernel' in vm_rec and 'PV_ramdisk' in vm_rec: @@ -927,6 +948,7 @@ def vbd_unplug_with_retry(session, vbd_ref): e.details[0] == 'DEVICE_DETACH_REJECTED'): LOG.debug(_('VBD.unplug rejected: retrying...')) time.sleep(1) + LOG.debug(_('Not sleeping anymore!')) elif (len(e.details) > 0 and e.details[0] == 'DEVICE_ALREADY_DETACHED'): LOG.debug(_('VBD.unplug successful eventually.')) @@ -1002,3 +1024,114 @@ def _write_partition(virtual_size, dev): def get_name_label_for_image(image): # TODO(sirp): This should eventually be the URI for the Glance image return _('Glance image %s') % image + + +def _mount_filesystem(dev_path, dir): + """mounts the device specified by dev_path in dir""" + try: + out, err = utils.execute('sudo', 'mount', + '-t', 'ext2,ext3', + dev_path, dir) + except exception.ProcessExecutionError as e: + err = str(e) + return err + + +def _find_guest_agent(base_dir, agent_rel_path): + """ + tries to locate a guest agent at the path + specificed by agent_rel_path + """ + agent_path = os.path.join(base_dir, agent_rel_path) + if os.path.isfile(agent_path): + # The presence of the guest agent + # file indicates that this instance can + # reconfigure the network from xenstore data, + # so manipulation of files in /etc is not + # required + LOG.info(_('XenServer tools installed in this ' + 'image are capable of network injection. ' + 'Networking files will not be' + 'manipulated')) + return True + xe_daemon_filename = os.path.join(base_dir, + 'usr', 'sbin', 'xe-daemon') + if os.path.isfile(xe_daemon_filename): + LOG.info(_('XenServer tools are present ' + 'in this image but are not capable ' + 'of network injection')) + else: + LOG.info(_('XenServer tools are not ' + 'installed in this image')) + return False + + +def _mounted_processing(device, key, net): + """Callback which runs with the image VDI attached""" + + dev_path = '/dev/' + device + '1' # NB: Partition 1 hardcoded + tmpdir = tempfile.mkdtemp() + try: + # Mount only Linux filesystems, to avoid disturbing NTFS images + err = _mount_filesystem(dev_path, tmpdir) + if not err: + try: + # This try block ensures that the umount occurs + if not _find_guest_agent(tmpdir, FLAGS.xenapi_agent_path): + LOG.info(_('Manipulating interface files ' + 'directly')) + disk.inject_data_into_fs(tmpdir, key, net, + utils.execute) + finally: + utils.execute('sudo', 'umount', dev_path) + else: + LOG.info(_('Failed to mount filesystem (expected for ' + 'non-linux instances): %s') % err) + finally: + # remove temporary directory + os.rmdir(tmpdir) + + +def _prepare_injectables(inst, networks_info): + """ + prepares the ssh key and the network configuration file to be + injected into the disk image + """ + #do the import here - Cheetah.Template will be loaded + #only if injection is performed + from Cheetah import Template as t + template = t.Template + template_data = open(FLAGS.injected_network_template).read() + + key = str(inst['key_data']) + net = None + if networks_info: + ifc_num = -1 + interfaces_info = [] + for (network_ref, info) in networks_info: + ifc_num += 1 + if not network_ref['injected']: + continue + + ip_v4 = ip_v6 = None + if 'ips' in info and len(info['ips']) > 0: + ip_v4 = info['ips'][0] + if 'ip6s' in info and len(info['ip6s']) > 0: + ip_v6 = info['ip6s'][0] + if len(info['dns']) > 0: + dns = info['dns'][0] + interface_info = {'name': 'eth%d' % ifc_num, + 'address': ip_v4 and ip_v4['ip'] or '', + 'netmask': ip_v4 and ip_v4['netmask'] or '', + 'gateway': info['gateway'], + 'broadcast': info['broadcast'], + 'dns': dns, + 'address_v6': ip_v6 and ip_v6['ip'] or '', + 'netmask_v6': ip_v6 and ip_v6['netmask'] or '', + 'gateway_v6': ip_v6 and ip_v6['gateway'] or '', + 'use_ipv6': FLAGS.use_ipv6} + interfaces_info.append(interface_info) + net = str(template(template_data, + searchList=[{'interfaces': interfaces_info, + 'use_ipv6': FLAGS.use_ipv6}])) + return key, net diff --git a/nova/virt/xenapi/vmops.py b/nova/virt/xenapi/vmops.py index 2e1f3ee91..52e1f9eba 100644 --- a/nova/virt/xenapi/vmops.py +++ b/nova/virt/xenapi/vmops.py @@ -33,6 +33,7 @@ from nova import context from nova import log as logging from nova import exception from nova import utils +from nova import flags from nova.auth.manager import AuthManager from nova.compute import power_state @@ -43,6 +44,7 @@ from nova.virt.xenapi.vm_utils import ImageType XenAPI = None LOG = logging.getLogger("nova.virt.xenapi.vmops") +FLAGS = flags.FLAGS class VMOps(object): @@ -53,7 +55,6 @@ class VMOps(object): self.XenAPI = session.get_imported_xenapi() self._session = session self.poll_rescue_last_ran = None - VMHelper.XenAPI = self.XenAPI def list_instances(self): @@ -168,6 +169,12 @@ class VMOps(object): # create it now. This goes away once nova-multi-nic hits. if network_info is None: network_info = self._get_network_info(instance) + + # Alter the image before VM start for, e.g. network injection + if FLAGS.xenapi_inject_image: + VMHelper.preconfigure_instance(self._session, instance, + vdi_ref, network_info) + self.create_vifs(vm_ref, network_info) self.inject_network_info(instance, vm_ref, network_info) return vm_ref @@ -237,26 +244,17 @@ class VMOps(object): obj = None try: # check for opaque ref - obj = self._session.get_xenapi().VM.get_record(instance_or_vm) + obj = self._session.get_xenapi().VM.get_uuid(instance_or_vm) return instance_or_vm except self.XenAPI.Failure: - # wasn't an opaque ref, must be an instance name + # wasn't an opaque ref, can be an instance name instance_name = instance_or_vm # if instance_or_vm is an int/long it must be instance id elif isinstance(instance_or_vm, (int, long)): ctx = context.get_admin_context() - try: - instance_obj = db.instance_get(ctx, instance_or_vm) - instance_name = instance_obj.name - except exception.NotFound: - # The unit tests screw this up, as they use an integer for - # the vm name. I'd fix that up, but that's a matter for - # another bug report. So for now, just try with the passed - # value - instance_name = instance_or_vm - - # otherwise instance_or_vm is an instance object + instance_obj = db.instance_get(ctx, instance_or_vm) + instance_name = instance_obj.name else: instance_name = instance_or_vm.name vm_ref = VMHelper.lookup(self._session, instance_name) @@ -692,7 +690,6 @@ class VMOps(object): vm_ref = VMHelper.lookup(self._session, instance.name) self._shutdown(instance, vm_ref) self._acquire_bootlock(vm_ref) - instance._rescue = True self.spawn_rescue(instance) rescue_vm_ref = VMHelper.lookup(self._session, instance.name) @@ -816,6 +813,7 @@ class VMOps(object): info = { 'label': network['label'], 'gateway': network['gateway'], + 'broadcast': network['broadcast'], 'mac': instance.mac_address, 'rxtx_cap': flavor['rxtx_cap'], 'dns': [network['dns']], diff --git a/nova/virt/xenapi_conn.py b/nova/virt/xenapi_conn.py index f20fb29d8..99fd35c61 100644 --- a/nova/virt/xenapi_conn.py +++ b/nova/virt/xenapi_conn.py @@ -107,8 +107,22 @@ flags.DEFINE_integer('xenapi_vhd_coalesce_max_attempts', 5, 'Max number of times to poll for VHD to coalesce.' ' Used only if connection_type=xenapi.') +flags.DEFINE_bool('xenapi_inject_image', + True, + 'Specifies whether an attempt to inject network/key' + ' data into the disk image should be made.' + ' Used only if connection_type=xenapi.') +flags.DEFINE_string('xenapi_agent_path', + 'usr/sbin/xe-update-networking', + 'Specifies the path in which the xenapi guest agent' + ' should be located. If the agent is present,' + ' network configuration is not injected into the image' + ' Used only if connection_type=xenapi.' + ' and xenapi_inject_image=True') + flags.DEFINE_string('xenapi_sr_base_path', '/var/run/sr-mount', 'Base path to the storage repository') + flags.DEFINE_string('target_host', None, 'iSCSI Target Host') |
