Browse Source

Merge "[OVN] Enhance port's extra DHCP options support" into stable/train

changes/05/738405/1
Zuul 1 week ago
committed by Gerrit Code Review
parent
commit
793bb1e6c5
7 changed files with 446 additions and 13 deletions
  1. +138
    -0
      doc/source/contributor/design/dhcp_opts.rst
  2. +1
    -0
      doc/source/contributor/design/index.rst
  3. +82
    -8
      networking_ovn/common/constants.py
  4. +46
    -5
      networking_ovn/common/utils.py
  5. +15
    -0
      networking_ovn/ml2/mech_driver.py
  6. +113
    -0
      networking_ovn/tests/unit/common/test_utils.py
  7. +51
    -0
      networking_ovn/tests/unit/ml2/test_mech_driver.py

+ 138
- 0
doc/source/contributor/design/dhcp_opts.rst View File

@@ -0,0 +1,138 @@
.. _ovn_dhcp_opts:

OVN supported DHCP options
==========================

This is a list of the current supported DHCP options in ML2/OVN:

IP version 4
~~~~~~~~~~~~

========================== ============================
Option name / code OVN value
========================== ============================
arp-timeout arp_cache_timeout
bootfile-name bootfile_name
classless-static-route classless_static_route
default-ttl default_ttl
dns-server dns_server
domain-name domain_name
ethernet-encap ethernet_encap
ip-forward-enable ip_forward_enable
lease-time lease_time
log-server log_server
lpr-server lpr_server
ms-classless-static-route ms_classless_static_route
mtu mtu
netmask netmask
nis-server nis_server
ntp-server ntp_server
path-prefix path_prefix
policy-filter policy_filter
router-discovery router_discovery
router router
router-solicitation router_solicitation
server-id server_id
server-ip-address tftp_server_address
swap-server swap_server
T1 T1
T2 T2
tcp-ttl tcp_ttl
tcp-keepalive tcp_keepalive_interval
tftp-server-address tftp_server_address
tftp-server tftp_server
wpad wpad
1 netmask
3 router
6 dns_server
7 log_server
9 lpr_server
15 domain_name
16 swap_server
19 ip_forward_enable
21 policy_filter
23 default_ttl
26 mtu
31 router_discovery
32 router_solicitation
35 arp_cache_timeout
36 ethernet_encap
37 tcp_ttl
38 tcp_keepalive_interval
41 nis_server
42 ntp_server
51 lease_time
54 server_id
58 T1
59 T2
66 tftp_server
67 bootfile_name
121 classless_static_route
150 tftp_server_address
210 path_prefix
249 ms_classless_static_route
252 wpad
========================== ============================

IP version 6
~~~~~~~~~~~~

================== =============
Option name / code OVN value
================== =============
dns-server dns_server
domain-search domain_search
ia-addr ip_addr
server-id server_id
2 server_id
5 ia_addr
23 dns_server
24 domain_search
================== =============

OVN Database information
~~~~~~~~~~~~~~~~~~~~~~~~

In OVN the DHCP options are stored on a table called ``DHCP_Options``
in the OVN Northbound database.

Let's add a DHCP option to a Neutron port:

.. code-block:: bash

$ neutron port-update --extra-dhcp-opt opt_name='server-ip-address',opt_value='10.0.0.1' b4c3f265-369e-4bf5-8789-7caa9a1efb9c
Updated port: b4c3f265-369e-4bf5-8789-7caa9a1efb9c

.. end

To find that port in OVN we can use command below:

.. code-block:: bash

$ ovn-nbctl find Logical_Switch_Port name=b4c3f265-369e-4bf5-8789-7caa9a1efb9c
...
dhcpv4_options : 5f00d1a2-c57d-4d1f-83ea-09bf8be13288
dhcpv6_options : []
...

.. end

For DHCP, the columns that we care about are the ``dhcpv4_options``
and ``dhcpv6_options``. These columns has the uuids of entries in the
``DHCP_Options`` table with the DHCP information for this port.

