Browse Source

Merge "[OVN] Update the DHCP options when the metadata port is modified" into stable/train

changes/04/812304/7
Zuul 7 months ago committed by Gerrit Code Review
parent
commit
0242cf8b46
  1. 94
      networking_ovn/common/ovn_client.py
  2. 2
      networking_ovn/ovsdb/impl_idl_ovn.py
  3. 87
      networking_ovn/tests/functional/test_mech_driver.py
  4. 1
      networking_ovn/tests/unit/fakes.py
  5. 136
      networking_ovn/tests/unit/ml2/test_mech_driver.py
  6. 4
      networking_ovn/tests/unit/ovsdb/test_impl_idl_ovn.py

94
networking_ovn/common/ovn_client.py

@ -507,6 +507,16 @@ class OVNClient(object):
else:
dhcpv6_options = [port_info.dhcpv6_options['uuid']]
if self.is_metadata_port(port):
context = n_context.get_admin_context()
network = self._plugin.get_network(context, port['network_id'])
subnet_ids = set(_ip['subnet_id'] for _ip in port['fixed_ips'])
for subnet_id in subnet_ids:
subnet = self._plugin.get_subnet(context, subnet_id)
if not subnet['enable_dhcp']:
continue
self._update_subnet_dhcp_options(subnet, network, txn)
# NOTE(mjozefcz): Do not set addresses if the port is not
# bound, has no device_owner and it is OVN LB VIP port.
# For more details check related bug #1789686.
@ -1984,11 +1994,15 @@ class OVNClient(object):
def create_subnet(self, subnet, network):
if subnet['enable_dhcp']:
if subnet['ip_version'] == 4:
mport_updated = False
if subnet['ip_version'] == const.IP_VERSION_4:
context = n_context.get_admin_context()
self.update_metadata_port(context, network['id'])
self._add_subnet_dhcp_options(subnet, network)
mport_updated = self.update_metadata_port(
context, network['id'], subnet=subnet)
if subnet['ip_version'] == const.IP_VERSION_6 or not mport_updated:
# NOTE(ralonsoh): if IPv4 but the metadata port has not been
# updated, the DHPC options register has not been created.
self._add_subnet_dhcp_options(subnet, network)
db_rev.bump_revision(subnet, ovn_const.TYPE_SUBNETS)
def _modify_subnet_dhcp_options(self, subnet, ovn_subnet, network, txn):
@ -2005,7 +2019,7 @@ class OVNClient(object):
if subnet['enable_dhcp'] or ovn_subnet:
context = n_context.get_admin_context()
self.update_metadata_port(context, network['id'])
self.update_metadata_port(context, network['id'], subnet=subnet)
check_rev_cmd = self._nb_idl.check_revision_number(
subnet['id'], subnet, ovn_const.TYPE_SUBNETS)
@ -2094,6 +2108,12 @@ class OVNClient(object):
self._process_security_group_rule(rule, is_add_acl=False)
db_rev.delete_revision(rule['id'], ovn_const.TYPE_SECURITY_GROUP_RULES)
@staticmethod
def is_metadata_port(port):
return (port['device_owner'] == 'network:distributed' or
(port['device_owner'] == const.DEVICE_OWNER_DHCP and
not utils.is_neutron_dhcp_agent_port(port)))
def _find_metadata_port(self, context, network_id):
if not config.is_ovn_metadata_enabled():
return
@ -2127,47 +2147,67 @@ class OVNClient(object):
# TODO(boden): rehome create_port into neutron-lib
p_utils.create_port(self._plugin, context, port)
def update_metadata_port(self, context, network_id):
def update_metadata_port(self, context, network_id, subnet=None):
"""Update metadata port.
This function will allocate an IP address for the metadata port of
the given network in all its IPv4 subnets.
the given network in all its IPv4 subnets or the given subnet. Returns
"True" if the metadata port has been updated and "False" if OVN
metadata is disabled or the metadata port does not exist.
"""
def update_metadata_port_fixed_ips(metadata_port, add_subnet_ids,
del_subnet_ids):
wanted_fixed_ips = [
{'subnet_id': fixed_ip['subnet_id'],
'ip_address': fixed_ip['ip_address']} for fixed_ip in
metadata_port['fixed_ips'] if
fixed_ip['subnet_id'] not in del_subnet_ids]
wanted_fixed_ips.extend({'subnet_id': s_id} for s_id in
add_subnet_ids)
port = {'id': metadata_port['id'],
'port': {'network_id': network_id,
'fixed_ips': wanted_fixed_ips}}
self._plugin.update_port(n_context.get_admin_context(),
metadata_port['id'], port)
if not config.is_ovn_metadata_enabled():
return
return False
# Retrieve the metadata port of this network
metadata_port = self._find_metadata_port(context, network_id)
if not metadata_port:
LOG.error("Metadata port couldn't be found for network %s",
network_id)
return
return False
port_subnet_ids = set(ip['subnet_id'] for ip in
metadata_port['fixed_ips'])
# If this method is called from "create_subnet" or "update_subnet",
# only the fixed IP address from this subnet should be updated in the
# metadata port.
if subnet and subnet['id']:
if subnet['enable_dhcp'] and subnet['id'] not in port_subnet_ids:
update_metadata_port_fixed_ips(metadata_port,
[subnet['id']], [])
elif not subnet['enable_dhcp'] and subnet['id'] in port_subnet_ids:
update_metadata_port_fixed_ips(metadata_port,
[], [subnet['id']])
return True
# Retrieve all subnets in this network
subnets = self._plugin.get_subnets(context, filters=dict(
network_id=[network_id], ip_version=[4]))
network_id=[network_id], ip_version=[const.IP_VERSION_4],
enable_dhcp=[True]))
subnet_ids = set(s['id'] for s in subnets)
port_subnet_ids = set(ip['subnet_id'] for ip in
metadata_port['fixed_ips'])
# Find all subnets where metadata port doesn't have an IP in and
# allocate one.
if subnet_ids != port_subnet_ids:
wanted_fixed_ips = []
for fixed_ip in metadata_port['fixed_ips']:
wanted_fixed_ips.append(
{'subnet_id': fixed_ip['subnet_id'],
'ip_address': fixed_ip['ip_address']})
wanted_fixed_ips.extend(
dict(subnet_id=s)
for s in subnet_ids - port_subnet_ids)
port = {'id': metadata_port['id'],
'port': {'network_id': network_id,
'fixed_ips': wanted_fixed_ips}}
self._plugin.update_port(n_context.get_admin_context(),
metadata_port['id'], port)
update_metadata_port_fixed_ips(metadata_port,
subnet_ids - port_subnet_ids,
port_subnet_ids - subnet_ids)
return True
def get_parent_port(self, port_id):
return self._nb_idl.get_parent_port(port_id)

