diff options
-rw-r--r-- | doc/source/support-matrix.ini | 2 | ||||
-rw-r--r-- | nova/api/openstack/compute/contrib/virtual_interfaces.py | 10 | ||||
-rw-r--r-- | nova/api/openstack/compute/plugins/v3/virtual_interfaces.py | 12 | ||||
-rw-r--r-- | nova/cells/messaging.py | 26 | ||||
-rw-r--r-- | nova/compute/api.py | 12 | ||||
-rw-r--r-- | nova/tests/unit/api/openstack/compute/contrib/test_virtual_interfaces.py | 12 | ||||
-rw-r--r-- | nova/tests/unit/cells/test_cells_messaging.py | 132 | ||||
-rw-r--r-- | nova/tests/unit/compute/test_compute_api.py | 20 | ||||
-rw-r--r-- | nova/tests/unit/virt/libvirt/test_driver.py | 82 | ||||
-rw-r--r-- | nova/tests/unit/virt/libvirt/test_host.py | 21 | ||||
-rw-r--r-- | nova/tests/unit/virt/test_block_device.py | 103 | ||||
-rw-r--r-- | nova/tests/unit/virt/test_virt_drivers.py | 1 | ||||
-rw-r--r-- | nova/virt/block_device.py | 38 | ||||
-rw-r--r-- | nova/virt/libvirt/driver.py | 41 | ||||
-rw-r--r-- | nova/virt/libvirt/host.py | 5 |
15 files changed, 376 insertions, 141 deletions
diff --git a/doc/source/support-matrix.ini b/doc/source/support-matrix.ini index be6d6cdfb6..00ab29c42f 100644 --- a/doc/source/support-matrix.ini +++ b/doc/source/support-matrix.ini @@ -807,7 +807,7 @@ driver-impl-libvirt-kvm-s390x=complete driver-impl-libvirt-qemu-x86=complete driver-impl-libvirt-lxc=complete driver-impl-libvirt-xen=complete -driver-impl-vmware=complete +driver-impl-vmware=missing driver-impl-hyperv=complete driver-impl-ironic=missing driver-impl-libvirt-parallels-vm=complete diff --git a/nova/api/openstack/compute/contrib/virtual_interfaces.py b/nova/api/openstack/compute/contrib/virtual_interfaces.py index db75afc533..1e319efa4e 100644 --- a/nova/api/openstack/compute/contrib/virtual_interfaces.py +++ b/nova/api/openstack/compute/contrib/virtual_interfaces.py @@ -15,9 +15,12 @@ """The virtual interfaces extension.""" +import webob + from nova.api.openstack import common from nova.api.openstack import extensions from nova import compute +from nova.i18n import _ from nova import network @@ -46,7 +49,12 @@ class ServerVirtualInterfaceController(object): context = req.environ['nova.context'] instance = common.get_instance(self.compute_api, context, server_id) - vifs = self.network_api.get_vifs_by_instance(context, instance) + try: + vifs = self.network_api.get_vifs_by_instance(context, instance) + except NotImplementedError: + msg = _('Listing virtual interfaces is not supported by this ' + 'cloud.') + raise webob.exc.HTTPBadRequest(explanation=msg) limited_list = common.limited(vifs, req) res = [entity_maker(context, vif) for vif in limited_list] return {'virtual_interfaces': res} diff --git a/nova/api/openstack/compute/plugins/v3/virtual_interfaces.py b/nova/api/openstack/compute/plugins/v3/virtual_interfaces.py index 50c86a754b..df35624c78 100644 --- a/nova/api/openstack/compute/plugins/v3/virtual_interfaces.py +++ b/nova/api/openstack/compute/plugins/v3/virtual_interfaces.py @@ -15,10 +15,13 @@ """The virtual interfaces extension.""" +import webob + from nova.api.openstack import common from nova.api.openstack import extensions from nova.api.openstack import wsgi from nova import compute +from nova.i18n import _ from nova import network @@ -49,12 +52,17 @@ class ServerVirtualInterfaceController(wsgi.Controller): authorize(context) instance = common.get_instance(self.compute_api, context, server_id) - vifs = self.network_api.get_vifs_by_instance(context, instance) + try: + vifs = self.network_api.get_vifs_by_instance(context, instance) + except NotImplementedError: + msg = _('Listing virtual interfaces is not supported by this ' + 'cloud.') + raise webob.exc.HTTPBadRequest(explanation=msg) limited_list = common.limited(vifs, req) res = [entity_maker(context, vif) for vif in limited_list] return {'virtual_interfaces': res} - @extensions.expected_errors((404)) + @extensions.expected_errors((400, 404)) def index(self, req, server_id): """Returns the list of VIFs for a given instance.""" return self._items(req, server_id, diff --git a/nova/cells/messaging.py b/nova/cells/messaging.py index a4ac21d481..baa6eef412 100644 --- a/nova/cells/messaging.py +++ b/nova/cells/messaging.py @@ -668,9 +668,15 @@ class _TargetedMessageMethods(_BaseMessageMethods): # 1st arg is instance_uuid that we need to turn into the # instance object. instance_uuid = args[0] + # NOTE: compute/api.py loads these when retrieving an instance for an + # API request, so there's a good chance that this is what was loaded. + expected_attrs = ['metadata', 'system_metadata', 'security_groups', + 'info_cache'] + try: - instance = self.db.instance_get_by_uuid(message.ctxt, - instance_uuid) + instance = objects.Instance.get_by_uuid(message.ctxt, + instance_uuid, expected_attrs=expected_attrs) + args[0] = instance except exception.InstanceNotFound: with excutils.save_and_reraise_exception(): # Must be a race condition. Let's try to resolve it by @@ -679,22 +685,6 @@ class _TargetedMessageMethods(_BaseMessageMethods): instance = {'uuid': instance_uuid} self.msg_runner.instance_destroy_at_top(message.ctxt, instance) - # FIXME(comstud): This is temporary/transitional until I can - # work out a better way to pass full objects down. - EXPECTS_OBJECTS = ['start', 'stop', 'delete_instance_metadata', - 'update_instance_metadata', 'shelve', 'unshelve'] - if method in EXPECTS_OBJECTS: - inst_obj = objects.Instance() - expected_attrs = None - # shelve and unshelve requires 'info_cache' and 'metadata', - # because of this fetching same from database. - if method in ['shelve', 'unshelve']: - expected_attrs = ['metadata', 'info_cache'] - - inst_obj._from_db_object(message.ctxt, inst_obj, instance, - expected_attrs=expected_attrs) - instance = inst_obj - args[0] = instance return fn(message.ctxt, *args, **method_info['method_kwargs']) def update_capabilities(self, message, cell_name, capabilities): diff --git a/nova/compute/api.py b/nova/compute/api.py index 433e855164..0fb8e6a23c 100644 --- a/nova/compute/api.py +++ b/nova/compute/api.py @@ -2490,7 +2490,7 @@ class API(base.Base): elevated, instance.uuid, 'finished') # reverse quota reservation for increased resource usage - deltas = self._reverse_upsize_quota_delta(context, migration) + deltas = self._reverse_upsize_quota_delta(context, instance) quotas = self._reserve_quota_delta(context, deltas, instance) instance.task_state = task_states.RESIZE_REVERTING @@ -2580,16 +2580,12 @@ class API(base.Base): return API._resize_quota_delta(context, new_flavor, old_flavor, 1, 1) @staticmethod - def _reverse_upsize_quota_delta(context, migration_ref): + def _reverse_upsize_quota_delta(context, instance): """Calculate deltas required to reverse a prior upsizing quota adjustment. """ - old_flavor = objects.Flavor.get_by_id( - context, migration_ref['old_instance_type_id']) - new_flavor = objects.Flavor.get_by_id( - context, migration_ref['new_instance_type_id']) - - return API._resize_quota_delta(context, new_flavor, old_flavor, -1, -1) + return API._resize_quota_delta(context, instance.new_flavor, + instance.old_flavor, -1, -1) @staticmethod def _downsize_quota_delta(context, instance): diff --git a/nova/tests/unit/api/openstack/compute/contrib/test_virtual_interfaces.py b/nova/tests/unit/api/openstack/compute/contrib/test_virtual_interfaces.py index 5a595ec232..87d71ff1ff 100644 --- a/nova/tests/unit/api/openstack/compute/contrib/test_virtual_interfaces.py +++ b/nova/tests/unit/api/openstack/compute/contrib/test_virtual_interfaces.py @@ -85,6 +85,18 @@ class ServerVirtualInterfaceTestV21(test.NoDBTestCase): self.controller.index, fake_req, 'fake_uuid') + def test_list_vifs_neutron_notimplemented(self): + """Tests that a 400 is returned when using neutron as the backend""" + # unset the get_vifs_by_instance stub from setUp + self.mox.UnsetStubs() + self.flags(network_api_class='nova.network.neutronv2.api.API') + # reset the controller to use the neutron network API + self._set_controller() + self.stubs.Set(compute.api.API, "get", compute_api_get) + req = fakes.HTTPRequest.blank('') + self.assertRaises(webob.exc.HTTPBadRequest, + self.controller.index, req, FAKE_UUID) + class ServerVirtualInterfaceTestV20(ServerVirtualInterfaceTestV21): diff --git a/nova/tests/unit/cells/test_cells_messaging.py b/nova/tests/unit/cells/test_cells_messaging.py index 3d1d1ace06..a1824ab814 100644 --- a/nova/tests/unit/cells/test_cells_messaging.py +++ b/nova/tests/unit/cells/test_cells_messaging.py @@ -683,120 +683,62 @@ class CellsTargetedMethodsTestCase(test.TestCase): self.src_msg_runner.build_instances(self.ctxt, self.tgt_cell_name, build_inst_kwargs) - def test_run_compute_api_method(self): - - instance_uuid = 'fake_instance_uuid' - method_info = {'method': 'backup', - 'method_args': (instance_uuid, 2, 3), - 'method_kwargs': {'arg1': 'val1', 'arg2': 'val2'}} - self.mox.StubOutWithMock(self.tgt_compute_api, 'backup') - self.mox.StubOutWithMock(self.tgt_db_inst, 'instance_get_by_uuid') - - self.tgt_db_inst.instance_get_by_uuid(self.ctxt, - instance_uuid).AndReturn('fake_instance') - self.tgt_compute_api.backup(self.ctxt, 'fake_instance', 2, 3, - arg1='val1', arg2='val2').AndReturn('fake_result') - self.mox.ReplayAll() - - response = self.src_msg_runner.run_compute_api_method( - self.ctxt, - self.tgt_cell_name, - method_info, - True) - result = response.value_or_raise() - self.assertEqual('fake_result', result) - - def _run_compute_api_method_expects_object(self, tgt_compute_api_function, - method_name, - expected_attrs=None): - # runs compute api methods which expects instance to be an object - instance_uuid = 'fake_instance_uuid' + def _run_compute_api_method(self, method_name): + instance = objects.Instance(self.ctxt, uuid=uuidutils.generate_uuid()) method_info = {'method': method_name, - 'method_args': (instance_uuid, 2, 3), + 'method_args': (instance.uuid, 2, 3), 'method_kwargs': {'arg1': 'val1', 'arg2': 'val2'}} - self.mox.StubOutWithMock(self.tgt_db_inst, 'instance_get_by_uuid') - - self.tgt_db_inst.instance_get_by_uuid(self.ctxt, - instance_uuid).AndReturn('fake_instance') - - def get_instance_mock(): - # NOTE(comstud): This block of code simulates the following - # mox code: - # - # self.mox.StubOutWithMock(objects, 'Instance', - # use_mock_anything=True) - # self.mox.StubOutWithMock(objects.Instance, - # '_from_db_object') - # instance_mock = self.mox.CreateMock(objects.Instance) - # objects.Instance().AndReturn(instance_mock) - # - # Unfortunately, the above code fails on py27 do to some - # issue with the Mock object do to similar issue as this: - # https://code.google.com/p/pymox/issues/detail?id=35 - # - class FakeInstance(object): - @classmethod - def _from_db_object(cls, ctxt, obj, db_obj, **kwargs): - pass - - instance_mock = FakeInstance() - - def fake_instance(): - return instance_mock - - self.stubs.Set(objects, 'Instance', fake_instance) - self.mox.StubOutWithMock(instance_mock, '_from_db_object') - return instance_mock - - instance = get_instance_mock() - instance._from_db_object(self.ctxt, - instance, - 'fake_instance', - expected_attrs=expected_attrs - ).AndReturn(instance) - tgt_compute_api_function(self.ctxt, instance, 2, 3, - arg1='val1', arg2='val2').AndReturn('fake_result') - self.mox.ReplayAll() - - response = self.src_msg_runner.run_compute_api_method( - self.ctxt, - self.tgt_cell_name, - method_info, - True) - result = response.value_or_raise() - self.assertEqual('fake_result', result) + expected_attrs = ['metadata', 'system_metadata', 'security_groups', + 'info_cache'] + + @mock.patch.object(self.tgt_compute_api, method_name, + return_value='fake-result') + @mock.patch.object(objects.Instance, 'get_by_uuid', + return_value=instance) + def run_method(mock_get_by_uuid, mock_method): + response = self.src_msg_runner.run_compute_api_method( + self.ctxt, + self.tgt_cell_name, + method_info, + True) + result = response.value_or_raise() + self.assertEqual('fake-result', result) + + mock_get_by_uuid.assert_called_once_with(self.ctxt, instance.uuid, + expected_attrs=expected_attrs) + mock_method.assert_called_once_with(self.ctxt, instance, 2, 3, + arg1='val1', arg2='val2') + + run_method() def test_run_compute_api_method_expects_obj(self): # Run compute_api start method - self.mox.StubOutWithMock(self.tgt_compute_api, 'start') - self._run_compute_api_method_expects_object(self.tgt_compute_api.start, - 'start') + self._run_compute_api_method('start') - def test_run_compute_api_method_expects_obj_with_info_cache(self): + def test_run_compute_api_method_shelve_with_info_cache(self): # Run compute_api shelve method as it requires info_cache and # metadata to be present in instance object - self.mox.StubOutWithMock(self.tgt_compute_api, 'shelve') - self._run_compute_api_method_expects_object( - self.tgt_compute_api.shelve, 'shelve', - expected_attrs=['metadata', 'info_cache']) + self._run_compute_api_method('shelve') def test_run_compute_api_method_unknown_instance(self): # Unknown instance should send a broadcast up that instance # is gone. - instance_uuid = 'fake_instance_uuid' - instance = {'uuid': instance_uuid} + instance = objects.Instance(self.ctxt, uuid=uuidutils.generate_uuid()) + instance_uuid = instance.uuid method_info = {'method': 'reboot', 'method_args': (instance_uuid, 2, 3), 'method_kwargs': {'arg1': 'val1', 'arg2': 'val2'}} - self.mox.StubOutWithMock(self.tgt_db_inst, 'instance_get_by_uuid') + self.mox.StubOutWithMock(objects.Instance, 'get_by_uuid') self.mox.StubOutWithMock(self.tgt_msg_runner, 'instance_destroy_at_top') - self.tgt_db_inst.instance_get_by_uuid(self.ctxt, - 'fake_instance_uuid').AndRaise( - exception.InstanceNotFound(instance_id=instance_uuid)) - self.tgt_msg_runner.instance_destroy_at_top(self.ctxt, instance) + objects.Instance.get_by_uuid(self.ctxt, instance.uuid, + expected_attrs=['metadata', 'system_metadata', + 'security_groups', 'info_cache']).AndRaise( + exception.InstanceNotFound(instance_id=instance_uuid)) + self.tgt_msg_runner.instance_destroy_at_top(self.ctxt, + {'uuid': instance.uuid}) self.mox.ReplayAll() diff --git a/nova/tests/unit/compute/test_compute_api.py b/nova/tests/unit/compute/test_compute_api.py index fc613e9cff..1f3916c43b 100644 --- a/nova/tests/unit/compute/test_compute_api.py +++ b/nova/tests/unit/compute/test_compute_api.py @@ -1168,7 +1168,7 @@ class _ComputeAPIUnitTestMixIn(object): self.context, fake_inst['uuid'], 'finished').AndReturn( fake_mig) self.compute_api._reverse_upsize_quota_delta( - self.context, fake_mig).AndReturn('deltas') + self.context, fake_inst).AndReturn('deltas') resvs = ['resvs'] fake_quotas = objects.Quotas.from_reservations(self.context, resvs) @@ -1226,7 +1226,7 @@ class _ComputeAPIUnitTestMixIn(object): delta = ['delta'] self.compute_api._reverse_upsize_quota_delta( - self.context, fake_mig).AndReturn(delta) + self.context, fake_inst).AndReturn(delta) resvs = ['resvs'] fake_quotas = objects.Quotas.from_reservations(self.context, resvs) self.compute_api._reserve_quota_delta( @@ -1244,6 +1244,22 @@ class _ComputeAPIUnitTestMixIn(object): self.context, fake_inst) + def test_reverse_quota_delta(self): + inst = self._create_instance_obj(params=None) + inst.old_flavor = self._create_flavor(vcpus=1, memory_mb=512) + inst.new_flavor = self._create_flavor(vcpus=2, memory_mb=4096) + + expected_deltas = { + 'cores': -1 * (inst.new_flavor['vcpus'] - + inst.old_flavor['vcpus']), + 'ram': -1 * (inst.new_flavor['memory_mb'] - + inst.old_flavor['memory_mb']) + } + + deltas = self.compute_api._reverse_upsize_quota_delta( + self.context, inst) + self.assertEqual(expected_deltas, deltas) + def _test_resize(self, flavor_id_passed=True, same_host=False, allow_same_host=False, allow_mig_same_host=False, diff --git a/nova/tests/unit/virt/libvirt/test_driver.py b/nova/tests/unit/virt/libvirt/test_driver.py index 8930d9dea7..6ef3997588 100644 --- a/nova/tests/unit/virt/libvirt/test_driver.py +++ b/nova/tests/unit/virt/libvirt/test_driver.py @@ -6608,6 +6608,43 @@ class LibvirtConnTestCase(test.NoDBTestCase): self.context, instance, block_device_info=None, network_info=[], disk_info={}, migrate_data={}) + def test_pre_live_migration_recreate_disk_info(self): + + migrate_data = {'is_shared_block_storage': False, + 'is_shared_instance_path': False, + 'block_migration': True, + 'instance_relative_path': '/some/path/'} + disk_info = [{'disk_size': 5368709120, 'type': 'raw', + 'virt_disk_size': 5368709120, + 'path': '/some/path/disk', + 'backing_file': '', 'over_committed_disk_size': 0}, + {'disk_size': 1073741824, 'type': 'raw', + 'virt_disk_size': 1073741824, + 'path': '/some/path/disk.eph0', + 'backing_file': '', 'over_committed_disk_size': 0}] + image_disk_info = {'/some/path/disk': 'raw', + '/some/path/disk.eph0': 'raw'} + + drvr = libvirt_driver.LibvirtDriver(fake.FakeVirtAPI(), False) + instance = objects.Instance(**self.test_instance) + instance_path = os.path.dirname(disk_info[0]['path']) + disk_info_path = os.path.join(instance_path, 'disk.info') + + with contextlib.nested( + mock.patch.object(os, 'mkdir'), + mock.patch.object(fake_libvirt_utils, 'write_to_file'), + mock.patch.object(drvr, '_create_images_and_backing') + ) as ( + mkdir, write_to_file, create_images_and_backing + ): + drvr.pre_live_migration(self.context, instance, + block_device_info=None, + network_info=[], + disk_info=jsonutils.dumps(disk_info), + migrate_data=migrate_data) + write_to_file.assert_called_with(disk_info_path, + jsonutils.dumps(image_disk_info)) + def test_get_instance_disk_info_works_correctly(self): # Test data instance = objects.Instance(**self.test_instance) @@ -11607,6 +11644,51 @@ class LibvirtDriverTestCase(test.NoDBTestCase): flavor_obj = objects.Flavor(**flavor) self._test_migrate_disk_and_power_off(flavor_obj) + @mock.patch('nova.utils.execute') + @mock.patch('nova.virt.libvirt.utils.copy_image') + @mock.patch('nova.virt.libvirt.driver.LibvirtDriver._destroy') + @mock.patch('nova.virt.libvirt.utils.get_instance_path') + @mock.patch('nova.virt.libvirt.driver.LibvirtDriver' + '._is_storage_shared_with') + @mock.patch('nova.virt.libvirt.driver.LibvirtDriver' + '.get_instance_disk_info') + def test_migrate_disk_and_power_off_resize_copy_disk_info(self, + mock_disk_info, + mock_shared, + mock_path, + mock_destroy, + mock_copy, + mock_execuate): + + instance = self._create_instance() + disk_info = self._disk_info() + disk_info_text = jsonutils.loads(disk_info) + instance_base = os.path.dirname(disk_info_text[0]['path']) + flavor = {'root_gb': 10, 'ephemeral_gb': 25} + flavor_obj = objects.Flavor(**flavor) + + mock_disk_info.return_value = disk_info + mock_path.return_value = instance_base + mock_shared.return_value = False + + src_disk_info_path = os.path.join(instance_base + '_resize', + 'disk.info') + + with mock.patch.object(os.path, 'exists', autospec=True) \ + as mock_exists: + # disk.info exists on the source + mock_exists.side_effect = \ + lambda path: path == src_disk_info_path + self.drvr.migrate_disk_and_power_off(context.get_admin_context(), + instance, mock.sentinel, + flavor_obj, None) + self.assertTrue(mock_exists.called) + + dst_disk_info_path = os.path.join(instance_base, 'disk.info') + mock_copy.assert_any_call(src_disk_info_path, dst_disk_info_path, + host=mock.sentinel, on_execute=mock.ANY, + on_completion=mock.ANY) + def test_wait_for_running(self): def fake_get_info(instance): if instance['name'] == "not_found": diff --git a/nova/tests/unit/virt/libvirt/test_host.py b/nova/tests/unit/virt/libvirt/test_host.py index 1f7f68ca6d..e7df6b40eb 100644 --- a/nova/tests/unit/virt/libvirt/test_host.py +++ b/nova/tests/unit/virt/libvirt/test_host.py @@ -660,6 +660,27 @@ class HostTestCase(test.NoDBTestCase): self.assertEqual(vconfig.LibvirtConfigCaps, type(caps)) self.assertNotIn('aes', [x.name for x in caps.host.cpu.features]) + def test_get_capabilities_no_host_cpu_model(self): + """Tests that cpu features are not retrieved when the host cpu model + is not in the capabilities. + """ + fake_caps_xml = ''' +<capabilities> + <host> + <uuid>cef19ce0-0ca2-11df-855d-b19fbce37686</uuid> + <cpu> + <arch>x86_64</arch> + <vendor>Intel</vendor> + </cpu> + </host> +</capabilities>''' + with mock.patch.object(fakelibvirt.virConnect, 'getCapabilities', + return_value=fake_caps_xml): + caps = self.host.get_capabilities() + self.assertEqual(vconfig.LibvirtConfigCaps, type(caps)) + self.assertIsNone(caps.host.cpu.model) + self.assertEqual(0, len(caps.host.cpu.features)) + @mock.patch.object(fakelibvirt.virConnect, "getHostname") def test_get_hostname_caching(self, mock_hostname): mock_hostname.return_value = "foo" diff --git a/nova/tests/unit/virt/test_block_device.py b/nova/tests/unit/virt/test_block_device.py index 15e1a3c4e0..937d242da2 100644 --- a/nova/tests/unit/virt/test_block_device.py +++ b/nova/tests/unit/virt/test_block_device.py @@ -363,13 +363,15 @@ class TestDriverBlockDevice(test.NoDBTestCase): fake_volume, check_attach=True, fail_check_attach=False, driver_attach=False, fail_driver_attach=False, volume_attach=True, - fail_volume_attach=False, access_mode='rw'): + fail_volume_attach=False, access_mode='rw', + availability_zone=None): elevated_context = self.context.elevated() self.stubs.Set(self.context, 'elevated', lambda: elevated_context) self.mox.StubOutWithMock(driver_bdm._bdm_obj, 'save') self.mox.StubOutWithMock(encryptors, 'get_encryption_metadata') - instance_detail = {'id': '123', 'uuid': 'fake_uuid'} + instance_detail = {'id': '123', 'uuid': 'fake_uuid', + 'availability_zone': availability_zone} instance = fake_instance.fake_instance_obj(self.context, **instance_detail) connector = {'ip': 'fake_ip', 'host': 'fake_host'} @@ -591,6 +593,35 @@ class TestDriverBlockDevice(test.NoDBTestCase): self.virt_driver, wait_func) self.assertEqual(test_bdm.volume_id, 'fake-volume-id-2') + def test_snapshot_attach_no_volume_cinder_cross_az_attach_false(self): + # Tests that the volume created from the snapshot has the same AZ as + # the instance. + self.flags(cross_az_attach=False, group='cinder') + no_volume_snapshot = self.snapshot_bdm.copy() + no_volume_snapshot['volume_id'] = None + test_bdm = self.driver_classes['snapshot'](no_volume_snapshot) + + snapshot = {'id': 'fake-volume-id-1', + 'attach_status': 'detached'} + volume = {'id': 'fake-volume-id-2', + 'attach_status': 'detached'} + + wait_func = self.mox.CreateMockAnything() + + self.volume_api.get_snapshot(self.context, + 'fake-snapshot-id-1').AndReturn(snapshot) + self.volume_api.create(self.context, 3, '', '', snapshot, + availability_zone='test-az').AndReturn(volume) + wait_func(self.context, 'fake-volume-id-2').AndReturn(None) + instance, expected_conn_info = self._test_volume_attach( + test_bdm, no_volume_snapshot, volume, + availability_zone='test-az') + self.mox.ReplayAll() + + test_bdm.attach(self.context, instance, self.volume_api, + self.virt_driver, wait_func) + self.assertEqual('fake-volume-id-2', test_bdm.volume_id) + def test_snapshot_attach_fail_volume(self): fail_volume_snapshot = self.snapshot_bdm.copy() fail_volume_snapshot['volume_id'] = None @@ -672,6 +703,32 @@ class TestDriverBlockDevice(test.NoDBTestCase): self.virt_driver, wait_func) self.assertEqual(test_bdm.volume_id, 'fake-volume-id-2') + def test_image_attach_no_volume_cinder_cross_az_attach_false(self): + # Tests that the volume created from the image has the same AZ as the + # instance. + self.flags(cross_az_attach=False, group='cinder') + no_volume_image = self.image_bdm.copy() + no_volume_image['volume_id'] = None + test_bdm = self.driver_classes['image'](no_volume_image) + + image = {'id': 'fake-image-id-1'} + volume = {'id': 'fake-volume-id-2', + 'attach_status': 'detached'} + + wait_func = self.mox.CreateMockAnything() + + self.volume_api.create(self.context, 1, '', '', image_id=image['id'], + availability_zone='test-az').AndReturn(volume) + wait_func(self.context, 'fake-volume-id-2').AndReturn(None) + instance, expected_conn_info = self._test_volume_attach( + test_bdm, no_volume_image, volume, + availability_zone='test-az') + self.mox.ReplayAll() + + test_bdm.attach(self.context, instance, self.volume_api, + self.virt_driver, wait_func) + self.assertEqual('fake-volume-id-2', test_bdm.volume_id) + def test_image_attach_fail_volume(self): fail_volume_image = self.image_bdm.copy() fail_volume_image['volume_id'] = None @@ -755,7 +812,7 @@ class TestDriverBlockDevice(test.NoDBTestCase): vol_create.assert_called_once_with( self.context, test_bdm.volume_size, 'fake-uuid-blank-vol', - '', availability_zone=instance.availability_zone) + '', availability_zone=None) vol_delete.assert_called_once_with( self.context, volume['id']) @@ -778,13 +835,42 @@ class TestDriverBlockDevice(test.NoDBTestCase): vol_create.assert_called_once_with( self.context, test_bdm.volume_size, 'fake-uuid-blank-vol', - '', availability_zone=instance.availability_zone) + '', availability_zone=None) vol_attach.assert_called_once_with(self.context, instance, self.volume_api, self.virt_driver, do_check_attach=True) self.assertEqual('fake-volume-id-2', test_bdm.volume_id) + def test_blank_attach_volume_cinder_cross_az_attach_false(self): + # Tests that the blank volume created is in the same availability zone + # as the instance. + self.flags(cross_az_attach=False, group='cinder') + no_blank_volume = self.blank_bdm.copy() + no_blank_volume['volume_id'] = None + test_bdm = self.driver_classes['blank'](no_blank_volume) + updates = {'uuid': 'fake-uuid', 'availability_zone': 'test-az'} + instance = fake_instance.fake_instance_obj(mock.sentinel.ctx, + **updates) + volume_class = self.driver_classes['volume'] + volume = {'id': 'fake-volume-id-2', + 'display_name': 'fake-uuid-blank-vol'} + + with mock.patch.object(self.volume_api, 'create', + return_value=volume) as vol_create: + with mock.patch.object(volume_class, 'attach') as vol_attach: + test_bdm.attach(self.context, instance, self.volume_api, + self.virt_driver) + + vol_create.assert_called_once_with( + self.context, test_bdm.volume_size, 'fake-uuid-blank-vol', + '', availability_zone='test-az') + vol_attach.assert_called_once_with(self.context, instance, + self.volume_api, + self.virt_driver, + do_check_attach=True) + self.assertEqual('fake-volume-id-2', test_bdm.volume_id) + def test_convert_block_devices(self): converted = driver_block_device._convert_block_devices( self.driver_classes['volume'], @@ -864,3 +950,12 @@ class TestDriverBlockDevice(test.NoDBTestCase): for bdm in (test_swap, test_ephemeral): self.assertFalse(driver_block_device.is_block_device_mapping( bdm._bdm_obj)) + + def test_get_volume_create_az_cinder_cross_az_attach_true(self): + # Tests that we get None back if cinder.cross_az_attach=True even if + # the instance has an AZ assigned. Note that since cross_az_attach + # defaults to True we don't need to set a flag explicitly for the test. + updates = {'availability_zone': 'test-az'} + instance = fake_instance.fake_instance_obj(self.context, **updates) + self.assertIsNone( + driver_block_device._get_volume_create_az_value(instance)) diff --git a/nova/tests/unit/virt/test_virt_drivers.py b/nova/tests/unit/virt/test_virt_drivers.py index 07fc30a8c2..250907e552 100644 --- a/nova/tests/unit/virt/test_virt_drivers.py +++ b/nova/tests/unit/virt/test_virt_drivers.py @@ -114,6 +114,7 @@ class _FakeDriverBackendTestCase(object): rescue_kernel_id="3", rescue_ramdisk_id=None, snapshots_directory='./', + sysinfo_serial='none', group='libvirt') def fake_extend(image, size): diff --git a/nova/virt/block_device.py b/nova/virt/block_device.py index 8499522d35..14202ba08d 100644 --- a/nova/virt/block_device.py +++ b/nova/virt/block_device.py @@ -16,6 +16,7 @@ import functools import itertools import operator +from oslo_config import cfg from oslo_log import log as logging from oslo_serialization import jsonutils from oslo_utils import excutils @@ -29,6 +30,9 @@ from nova import objects from nova.objects import base as obj_base from nova.volume import encryptors +CONF = cfg.CONF +CONF.import_opt('cross_az_attach', 'nova.volume.cinder', group='cinder') + LOG = logging.getLogger(__name__) @@ -55,6 +59,34 @@ def update_db(method): return wrapped +def _get_volume_create_az_value(instance): + """Determine az to use when creating a volume + + Uses the cinder.cross_az_attach config option to determine the availability + zone value to use when creating a volume. + + :param nova.objects.Instance instance: The instance for which the volume + will be created and attached. + :returns: The availability_zone value to pass to volume_api.create + """ + # If we're allowed to attach a volume in any AZ to an instance in any AZ, + # then we don't care what AZ the volume is in so don't specify anything. + if CONF.cinder.cross_az_attach: + return None + # Else the volume has to be in the same AZ as the instance otherwise we + # fail. If the AZ is not in Cinder the volume create will fail. But on the + # other hand if the volume AZ and instance AZ don't match and + # cross_az_attach is False, then volume_api.check_attach will fail too, so + # we can't really win. :) + # TODO(mriedem): It would be better from a UX perspective if we could do + # some validation in the API layer such that if we know we're going to + # specify the AZ when creating the volume and that AZ is not in Cinder, we + # could fail the boot from volume request early with a 400 rather than + # fail to build the instance on the compute node which results in a + # NoValidHost error. + return instance.availability_zone + + class DriverBlockDevice(dict): """A dict subclass that represents block devices used by the virt layer. @@ -327,7 +359,7 @@ class DriverSnapshotBlockDevice(DriverVolumeBlockDevice): virt_driver, wait_func=None, do_check_attach=True): if not self.volume_id: - av_zone = instance.availability_zone + av_zone = _get_volume_create_az_value(instance) snapshot = volume_api.get_snapshot(context, self.snapshot_id) vol = volume_api.create(context, self.volume_size, '', '', @@ -351,7 +383,7 @@ class DriverImageBlockDevice(DriverVolumeBlockDevice): def attach(self, context, instance, volume_api, virt_driver, wait_func=None, do_check_attach=True): if not self.volume_id: - av_zone = instance.availability_zone + av_zone = _get_volume_create_az_value(instance) vol = volume_api.create(context, self.volume_size, '', '', image_id=self.image_id, availability_zone=av_zone) @@ -374,7 +406,7 @@ class DriverBlankBlockDevice(DriverVolumeBlockDevice): virt_driver, wait_func=None, do_check_attach=True): if not self.volume_id: vol_name = instance.uuid + '-blank-vol' - av_zone = instance.availability_zone + av_zone = _get_volume_create_az_value(instance) vol = volume_api.create(context, self.volume_size, vol_name, '', availability_zone=av_zone) if wait_func: diff --git a/nova/virt/libvirt/driver.py b/nova/virt/libvirt/driver.py index 1b5c4b9a99..37c06ed50b 100644 --- a/nova/virt/libvirt/driver.py +++ b/nova/virt/libvirt/driver.py @@ -5206,7 +5206,8 @@ class LibvirtDriver(driver.ComputeDriver): else: cpu = self._vcpu_model_to_cpu_config(guest_cpu) - u = "http://libvirt.org/html/libvirt-libvirt.html#virCPUCompareResult" + u = ("http://libvirt.org/html/libvirt-libvirt-host.html#" + "virCPUCompareResult") m = _("CPU doesn't have compatibility.\n\n%(ret)s\n\nRefer to %(u)s") # unknown character exists in xml, then libvirt complains try: @@ -5825,6 +5826,24 @@ class LibvirtDriver(driver.ComputeDriver): raise exception.DestinationDiskExists(path=instance_dir) os.mkdir(instance_dir) + # Recreate the disk.info file and in doing so stop the + # imagebackend from recreating it incorrectly by inspecting the + # contents of each file when using the Raw backend. + if disk_info: + image_disk_info = {} + for info in jsonutils.loads(disk_info): + image_file = os.path.basename(info['path']) + image_path = os.path.join(instance_dir, image_file) + image_disk_info[image_path] = info['type'] + + LOG.debug('Creating disk.info with the contents: %s', + image_disk_info, instance=instance) + + image_disk_info_path = os.path.join(instance_dir, + 'disk.info') + libvirt_utils.write_to_file(image_disk_info_path, + jsonutils.dumps(image_disk_info)) + if not is_shared_block_storage: # Ensure images and backing files are present. self._create_images_and_backing( @@ -6320,6 +6339,11 @@ class LibvirtDriver(driver.ComputeDriver): dest = None utils.execute('mkdir', '-p', inst_base) + on_execute = lambda process: \ + self.job_tracker.add_job(instance, process.pid) + on_completion = lambda process: \ + self.job_tracker.remove_job(instance, process.pid) + active_flavor = instance.get_flavor() for info in disk_info: # assume inst_base == dirname(info['path']) @@ -6339,11 +6363,6 @@ class LibvirtDriver(driver.ComputeDriver): # finish_migration/_create_image to re-create it for us. continue - on_execute = lambda process: self.job_tracker.add_job( - instance, process.pid) - on_completion = lambda process: self.job_tracker.remove_job( - instance, process.pid) - if info['type'] == 'qcow2' and info['backing_file']: tmp_path = from_path + "_rbase" # merge backing file @@ -6362,6 +6381,16 @@ class LibvirtDriver(driver.ComputeDriver): libvirt_utils.copy_image(from_path, img_path, host=dest, on_execute=on_execute, on_completion=on_completion) + + # Ensure disk.info is written to the new path to avoid disks being + # reinspected and potentially changing format. + src_disk_info_path = os.path.join(inst_base_resize, 'disk.info') + if os.path.exists(src_disk_info_path): + dst_disk_info_path = os.path.join(inst_base, 'disk.info') + libvirt_utils.copy_image(src_disk_info_path, + dst_disk_info_path, + host=dest, on_execute=on_execute, + on_completion=on_completion) except Exception: with excutils.save_and_reraise_exception(): self._cleanup_remote_migration(dest, inst_base, diff --git a/nova/virt/libvirt/host.py b/nova/virt/libvirt/host.py index 36a9942673..ea35b856e3 100644 --- a/nova/virt/libvirt/host.py +++ b/nova/virt/libvirt/host.py @@ -743,7 +743,10 @@ class Host(object): LOG.info(_LI("Libvirt host capabilities %s"), xmlstr) self._caps = vconfig.LibvirtConfigCaps() self._caps.parse_str(xmlstr) - if hasattr(libvirt, 'VIR_CONNECT_BASELINE_CPU_EXPAND_FEATURES'): + # NOTE(mriedem): Don't attempt to get baseline CPU features + # if libvirt can't determine the host cpu model. + if (hasattr(libvirt, 'VIR_CONNECT_BASELINE_CPU_EXPAND_FEATURES') + and self._caps.host.cpu.model is not None): try: features = self.get_connection().baselineCPU( [self._caps.host.cpu.to_xml()], |