.. code-block:: bash

$ ovn-nbctl list DHCP_Options 5f00d1a2-c57d-4d1f-83ea-09bf8be13288
_uuid : 5f00d1a2-c57d-4d1f-83ea-09bf8be13288
cidr : "10.0.0.0/26"
external_ids : {"neutron:revision_number"="0", port_id="b4c3f265-369e-4bf5-8789-7caa9a1efb9c", subnet_id="5157ed8b-e7f1-4c56-b789-fa420098a687"}
options : {classless_static_route="{169.254.169.254/32,10.0.0.2, 0.0.0.0/0,10.0.0.1}", dns_server="{8.8.8.8}", domain_name="\"openstackgate.local\"", lease_time="43200", log_server="127.0.0.3", mtu="1442", router="10.0.0.1", server_id="10.0.0.1", server_mac="fa:16:3e:dc:57:22", tftp_server_address="10.0.0.1"}

.. end

Here you can see that the option ``tftp_server_address`` has been set in
the **options** column. Note that, the ``tftp_server_address`` option is
the OVN translated name for ``server-ip-address`` (option 150). Take a
look at the table in this document to find out more about the supported
options and their counterpart names in OVN.

+ 1
- 0
doc/source/contributor/design/index.rst View File

@@ -13,3 +13,4 @@ Design Notes
acl_optimizations
loadbalancer
distributed_ovsdb_events
dhcp_opts

+ 82
- 8
networking_ovn/common/constants.py View File

@@ -88,14 +88,88 @@ ACL_ACTION_ALLOW = 'allow'
# unhosted router gateways to schedule.
OVN_GATEWAY_INVALID_CHASSIS = 'neutron-ovn-invalid-chassis'

SUPPORTED_DHCP_OPTS = {
4: ['netmask', 'router', 'dns-server', 'log-server',
'lpr-server', 'swap-server', 'ip-forward-enable',
'policy-filter', 'default-ttl', 'mtu', 'router-discovery',
'router-solicitation', 'arp-timeout', 'ethernet-encap',
'tcp-ttl', 'tcp-keepalive', 'nis-server', 'ntp-server',
'tftp-server'],
6: ['server-id', 'dns-server', 'domain-search']}
# NOTE(lucasagomes): These options were last synced from
# https://github.com/ovn-org/ovn/blob/feb5d6e81d5a0290aa3618a229c860d01200422e/lib/ovn-l7.h
#
# NOTE(lucasagomes): Whenever we update these lists please also update
# the related documentation at doc/source/ovn/dhcp_opts.rst
#
# Mappping between Neutron option names and OVN ones
SUPPORTED_DHCP_OPTS_MAPPING = {
4: {'arp-timeout': 'arp_cache_timeout',
'tcp-keepalive': 'tcp_keepalive_interval',
'netmask': 'netmask',
'router': 'router',
'dns-server': 'dns_server',
'log-server': 'log_server',
'lpr-server': 'lpr_server',
'domain-name': 'domain_name',
'swap-server': 'swap_server',
'policy-filter': 'policy_filter',
'router-solicitation': 'router_solicitation',
'nis-server': 'nis_server',
'ntp-server': 'ntp_server',
'server-id': 'server_id',
'tftp-server': 'tftp_server',
'classless-static-route': 'classless_static_route',
'ms-classless-static-route': 'ms_classless_static_route',
'ip-forward-enable': 'ip_forward_enable',
'router-discovery': 'router_discovery',
'ethernet-encap': 'ethernet_encap',
'default-ttl': 'default_ttl',
'tcp-ttl': 'tcp_ttl',
'mtu': 'mtu',
'lease-time': 'lease_time',
'T1': 'T1',
'T2': 'T2',
'bootfile-name': 'bootfile_name',
'wpad': 'wpad',
'path-prefix': 'path_prefix',
'tftp-server-address': 'tftp_server_address',
'server-ip-address': 'tftp_server_address',
'1': 'netmask',
'3': 'router',
'6': 'dns_server',
'7': 'log_server',
'9': 'lpr_server',
'15': 'domain_name',
'16': 'swap_server',
'21': 'policy_filter',
'32': 'router_solicitation',
'35': 'arp_cache_timeout',
'38': 'tcp_keepalive_interval',
'41': 'nis_server',
'42': 'ntp_server',
'54': 'server_id',
'66': 'tftp_server',
'121': 'classless_static_route',
'249': 'ms_classless_static_route',
'19': 'ip_forward_enable',
'31': 'router_discovery',
'36': 'ethernet_encap',
'23': 'default_ttl',
'37': 'tcp_ttl',
'26': 'mtu',
'51': 'lease_time',
'58': 'T1',
'59': 'T2',
'67': 'bootfile_name',
'252': 'wpad',
'210': 'path_prefix',
'150': 'tftp_server_address'},
6: {'server-id': 'server_id',
'dns-server': 'dns_server',
'domain-search': 'domain_search',
'ia-addr': 'ip_addr',
'2': 'server_id',
'5': 'ia_addr',
'24': 'domain_search',
'23': 'dns_server'},
}