2
networking_ovn/ovsdb/impl_idl_ovn.py

@ -523,7 +523,7 @@ class OvsdbNbOvnIdl(nb_impl_idl.OvnNbApiIdlImpl, Backend):
'external_ids': ext_ids, 'uuid': row.uuid}
def get_subnet_dhcp_options(self, subnet_id, with_ports=False):
subnet = None
subnet = {}
ports = []
for row in self._tables['DHCP_Options'].rows.values():
external_ids = getattr(row, 'external_ids', {})

87
networking_ovn/tests/functional/test_mech_driver.py

@ -13,14 +13,18 @@
# under the License.
import mock
import re
import netaddr
from neutron.tests import base as tests_base
from neutron_lib.api.definitions import portbindings
from neutron_lib import constants
from oslo_config import cfg
from oslo_utils import uuidutils
from networking_ovn.common import config
from networking_ovn.common import constants as ovn_const
from networking_ovn.common import ovn_client
from networking_ovn.common import utils
from networking_ovn.db import revision as db_rev
from networking_ovn.tests.functional import base
@ -689,3 +693,86 @@ class TestAgentApi(base.TestOVNFunctionalBase):
for agent_type in ('test', ovn_const.OVN_CONTROLLER_AGENT):
agent = self.get_agent(agent_type)
self.assertTrue(self.plugin.get_agent(self.context, agent['id']))
class TestMetadataPorts(base.TestOVNFunctionalBase):
def setUp(self, *args, **kwargs):
super(TestMetadataPorts, self).setUp(*args, **kwargs)
self._ovn_client = self.mech_driver._ovn_client
self.meta_regex = re.compile(
r'169.254.169.254/32,(\d+\.\d+\.\d+\.\d+)')
def _create_network_ovn(self, metadata_enabled=True):
self.mock_is_ovn_metadata_enabled = mock.patch.object(
config, 'is_ovn_metadata_enabled').start()
self.mock_is_ovn_metadata_enabled.return_value = metadata_enabled
self.n1 = self._make_network(self.fmt, 'n1', True)
self.n1_id = self.n1['network']['id']
def _create_subnet_ovn(self, cidr, enable_dhcp=True):
_cidr = netaddr.IPNetwork(cidr)
res = self._create_subnet(self.fmt, self.n1_id, cidr,
enable_dhcp=enable_dhcp,
ip_version=_cidr.version)
return self.deserialize(self.fmt, res)['subnet']
def _list_ports_ovn(self, net_id=None):
res = self._list_ports(self.fmt, net_id=net_id)
return self.deserialize(self.fmt, res)['ports']
def _check_metadata_port(self, net_id, fixed_ip):
for port in self._list_ports_ovn(net_id=net_id):
if ovn_client.OVNClient.is_metadata_port(port):
self.assertEqual(net_id, port['network_id'])
if fixed_ip:
self.assertIn(fixed_ip, port['fixed_ips'])
else:
self.assertEqual([], port['fixed_ips'])
return port['id']
self.fail('Metadata port is not present in network %s or data is not '
'correct' % self.n1_id)
def _check_subnet_dhcp_options(self, subnet_id, cidr):
# This method checks the DHCP options CIDR and returns, if exits, the
# metadata port IP address, included in the classless static routes.
dhcp_opts = self._ovn_client._nb_idl.get_subnet_dhcp_options(subnet_id)
self.assertEqual(cidr, dhcp_opts['subnet']['cidr'])
routes = dhcp_opts['subnet']['options'].get('classless_static_route')
if not routes:
return
match = self.meta_regex.search(routes)
if match:
return match.group(1)
def test_subnet_ipv4(self):
self._create_network_ovn(metadata_enabled=True)
subnet = self._create_subnet_ovn('10.0.0.0/24')
metatada_ip = self._check_subnet_dhcp_options(subnet['id'],
'10.0.0.0/24')
fixed_ip = {'subnet_id': subnet['id'], 'ip_address': metatada_ip}
port_id = self._check_metadata_port(self.n1_id, fixed_ip)
# Update metatada port IP address to 10.0.0.5
data = {'port': {'fixed_ips': [{'subnet_id': subnet['id'],
'ip_address': '10.0.0.5'}]}}
req = self.new_update_request('ports', data, port_id)
req.get_response(self.api)
metatada_ip = self._check_subnet_dhcp_options(subnet['id'],
'10.0.0.0/24')
self.assertEqual('10.0.0.5', metatada_ip)
fixed_ip = {'subnet_id': subnet['id'], 'ip_address': metatada_ip}
self._check_metadata_port(self.n1_id, fixed_ip)
def test_subnet_ipv4_no_metadata(self):
self._create_network_ovn(metadata_enabled=False)
subnet = self._create_subnet_ovn('10.0.0.0/24')
self.assertIsNone(self._check_subnet_dhcp_options(subnet['id'],
'10.0.0.0/24'))
self.assertEqual([], self._list_ports_ovn(self.n1_id))
def test_subnet_ipv6(self):
self._create_network_ovn(metadata_enabled=True)
subnet = self._create_subnet_ovn('2001:db8::/64')
self.assertIsNone(self._check_subnet_dhcp_options(subnet['id'],
'2001:db8::/64'))
self._check_metadata_port(self.n1_id, [])

