Handle neutron without the fip-port-details extension

The 'fip-port-details' API extension was added to neutron in Rocky [1]
and is optional. As a result, we cannot rely on the 'port_details' field
being present in API responses. If it is not, we need to make a second
query for all ports and build 'port_details' using the 'port_id' field.

[1] https://docs.openstack.org/releasenotes/neutron-lib/rocky.html#relnotes-1-14-0-stable-rocky-new-features

Change-Id: Ifb96f31f471cc0a25c1dfce2161a669b97a384ae
Signed-off-by: Stephen Finucane <sfinucan@redhat.com>
Closes-bug: #1861876
This commit is contained in:
Stephen Finucane 2020-02-04 15:44:54 +00:00
parent 7601efa5e3
commit eef658bf53
4 changed files with 208 additions and 6 deletions
nova
network
tests
functional/api_sample_tests
unit/network

@ -18,6 +18,7 @@ NET_EXTERNAL = 'router:external'
VNIC_INDEX_EXT = 'VNIC Index'
DNS_INTEGRATION = 'DNS Integration'
MULTI_NET_EXT = 'Multi Provider Network'
FIP_PORT_DETAILS = 'Floating IP Port Details Extension'
SUBSTR_PORT_FILTERING = 'IP address substring filtering'
PORT_BINDING_EXTENDED = 'Port Bindings Extended'
LIVE_MIGRATION = 'live-migration'

@ -1265,6 +1265,10 @@ class API(base.Base):
self._refresh_neutron_extensions_cache(context, neutron=neutron)
return constants.QOS_QUEUE in self.extensions
def _has_fip_port_details_extension(self, context, neutron=None):
self._refresh_neutron_extensions_cache(context, neutron=neutron)
return constants.FIP_PORT_DETAILS in self.extensions
def has_substr_port_filtering_extension(self, context):
self._refresh_neutron_extensions_cache(context)
return constants.SUBSTR_PORT_FILTERING in self.extensions
@ -2599,13 +2603,18 @@ class API(base.Base):
except neutron_client_exc.NetworkNotFoundClient:
raise exception.NetworkNotFound(network_id=network_uuid)
return fip
# ...and retrieve the port details for the same reason, but only if
# they're not already there because the fip-port-details extension is
# present
if not self._has_fip_port_details_extension(context, client):
port_id = fip['port_id']
try:
fip['port_details'] = client.show_port(
port_id)['port']
except neutron_client_exc.PortNotFoundClient:
raise exception.PortNotFound(port_id=port_id)
def get_floating_ip_pools(self, context):
"""Return floating IP pools a.k.a. external networks."""
client = get_client(context)
data = client.list_networks(**{constants.NET_EXTERNAL: True})
return data['networks']
return fip
def get_floating_ip_by_address(self, context, address):
"""Return a floating IP given an address."""
@ -2621,12 +2630,31 @@ class API(base.Base):
except neutron_client_exc.NetworkNotFoundClient:
raise exception.NetworkNotFound(network_id=network_uuid)
# ...and retrieve the port details for the same reason, but only if
# they're not already there because the fip-port-details extension is
# present
if not self._has_fip_port_details_extension(context, client):
port_id = fip['port_id']
try:
fip['port_details'] = client.show_port(
port_id)['port']
except neutron_client_exc.PortNotFoundClient:
raise exception.PortNotFound(port_id=port_id)
return fip
def get_floating_ip_pools(self, context):
"""Return floating IP pools a.k.a. external networks."""
client = get_client(context)
data = client.list_networks(**{constants.NET_EXTERNAL: True})
return data['networks']
def get_floating_ips_by_project(self, context):
client = get_client(context)
project_id = context.project_id
fips = self._safe_get_floating_ips(client, tenant_id=project_id)
if not fips:
return fips
# retrieve and cache the network details now since many callers need
# the network name which isn't present in the response from neutron
@ -2643,6 +2671,19 @@ class API(base.Base):
fip['network_details'] = networks[network_uuid]
# ...and retrieve the port details for the same reason, but only if
# they're not already there because the fip-port-details extension is
# present
if not self._has_fip_port_details_extension(context, client):
ports = {port['id']: port for port in client.list_ports(
**{'tenant_id': project_id})['ports']}
for fip in fips:
port_id = fip['port_id']
if port_id not in ports:
raise exception.PortNotFound(port_id=port_id)
fip['port_details'] = ports[port_id]
return fips
def get_instance_id_by_floating_address(self, context, address):

@ -15,6 +15,7 @@
import copy
import nova.conf
from nova.network import constants
from nova.tests import fixtures
from nova.tests.functional.api_sample_tests import api_sample_base
@ -160,6 +161,21 @@ class NeutronFixture(fixtures.NeutronFixture):
def list_floatingips(self, retrieve_all=True, **_params):
return {'floatingips': copy.deepcopy(list(self._floatingips.values()))}
def list_extensions(self, *args, **kwargs):
extensions = super().list_extensions(*args, **kwargs)
extensions['extensions'].append(
{
# Copied from neutron-lib fip_port_details.py
'updated': '2018-04-09T10:00:00-00:00',
'name': constants.FIP_PORT_DETAILS,
'links': [],
'alias': 'fip-port-details',
'description': 'Add port_details attribute to Floating IP '
'resource',
},
)
return extensions
class FloatingIpsTest(api_sample_base.ApiSampleTestBaseV21):
sample_dir = "os-floating-ips"

