Skip to content

Commit c54641f

Browse files
committed
[ML2/OVN] Validate allowed address pairs and distributed ports
In the ML2/OVN backend, if IP address of the unbound port is added to the other port as `allowed_address_pair`, OVN treats this port as `virtual`. This could break connectivity to the metadata service as it uses "special" port with device_owner set to `network:distributed` and this port is `unbound`. So if someone would add IP address assigned to such `network:distributed` port to the allowed_address_pair of the other port, connectivity to the metadata will be broken. This patch adds new validation of the allowed_address_pairs by the OVN mech_driver. If IP address set as allowed_address_pair is used by the `network:distributed` port, such API request will return BadRequest error code and allowed_address_pair will not be set for the port. Closes-Bug: #2116249 Depends-On: https://review.opendev.org/c/openstack/tempest/+/955569 Change-Id: I9b54e12fbd9b930a79660f2be195641107a5754e Signed-off-by: Slawek Kaplonski <[email protected]> (cherry picked from commit 79e9b02)
1 parent c8076da commit c54641f

File tree

6 files changed

+120
-1
lines changed

6 files changed

+120
-1
lines changed

neutron/db/allowedaddresspairs_db.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
from neutron_lib.api.definitions import allowedaddresspairs as addr_apidef
1818
from neutron_lib.api.definitions import port as port_def
1919
from neutron_lib.api import validators
20+
from neutron_lib.callbacks import events
21+
from neutron_lib.callbacks import registry
2022
from neutron_lib.db import api as db_api
2123
from neutron_lib.db import resource_extend
2224
from neutron_lib.db import utils as db_utils
@@ -36,6 +38,20 @@ def _process_create_allowed_address_pairs(self, context, port,
3638
allowed_address_pairs):
3739
if not validators.is_attr_set(allowed_address_pairs):
3840
return []
41+
42+
desired_state = {
43+
'context': context,
44+
'network_id': port['network_id'],
45+
'allowed_address_pairs': allowed_address_pairs,
46+
}
47+
# TODO(slaweq): use constant from neutron_lib.callbacks.resources once
48+
# it will be available and released
49+
registry.publish(
50+
'allowed_address_pair', events.BEFORE_CREATE, self,
51+
payload=events.DBEventPayload(
52+
context,
53+
resource_id=port['id'],
54+
desired_state=desired_state))
3955
try:
4056
with db_api.CONTEXT_WRITER.using(context):
4157
for address_pair in allowed_address_pairs:

neutron/plugins/ml2/drivers/ovn/mech_driver/mech_driver.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import types
2424
import uuid
2525

26+
import netaddr
2627
from neutron_lib.api.definitions import portbindings
2728
from neutron_lib.api.definitions import provider_net
2829
from neutron_lib.api.definitions import segment as segment_def
@@ -305,6 +306,11 @@ def subscribe(self):
305306
registry.subscribe(self.delete_segment_provnet_port,
306307
resources.SEGMENT,
307308
events.AFTER_DELETE)
309+
# TODO(slaweq): use constant from neutron_lib.callbacks.resources once
310+
# it will be available and released
311+
registry.subscribe(self._validate_allowed_address_pairs,
312+
'allowed_address_pair',
313+
events.BEFORE_CREATE)
308314

309315
# Handle security group/rule or address group notifications
310316
if self.sg_enabled:
@@ -623,6 +629,49 @@ def _validate_network_segments(self, network_segments):
623629
)
624630
raise n_exc.InvalidInput(error_message=m)
625631

632+
def _validate_allowed_address_pairs(self, resource, event, trigger,
633+
payload):
634+
context = payload.desired_state['context']
635+
allowed_address_pairs = payload.desired_state['allowed_address_pairs']
636+
network_id = payload.desired_state['network_id']
637+
if not allowed_address_pairs:
638+
return
639+
640+
port_allowed_address_pairs_ip_addresses = [
641+
netaddr.IPNetwork(pair['ip_address'])
642+
for pair in allowed_address_pairs]
643+
644+
distributed_ports = self._plugin.get_ports(
645+
context.elevated(),
646+
filters={'device_owner': [const.DEVICE_OWNER_DISTRIBUTED],
647+
'network_id': [network_id]})
648+
if not distributed_ports:
649+
return
650+
651+
def _get_common_ips(ip_addresses, ip_networks):
652+
common_ips = set()
653+
for ip_address in ip_addresses:
654+
if any(ip_address in ip_net for ip_net in ip_networks):
655+
common_ips.add(str(ip_address))
656+
return common_ips
657+
658+
for distributed_port in distributed_ports:
659+
distributed_port_ip_addresses = [
660+
netaddr.IPAddress(fixed_ip['ip_address']) for fixed_ip in
661+
distributed_port.get('fixed_ips', [])]
662+
663+
common_ips = _get_common_ips(
664+
distributed_port_ip_addresses,
665+
port_allowed_address_pairs_ip_addresses)
666+
667+
if common_ips:
668+
err_msg = (
669+
_("IP addresses '%s' already used by the '%s' port(s) in "
670+
"the same network" % (";".join(common_ips),
671+
const.DEVICE_OWNER_DISTRIBUTED))
672+
)
673+
raise n_exc.InvalidInput(error_message=err_msg)
674+
626675
def create_segment_provnet_port(self, resource, event, trigger,
627676
payload=None):
628677
segment = payload.latest_state