# Special option for disabling DHCP via extra DHCP options
DHCP_DISABLED_OPT = 'dhcp_disabled'

DHCPV6_STATELESS_OPT = 'dhcpv6_stateless'

# When setting global DHCP options, these options will be ignored


+ 46
- 5
networking_ovn/common/utils.py View File

@@ -27,6 +27,7 @@ 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 as n_utils
from oslo_log import log
from oslo_utils import netutils
from oslo_utils import strutils
from ovs.db import idl
@@ -37,11 +38,16 @@ from networking_ovn._i18n import _
from networking_ovn.common import constants
from networking_ovn.common import exceptions as ovn_exc

LOG = log.getLogger(__name__)

DNS_RESOLVER_FILE = "/etc/resolv.conf"

AddrPairsDiff = collections.namedtuple(
'AddrPairsDiff', ['added', 'removed', 'changed'])

PortExtraDHCPValidation = collections.namedtuple(
'PortExtraDHCPValidation', ['failed', 'invalid_ipv4', 'invalid_ipv6'])


def ovn_name(id):
# The name of the OVN entry will be neutron-<UUID>
@@ -108,6 +114,39 @@ def is_network_device_port(port):
const.DEVICE_OWNER_PREFIXES)


def _is_dhcp_disabled(dhcp_opt):
return (dhcp_opt['opt_name'] == constants.DHCP_DISABLED_OPT and
dhcp_opt.get('opt_value', '').lower() == 'true')


def validate_port_extra_dhcp_opts(port):
"""Validate port's extra DHCP options.

:param port: A neutron port.
:returns: A PortExtraDHCPValidation object.
"""
invalid = {const.IP_VERSION_4: [], const.IP_VERSION_6: []}
failed = False
for edo in port.get(edo_ext.EXTRADHCPOPTS, []):
ip_version = edo['ip_version']
opt_name = edo['opt_name']

# If DHCP is disabled for this port via this special option,
# always succeed the validation
if _is_dhcp_disabled(edo):
failed = False
break

if opt_name not in constants.SUPPORTED_DHCP_OPTS_MAPPING[ip_version]:
invalid[ip_version].append(opt_name)
failed = True

return PortExtraDHCPValidation(
failed=failed,
invalid_ipv4=invalid[const.IP_VERSION_4] if failed else [],
invalid_ipv6=invalid[const.IP_VERSION_6] if failed else [])


