NSX|v+v3: forbid multiple fixed ips in a port

The nsx backend does not allow adding different static DHCP bindings
with the same mac, so an instance cannot be deployed with a port with
multiple ips.
This patch prevent create/update of a port with more than 1 fixed ip.

Change-Id: I4b0ba1f3dc593604a3d6a53f8e0aac385faed985
This commit is contained in:
Adit Sarfaty 2017-08-02 14:02:34 +03:00
parent 7bcd7d009f
commit 1dfc0fe59e
8 changed files with 166 additions and 195 deletions

View File

@ -22,6 +22,7 @@
r="^(?!.*"
r="$r(?:tempest\.api\.network\.test_ports\.PortsTestJSON\.test_create_update_port_with_second_ip.*)"
r="$r|(?:tempest\.api\.network\.test_floating_ips\.FloatingIPTestJSON\.test_create_update_floatingip_with_port_multiple_ip_address.*)"
# End list of exclusions.
r="$r)"

View File

@ -32,6 +32,7 @@ r="$r|(?:tempest\.api\.network\.test_ports\.PortsTestJSON\.test_create_update_po
r="$r|(?:tempest\.api\.network\.test_ports\.PortsTestJSON\.test_update_port_with_security_group_and_extra_attributes.*)"
r="$r|(?:tempest\.api\.network\.test_ports\.PortsTestJSON\.test_update_port_with_two_security_groups_and_extra_attributes.*)"
r="$r|(?:tempest\.api\.network\.test_extra_dhcp_options\.ExtraDHCPOptionsTestJSON\.test_.*_with_extra_dhcp_options.*)"
r="$r|(?:tempest\.api\.network\.test_floating_ips\.FloatingIPTestJSON\.test_create_update_floatingip_with_port_multiple_ip_address.*)"
# End list of exclusions.
r="$r)"

View File