neutron/tests/common/test_db_base_plugin_v2.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -660,7 +660,7 @@ def _make_port(self, fmt, net_id, expected_res_status=None,
660660
as_admin=False, **kwargs):
661661
res = self._create_port(fmt, net_id, expected_res_status,
662662
is_admin=as_admin, **kwargs)
663-
self._check_http_response(res)
663+
self._check_http_response(res, expected_res_status)
664664
return self.deserialize(fmt, res)
665665

666666
def _make_security_group(self, fmt, name=None, expected_res_status=None,

neutron/tests/unit/db/test_l3_db.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1081,6 +1081,11 @@ def test_update_router_gw_notify(self):
10811081
self.mixin, payload=mock.ANY),
10821082
mock.call(resources.PORT, events.BEFORE_CREATE,
10831083
mock.ANY, payload=mock.ANY),
1084+
# TODO(slaweq): use constant from
1085+
# neutron_lib.callbacks.resources once it will be available
1086+
# and released
1087+
mock.call('allowed_address_pair', events.BEFORE_CREATE,
1088+
mock.ANY, payload=mock.ANY),
10841089
mock.call(resources.PORT, events.PRECOMMIT_CREATE,
10851090
mock.ANY, payload=mock.ANY),
10861091
mock.call(resources.PORT, events.AFTER_CREATE,

neutron/tests/unit/plugins/ml2/drivers/ovn/mech_driver/test_mech_driver.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3183,6 +3183,47 @@ def test_node_uuid_worker_id(self, *args):
31833183
def test_node_uuid_no_worker_id(self, *args):
31843184
self.assertEqual(123456789, self.mech_driver.node_uuid)
31853185

3186+
def test_create_port_with_allowed_address_pairs(self):
3187+
with self.network() as network:
3188+
with self.subnet(network, cidr='10.0.0.0/24'):
3189+
self._make_port(
3190+
self.fmt, network['network']['id'],
3191+
device_owner=const.DEVICE_OWNER_DISTRIBUTED,
3192+
fixed_ips=[{'ip_address': '10.0.0.2'}],
3193+
as_admin=True,
3194+
arg_list=('device_owner', 'fixed_ips'))
3195+
port1 = self._make_port(
3196+
self.fmt, network['network']['id'],
3197+
allowed_address_pairs=[{'ip_address': '10.0.0.3'}],
3198+
as_admin=True,
3199+
arg_list=('allowed_address_pairs',))['port']
3200+
self.assertEqual(
3201+
[{'ip_address': '10.0.0.3',
3202+
'mac_address': port1['mac_address']}],
3203+
port1['allowed_address_pairs'])
3204+
self._make_port(
3205+
self.fmt, network['network']['id'],
3206+
allowed_address_pairs=[{'ip_address': '10.0.0.2'}],
3207+
expected_res_status=exc.HTTPBadRequest.code,
3208+
arg_list=('allowed_address_pairs',))
3209+
port2 = self._show('ports', port1['id'])['port']
3210+
self.assertEqual(
3211+
[{'ip_address': '10.0.0.3',
3212+
'mac_address': port2['mac_address']}],
3213+
port2['allowed_address_pairs'])
3214+
3215+
# Now test the same but giving a subnet as allowed address pair
3216+
self._make_port(
3217+
self.fmt, network['network']['id'],
3218+
allowed_address_pairs=[{'ip_address': '10.0.0.2/26'}],
3219+
expected_res_status=exc.HTTPBadRequest.code,
3220+
arg_list=('allowed_address_pairs',))
3221+
port3 = self._show('ports', port1['id'])['port']
3222+
self.assertEqual(
3223+
[{'ip_address': '10.0.0.3',
3224+
'mac_address': port3['mac_address']}],
3225+
port3['allowed_address_pairs'])
3226+
31863227

31873228
class OVNMechanismDriverTestCase(MechDriverSetupBase,
31883229
test_plugin.Ml2PluginV2TestCase):
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
fixes:
3+
- |
4+
When ML2/OVN backend is used, usage of the metadata port IP address as a
5+
virtual IP address is blocked. That means that setting such IP address as
6+
allowed_address_pair for other port is not allowed and API will return 400
7+
error in such case. For more information, see bug
8+
`2116249 <https://bugs.launchpad.net/neutron/+bug/2116249>`_.

0 commit comments

Comments
 (0)