def get_lsp_dhcp_opts(port, ip_version):
# Get dhcp options from Neutron port, for setting DHCP_Options row
# in OVN.
@@ -116,12 +155,12 @@ def get_lsp_dhcp_opts(port, ip_version):
if is_network_device_port(port):
lsp_dhcp_disabled = True
else:
mapping = constants.SUPPORTED_DHCP_OPTS_MAPPING[ip_version]
for edo in port.get(edo_ext.EXTRADHCPOPTS, []):
if edo['ip_version'] != ip_version:
continue

if edo['opt_name'] == 'dhcp_disabled' and (
edo['opt_value'] in ['True', 'true']):
if _is_dhcp_disabled(edo):
# OVN native DHCP is disabled on this port
lsp_dhcp_disabled = True
# Make sure return value behavior not depends on the order and
@@ -129,11 +168,13 @@ def get_lsp_dhcp_opts(port, ip_version):
lsp_dhcp_opts.clear()
break

if edo['opt_name'] not in (
constants.SUPPORTED_DHCP_OPTS[ip_version]):
if edo['opt_name'] not in mapping:
LOG.warning('The DHCP option %(opt_name)s on port %(port)s '
'is not suppported by OVN, ignoring it',
{'opt_name': edo['opt_name'], 'port': port['id']})
continue

opt = edo['opt_name'].replace('-', '_')
opt = mapping[edo['opt_name']]
lsp_dhcp_opts[opt] = edo['opt_value']

return (lsp_dhcp_disabled, lsp_dhcp_opts)


+ 15
- 0
networking_ovn/ml2/mech_driver.py View File

@@ -445,6 +445,19 @@ class OVNMechanismDriver(api.MechanismDriver):
def delete_subnet_postcommit(self, context):
self._ovn_client.delete_subnet(context.current['id'])

def _validate_port_extra_dhcp_opts(self, port):
result = utils.validate_port_extra_dhcp_opts(port)
if not result.failed:
return
ipv4_opts = ', '.join(result.invalid_ipv4)
ipv6_opts = ', '.join(result.invalid_ipv6)
msg = (_('The following extra DHCP options for port %(port_id)s '
'are not supported by OVN. IPv4: "%(ipv4_opts)s" and '
'IPv6: "%(ipv6_opts)s"') %
{'port_id': port['id'], 'ipv4_opts': ipv4_opts,
'ipv6_opts': ipv6_opts})
raise OVNPortUpdateError(resource='port', msg=msg)

