diff --git a/nova/compute/manager.py b/nova/compute/manager.py index 7ab524ea394b..5f16bf2d6da8 100644 --- a/nova/compute/manager.py +++ b/nova/compute/manager.py @@ -2542,10 +2542,15 @@ class ComputeManager(manager.Manager): LOG.debug('Events pending at deletion: %(events)s', {'events': ','.join(events.keys())}, instance=instance) - instance.info_cache.delete() self._notify_about_instance_usage(context, instance, "delete.start") self._shutdown_instance(context, instance, bdms) + # NOTE(dims): instance.info_cache.delete() should be called after + # _shutdown_instance in the compute manager as shutdown calls + # deallocate_for_instance so the info_cache is still needed + # at this point. + instance.info_cache.delete() + # NOTE(vish): We have already deleted the instance, so we have # to ignore problems cleaning up the volumes. It # would be nice to let the user know somehow that diff --git a/nova/network/model.py b/nova/network/model.py index 87231e20485f..fb9e009e12b2 100644 --- a/nova/network/model.py +++ b/nova/network/model.py @@ -299,7 +299,7 @@ class VIF(Model): details=None, devname=None, ovs_interfaceid=None, qbh_params=None, qbg_params=None, active=False, vnic_type=VNIC_TYPE_NORMAL, profile=None, - **kwargs): + preserve_on_delete=False, **kwargs): super(VIF, self).__init__() self['id'] = id @@ -315,6 +315,7 @@ class VIF(Model): self['active'] = active self['vnic_type'] = vnic_type self['profile'] = profile + self['preserve_on_delete'] = preserve_on_delete self._set_meta(kwargs) @@ -322,7 +323,7 @@ class VIF(Model): keys = ['id', 'address', 'network', 'vnic_type', 'type', 'profile', 'details', 'devname', 'ovs_interfaceid', 'qbh_params', 'qbg_params', - 'active'] + 'active', 'preserve_on_delete'] return all(self[k] == other[k] for k in keys) def __ne__(self, other): diff --git a/nova/network/neutronv2/api.py b/nova/network/neutronv2/api.py index 3d1409a9e0b9..6d09feb6d628 100644 --- a/nova/network/neutronv2/api.py +++ b/nova/network/neutronv2/api.py @@ -312,8 +312,10 @@ class API(base_api.NetworkAPI): raise exception.ExternalNetworkAttachForbidden( network_uuid=net['id']) - def _unbind_ports(self, context, ports, neutron, port_client=None): - """Unbind the specified ports. + def _unbind_ports(self, context, ports, + neutron, port_client=None): + """Unbind the given ports by clearing their device_id and + device_owner. :param context: The request context. :param ports: list of port IDs. @@ -321,22 +323,21 @@ class API(base_api.NetworkAPI): :param port_client: The client with appropriate karma for updating the ports. """ + port_binding = self._has_port_binding_extension(context, + refresh_cache=True, neutron=neutron) + if port_client is None: + # Requires admin creds to set port bindings + port_client = (neutron if not port_binding else + get_client(context, admin=True)) for port_id in ports: + port_req_body = {'port': {'device_id': '', 'device_owner': ''}} + if port_binding: + port_req_body['port']['binding:host_id'] = None try: - port_req_body = {'port': {'device_id': ''}} - if port_client is None: - # Reset device_id and device_owner for the ports - port_req_body.update({'device_owner': ''}) - neutron.update_port(port_id, port_req_body) - else: - # Requires admin creds to set port bindings - if self._has_port_binding_extension(context, - neutron=neutron): - port_req_body['port']['binding:host_id'] = None - port_client.update_port(port_id, port_req_body) + port_client.update_port(port_id, port_req_body) except Exception: - LOG.exception(_LE("Unable to reset device ID for port %s"), - port_id) + LOG.exception(_LE("Unable to clear device ID " + "for port '%s'"), port_id) def allocate_for_instance(self, context, instance, **kwargs): """Allocate network resources for the instance. @@ -486,7 +487,7 @@ class API(base_api.NetworkAPI): elif uuid_match: security_group_ids.append(uuid_match) - touched_port_ids = [] + preexisting_port_ids = [] created_port_ids = [] ports_in_requested_order = [] nets_in_requested_order = [] @@ -527,7 +528,7 @@ class API(base_api.NetworkAPI): if request.port_id: port = ports[request.port_id] port_client.update_port(port['id'], port_req_body) - touched_port_ids.append(port['id']) + preexisting_port_ids.append(port['id']) ports_in_requested_order.append(port['id']) else: created_port = self._create_port( @@ -538,14 +539,15 @@ class API(base_api.NetworkAPI): ports_in_requested_order.append(created_port) except Exception: with excutils.save_and_reraise_exception(): - self._unbind_ports(context, touched_port_ids, neutron, - port_client) + self._unbind_ports(context, + preexisting_port_ids, + neutron, port_client) self._delete_ports(neutron, instance, created_port_ids) - - nw_info = self.get_instance_nw_info(context, instance, - networks=nets_in_requested_order, - port_ids=ports_in_requested_order, - admin_client=admin_client) + nw_info = self.get_instance_nw_info( + context, instance, networks=nets_in_requested_order, + port_ids=ports_in_requested_order, + admin_client=admin_client, + preexisting_port_ids=preexisting_port_ids) # NOTE(danms): Only return info about ports we created in this run. # In the initial allocation case, this will be everything we created, # and in later runs will only be what was created that time. Thus, @@ -553,7 +555,7 @@ class API(base_api.NetworkAPI): # method. return network_model.NetworkInfo([vif for vif in nw_info if vif['id'] in created_port_ids + - touched_port_ids]) + preexisting_port_ids]) def _refresh_neutron_extensions_cache(self, context, neutron=None): """Refresh the neutron extensions cache when necessary.""" @@ -638,10 +640,18 @@ class API(base_api.NetworkAPI): # NOTE(danms): Temporary and transitional if isinstance(requested_networks, objects.NetworkRequestList): requested_networks = requested_networks.as_tuples() - ports_to_skip = [port_id for nets, fips, port_id, pci_request_id - in requested_networks] - ports = set(ports) - set(ports_to_skip) + ports_to_skip = set([port_id for nets, fips, port_id, pci_request_id + in requested_networks]) + # NOTE(boden): requested_networks only passed in when deallocating + # from a failed build / spawn call. Therefore we need to include + # preexisting ports when deallocating from a standard delete op + # in which case requested_networks is not provided. + ports_to_skip |= set(self._get_preexisting_port_ids(instance)) + ports = set(ports) - ports_to_skip + + # Reset device_id and device_owner for the ports that are skipped self._unbind_ports(context, ports_to_skip, neutron) + # Delete the rest of the ports self._delete_ports(neutron, instance, ports, raise_if_fail=True) # NOTE(arosen): This clears out the network_cache only if the instance @@ -668,7 +678,12 @@ class API(base_api.NetworkAPI): Return network information for the instance """ neutron = get_client(context) - self._delete_ports(neutron, instance, [port_id], raise_if_fail=True) + preexisting_ports = self._get_preexisting_port_ids(instance) + if port_id in preexisting_ports: + self._unbind_ports(context, [port_id], neutron) + else: + self._delete_ports(neutron, instance, [port_id], + raise_if_fail=True) return self.get_instance_nw_info(context, instance) def list_ports(self, context, **search_opts): @@ -686,7 +701,8 @@ class API(base_api.NetworkAPI): def get_instance_nw_info(self, context, instance, networks=None, port_ids=None, use_slave=False, - admin_client=None): + admin_client=None, + preexisting_port_ids=None): """Return network information for specified instance and update cache. """ @@ -695,7 +711,8 @@ class API(base_api.NetworkAPI): # the master. For now we just ignore this arg. with lockutils.lock('refresh_cache-%s' % instance.uuid): result = self._get_instance_nw_info(context, instance, networks, - port_ids, admin_client) + port_ids, admin_client, + preexisting_port_ids) base_api.update_instance_cache_with_nw_info(self, context, instance, nw_info=result, @@ -703,13 +720,15 @@ class API(base_api.NetworkAPI): return result def _get_instance_nw_info(self, context, instance, networks=None, - port_ids=None, admin_client=None): + port_ids=None, admin_client=None, + preexisting_port_ids=None): # NOTE(danms): This is an inner method intended to be called # by other code that updates instance nwinfo. It *must* be # called with the refresh_cache-%(instance_uuid) lock held! LOG.debug('_get_instance_nw_info()', instance=instance) nw_info = self._build_network_info_model(context, instance, networks, - port_ids, admin_client) + port_ids, admin_client, + preexisting_port_ids) return network_model.NetworkInfo.hydrate(nw_info) def _gather_port_ids_and_networks(self, context, instance, networks=None, @@ -1422,8 +1441,21 @@ class API(base_api.NetworkAPI): network['should_create_bridge'] = should_create_bridge return network, ovs_interfaceid + def _get_preexisting_port_ids(self, instance): + """Retrieve the preexisting ports associated with the given instance. + These ports were not created by nova and hence should not be + deallocated upon instance deletion. + """ + net_info = compute_utils.get_nw_info_for_instance(instance) + if not net_info: + LOG.debug('Instance cache missing network info.', + instance=instance) + return [vif['id'] for vif in net_info + if vif.get('preserve_on_delete')] + def _build_network_info_model(self, context, instance, networks=None, - port_ids=None, admin_client=None): + port_ids=None, admin_client=None, + preexisting_port_ids=None): """Return list of ordered VIFs attached to instance. :param context - request context. @@ -1436,6 +1468,11 @@ class API(base_api.NetworkAPI): this value will be populated from the existing cached value. :param admin_client - a neutron client for the admin context. + :param preexisting_port_ids - List of port_ids that nova didn't + allocate and therefore shouldn't be + deleted when an instance is deallocated. + If value is None or empty the value will + be populated from existing cached value. """ search_opts = {'tenant_id': instance.project_id, @@ -1453,6 +1490,9 @@ class API(base_api.NetworkAPI): context, instance, networks, port_ids) nw_info = network_model.NetworkInfo() + if not preexisting_port_ids: + preexisting_port_ids = self._get_preexisting_port_ids(instance) + current_neutron_port_map = {} for current_neutron_port in current_neutron_ports: current_neutron_port_map[current_neutron_port['id']] = ( @@ -1478,6 +1518,8 @@ class API(base_api.NetworkAPI): network, ovs_interfaceid = ( self._nw_info_build_network(current_neutron_port, networks, subnets)) + preserve_on_delete = (current_neutron_port['id'] in + preexisting_port_ids) nw_info.append(network_model.VIF( id=current_neutron_port['id'], @@ -1490,7 +1532,8 @@ class API(base_api.NetworkAPI): details=current_neutron_port.get('binding:vif_details'), ovs_interfaceid=ovs_interfaceid, devname=devname, - active=vif_active)) + active=vif_active, + preserve_on_delete=preserve_on_delete)) elif nw_info_refresh: LOG.info(_LI('Port %s from network info_cache is no ' diff --git a/nova/tests/unit/compute/test_compute_mgr.py b/nova/tests/unit/compute/test_compute_mgr.py index 5e9cb5f3653f..af29ff4056f0 100644 --- a/nova/tests/unit/compute/test_compute_mgr.py +++ b/nova/tests/unit/compute/test_compute_mgr.py @@ -82,6 +82,45 @@ class ComputeManagerUnitTestCase(test.NoDBTestCase): mock_sync.assert_called_with(mock.ANY, mock_get.return_value, pwr_state) + def test_delete_instance_info_cache_delete_ordering(self): + call_tracker = mock.Mock() + call_tracker.clear_events_for_instance.return_value = None + mgr_class = self.compute.__class__ + orig_delete = mgr_class._delete_instance + specd_compute = mock.create_autospec(mgr_class) + # spec out everything except for the method we really want + # to test, then use call_tracker to verify call sequence + specd_compute._delete_instance = orig_delete + + mock_inst = mock.Mock() + mock_inst.uuid = 'inst-1' + mock_inst.save = mock.Mock() + mock_inst.destroy = mock.Mock() + mock_inst.system_metadata = mock.Mock() + + def _mark_notify(*args, **kwargs): + call_tracker._notify_about_instance_usage(*args, **kwargs) + + def _mark_shutdown(*args, **kwargs): + call_tracker._shutdown_instance(*args, **kwargs) + + specd_compute.instance_events = call_tracker + specd_compute._notify_about_instance_usage = _mark_notify + specd_compute._shutdown_instance = _mark_shutdown + mock_inst.info_cache = call_tracker + + specd_compute._delete_instance(specd_compute, + self.context, + mock_inst, + mock.Mock(), + mock.Mock()) + + methods_called = [n for n, a, k in call_tracker.mock_calls] + self.assertEqual(['clear_events_for_instance', + '_notify_about_instance_usage', + '_shutdown_instance', 'delete'], + methods_called) + def test_allocate_network_succeeds_after_retries(self): self.flags(network_allocate_retries=8) diff --git a/nova/tests/unit/network/test_network_info.py b/nova/tests/unit/network/test_network_info.py index 43057822704d..d0f1afbf6583 100644 --- a/nova/tests/unit/network/test_network_info.py +++ b/nova/tests/unit/network/test_network_info.py @@ -390,6 +390,10 @@ class VIFTests(test.NoDBTestCase): vif2 = model.VIF(profile={'pci_slot': '0000:0a:00.2'}) self.assertNotEqual(vif1, vif2) + vif1 = model.VIF(preserve_on_delete=True) + vif2 = model.VIF(preserve_on_delete=False) + self.assertNotEqual(vif1, vif2) + def test_create_vif_with_type(self): vif_dict = dict( id=1, diff --git a/nova/tests/unit/network/test_neutronv2.py b/nova/tests/unit/network/test_neutronv2.py index 606fe379e3d9..f6cf0327d5a3 100644 --- a/nova/tests/unit/network/test_neutronv2.py +++ b/nova/tests/unit/network/test_neutronv2.py @@ -206,11 +206,13 @@ class TestNeutronv2Base(test.TestCase): 'display_name': 'test_instance', 'availability_zone': 'nova', 'host': 'some_host', + 'info_cache': {'network_info': []}, 'security_groups': []} self.instance2 = {'project_id': '9d049e4b60b64716978ab415e6fbd5c0', 'uuid': str(uuid.uuid4()), 'display_name': 'test_instance2', 'availability_zone': 'nova', + 'info_cache': {'network_info': []}, 'security_groups': []} self.nets1 = [{'id': 'my_netid1', 'name': 'my_netname1', @@ -488,6 +490,7 @@ class TestNeutronv2Base(test.TestCase): self.mox.ReplayAll() return api + preexisting_port_ids = [] ports_in_requested_net_order = [] nets_in_requested_net_order = [] for request in ordered_networks: @@ -527,6 +530,7 @@ class TestNeutronv2Base(test.TestCase): ).AndReturn( {'port': port}) ports_in_requested_net_order.append(request.port_id) + preexisting_port_ids.append(request.port_id) else: request.address = fixed_ips.get(request.network_id) if request.address: @@ -557,7 +561,8 @@ class TestNeutronv2Base(test.TestCase): self.instance, networks=nets_in_requested_net_order, port_ids=ports_in_requested_net_order, - admin_client=None + admin_client=None, + preexisting_port_ids=preexisting_port_ids ).AndReturn(self._returned_nw_info) self.mox.ReplayAll() return api @@ -912,7 +917,9 @@ class TestNeutronv2(TestNeutronv2Base): objects=[objects.NetworkRequest(port_id='my_portid1')])) self.assertEqual(self.port_data1, result) - def test_allocate_for_instance_not_enough_macs_via_ports(self): + @mock.patch('nova.network.neutronv2.api.API._unbind_ports') + def test_allocate_for_instance_not_enough_macs_via_ports(self, + mock_unbind): # using a hypervisor MAC via a pre-created port will stop it being # used to dynamically create a port on a network. We put the network # first in requested_networks so that if the code were to not pre-check @@ -929,8 +936,11 @@ class TestNeutronv2(TestNeutronv2Base): api.allocate_for_instance, self.context, self.instance, requested_networks=requested_networks, macs=set(['my_mac1'])) + mock_unbind.assert_called_once_with(self.context, [], + self.moxed_client, mock.ANY) - def test_allocate_for_instance_not_enough_macs(self): + @mock.patch('nova.network.neutronv2.api.API._unbind_ports') + def test_allocate_for_instance_not_enough_macs(self, mock_unbind): # If not enough MAC addresses are available to allocate to networks, an # error should be raised. # We could pass in macs=set(), but that wouldn't tell us that @@ -949,6 +959,8 @@ class TestNeutronv2(TestNeutronv2Base): self.instance, requested_networks=requested_networks, macs=set(['my_mac2'])) + mock_unbind.assert_called_once_with(self.context, [], + self.moxed_client, mock.ANY) def test_allocate_for_instance_two_macs_two_networks(self): # If two MACs are available and two networks requested, two new ports @@ -1039,7 +1051,11 @@ class TestNeutronv2(TestNeutronv2Base): nwinfo = api.allocate_for_instance(self.context, self.instance) self.assertEqual(len(nwinfo), 0) - def test_allocate_for_instance_ex1(self): + @mock.patch('nova.network.neutronv2.api.API._get_preexisting_port_ids') + @mock.patch('nova.network.neutronv2.api.API._unbind_ports') + def test_allocate_for_instance_ex1(self, + mock_unbind, + mock_preexisting): """verify we will delete created ports if we fail to allocate all net resources. @@ -1048,6 +1064,7 @@ class TestNeutronv2(TestNeutronv2Base): """ self.instance = fake_instance.fake_instance_obj(self.context, **self.instance) + mock_preexisting.return_value = [] api = neutronapi.API() self.mox.StubOutWithMock(api, '_populate_neutron_extension_values') self.mox.StubOutWithMock(api, '_has_port_binding_extension') @@ -1094,6 +1111,8 @@ class TestNeutronv2(TestNeutronv2Base): api.allocate_for_instance, self.context, self.instance, requested_networks=requested_networks) + mock_unbind.assert_called_once_with(self.context, [], + self.moxed_client, mock.ANY) def test_allocate_for_instance_ex2(self): """verify we have no port to delete @@ -1276,6 +1295,7 @@ class TestNeutronv2(TestNeutronv2Base): self.moxed_client.list_ports( device_id=self.instance.uuid).AndReturn( {'ports': ret_data}) + self.moxed_client.list_extensions().AndReturn({'extensions': []}) if requested_networks: for net, fip, port, request_id in requested_networks: self.moxed_client.update_port(port) @@ -1292,7 +1312,9 @@ class TestNeutronv2(TestNeutronv2Base): api.deallocate_for_instance(self.context, self.instance, requested_networks=requested_networks) - def test_deallocate_for_instance_1_with_requested(self): + @mock.patch('nova.network.neutronv2.api.API._get_preexisting_port_ids') + def test_deallocate_for_instance_1_with_requested(self, mock_preexisting): + mock_preexisting.return_value = [] requested = objects.NetworkRequestList( objects=[objects.NetworkRequest(network_id='fake-net', address='1.2.3.4', @@ -1300,7 +1322,9 @@ class TestNeutronv2(TestNeutronv2Base): # Test to deallocate in one port env. self._deallocate_for_instance(1, requested_networks=requested) - def test_deallocate_for_instance_2_with_requested(self): + @mock.patch('nova.network.neutronv2.api.API._get_preexisting_port_ids') + def test_deallocate_for_instance_2_with_requested(self, mock_preexisting): + mock_preexisting.return_value = [] requested = objects.NetworkRequestList( objects=[objects.NetworkRequest(network_id='fake-net', address='1.2.3.4', @@ -1308,24 +1332,32 @@ class TestNeutronv2(TestNeutronv2Base): # Test to deallocate in one port env. self._deallocate_for_instance(2, requested_networks=requested) - def test_deallocate_for_instance_1(self): + @mock.patch('nova.network.neutronv2.api.API._get_preexisting_port_ids') + def test_deallocate_for_instance_1(self, mock_preexisting): + mock_preexisting.return_value = [] # Test to deallocate in one port env. self._deallocate_for_instance(1) - def test_deallocate_for_instance_2(self): + @mock.patch('nova.network.neutronv2.api.API._get_preexisting_port_ids') + def test_deallocate_for_instance_2(self, mock_preexisting): + mock_preexisting.return_value = [] # Test to deallocate in two ports env. self._deallocate_for_instance(2) - def test_deallocate_for_instance_port_not_found(self): + @mock.patch('nova.network.neutronv2.api.API._get_preexisting_port_ids') + def test_deallocate_for_instance_port_not_found(self, + mock_preexisting): # TODO(mriedem): Remove this conversion when all neutronv2 APIs are # converted to handling instance objects. self.instance = fake_instance.fake_instance_obj(self.context, **self.instance) + mock_preexisting.return_value = [] port_data = self.port_data1 self.moxed_client.list_ports( device_id=self.instance.uuid).AndReturn( {'ports': port_data}) + self.moxed_client.list_extensions().AndReturn({'extensions': []}) NeutronNotFound = exceptions.NeutronClientException(status_code=404) for port in reversed(port_data): self.moxed_client.delete_port(port['id']).AndRaise( @@ -2466,7 +2498,12 @@ class TestNeutronv2(TestNeutronv2Base): def test_build_network_info_model(self): api = neutronapi.API() - fake_inst = {'project_id': 'fake', 'uuid': 'uuid'} + + fake_inst = objects.Instance() + fake_inst.project_id = 'fake' + fake_inst.uuid = 'uuid' + fake_inst.info_cache = objects.InstanceInfoCache() + fake_inst.info_cache.network_info = model.NetworkInfo() fake_ports = [ # admin_state_up=True and status='ACTIVE' thus vif.active=True {'id': 'port1', @@ -2577,11 +2614,10 @@ class TestNeutronv2(TestNeutronv2Base): self.mox.ReplayAll() neutronapi.get_client('fake') - instance = self._fake_instance_object(fake_inst) - instance.info_cache = objects.InstanceInfoCache.new( + fake_inst.info_cache = objects.InstanceInfoCache.new( self.context, 'fake-uuid') - instance.info_cache.network_info = model.NetworkInfo.hydrate([]) - nw_infos = api._build_network_info_model(self.context, instance, + fake_inst.info_cache.network_info = model.NetworkInfo.hydrate([]) + nw_infos = api._build_network_info_model(self.context, fake_inst, fake_nets, [fake_ports[2]['id'], fake_ports[0]['id'], @@ -2696,6 +2732,7 @@ class TestNeutronv2(TestNeutronv2Base): if binding_vnic_type: test_port['port']['binding:vnic_type'] = binding_vnic_type + mock_get_client.reset_mock() mock_client = mock_get_client() mock_client.show_port.return_value = test_port vnic_type, phynet_name = api._get_port_vnic_info( @@ -2991,9 +3028,15 @@ class TestNeutronv2WithMock(test.TestCase): network_uuid) fake_show_network.assert_called_once_with(network_uuid) - def test_deallocate_for_instance_uses_delete_helper(self): + @mock.patch('nova.network.neutronv2.api.API._get_preexisting_port_ids') + @mock.patch('nova.network.neutronv2.api.API.' + '_refresh_neutron_extensions_cache') + def test_deallocate_for_instance_uses_delete_helper(self, + mock_refresh, + mock_preexisting): # setup fake data instance = fake_instance.fake_instance_obj(self.context) + mock_preexisting.return_value = [] port_data = {'ports': [{'id': str(uuid.uuid4())}]} ports = set([port['id'] for port in port_data.get('ports')]) api = neutronapi.API() @@ -3039,7 +3082,9 @@ class TestNeutronv2WithMock(test.TestCase): raise_if_fail=True) mock_client.delete_port.assert_called_once_with('port1') - def test_deallocate_port_for_instance_fails(self): + @mock.patch('nova.network.neutronv2.api.API._get_preexisting_port_ids') + def test_deallocate_port_for_instance_fails(self, mock_preexisting): + mock_preexisting.return_value = [] mock_client = mock.Mock() api = neutronapi.API() with contextlib.nested( @@ -3124,6 +3169,191 @@ class TestNeutronv2WithMock(test.TestCase): update_port_mock.assert_called_once_with( 'fake-port-2', {'port': {'binding:host_id': instance.host}}) + @mock.patch('nova.network.neutronv2.api.compute_utils') + def test_get_preexisting_port_ids(self, mocked_comp_utils): + mocked_comp_utils.get_nw_info_for_instance.return_value = [model.VIF( + id='1', preserve_on_delete=False), model.VIF( + id='2', preserve_on_delete=True), model.VIF( + id='3', preserve_on_delete=True)] + result = self.api._get_preexisting_port_ids(None) + self.assertEqual(['2', '3'], result, "Invalid preexisting ports") + + def _test_unbind_ports_get_client(self, mock_neutron, + mock_has_ext, has_ext=False): + mock_ctx = mock.Mock(is_admin=False) + mock_has_ext.return_value = has_ext + ports = ["1", "2", "3"] + + self.api._unbind_ports(mock_ctx, ports, mock_neutron) + + get_client_calls = [] + get_client_calls.append(mock.call(mock_ctx) + if not has_ext else + mock.call(mock_ctx, admin=True)) + + if has_ext: + self.assertEqual(1, mock_neutron.call_count) + mock_neutron.assert_has_calls(get_client_calls, True) + else: + self.assertEqual(0, mock_neutron.call_count) + + @mock.patch('nova.network.neutronv2.api.API._has_port_binding_extension') + @mock.patch('nova.network.neutronv2.api.get_client') + def test_unbind_ports_get_client_binding_extension(self, + mock_neutron, + mock_has_ext): + self._test_unbind_ports_get_client(mock_neutron, mock_has_ext, True) + + @mock.patch('nova.network.neutronv2.api.API._has_port_binding_extension') + @mock.patch('nova.network.neutronv2.api.get_client') + def test_unbind_ports_get_client(self, mock_neutron, mock_has_ext): + self._test_unbind_ports_get_client(mock_neutron, mock_has_ext) + + def _test_unbind_ports(self, mock_neutron, mock_has_ext, has_ext=False): + mock_client = mock.Mock() + mock_update_port = mock.Mock() + mock_client.update_port = mock_update_port + mock_ctx = mock.Mock(is_admin=False) + mock_has_ext.return_value = has_ext + mock_neutron.return_value = mock_client + ports = ["1", "2", "3"] + + api = neutronapi.API() + api._unbind_ports(mock_ctx, ports, mock_client) + + body = {'port': {'device_id': '', 'device_owner': ''}} + if has_ext: + body['port']['binding:host_id'] = None + update_port_calls = [] + for p in ports: + update_port_calls.append(mock.call(p, body)) + + self.assertEqual(3, mock_update_port.call_count) + mock_update_port.assert_has_calls(update_port_calls) + + @mock.patch('nova.network.neutronv2.api.API._has_port_binding_extension') + @mock.patch('nova.network.neutronv2.api.get_client') + def test_unbind_ports_binding_ext(self, mock_neutron, mock_has_ext): + self._test_unbind_ports(mock_neutron, mock_has_ext, True) + + @mock.patch('nova.network.neutronv2.api.API._has_port_binding_extension') + @mock.patch('nova.network.neutronv2.api.get_client') + def test_unbind_ports(self, mock_neutron, mock_has_ext): + self._test_unbind_ports(mock_neutron, mock_has_ext, False) + + @mock.patch('nova.network.neutronv2.api.API.get_instance_nw_info') + @mock.patch('nova.network.neutronv2.api.excutils') + @mock.patch('nova.network.neutronv2.api.API._delete_ports') + @mock.patch('nova.network.neutronv2.api.API.' + '_check_external_network_attach') + @mock.patch('nova.network.neutronv2.api.LOG') + @mock.patch('nova.network.neutronv2.api.API._unbind_ports') + @mock.patch('nova.network.neutronv2.api.API._has_port_binding_extension') + @mock.patch('nova.network.neutronv2.api.API.' + '_populate_neutron_extension_values') + @mock.patch('nova.network.neutronv2.api.API._get_available_networks') + @mock.patch('nova.network.neutronv2.api.get_client') + def test_allocate_for_instance_unbind(self, mock_ntrn, + mock_avail_nets, + mock_ext_vals, + mock_has_pbe, + mock_unbind, + mock_log, + mock_cena, + mock_del_ports, + mock_exeu, + mock_giwn): + mock_nc = mock.Mock() + + def show_port(port_id): + return {'port': {'network_id': 'net-1', 'id': port_id}} + mock_nc.show_port = show_port + + mock_ntrn.return_value = mock_nc + mock_nc.update_port.side_effect = [True, True, Exception] + mock_inst = mock.Mock(project_id="proj-1", + availability_zone='zone-1', + uuid='inst-1') + mock_has_pbe.return_value = False + nw_req = objects.NetworkRequestList( + objects = [objects.NetworkRequest(port_id='fake-port1'), + objects.NetworkRequest(port_id='fake-port2'), + objects.NetworkRequest(port_id='fail-port')]) + mock_avail_nets.return_value = [{'id': 'net-1'}] + + self.api.allocate_for_instance(mock.sentinel.ctx, + mock_inst, + requested_networks=nw_req) + + mock_unbind.assert_called_once_with(mock.sentinel.ctx, + ['fake-port1', 'fake-port2'], + mock.ANY, + mock.ANY) + + @mock.patch('nova.objects.network_request.utils') + @mock.patch('nova.network.neutronv2.api.LOG') + @mock.patch('nova.network.neutronv2.api.base_api') + @mock.patch('nova.network.neutronv2.api.API._delete_ports') + @mock.patch('nova.network.neutronv2.api.API._unbind_ports') + @mock.patch('nova.network.neutronv2.api.API._get_preexisting_port_ids') + @mock.patch('nova.network.neutronv2.api.get_client') + def test_preexisting_deallocate_for_instance(self, mock_ntrn, + mock_gppids, + mock_unbind, + mock_deletep, + mock_baseapi, + mock_log, + req_utils): + req_utils.is_neutron.return_value = True + mock_inst = mock.Mock(project_id="proj-1", + availability_zone='zone-1', + uuid='inst-1') + mock_nc = mock.Mock() + mock_ntrn.return_value = mock_nc + mock_nc.list_ports.return_value = {'ports': [ + {'id': 'port-1'}, {'id': 'port-2'}, {'id': 'port-3'} + ]} + nw_req = objects.NetworkRequestList( + objects = [objects.NetworkRequest(network_id='net-1', + address='192.168.0.3', + port_id='port-1', + pci_request_id='pci-1')]) + mock_gppids.return_value = ['port-3'] + + self.api.deallocate_for_instance(mock.sentinel.ctx, mock_inst, + requested_networks=nw_req) + + mock_unbind.assert_called_once_with(mock.sentinel.ctx, + set(['port-1', 'port-3']), + mock.ANY) + mock_deletep.assert_called_once_with(mock_nc, + mock_inst, + set(['port-2']), + raise_if_fail=True) + + @mock.patch('nova.network.neutronv2.api.API.get_instance_nw_info') + @mock.patch('nova.network.neutronv2.api.API._unbind_ports') + @mock.patch('nova.network.neutronv2.api.compute_utils') + @mock.patch('nova.network.neutronv2.api.get_client') + def test_preexisting_deallocate_port_for_instance(self, + mock_ntrn, + mock_comp_utils, + mock_unbind, + mock_netinfo): + mock_comp_utils.get_nw_info_for_instance.return_value = [model.VIF( + id='1', preserve_on_delete=False), model.VIF( + id='2', preserve_on_delete=True), model.VIF( + id='3', preserve_on_delete=True)] + mock_inst = mock.Mock(project_id="proj-1", + availability_zone='zone-1', + uuid='inst-1') + mock_client = mock.Mock() + mock_ntrn.return_value = mock_client + self.api.deallocate_port_for_instance(mock.sentinel.ctx, + mock_inst, '2') + mock_unbind.assert_called_once_with(mock.sentinel.ctx, ['2'], + mock_client) + class TestNeutronv2ModuleMethods(test.TestCase):