@ -26,9 +26,11 @@ from neutron_lib.api.definitions import address_scope as ext_address_scope
from neutron_lib.api.definitions import network as net_def
from neutron_lib.api.definitions import port as port_def
from neutron_lib.api.definitions import subnet as subnet_def
from neutron_lib.api import validators
from neutron_lib import context as n_context
from neutron_lib import exceptions as n_exc
from neutron_lib.plugins import directory
from neutron_lib.utils import net
from vmware_nsx._i18n import _
from vmware_nsx.common import exceptions as nsx_exc
@ -254,3 +256,17 @@ class NsxPluginBase(db_base_plugin_v2.NeutronDbPluginV2,
self.recalculate_snat_rules_for_router(context, rtr,
affected_subnets)
def _validate_max_ips_per_port(self, fixed_ip_list, device_owner):
"""Validate the number of fixed ips on a port
Do not allow multiple ip addresses on a port since the nsx backend
cannot add multiple static dhcp bindings with the same port
"""
if (device_owner and
net.is_port_trusted({'device_owner': device_owner})):
return
if validators.is_attr_set(fixed_ip_list) and len(fixed_ip_list) > 1:
msg = _('Exceeded maximum amount of fixed ips per port')
raise n_exc.InvalidInput(error_message=msg)

View File

@ -1718,6 +1718,8 @@ class NsxVPluginV2(addr_pair_db.AllowedAddressPairsMixin,
port_data = port['port']
dhcp_opts = port_data.get(ext_edo.EXTRADHCPOPTS)
self._validate_extra_dhcp_options(dhcp_opts)
self._validate_max_ips_per_port(port_data.get('fixed_ips', []),
port_data.get('device_owner'))
with db_api.context_manager.writer.using(context):
# First we allocate port in neutron database
@ -1926,6 +1928,9 @@ class NsxVPluginV2(addr_pair_db.AllowedAddressPairsMixin,
self._validate_extra_dhcp_options(dhcp_opts)
if addr_pair.ADDRESS_PAIRS in attrs:
self._validate_address_pairs(attrs, original_port)
self._validate_max_ips_per_port(
port_data.get('fixed_ips', []),
port_data.get('device_owner', original_port['device_owner']))
orig_has_port_security = (cfg.CONF.nsxv.spoofguard_enabled and
original_port[psec.PORTSECURITY])
port_ip_change = port_data.get('fixed_ips') is not None

View File

@ -2081,6 +2081,8 @@ class NsxV3Plugin(agentschedulers_db.AZDhcpAgentSchedulerDbMixin,
port_data = port['port']
dhcp_opts = port_data.get(ext_edo.EXTRADHCPOPTS)
self._validate_extra_dhcp_options(dhcp_opts)
self._validate_max_ips_per_port(port_data.get('fixed_ips', []),
port_data.get('device_owner'))
# TODO(salv-orlando): Undo logical switch creation on failure
with db_api.context_manager.writer.using(context):
@ -2504,31 +2506,35 @@ class NsxV3Plugin(agentschedulers_db.AZDhcpAgentSchedulerDbMixin,
with db_api.context_manager.writer.using(context):
original_port = super(NsxV3Plugin, self).get_port(context, id)
port_data = port['port']
nsx_lswitch_id, nsx_lport_id = nsx_db.get_nsx_switch_and_port_id(
context.session, id)
is_external_net = self._network_is_external(
context, original_port['network_id'])
if is_external_net:
self._assert_on_external_net_with_compute(port['port'])
self._assert_on_external_net_port_with_qos(port['port'])
self._assert_on_external_net_with_compute(port_data)
self._assert_on_external_net_port_with_qos(port_data)
dhcp_opts = port['port'].get(ext_edo.EXTRADHCPOPTS)
dhcp_opts = port_data.get(ext_edo.EXTRADHCPOPTS)
self._validate_extra_dhcp_options(dhcp_opts)
device_owner = (port['port']['device_owner']
if 'device_owner' in port['port']
device_owner = (port_data['device_owner']
if 'device_owner' in port_data
else original_port.get('device_owner'))
self._assert_on_router_port_with_qos(
port['port'], device_owner)
port_data, device_owner)
self._validate_max_ips_per_port(
port_data.get('fixed_ips', []), device_owner)
updated_port = super(NsxV3Plugin, self).update_port(context,
id, port)
self._extension_manager.process_update_port(context, port['port'],
self._extension_manager.process_update_port(context, port_data,
updated_port)
# copy values over - except fixed_ips as
# they've already been processed
port['port'].pop('fixed_ips', None)
updated_port.update(port['port'])
port_data.pop('fixed_ips', None)
updated_port.update(port_data)
updated_port = self._update_port_preprocess_security(
context, port, id, updated_port, validate_port_sec)
@ -2544,7 +2550,7 @@ class NsxV3Plugin(agentschedulers_db.AZDhcpAgentSchedulerDbMixin,
(port_security, has_ip) = self._determine_port_security_and_has_ip(
context, updated_port)
self._process_portbindings_create_and_update(
context, port['port'], updated_port)
context, port_data, updated_port)
self._extend_nsx_port_dict_binding(context, updated_port)
mac_learning_state = updated_port.get(mac_ext.MAC_LEARNING)
if mac_learning_state is not None:

View File

@ -874,15 +874,7 @@ class TestPortsV2(NsxVPluginV2TestCase,
self.skipTest('No DHCP v6 Support yet')
def test_create_port_anticipating_allocation(self):
with self.network(shared=True) as network:
with self.subnet(network=network, cidr='10.0.0.0/24',
enable_dhcp=False) as subnet:
fixed_ips = [{'subnet_id': subnet['subnet']['id']},
{'subnet_id': subnet['subnet']['id'],
'ip_address': '10.0.0.2'}]
self._create_port(self.fmt, network['network']['id'],
webob.exc.HTTPCreated.code,
fixed_ips=fixed_ips)
self.skipTest('Multiple fixed ips on a port are not supported')
def test_update_port_mac_ip(self):
with self.subnet(enable_dhcp=False) as subnet:
@ -1209,94 +1201,10 @@ class TestPortsV2(NsxVPluginV2TestCase,
self.vnic_type))
def test_range_allocation(self):
with self.subnet(enable_dhcp=False, gateway_ip='10.0.0.3',
cidr='10.0.0.0/29') as subnet:
kwargs = {"fixed_ips":
[{'subnet_id': subnet['subnet']['id']},
{'subnet_id': subnet['subnet']['id']},
{'subnet_id': subnet['subnet']['id']},
{'subnet_id': subnet['subnet']['id']},
{'subnet_id': subnet['subnet']['id']}]}
net_id = subnet['subnet']['network_id']
res = self._create_port(self.fmt, net_id=net_id, **kwargs)
port = self.deserialize(self.fmt, res)
ips = port['port']['fixed_ips']
self.assertEqual(len(ips), 5)
alloc = ['10.0.0.1', '10.0.0.2', '10.0.0.4', '10.0.0.5',
'10.0.0.6']
for ip in ips:
self.assertIn(ip['ip_address'], alloc)
self.assertEqual(ip['subnet_id'],
subnet['subnet']['id'])
alloc.remove(ip['ip_address'])
self.assertEqual(len(alloc), 0)
self._delete('ports', port['port']['id'])
with self.subnet(enable_dhcp=False, gateway_ip='11.0.0.6',
cidr='11.0.0.0/29') as subnet:
kwargs = {"fixed_ips":
[{'subnet_id': subnet['subnet']['id']},
{'subnet_id': subnet['subnet']['id']},
{'subnet_id': subnet['subnet']['id']},
{'subnet_id': subnet['subnet']['id']},
{'subnet_id': subnet['subnet']['id']}]}
net_id = subnet['subnet']['network_id']
res = self._create_port(self.fmt, net_id=net_id, **kwargs)
port = self.deserialize(self.fmt, res)
ips = port['port']['fixed_ips']
self.assertEqual(len(ips), 5)
alloc = ['11.0.0.1', '11.0.0.2', '11.0.0.3', '11.0.0.4',
'11.0.0.5']
for ip in ips:
self.assertIn(ip['ip_address'], alloc)
self.assertEqual(ip['subnet_id'],
subnet['subnet']['id'])
alloc.remove(ip['ip_address'])
self.assertEqual(len(alloc), 0)
self._delete('ports', port['port']['id'])
self.skipTest('Multiple fixed ips on a port are not supported')
def test_requested_subnet_id_v4_and_v6(self):
with self.subnet(enable_dhcp=False) as subnet:
# Get an IPv4 and IPv6 address
tenant_id = subnet['subnet']['tenant_id']
net_id = subnet['subnet']['network_id']
res = self._create_subnet(
self.fmt,
tenant_id=tenant_id,
net_id=net_id,
cidr='2607:f0d0:1002:51::/124',
ip_version=6,
gateway_ip=constants.ATTR_NOT_SPECIFIED,
enable_dhcp=False)
subnet2 = self.deserialize(self.fmt, res)
kwargs = {"fixed_ips":
[{'subnet_id': subnet['subnet']['id']},
{'subnet_id': subnet2['subnet']['id']}]}
res = self._create_port(self.fmt, net_id=net_id, **kwargs)
port3 = self.deserialize(self.fmt, res)
ips = port3['port']['fixed_ips']
cidr_v4 = subnet['subnet']['cidr']
cidr_v6 = subnet2['subnet']['cidr']
self.assertEqual(2, len(ips))
self._test_requested_port_subnet_ids(ips,
[subnet['subnet']['id'],
subnet2['subnet']['id']])
self._test_dual_stack_port_ip_addresses_in_subnets(ips,
cidr_v4,
cidr_v6)
res = self._create_port(self.fmt, net_id=net_id)
port4 = self.deserialize(self.fmt, res)
# Check that a v4 and a v6 address are allocated
ips = port4['port']['fixed_ips']
self.assertEqual(len(ips), 2)
self._test_requested_port_subnet_ids(ips,
[subnet['subnet']['id'],
subnet2['subnet']['id']])
self._test_dual_stack_port_ip_addresses_in_subnets(ips,
cidr_v4,
cidr_v6)
self._delete('ports', port3['port']['id'])
self._delete('ports', port4['port']['id'])
self.skipTest('Multiple fixed ips on a port are not supported')
def test_update_port_update_ip(self):
"""Test update of port IP.
@ -1496,36 +1404,47 @@ class TestPortsV2(NsxVPluginV2TestCase,
old_ip,
None, None)
def test_update_port_update_ip_address_only(self):
with self.subnet(enable_dhcp=False) as subnet:
ip_address = '10.0.0.2'
fixed_ip_data = [{'ip_address': ip_address,
'subnet_id': subnet['subnet']['id']}]
with self.port(subnet=subnet, fixed_ips=fixed_ip_data) as port:
ips = port['port']['fixed_ips']
self.assertEqual(1, len(ips))
self.assertEqual(ip_address, ips[0]['ip_address'])
self.assertEqual(ips[0]['subnet_id'], subnet['subnet']['id'])
data = {'port': {'fixed_ips': [{'subnet_id':
subnet['subnet']['id'],
'ip_address': "10.0.0.10"},
{'ip_address': ip_address}]}}
def test_update_port_add_additional_ip(self):
"""Test update of port with additional IP fails."""
with self.subnet() as subnet:
with self.port(subnet=subnet) as port:
data = {'port': {'admin_state_up': False,
'fixed_ips': [{'subnet_id':
subnet['subnet']['id']},
{'subnet_id':
subnet['subnet']['id']}]}}
req = self.new_update_request('ports', data,
port['port']['id'])
res = self.deserialize(self.fmt, req.get_response(self.api))
ips = res['port']['fixed_ips']
self.assertEqual(2, len(ips))
self.assertIn({'ip_address': ip_address,
'subnet_id': subnet['subnet']['id']}, ips)
self.assertIn({'ip_address': '10.0.0.10',
'subnet_id': subnet['subnet']['id']}, ips)
res = req.get_response(self.api)
self.assertEqual(webob.exc.HTTPBadRequest.code,
res.status_int)
def test_create_port_additional_ip(self):
"""Test that creation of port with additional IP fails."""
with self.subnet() as subnet:
data = {'port': {'network_id': subnet['subnet']['network_id'],
'tenant_id': subnet['subnet']['tenant_id'],
'fixed_ips': [{'subnet_id':
subnet['subnet']['id']},
{'subnet_id':
subnet['subnet']['id']}]}}
port_req = self.new_create_request('ports', data)
res = port_req.get_response(self.api)
self.assertEqual(webob.exc.HTTPBadRequest.code,
res.status_int)
def test_update_port_update_ip_address_only(self):
self.skipTest('Multiple fixed ips on a port are not supported')
def test_requested_invalid_fixed_ips(self):
self.skipTest('Multiple fixed ips on a port are not supported')
def test_requested_subnet_id_v4_and_v6_slaac(self):
self.skipTest('Multiple fixed ips on a port are not supported')
def test_update_dhcp_port_with_exceeding_fixed_ips(self):
self.skipTest('Updating dhcp port IP is not supported')
def test_requested_subnet_id_v4_and_v6_slaac(self):
self.skipTest('No DHCP v6 Support yet')
def test_create_router_port_ipv4_and_ipv6_slaac_no_fixed_ips(self):
self.skipTest('No DHCP v6 Support yet')
@ -2246,6 +2165,21 @@ class L3NatTest(test_l3_plugin.L3BaseForIntTests, NsxVPluginV2TestCase):
def test_router_add_gateway_no_subnet(self):
self.skipTest('No support for no subnet gateway set')
def test_floatingip_create_different_fixed_ip_same_port(self):
self.skipTest('Multiple fixed ips on a port are not supported')
def test_router_add_interface_multiple_ipv4_subnet_port_returns_400(self):
self.skipTest('Multiple fixed ips on a port are not supported')
def test_router_add_interface_multiple_ipv6_subnet_port(self):
self.skipTest('Multiple fixed ips on a port are not supported')
def test_floatingip_update_different_fixed_ip_same_port(self):
self.skipTest('Multiple fixed ips on a port are not supported')
def test_create_multiple_floatingips_same_fixed_ip_same_port(self):
self.skipTest('Multiple fixed ips on a port are not supported')
def _set_net_external(self, net_id):
self._update('networks', net_id,
{'network': {external_net.EXTERNAL: True}})
@ -2666,29 +2600,6 @@ class L3NatTestCaseBase(test_l3_plugin.L3NatTestCaseMixin):
s2['subnet']['id'],
None)
def test_router_add_interface_multiple_ipv6_subnet_port(self):
"""A port with multiple IPv6 subnets can be added to a router
Create a port with multiple associated IPv6 subnets and attach
it to a router. The action should succeed.
"""
with self.network() as n, self.router() as r:
with self.subnet(network=n, cidr='fd00::/64',
ip_version=6, enable_dhcp=False) as s1, (
self.subnet(network=n, cidr='fd01::/64',
ip_version=6, enable_dhcp=False)) as s2:
fixed_ips = [{'subnet_id': s1['subnet']['id']},
{'subnet_id': s2['subnet']['id']}]
with self.port(subnet=s1, fixed_ips=fixed_ips) as p:
self._router_interface_action('add',
r['router']['id'],
None,
p['port']['id'])
self._router_interface_action('remove',
r['router']['id'],
None,
p['port']['id'])
def test_router_add_interface_ipv6_subnet_without_gateway_ip(self):
with self.router() as r:
with self.subnet(ip_version=6, cidr='fe80::/64',

View File

@ -765,49 +765,6 @@ class NsxNativeDhcpTestCase(test_plugin.NsxV3PluginTestCaseMixin):
context.get_admin_context(), port['port']['id'], data)
update_dhcp_binding.assert_not_called()
def test_dhcp_binding_with_multiple_ips(self):
# Test create/update/delete DHCP binding with multiple IPs on a
# compute port.
with mock.patch.object(nsx_resources.LogicalDhcpServer,
'create_binding',
side_effect=[{"id": uuidutils.generate_uuid()},
{"id": uuidutils.generate_uuid()}]
) as create_dhcp_binding:
with mock.patch.object(nsx_resources.LogicalDhcpServer,
'update_binding'
) as update_dhcp_binding:
with mock.patch.object(nsx_resources.LogicalDhcpServer,
'delete_binding'
) as delete_dhcp_binding:
with self.subnet(cidr='10.0.0.0/24', enable_dhcp=True
) as subnet:
device_owner = (constants.DEVICE_OWNER_COMPUTE_PREFIX +
'None')
device_id = uuidutils.generate_uuid()
fixed_ips = [{'subnet_id': subnet['subnet']['id'],
'ip_address': '10.0.0.3'},
{'subnet_id': subnet['subnet']['id'],
'ip_address': '10.0.0.4'}]
with self.port(subnet=subnet,
device_owner=device_owner,
device_id=device_id,
fixed_ips=fixed_ips) as port:
self.assertEqual(create_dhcp_binding.call_count, 2)
new_fixed_ips = [
{'subnet_id': subnet['subnet']['id'],
'ip_address': '10.0.0.5'},
{'subnet_id': subnet['subnet']['id'],
'ip_address': '10.0.0.6'}]
self.plugin.update_port(
context.get_admin_context(),
port['port']['id'],
{'port': {'fixed_ips': new_fixed_ips}})
self.assertEqual(update_dhcp_binding.call_count, 2)
self.plugin.delete_port(
context.get_admin_context(),
port['port']['id'])
self.assertEqual(delete_dhcp_binding.call_count, 2)
def test_create_network_with_bad_az_hint(self):
p = directory.get_plugin()
ctx = context.get_admin_context()

View File

@ -282,6 +282,12 @@ class TestSubnetsV2(test_plugin.TestSubnetsV2, NsxV3PluginTestCaseMixin):
self.plugin.create_subnet,
context.get_admin_context(), data)
def test_subnet_update_ipv4_and_ipv6_pd_v6stateless_subnets(self):
self.skipTest('Multiple fixed ips on a port are not supported')
def test_subnet_update_ipv4_and_ipv6_pd_slaac_subnets(self):
self.skipTest('Multiple fixed ips on a port are not supported')
class TestPortsV2(test_plugin.TestPortsV2, NsxV3PluginTestCaseMixin,
test_bindings.PortBindingsTestCase,
@ -502,6 +508,59 @@ class TestPortsV2(test_plugin.TestPortsV2, NsxV3PluginTestCaseMixin,
networks = self.plugin.get_ports(ctx)
self.assertListEqual([], networks)
def test_update_port_add_additional_ip(self):
"""Test update of port with additional IP fails."""
with self.subnet() as subnet:
with self.port(subnet=subnet) as port:
data = {'port': {'admin_state_up': False,
'fixed_ips': [{'subnet_id':
subnet['subnet']['id']},
{'subnet_id':
subnet['subnet']['id']}]}}
req = self.new_update_request('ports', data,
port['port']['id'])
res = req.get_response(self.api)
self.assertEqual(exc.HTTPBadRequest.code,
res.status_int)
def test_create_port_additional_ip(self):
"""Test that creation of port with additional IP fails."""
with self.subnet() as subnet:
data = {'port': {'network_id': subnet['subnet']['network_id'],
'tenant_id': subnet['subnet']['tenant_id'],
'fixed_ips': [{'subnet_id':
subnet['subnet']['id']},
{'subnet_id':
subnet['subnet']['id']}]}}
port_req = self.new_create_request('ports', data)
res = port_req.get_response(self.api)
self.assertEqual(exc.HTTPBadRequest.code,
res.status_int)
def test_update_port_update_ip_address_only(self):
self.skipTest('Multiple fixed ips on a port are not supported')
def test_update_port_with_new_ipv6_slaac_subnet_in_fixed_ips(self):
self.skipTest('Multiple fixed ips on a port are not supported')
def test_update_port_mac_v6_slaac(self):
self.skipTest('Multiple fixed ips on a port are not supported')
def test_requested_subnet_id_v4_and_v6(self):
self.skipTest('Multiple fixed ips on a port are not supported')
def test_requested_invalid_fixed_ips(self):
self.skipTest('Multiple fixed ips on a port are not supported')
def test_requested_subnet_id_v4_and_v6_slaac(self):
self.skipTest('Multiple fixed ips on a port are not supported')
def test_range_allocation(self):
self.skipTest('Multiple fixed ips on a port are not supported')
def test_create_port_anticipating_allocation(self):
self.skipTest('Multiple fixed ips on a port are not supported')
class DHCPOptsTestCase(test_dhcpopts.TestExtraDhcpOpt,
NsxV3PluginTestCaseMixin):
@ -588,6 +647,21 @@ class L3NatTest(test_l3_plugin.L3BaseForIntTests, NsxV3PluginTestCaseMixin,
self.plugin_instance.__class__.__name__)
self._plugin_class = self.plugin_instance.__class__
def test_floatingip_create_different_fixed_ip_same_port(self):
self.skipTest('Multiple fixed ips on a port are not supported')
def test_router_add_interface_multiple_ipv4_subnet_port_returns_400(self):
self.skipTest('Multiple fixed ips on a port are not supported')
def test_router_add_interface_multiple_ipv6_subnet_port(self):
self.skipTest('Multiple fixed ips on a port are not supported')
def test_floatingip_update_different_fixed_ip_same_port(self):
self.skipTest('Multiple fixed ips on a port are not supported')
def test_create_multiple_floatingips_same_fixed_ip_same_port(self):
self.skipTest('Multiple fixed ips on a port are not supported')
class TestL3NatTestCase(L3NatTest,
test_l3_plugin.L3NatDBIntTestCase,