1
networking_ovn/tests/unit/fakes.py

@ -140,6 +140,7 @@ class FakeOvsdbNbOvnIdl(object):
self.ls_get = mock.Mock()
self.check_liveness = mock.Mock()
self.ha_chassis_group_get = mock.Mock()
self.ha_chassis_group_del = mock.Mock()
self.qos_add = mock.Mock()
self.qos_del = mock.Mock()

136
networking_ovn/tests/unit/ml2/test_mech_driver.py

@ -1450,10 +1450,9 @@ class TestOVNMechanismDriver(test_plugin.Ml2PluginV2TestCase):
umd.assert_not_called()
def test_update_subnet_postcommit_enable_dhcp(self):
context = fakes.FakeSubnetContext(
subnet={'enable_dhcp': True, 'ip_version': 4, 'network_id': 'id',
'id': 'subnet_id'},
network={'id': 'id'})
subnet = {'enable_dhcp': True, 'ip_version': 4, 'network_id': 'id',
'id': 'subnet_id'}
context = fakes.FakeSubnetContext(subnet=subnet, network={'id': 'id'})
with mock.patch.object(
self.mech_driver._ovn_client,
'_enable_subnet_dhcp_options') as esd,\
@ -1463,15 +1462,14 @@ class TestOVNMechanismDriver(test_plugin.Ml2PluginV2TestCase):
self.mech_driver.update_subnet_postcommit(context)
esd.assert_called_once_with(
context.current, context.network.current, mock.ANY)
umd.assert_called_once_with(mock.ANY, 'id')
umd.assert_called_once_with(mock.ANY, 'id', subnet=subnet)
def test_update_subnet_postcommit_disable_dhcp(self):
self.mech_driver._nb_ovn.get_subnet_dhcp_options.return_value = {
'subnet': mock.sentinel.subnet, 'ports': []}
context = fakes.FakeSubnetContext(
subnet={'enable_dhcp': False, 'id': 'fake_id', 'ip_version': 4,
'network_id': 'id'},
network={'id': 'id'})
subnet = {'enable_dhcp': False, 'id': 'subnet_id', 'ip_version': 4,
'network_id': 'id'}
context = fakes.FakeSubnetContext(subnet=subnet, network={'id': 'id'})
with mock.patch.object(
self.mech_driver._ovn_client,
'_remove_subnet_dhcp_options') as dsd,\
@ -1480,15 +1478,14 @@ class TestOVNMechanismDriver(test_plugin.Ml2PluginV2TestCase):
'update_metadata_port') as umd:
self.mech_driver.update_subnet_postcommit(context)
dsd.assert_called_once_with(context.current['id'], mock.ANY)
umd.assert_called_once_with(mock.ANY, 'id')
umd.assert_called_once_with(mock.ANY, 'id', subnet=subnet)
def test_update_subnet_postcommit_update_dhcp(self):
self.mech_driver._nb_ovn.get_subnet_dhcp_options.return_value = {
'subnet': mock.sentinel.subnet, 'ports': []}
context = fakes.FakeSubnetContext(
subnet={'enable_dhcp': True, 'ip_version': 4, 'network_id': 'id',
'id': 'subnet_id'},
network={'id': 'id'})
subnet = {'enable_dhcp': True, 'id': 'subnet_id', 'ip_version': 4,
'network_id': 'id'}
context = fakes.FakeSubnetContext(subnet=subnet, network={'id': 'id'})
with mock.patch.object(
self.mech_driver._ovn_client,
'_update_subnet_dhcp_options') as usd,\
@ -1498,7 +1495,109 @@ class TestOVNMechanismDriver(test_plugin.Ml2PluginV2TestCase):
self.mech_driver.update_subnet_postcommit(context)
usd.assert_called_once_with(
context.current, context.network.current, mock.ANY)
umd.assert_called_once_with(mock.ANY, 'id')
umd.assert_called_once_with(mock.ANY, 'id', subnet=subnet)
def test_update_metadata_port_with_subnet(self):
cfg.CONF.set_override('ovn_metadata_enabled', True, group='ovn')
with mock.patch.object(
self.mech_driver._ovn_client, '_find_metadata_port') as \
mock_metaport, \
mock.patch.object(self.mech_driver._plugin, 'get_subnets') as \
mock_get_subnets, \
mock.patch.object(self.mech_driver._plugin, 'update_port') as \
mock_update_port:
# Subnet with DHCP, present in port.
fixed_ips = [{'subnet_id': 'subnet1', 'ip_address': 'ip_add1'}]
mock_metaport.return_value = {'fixed_ips': fixed_ips,
'id': 'metadata_id'}
mock_get_subnets.return_value = [{'id': 'subnet1'}]
subnet = {'id': 'subnet1', 'enable_dhcp': True}
self.mech_driver._ovn_client.update_metadata_port(
self.context, 'net_id', subnet=subnet)
mock_update_port.assert_not_called()
# Subnet without DHCP, present in port.
fixed_ips = [{'subnet_id': 'subnet1', 'ip_address': 'ip_add1'}]
mock_metaport.return_value = {'fixed_ips': fixed_ips,
'id': 'metadata_id'}
mock_get_subnets.return_value = [{'id': 'subnet1'}]
subnet = {'id': 'subnet1', 'enable_dhcp': False}
self.mech_driver._ovn_client.update_metadata_port(
self.context, 'net_id', subnet=subnet)
port = {'id': 'metadata_id',
'port': {'network_id': 'net_id', 'fixed_ips': []}}
mock_update_port.assert_called_once_with(mock.ANY, 'metadata_id',
port)
mock_update_port.reset_mock()
# Subnet with DHCP, not present in port.
mock_metaport.return_value = {'fixed_ips': [],
'id': 'metadata_id'}
mock_get_subnets.return_value = []
subnet = {'id': 'subnet1', 'enable_dhcp': True}
self.mech_driver._ovn_client.update_metadata_port(
self.context, 'net_id', subnet=subnet)
fixed_ips = [{'subnet_id': 'subnet1'}]
port = {'id': 'metadata_id',
'port': {'network_id': 'net_id', 'fixed_ips': fixed_ips}}
mock_update_port.assert_called_once_with(mock.ANY, 'metadata_id',
port)
mock_update_port.reset_mock()
# Subnet without DHCP, not present in port.
mock_metaport.return_value = {'fixed_ips': [],
'id': 'metadata_id'}
mock_get_subnets.return_value = []
subnet = {'id': 'subnet1', 'enable_dhcp': False}
self.mech_driver._ovn_client.update_metadata_port(
self.context, 'net_id', subnet=subnet)
mock_update_port.assert_not_called()
def test_update_metadata_port_no_subnet(self):
cfg.CONF.set_override('ovn_metadata_enabled', True, group='ovn')
with mock.patch.object(
self.mech_driver._ovn_client, '_find_metadata_port') as \
mock_metaport, \
mock.patch.object(self.mech_driver._plugin, 'get_subnets') as \
mock_get_subnets, \
mock.patch.object(self.mech_driver._plugin, 'update_port') as \
mock_update_port:
# Port with IP in subnet1; subnet1 and subnet2 with DHCP.
mock_get_subnets.return_value = [{'id': 'subnet1'},
{'id': 'subnet2'}]
fixed_ips = [{'subnet_id': 'subnet1', 'ip_address': 'ip_add1'}]
mock_metaport.return_value = {'fixed_ips': fixed_ips,
'id': 'metadata_id'}
self.mech_driver._ovn_client.update_metadata_port(self.context,
'net_id')
port = {'id': 'metadata_id',
'port': {'network_id': 'net_id', 'fixed_ips': fixed_ips}}
fixed_ips.append({'subnet_id': 'subnet2'})
mock_update_port.assert_called_once_with(
mock.ANY, 'metadata_id', port)
mock_update_port.reset_mock()
# Port with IP in subnet1; subnet1 with DHCP, subnet2 without DHCP.
mock_get_subnets.return_value = [{'id': 'subnet1'}]
fixed_ips = [{'subnet_id': 'subnet1', 'ip_address': 'ip_add1'}]
mock_metaport.return_value = {'fixed_ips': fixed_ips,
'id': 'metadata_id'}
self.mech_driver._ovn_client.update_metadata_port(self.context,
'net_id')
mock_update_port.assert_not_called()
# Port with IP in subnet1; subnet1 without DHCP.
mock_get_subnets.return_value = []
fixed_ips = [{'subnet_id': 'subnet1', 'ip_address': 'ip_add1'}]
mock_metaport.return_value = {'fixed_ips': fixed_ips,
'id': 'metadata_id'}
self.mech_driver._ovn_client.update_metadata_port(self.context,
'net_id')
port = {'id': 'metadata_id',
'port': {'network_id': 'net_id', 'fixed_ips': []}}
mock_update_port.assert_called_once_with(
mock.ANY, 'metadata_id', port)
mock_update_port.reset_mock()
@mock.patch.object(provisioning_blocks, 'is_object_blocked')
@mock.patch.object(provisioning_blocks, 'provisioning_complete')
@ -2106,6 +2205,11 @@ class TestOVNMechanismDriverSegment(test_segment.HostSegmentMappingTestCase):
lswitch_name=ovn_utils.ovn_name(net['id']))
def _test_segments_helper(self):
self.mech_driver.nb_ovn.get_subnet_dhcp_options.return_value = {
'subnet': {'uuid': 'foo-uuid',
'options': {'server_mac': 'ca:fe:ca:fe:ca:fe'},
'cidr': '1.2.3.4/5'},
'ports': {}}
ovn_config.cfg.CONF.set_override('ovn_metadata_enabled', True,
group='ovn')
@ -3120,6 +3224,8 @@ class TestOVNMechanismDriverMetadataPort(test_plugin.Ml2PluginV2TestCase):
Check that the metadata port is updated with a new IP address when a
subnet is created.
"""
self.mech_driver.nb_ovn.get_subnet_dhcp_options.return_value = {
'subnet': {}, 'ports': {}}
with self.network(set_context=True, tenant_id='test') as net1:
with self.subnet(network=net1, cidr='10.0.0.0/24') as subnet1:
# Create a network:dhcp owner port just as how Neutron DHCP

4
networking_ovn/tests/unit/ovsdb/test_impl_idl_ovn.py

@ -670,10 +670,10 @@ class TestNBImplIdlOvn(TestDBImplIdlOvn):
'ports': []}, subnet_options)
subnet_options = self.nb_ovn_idl.get_subnet_dhcp_options(
'subnet-id-11-0-2-0')['subnet']
self.assertIsNone(subnet_options)
self.assertEqual({}, subnet_options)
subnet_options = self.nb_ovn_idl.get_subnet_dhcp_options(
'port-id-30-0-1-0')['subnet']
self.assertIsNone(subnet_options)
self.assertEqual({}, subnet_options)
def test_get_subnet_dhcp_options_with_ports(self):
# Test empty

Loading…
Cancel
Save