def create_port_precommit(self, context):
"""Allocate resources for a new port.

@@ -459,6 +472,7 @@ class OVNMechanismDriver(api.MechanismDriver):
if utils.is_lsp_ignored(port):
return
utils.validate_and_get_data_from_binding_profile(port)
self._validate_port_extra_dhcp_opts(port)
if self._is_port_provisioning_required(port, context.host):
self._insert_port_provisioning_block(context._plugin_context,
port['id'])
@@ -573,6 +587,7 @@ class OVNMechanismDriver(api.MechanismDriver):
original_port = context.original
self._validate_ignored_port(port, original_port)
utils.validate_and_get_data_from_binding_profile(port)
self._validate_port_extra_dhcp_opts(port)
if self._is_port_provisioning_required(port, context.host,
context.original_host):
self._insert_port_provisioning_block(context._plugin_context,


+ 113
- 0
networking_ovn/tests/unit/common/test_utils.py View File

@@ -14,6 +14,8 @@
# under the License.

import fixtures
import mock
from neutron_lib.api.definitions import extra_dhcp_opt as edo_ext

from networking_ovn.common import constants
from networking_ovn.common import utils
@@ -118,3 +120,114 @@ class TestGateWayChassisValidity(base.TestCase):
self.assertTrue(utils.is_gateway_chassis_invalid(
self.chassis_name, self.gw_chassis, self.physnet,
self.chassis_physnets))


class TestDHCPUtils(base.TestCase):

def test_validate_port_extra_dhcp_opts_empty(self):
port = {edo_ext.EXTRADHCPOPTS: []}
result = utils.validate_port_extra_dhcp_opts(port)
self.assertFalse(result.failed)
self.assertEqual([], result.invalid_ipv4)
self.assertEqual([], result.invalid_ipv6)

def test_validate_port_extra_dhcp_opts_dhcp_disabled(self):
opt0 = {'opt_name': 'not-valid-ipv4',
'opt_value': 'joe rogan',
'ip_version': 4}
opt1 = {'opt_name': 'dhcp_disabled',
'opt_value': 'True',
'ip_version': 4}
port = {edo_ext.EXTRADHCPOPTS: [opt0, opt1]}

# Validation always succeeds if the "dhcp_disabled" option is enabled
result = utils.validate_port_extra_dhcp_opts(port)
self.assertFalse(result.failed)
self.assertEqual([], result.invalid_ipv4)
self.assertEqual([], result.invalid_ipv6)

def test_validate_port_extra_dhcp_opts(self):
opt0 = {'opt_name': 'bootfile-name',
'opt_value': 'homer_simpson.bin',
'ip_version': 4}
opt1 = {'opt_name': 'dns-server',
'opt_value': '2001:4860:4860::8888',
'ip_version': 6}
port = {edo_ext.EXTRADHCPOPTS: [opt0, opt1]}

result = utils.validate_port_extra_dhcp_opts(port)
self.assertFalse(result.failed)
self.assertEqual([], result.invalid_ipv4)
self.assertEqual([], result.invalid_ipv6)

def test_validate_port_extra_dhcp_opts_invalid(self):
# Two value options and two invalid, assert the validation
# will fail and only the invalid options will be returned as
# not supported
opt0 = {'opt_name': 'bootfile-name',
'opt_value': 'homer_simpson.bin',
'ip_version': 4}
opt1 = {'opt_name': 'dns-server',
'opt_value': '2001:4860:4860::8888',
'ip_version': 6}
opt2 = {'opt_name': 'not-valid-ipv4',
'opt_value': 'joe rogan',
'ip_version': 4}
opt3 = {'opt_name': 'not-valid-ipv6',
'opt_value': 'young jamie',
'ip_version': 6}
port = {edo_ext.EXTRADHCPOPTS: [opt0, opt1, opt2, opt3]}

result = utils.validate_port_extra_dhcp_opts(port)
self.assertTrue(result.failed)
self.assertEqual(['not-valid-ipv4'], result.invalid_ipv4)
self.assertEqual(['not-valid-ipv6'], result.invalid_ipv6)

def test_get_lsp_dhcp_opts_empty(self):
port = {edo_ext.EXTRADHCPOPTS: []}
dhcp_disabled, options = utils.get_lsp_dhcp_opts(port, 4)
self.assertFalse(dhcp_disabled)
self.assertEqual({}, options)

def test_get_lsp_dhcp_opts_empty_dhcp_disabled(self):
opt0 = {'opt_name': 'bootfile-name',
'opt_value': 'homer_simpson.bin',
'ip_version': 4}
opt1 = {'opt_name': 'dhcp_disabled',
'opt_value': 'True',
'ip_version': 4}
port = {edo_ext.EXTRADHCPOPTS: [opt0, opt1]}

# Validation always succeeds if the "dhcp_disabled" option is enabled
dhcp_disabled, options = utils.get_lsp_dhcp_opts(port, 4)
self.assertTrue(dhcp_disabled)
self.assertEqual({}, options)

@mock.patch.object(utils, 'is_network_device_port')
def test_get_lsp_dhcp_opts_is_network_device_port(self, mock_device_port):
mock_device_port.return_value = True
port = {}
dhcp_disabled, options = utils.get_lsp_dhcp_opts(port, 4)
# Assert OVN DHCP is disabled
self.assertTrue(dhcp_disabled)
self.assertEqual({}, options)

def test_get_lsp_dhcp_opts(self):
opt0 = {'opt_name': 'bootfile-name',
'opt_value': 'homer_simpson.bin',
'ip_version': 4}
opt1 = {'opt_name': 'server-ip-address',
'opt_value': '10.0.0.1',
'ip_version': 4}
opt2 = {'opt_name': '42',
'opt_value': '10.0.2.1',
'ip_version': 4}
port = {edo_ext.EXTRADHCPOPTS: [opt0, opt1, opt2]}

dhcp_disabled, options = utils.get_lsp_dhcp_opts(port, 4)
self.assertFalse(dhcp_disabled)
# Assert the names got translated to their OVN names
expected_options = {'tftp_server_address': '10.0.0.1',
'ntp_server': '10.0.2.1',
'bootfile_name': 'homer_simpson.bin'}
self.assertEqual(expected_options, options)

+ 51
- 0
networking_ovn/tests/unit/ml2/test_mech_driver.py View File

@@ -27,6 +27,7 @@ from neutron.tests.unit.plugins.ml2 import test_ext_portsecurity
from neutron.tests.unit.plugins.ml2 import test_plugin
from neutron.tests.unit.plugins.ml2 import test_security_group
from neutron_lib.api.definitions import external_net
from neutron_lib.api.definitions import extra_dhcp_opt as edo_ext
from neutron_lib.api.definitions import portbindings
from neutron_lib.api.definitions import provider_net as pnet
from neutron_lib.callbacks import events
@@ -311,6 +312,56 @@ class TestOVNMechanismDriver(test_plugin.Ml2PluginV2TestCase):
self.mech_driver._validate_ignored_port,
p, ori_p)

def test__validate_port_extra_dhcp_opts(self):
opt = {'opt_name': 'bootfile-name',
'opt_value': 'homer_simpson.bin',
'ip_version': 4}
port = {edo_ext.EXTRADHCPOPTS: [opt], 'id': 'fake-port'}
self.assertIsNone(
self.mech_driver._validate_port_extra_dhcp_opts(port))

def test__validate_port_extra_dhcp_opts_invalid(self):
opt = {'opt_name': 'not-valid',
'opt_value': 'spongebob squarepants',
'ip_version': 4}
port = {edo_ext.EXTRADHCPOPTS: [opt], 'id': 'fake-port'}
self.assertRaises(mech_driver.OVNPortUpdateError,
self.mech_driver._validate_port_extra_dhcp_opts,
port)

def test_create_port_invalid_extra_dhcp_opts(self):
extra_dhcp_opts = {
'extra_dhcp_opts': [{'ip_version': 4, 'opt_name': 'banana',
'opt_value': 'banana'},
{'ip_version': 6, 'opt_name': 'orange',
'opt_value': 'orange'}]
}
with self.network() as n:
with self.subnet(n):
res = self._create_port(self.fmt, n['network']['id'],
arg_list=('extra_dhcp_opts',),
**extra_dhcp_opts)
# Assert 400 (BadRequest) was returned
self.assertEqual(400, res.status_code)
response = self.deserialize(self.fmt, res)
self.assertIn('banana', response['NeutronError']['message'])
self.assertIn('orange', response['NeutronError']['message'])

def test_update_port_invalid_extra_dhcp_opts(self):
data = {
'port': {'extra_dhcp_opts': [{'ip_version': 4, 'opt_name': 'apple',
'opt_value': 'apple'},
{'ip_version': 6, 'opt_name': 'grape',
'opt_value': 'grape'}]}}
with self.network(set_context=True, tenant_id='test') as net:
with self.subnet(network=net) as subnet:
with self.port(subnet=subnet,
set_context=True, tenant_id='test') as port:
res = self._update('ports', port['port']['id'], data,
expected_code=400)
self.assertIn('apple', res['NeutronError']['message'])
self.assertIn('grape', res['NeutronError']['message'])

def test_create_and_update_ignored_fip_port(self):
with self.network(set_context=True, tenant_id='test') as net1:
with self.subnet(network=net1) as subnet1:


Loading…
Cancel
Save