diff --git a/nova/compute/manager.py b/nova/compute/manager.py index 451d9b37ef4..6f1c0012a60 100644 --- a/nova/compute/manager.py +++ b/nova/compute/manager.py @@ -76,6 +76,7 @@ from nova.image import glance from nova import manager from nova import network +from nova.network import base_api as base_net_api from nova.network import model as network_model from nova.network.security_group import openstack_driver from nova import objects @@ -6228,6 +6229,33 @@ def _process_instance_event(self, instance, event): {'event': event.key}, instance=instance) _event.send(event) + def _process_instance_vif_deleted_event(self, context, instance, + deleted_vif_id): + # If an attached port is deleted by neutron, it needs to + # be detached from the instance. + # And info cache needs to be updated. + network_info = instance.info_cache.network_info + for index, vif in enumerate(network_info): + if vif['id'] == deleted_vif_id: + LOG.info(_LI('Neutron deleted interface %(intf)s; ' + 'detaching it from the instance and ' + 'deleting it from the info cache'), + {'intf': vif['id']}, + instance=instance) + del network_info[index] + base_net_api.update_instance_cache_with_nw_info( + self.network_api, context, + instance, + nw_info=network_info) + try: + self.driver.detach_interface(instance, vif) + except exception.NovaException as ex: + LOG.warning(_LW("Detach interface failed, " + "port_id=%(port_id)s, reason: %(msg)s"), + {'port_id': deleted_vif_id, 'msg': ex}, + instance=instance) + break + @wrap_exception() def external_instance_event(self, context, instances, events): # NOTE(danms): Some event types are handled by the manager, such @@ -6242,6 +6270,10 @@ def external_instance_event(self, context, instances, events): instance=instance) if event.name == 'network-changed': self.network_api.get_instance_nw_info(context, instance) + elif event.name == 'network-vif-deleted': + self._process_instance_vif_deleted_event(context, + instance, + event.tag) else: self._process_instance_event(instance, event) diff --git a/nova/objects/external_event.py b/nova/objects/external_event.py index 1d884e6e020..beea55d4ba1 100644 --- a/nova/objects/external_event.py +++ b/nova/objects/external_event.py @@ -22,6 +22,7 @@ # VIF plugging notifications, tag is port_id 'network-vif-plugged', 'network-vif-unplugged', + 'network-vif-deleted', ] @@ -34,7 +35,8 @@ class InstanceExternalEvent(obj_base.NovaObject, obj_base.NovaObjectDictCompat): # Version 1.0: Initial version # Supports network-changed and vif-plugged - VERSION = '1.0' + # Version 1.1: adds network-vif-deleted event + VERSION = '1.1' fields = { 'instance_uuid': fields.UUIDField(), diff --git a/nova/tests/unit/compute/test_compute_mgr.py b/nova/tests/unit/compute/test_compute_mgr.py index c67afd0392e..5f54379ddb8 100644 --- a/nova/tests/unit/compute/test_compute_mgr.py +++ b/nova/tests/unit/compute/test_compute_mgr.py @@ -47,6 +47,7 @@ from nova.tests.unit.compute import fake_resource_tracker from nova.tests.unit import fake_block_device from nova.tests.unit import fake_instance +from nova.tests.unit import fake_network_cache_model from nova.tests.unit import fake_server_actions from nova.tests.unit.objects import test_instance_fault from nova.tests.unit.objects import test_instance_info_cache @@ -1744,27 +1745,107 @@ def test_process_instance_event(self): self.assertEqual(event_obj, event.wait()) self.assertEqual({}, self.compute.instance_events._events) + def test_process_instance_vif_deleted_event(self): + vif1 = fake_network_cache_model.new_vif() + vif1['id'] = '1' + vif2 = fake_network_cache_model.new_vif() + vif2['id'] = '2' + nw_info = network_model.NetworkInfo([vif1, vif2]) + info_cache = objects.InstanceInfoCache(network_info=nw_info, + instance_uuid='uuid') + inst_obj = objects.Instance(id=3, uuid='uuid', info_cache=info_cache) + + @mock.patch.object(manager.base_net_api, + 'update_instance_cache_with_nw_info') + @mock.patch.object(self.compute.driver, 'detach_interface') + def do_test(detach_interface, update_instance_cache_with_nw_info): + self.compute._process_instance_vif_deleted_event(self.context, + inst_obj, + vif2['id']) + update_instance_cache_with_nw_info.assert_called_once_with( + self.compute.network_api, + self.context, + inst_obj, + nw_info=[vif1]) + detach_interface.assert_called_once_with(inst_obj, vif2) + do_test() + def test_external_instance_event(self): instances = [ objects.Instance(id=1, uuid='uuid1'), - objects.Instance(id=2, uuid='uuid2')] + objects.Instance(id=2, uuid='uuid2'), + objects.Instance(id=3, uuid='uuid3')] events = [ objects.InstanceExternalEvent(name='network-changed', tag='tag1', instance_uuid='uuid1'), objects.InstanceExternalEvent(name='network-vif-plugged', instance_uuid='uuid2', - tag='tag2')] + tag='tag2'), + objects.InstanceExternalEvent(name='network-vif-deleted', + instance_uuid='uuid3', + tag='tag3')] + @mock.patch.object(self.compute, '_process_instance_vif_deleted_event') @mock.patch.object(self.compute.network_api, 'get_instance_nw_info') @mock.patch.object(self.compute, '_process_instance_event') - def do_test(_process_instance_event, get_instance_nw_info): + def do_test(_process_instance_event, get_instance_nw_info, + _process_instance_vif_deleted_event): self.compute.external_instance_event(self.context, instances, events) get_instance_nw_info.assert_called_once_with(self.context, instances[0]) _process_instance_event.assert_called_once_with(instances[1], events[1]) + _process_instance_vif_deleted_event.assert_called_once_with( + self.context, instances[2], events[2].tag) + do_test() + + def test_external_instance_event_with_exception(self): + vif1 = fake_network_cache_model.new_vif() + vif1['id'] = '1' + vif2 = fake_network_cache_model.new_vif() + vif2['id'] = '2' + nw_info = network_model.NetworkInfo([vif1, vif2]) + info_cache = objects.InstanceInfoCache(network_info=nw_info, + instance_uuid='uuid2') + instances = [ + objects.Instance(id=1, uuid='uuid1'), + objects.Instance(id=2, uuid='uuid2', info_cache=info_cache), + objects.Instance(id=3, uuid='uuid3')] + events = [ + objects.InstanceExternalEvent(name='network-changed', + tag='tag1', + instance_uuid='uuid1'), + objects.InstanceExternalEvent(name='network-vif-deleted', + instance_uuid='uuid2', + tag='2'), + objects.InstanceExternalEvent(name='network-vif-plugged', + instance_uuid='uuid3', + tag='tag3')] + + # Make sure all the three events are handled despite the exception in + # processing event 2 + @mock.patch.object(manager.base_net_api, + 'update_instance_cache_with_nw_info') + @mock.patch.object(self.compute.driver, 'detach_interface', + side_effect=exception.NovaException) + @mock.patch.object(self.compute.network_api, 'get_instance_nw_info') + @mock.patch.object(self.compute, '_process_instance_event') + def do_test(_process_instance_event, get_instance_nw_info, + detach_interface, update_instance_cache_with_nw_info): + self.compute.external_instance_event(self.context, + instances, events) + get_instance_nw_info.assert_called_once_with(self.context, + instances[0]) + update_instance_cache_with_nw_info.assert_called_once_with( + self.compute.network_api, + self.context, + instances[1], + nw_info=[vif1]) + detach_interface.assert_called_once_with(instances[1], vif2) + _process_instance_event.assert_called_once_with(instances[2], + events[2]) do_test() def test_cancel_all_events(self): diff --git a/nova/tests/unit/objects/test_objects.py b/nova/tests/unit/objects/test_objects.py index ae71dbf8051..427f23fc04c 100644 --- a/nova/tests/unit/objects/test_objects.py +++ b/nova/tests/unit/objects/test_objects.py @@ -1118,7 +1118,7 @@ def test_serialize_args(self): 'InstanceActionEvent': '1.1-e56a64fa4710e43ef7af2ad9d6028b33', 'InstanceActionEventList': '1.1-13d92fb953030cdbfee56481756e02be', 'InstanceActionList': '1.0-4a53826625cc280e15fae64a575e0879', - 'InstanceExternalEvent': '1.0-33cc4a1bbd0655f68c0ee791b95da7e6', + 'InstanceExternalEvent': '1.1-6e446ceaae5f475ead255946dd443417', 'InstanceFault': '1.2-7ef01f16f1084ad1304a513d6d410a38', 'InstanceFaultList': '1.1-f8ec07cbe3b60f5f07a8b7a06311ac0d', 'InstanceGroup': '1.9-a413a4ec0ff391e3ef0faa4e3e2a96d0',