@ -2345,6 +2345,51 @@ class TestAPI(TestAPIBase):
mock_get_client.assert_called_once_with(self.context)
mocked_client.show_floatingip.assert_called_once_with(floating_ip_id)
@mock.patch.object(neutronapi.API, '_refresh_neutron_extensions_cache')
@mock.patch.object(neutronapi, 'get_client')
def _test_get_floating_ip(
self, fip_ext_enabled, mock_ntrn, mock_refresh):
mock_nc = mock.Mock()
mock_ntrn.return_value = mock_nc
# NOTE(stephenfin): These are clearly not full responses
mock_nc.show_floatingip.return_value = {
'floatingip': {
'id': uuids.fip_id,
'floating_network_id': uuids.fip_net_id,
'port_id': uuids.fip_port_id,
}
}
mock_nc.show_network.return_value = {
'network': {
'id': uuids.fip_net_id,
},
}
mock_nc.show_port.return_value = {
'port': {
'id': uuids.fip_port_id,
},
}
if fip_ext_enabled:
self.api.extensions = [constants.FIP_PORT_DETAILS]
else:
self.api.extensions = []
fip = self.api.get_floating_ip(self.context, uuids.fip_id)
if fip_ext_enabled:
mock_nc.show_port.assert_not_called()
self.assertNotIn('port_details', fip)
else:
mock_nc.show_port.assert_called_once_with(uuids.fip_port_id)
self.assertIn('port_details', fip)
def test_get_floating_ip_with_fip_port_details_ext(self):
self._test_get_floating_ip(True)
def test_get_floating_ip_without_fip_port_details_ext(self):
self._test_get_floating_ip(False)
@mock.patch.object(neutronapi, 'get_client')
def test_get_floating_ip_by_address_multiple_found(self, mock_get_client):
mocked_client = mock.create_autospec(client.Client)
@ -2359,6 +2404,53 @@ class TestAPI(TestAPIBase):
mocked_client.list_floatingips.assert_called_once_with(
floating_ip_address=address)
@mock.patch.object(neutronapi.API, '_refresh_neutron_extensions_cache')
@mock.patch.object(neutronapi, 'get_client')
def _test_get_floating_ip_by_address(
self, fip_ext_enabled, mock_ntrn, mock_refresh):
mock_nc = mock.Mock()
mock_ntrn.return_value = mock_nc
# NOTE(stephenfin): These are clearly not full responses
mock_nc.list_floatingips.return_value = {
'floatingips': [
{
'id': uuids.fip_id,
'floating_network_id': uuids.fip_net_id,
'port_id': uuids.fip_port_id,
},
]
}
mock_nc.show_network.return_value = {
'network': {
'id': uuids.fip_net_id,
},
}
mock_nc.show_port.return_value = {
'port': {
'id': uuids.fip_port_id,
},
}
if fip_ext_enabled:
self.api.extensions = [constants.FIP_PORT_DETAILS]
else:
self.api.extensions = []
fip = self.api.get_floating_ip_by_address(self.context, '172.1.2.3')
if fip_ext_enabled:
mock_nc.show_port.assert_not_called()
self.assertNotIn('port_details', fip)
else:
mock_nc.show_port.assert_called_once_with(uuids.fip_port_id)
self.assertIn('port_details', fip)
def test_get_floating_ip_by_address_with_fip_port_details_ext(self):
self._test_get_floating_ip_by_address(True)
def test_get_floating_ip_by_address_without_fip_port_details_ext(self):
self._test_get_floating_ip_by_address(False)
@mock.patch.object(neutronapi, 'get_client')
def _test_get_instance_id_by_floating_address(self, fip_data,
mock_get_client,
@ -5304,6 +5396,58 @@ class TestAPI(TestAPIBase):
self.api.get_floating_ips_by_project,
self.context)
@mock.patch.object(neutronapi.API, '_refresh_neutron_extensions_cache')
@mock.patch.object(neutronapi, 'get_client')
def _test_get_floating_ips_by_project(
self, fip_ext_enabled, mock_ntrn, mock_refresh):
mock_nc = mock.Mock()
mock_ntrn.return_value = mock_nc
# NOTE(stephenfin): These are clearly not full responses
mock_nc.list_floatingips.return_value = {
'floatingips': [
{
'id': uuids.fip_id,
'floating_network_id': uuids.fip_net_id,
'port_id': uuids.fip_port_id,
}
]
}
mock_nc.show_network.return_value = {
'network': {
'id': uuids.fip_net_id,
},
}
mock_nc.list_ports.return_value = {
'ports': [
{
'id': uuids.fip_port_id,
},
],
}
if fip_ext_enabled:
self.api.extensions = [constants.FIP_PORT_DETAILS]
else:
self.api.extensions = []
fips = self.api.get_floating_ips_by_project(self.context)
self.assertEqual(1, len(fips))
if fip_ext_enabled:
mock_nc.list_ports.assert_not_called()
self.assertNotIn('port_details', fips[0])
else:
mock_nc.list_ports.assert_called_once_with(
tenant_id=self.context.project_id)
self.assertIn('port_details', fips[0])
def test_get_floating_ips_by_project_with_fip_port_details_ext(self):
self._test_get_floating_ips_by_project(True)
def test_get_floating_ips_by_project_without_fip_port_details_ext(self):
self._test_get_floating_ips_by_project(False)
@mock.patch('nova.network.neutron.API._show_port')
def test_unbind_ports_reset_dns_name_by_admin(self, mock_show):
neutron = mock.Mock()