From 56e8498a4dac9e4db6185cc5d97612d824c6e8d5 Mon Sep 17 00:00:00 2001 From: LIU Yulong Date: Fri, 28 Jun 2019 10:55:01 +0800 Subject: [PATCH] Add agent extension 'dhcp' for ovs agent Add a new ovs agent extension to support distributed DHCP for VMs in compute nodes directly. For large scale deployment, this can be used to reduce the number of neutron agents. Large scale cloud can benefit from it. From the perspective of virtual machine, this will reduce the probability of DHCP request failure. The VMs will get a higher level availability for DHCP R/R, no single point of failure permanently. If one host goes down, VMs in other hosts will not be influnced by it. For the perspective of network performance, after using this extension, the DHCP broadcasting packages will be limited to the host locally. Partially-Implements: bp/distributed-dhcp-for-ml2-ovs Closes-Bug: #1900934 Change-Id: Id8a4c501daad7c2185e6d69441182666ef987e61 --- neutron/agent/l2/extensions/dhcp/extension.py | 155 ++++++++++++++++++ neutron/conf/plugins/ml2/drivers/ovs_conf.py | 4 + .../openvswitch/agent/common/constants.py | 6 + .../agent/openflow/native/br_int.py | 93 ++++++++++- .../agent/ovs_agent_extension_api.py | 4 +- .../openvswitch/agent/ovs_neutron_agent.py | 10 +- .../agent/l2/extensions/dhcp/test_base.py | 6 +- .../l2/extensions/dhcp/test_extension.py | 96 +++++++++++ .../agent/openflow/native/test_br_int.py | 39 ++++- .../agent/test_ovs_neutron_agent.py | 5 + .../openvswitch/agent/test_ovs_tunnel.py | 5 +- ...cp_ext_for_ovs_agent-ee895747687c7b58.yaml | 9 + setup.cfg | 1 + 13 files changed, 426 insertions(+), 7 deletions(-) create mode 100644 neutron/agent/l2/extensions/dhcp/extension.py create mode 100644 neutron/tests/unit/agent/l2/extensions/dhcp/test_extension.py create mode 100644 releasenotes/notes/dhcp_ext_for_ovs_agent-ee895747687c7b58.yaml diff --git a/neutron/agent/l2/extensions/dhcp/extension.py b/neutron/agent/l2/extensions/dhcp/extension.py new file mode 100644 index 00000000000..df57710dcbe --- /dev/null +++ b/neutron/agent/l2/extensions/dhcp/extension.py @@ -0,0 +1,155 @@ +# Copyright (c) 2021 China Unicom Cloud Data Co.,Ltd. +# Copyright (c) 2019 - 2020 China Telecom Corporation +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import netaddr +from neutron_lib.agent import l2_extension as l2_agent_extension +from neutron_lib import constants +from os_ken.base import app_manager +from oslo_config import cfg +from oslo_log import log as logging + +from neutron.agent.l2.extensions.dhcp import ipv4 +from neutron.agent.l2.extensions.dhcp import ipv6 +from neutron.api.rpc.callbacks import resources + +LOG = logging.getLogger(__name__) + + +class DHCPExtensionPortInfoAPI(object): + + def __init__(self, cache_api): + self.cache_api = cache_api + + def get_port_info(self, port_id): + port_obj = self.cache_api.get_resource_by_id( + resources.PORT, port_id) + if not port_obj or not port_obj.device_owner.startswith( + constants.DEVICE_OWNER_COMPUTE_PREFIX): + return + + mac_addr = str(netaddr.EUI(str(port_obj.mac_address), + dialect=netaddr.mac_unix_expanded)) + + entry = { + 'network_id': port_obj.network_id, + 'port_id': port_obj.id, + 'mac_address': mac_addr, + 'admin_state_up': port_obj.admin_state_up, + 'device_owner': port_obj.device_owner + } + + # fixed_ips info for DHCP + fixed_ips = [] + for ip in port_obj.fixed_ips: + subnet = self.cache_api.get_resource_by_id( + resources.SUBNET, ip.subnet_id) + if not subnet.enable_dhcp: + continue + info = {'subnet_id': ip.subnet_id, + 'ip_address': str(ip.ip_address), + 'version': subnet.ip_version, + 'cidr': subnet.cidr, + 'host_routes': subnet.host_routes, + 'dns_nameservers': subnet.dns_nameservers, + 'gateway_ip': subnet.gateway_ip} + fixed_ips.append(info) + net = self.cache_api.get_resource_by_id( + resources.NETWORK, port_obj.network_id) + extra_info = {'fixed_ips': fixed_ips, + 'mtu': net.mtu} + entry.update(extra_info) + + LOG.debug("DHCP extension API return port info: %s", entry) + return entry + + +class DHCPAgentExtension(l2_agent_extension.L2AgentExtension): + + VIF_PORT_CACHE = {} + + def consume_api(self, agent_api): + """Allows an extension to gain access to resources internal to the + neutron agent and otherwise unavailable to the extension. + """ + self.agent_api = agent_api + self.rcache_api = agent_api.plugin_rpc.remote_resource_cache + + def initialize(self, connection, driver_type): + """Initialize agent extension.""" + self.ext_api = DHCPExtensionPortInfoAPI(self.rcache_api) + self.int_br = self.agent_api.request_int_br() + self.app_mgr = app_manager.AppManager.get_instance() + self.start_dhcp() + + if cfg.CONF.DHCP.enable_ipv6: + self.start_dhcp(version=constants.IP_VERSION_6) + + def start_dhcp(self, version=constants.IP_VERSION_4): + responder = ( + ipv4.DHCPIPv4Responder if version == constants.IP_VERSION_4 else ( + ipv6.DHCPIPv6Responder)) + + app = self.app_mgr.instantiate( + responder, + self.agent_api, + self.ext_api) + app.start() + if version == constants.IP_VERSION_4: + self.dhcp4_app = app + else: + self.dhcp6_app = app + + def handle_port(self, context, port_detail): + fixed_ips = port_detail.get('fixed_ips') + port = port_detail['vif_port'] + # TODO(liuyulong): DHCP for baremetal + if (not port_detail['device_owner'].startswith( + constants.DEVICE_OWNER_COMPUTE_PREFIX) or ( + not fixed_ips)): + return + LOG.info("DHCP extension add DHCP related flows for port %s", + port_detail['port_id']) + self.int_br.add_dhcp_ipv4_flow(port_detail['port_id'], + port.ofport, + port.vif_mac) + if cfg.CONF.DHCP.enable_ipv6: + self.int_br.add_dhcp_ipv6_flow(port_detail['port_id'], + port.ofport, + port.vif_mac) + self.VIF_PORT_CACHE[port_detail['port_id']] = port + + def get_ofport(self, port_id): + vifs = self.int_br.get_vif_ports() + for vif in vifs: + if vif.vif_id == port_id: + return vif + + def delete_port(self, context, port_detail): + port_id = port_detail['port_id'] + port = port_detail.get('vif_port') + cached_port = self.VIF_PORT_CACHE.pop(port_id, None) + if not port or port.ofport <= 0: + port = cached_port + if not port: + port = self.get_ofport(port_id) + if not port: + LOG.warning("DHCP extension skipping delete DHCP related flow, " + "failed to get port %s ofport and MAC.", + port_id) + return + LOG.info("DHCP extension remove DHCP related flows for port %s", + port_id) + self.int_br.del_dhcp_flow(port.ofport, port.vif_mac) diff --git a/neutron/conf/plugins/ml2/drivers/ovs_conf.py b/neutron/conf/plugins/ml2/drivers/ovs_conf.py index 2f5f47706d2..ba7a3990512 100644 --- a/neutron/conf/plugins/ml2/drivers/ovs_conf.py +++ b/neutron/conf/plugins/ml2/drivers/ovs_conf.py @@ -180,6 +180,10 @@ agent_opts = [ ] dhcp_opts = [ + cfg.BoolOpt('enable_ipv6', default=True, + help=_("When set to True, the OVS agent DHCP " + "extension will add related flows for " + "DHCPv6 packets.")), cfg.IntOpt('renewal_time', default=0, help=_("DHCP renewal time T1 (in seconds). If set to 0, it " "will default to half of the lease time.")), diff --git a/neutron/plugins/ml2/drivers/openvswitch/agent/common/constants.py b/neutron/plugins/ml2/drivers/openvswitch/agent/common/constants.py index 3497e4286fd..ab1ad764267 100644 --- a/neutron/plugins/ml2/drivers/openvswitch/agent/common/constants.py +++ b/neutron/plugins/ml2/drivers/openvswitch/agent/common/constants.py @@ -62,6 +62,10 @@ TRANSIENT_TABLE = 60 LOCAL_MAC_DIRECT = 61 TRANSIENT_EGRESS_TABLE = 62 +# Table for DHCP +DHCP_IPV4_TABLE = 77 +DHCP_IPV6_TABLE = 78 + # Tables used for ovs firewall BASE_EGRESS_TABLE = 71 RULES_EGRESS_TABLE = 72 @@ -96,6 +100,8 @@ INT_BR_ALL_TABLES = ( BASE_EGRESS_TABLE, RULES_EGRESS_TABLE, ACCEPT_OR_INGRESS_TABLE, + DHCP_IPV4_TABLE, + DHCP_IPV6_TABLE, BASE_INGRESS_TABLE, RULES_INGRESS_TABLE, ACCEPTED_EGRESS_TRAFFIC_TABLE, diff --git a/neutron/plugins/ml2/drivers/openvswitch/agent/openflow/native/br_int.py b/neutron/plugins/ml2/drivers/openvswitch/agent/openflow/native/br_int.py index 415cfc1b2e9..149c5fb7db0 100644 --- a/neutron/plugins/ml2/drivers/openvswitch/agent/openflow/native/br_int.py +++ b/neutron/plugins/ml2/drivers/openvswitch/agent/openflow/native/br_int.py @@ -21,6 +21,7 @@ import netaddr +from neutron_lib import constants as lib_consts from os_ken.lib.packet import ether_types from os_ken.lib.packet import icmpv6 from os_ken.lib.packet import in_proto @@ -35,6 +36,12 @@ from neutron.plugins.ml2.drivers.openvswitch.agent.openflow.native \ LOG = logging.getLogger(__name__) +# TODO(liuyulong): move to neutron-lib. +IPV4_NETWORK_BROADCAST = "255.255.255.255" +# All_DHCP_Relay_Agents_and_Servers +# [RFC8415] https://datatracker.ietf.org/doc/html/rfc8415 +IPV6_All_DHCP_RELAY_AGENYS_AND_SERVERS = "ff02::1:2" + class OVSIntegrationBridge(ovs_bridge.OVSAgentBridge, br_dvr_process.OVSDVRInterfaceMixin): @@ -42,10 +49,13 @@ class OVSIntegrationBridge(ovs_bridge.OVSAgentBridge, of_tables = constants.INT_BR_ALL_TABLES - def setup_default_table(self): + def setup_default_table(self, enable_openflow_dhcp=False, + enable_dhcpv6=False): self.setup_canary_table() self.install_goto(dest_table_id=constants.TRANSIENT_TABLE) self.install_normal(table_id=constants.TRANSIENT_TABLE, priority=3) + self.init_dhcp(enable_openflow_dhcp=enable_openflow_dhcp, + enable_dhcpv6=enable_dhcpv6) self.install_drop(table_id=constants.ARP_SPOOF_TABLE) self.install_drop(table_id=constants.LOCAL_SWITCHING, priority=constants.OPENFLOW_MAX_PRIORITY, @@ -55,6 +65,87 @@ class OVSIntegrationBridge(ovs_bridge.OVSAgentBridge, self.install_normal(table_id=constants.TRANSIENT_EGRESS_TABLE, priority=3) + def init_dhcp(self, enable_openflow_dhcp=False, enable_dhcpv6=False): + if not enable_openflow_dhcp: + return + # DHCP IPv4 + self.install_goto(dest_table_id=constants.DHCP_IPV4_TABLE, + table_id=constants.TRANSIENT_TABLE, + priority=101, + eth_type=ether_types.ETH_TYPE_IP, + ip_proto=in_proto.IPPROTO_UDP, + ipv4_dst=IPV4_NETWORK_BROADCAST, + udp_src=lib_consts.DHCP_CLIENT_PORT, + udp_dst=lib_consts.DHCP_RESPONSE_PORT) + self.install_drop(table_id=constants.DHCP_IPV4_TABLE) + + if not enable_dhcpv6: + return + # DHCP IPv6 + self.install_goto(dest_table_id=constants.DHCP_IPV6_TABLE, + table_id=constants.TRANSIENT_TABLE, + priority=101, + eth_type=ether_types.ETH_TYPE_IPV6, + ip_proto=in_proto.IPPROTO_UDP, + ipv6_dst=IPV6_All_DHCP_RELAY_AGENYS_AND_SERVERS, + udp_src=lib_consts.DHCPV6_CLIENT_PORT, + udp_dst=lib_consts.DHCPV6_RESPONSE_PORT) + self.install_drop(table_id=constants.DHCP_IPV6_TABLE) + + def add_dhcp_ipv4_flow(self, port_id, ofport, port_mac): + (_dp, ofp, ofpp) = self._get_dp() + match = ofpp.OFPMatch(eth_type=ether_types.ETH_TYPE_IP, + ip_proto=in_proto.IPPROTO_UDP, + in_port=ofport, + eth_src=port_mac, + udp_src=68, + udp_dst=67) + actions = [ + ofpp.OFPActionOutput(ofp.OFPP_CONTROLLER, 0), + ] + instructions = [ + ofpp.OFPInstructionActions(ofp.OFPIT_APPLY_ACTIONS, actions), + ] + self.install_instructions(table_id=constants.DHCP_IPV4_TABLE, + priority=100, + instructions=instructions, + match=match) + + def add_dhcp_ipv6_flow(self, port_id, ofport, port_mac): + (_dp, ofp, ofpp) = self._get_dp() + match = ofpp.OFPMatch(eth_type=ether_types.ETH_TYPE_IPV6, + ip_proto=in_proto.IPPROTO_UDP, + in_port=ofport, + eth_src=port_mac, + udp_src=546, + udp_dst=547) + actions = [ + ofpp.OFPActionOutput(ofp.OFPP_CONTROLLER, 0), + ] + instructions = [ + ofpp.OFPInstructionActions(ofp.OFPIT_APPLY_ACTIONS, actions), + ] + self.install_instructions(table_id=constants.DHCP_IPV6_TABLE, + priority=100, + instructions=instructions, + match=match) + + def del_dhcp_flow(self, ofport, port_mac): + self.uninstall_flows(table_id=constants.DHCP_IPV4_TABLE, + eth_type=ether_types.ETH_TYPE_IP, + ip_proto=in_proto.IPPROTO_UDP, + in_port=ofport, + eth_src=port_mac, + udp_src=68, + udp_dst=67) + self.uninstall_flows(table_id=constants.DHCP_IPV6_TABLE, + eth_type=ether_types.ETH_TYPE_IPV6, + ip_proto=in_proto.IPPROTO_UDP, + in_port=ofport, + eth_src=port_mac, + udp_src=546, + udp_dst=547) + def setup_canary_table(self): self.install_drop(constants.CANARY_TABLE) diff --git a/neutron/plugins/ml2/drivers/openvswitch/agent/ovs_agent_extension_api.py b/neutron/plugins/ml2/drivers/openvswitch/agent/ovs_agent_extension_api.py index 1564446df61..c6ec265a0b2 100644 --- a/neutron/plugins/ml2/drivers/openvswitch/agent/ovs_agent_extension_api.py +++ b/neutron/plugins/ml2/drivers/openvswitch/agent/ovs_agent_extension_api.py @@ -40,11 +40,13 @@ class OVSAgentExtensionAPI(object): method which has been added to the AgentExtension class. ''' - def __init__(self, int_br, tun_br, phys_brs=None): + def __init__(self, int_br, tun_br, phys_brs=None, + plugin_rpc=None): super(OVSAgentExtensionAPI, self).__init__() self.br_int = int_br self.br_tun = tun_br self.br_phys = phys_brs or {} + self.plugin_rpc = plugin_rpc def request_int_br(self): """Allows extensions to request an integration bridge to use for diff --git a/neutron/plugins/ml2/drivers/openvswitch/agent/ovs_neutron_agent.py b/neutron/plugins/ml2/drivers/openvswitch/agent/ovs_neutron_agent.py index 862b5aab01e..dafd2b99da8 100644 --- a/neutron/plugins/ml2/drivers/openvswitch/agent/ovs_neutron_agent.py +++ b/neutron/plugins/ml2/drivers/openvswitch/agent/ovs_neutron_agent.py @@ -158,6 +158,8 @@ class OVSNeutronAgent(l2population_rpc.L2populationRpcCallBackTunnelMixin, agent_conf = self.conf.AGENT ovs_conf = self.conf.OVS + self.enable_openflow_dhcp = 'dhcp' in self.ext_manager.names() + self.fullsync = False # init bridge classes with configured datapath type. self.br_int_cls, self.br_phys_cls, self.br_tun_cls = ( @@ -281,7 +283,8 @@ class OVSNeutronAgent(l2population_rpc.L2populationRpcCallBackTunnelMixin, agent_api = ovs_ext_api.OVSAgentExtensionAPI(self.int_br, self.tun_br, - self.phys_brs) + self.phys_brs, + self.plugin_rpc) self.ext_manager.initialize( self.connection, constants.EXTENSION_DRIVER_TYPE, agent_api) @@ -1351,7 +1354,10 @@ class OVSNeutronAgent(l2population_rpc.L2populationRpcCallBackTunnelMixin, # while flows are missing. self.int_br.delete_port(self.conf.OVS.int_peer_patch_port) self.int_br.uninstall_flows(cookie=ovs_lib.COOKIE_ANY) - self.int_br.setup_default_table() + + self.int_br.setup_default_table( + enable_openflow_dhcp=self.enable_openflow_dhcp, + enable_dhcpv6=self.conf.DHCP.enable_ipv6) def setup_ancillary_bridges(self, integ_br, tun_br): '''Setup ancillary bridges - for example br-ex.''' diff --git a/neutron/tests/unit/agent/l2/extensions/dhcp/test_base.py b/neutron/tests/unit/agent/l2/extensions/dhcp/test_base.py index eba171056c5..e414a9f1939 100644 --- a/neutron/tests/unit/agent/l2/extensions/dhcp/test_base.py +++ b/neutron/tests/unit/agent/l2/extensions/dhcp/test_base.py @@ -109,10 +109,14 @@ class DHCPResponderBaseTestCase(base.BaseTestCase): super(DHCPResponderBaseTestCase, self).setUp() self.int_br = mock.Mock() self.tun_br = mock.Mock() + self.plugin_rpc = mock.Mock() + self.remote_resource_cache = mock.Mock() + self.plugin_rpc.remote_resource_cache = self.remote_resource_cache self.agent_api = ovs_ext_api.OVSAgentExtensionAPI( self.int_br, self.tun_br, - phys_brs=None) + phys_brs=None, + plugin_rpc=self.plugin_rpc) self.ext_api = mock.Mock() self.base_responer = dhcp_resp_base.DHCPResponderBase(self.agent_api, self.ext_api) diff --git a/neutron/tests/unit/agent/l2/extensions/dhcp/test_extension.py b/neutron/tests/unit/agent/l2/extensions/dhcp/test_extension.py new file mode 100644 index 00000000000..c10d515ed31 --- /dev/null +++ b/neutron/tests/unit/agent/l2/extensions/dhcp/test_extension.py @@ -0,0 +1,96 @@ +# Copyright (c) 2021 China Unicom Cloud Data Co.,Ltd. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from unittest import mock + +from neutron_lib import context +from oslo_config import cfg + +from neutron.agent.common import ovs_lib +from neutron.agent.l2.extensions.dhcp import extension as ovs_dhcp +from neutron.plugins.ml2.drivers.openvswitch.agent \ + import ovs_agent_extension_api as ovs_ext_api +from neutron.tests import base + + +class DHCPAgentExtensionTestCase(base.BaseTestCase): + + def setUp(self): + super(DHCPAgentExtensionTestCase, self).setUp() + cfg.CONF.set_override('enable_ipv6', True, group='DHCP') + self.context = context.get_admin_context() + self.int_br = mock.Mock() + self.tun_br = mock.Mock() + self.plugin_rpc = mock.Mock() + self.remote_resource_cache = mock.Mock() + self.plugin_rpc.remote_resource_cache = self.remote_resource_cache + self.ovs_dhcp = ovs_dhcp.DHCPAgentExtension() + self.agent_api = ovs_ext_api.OVSAgentExtensionAPI( + self.int_br, + self.tun_br, + phys_brs=None, + plugin_rpc=self.plugin_rpc) + self.ovs_dhcp.consume_api(self.agent_api) + self.ovs_dhcp.initialize(None, None) + + def tearDown(self): + self.ovs_dhcp.app_mgr.uninstantiate(self.ovs_dhcp.dhcp4_app.name) + self.ovs_dhcp.app_mgr.uninstantiate(self.ovs_dhcp.dhcp6_app.name) + super(DHCPAgentExtensionTestCase, self).tearDown() + + def test_handle_port(self): + port = {"port_id": "p1", + "fixed_ips": [{"ip_address": "1.1.1.1"}], + "vif_port": ovs_lib.VifPort("tap-p1", "1", "p1", + "aa:aa:aa:aa:aa:aa", "br-int"), + "device_owner": "compute:test"} + self.ovs_dhcp.handle_port(self.context, port) + self.ovs_dhcp.int_br.add_dhcp_ipv4_flow.assert_called_once_with( + port['port_id'], + port["vif_port"].ofport, + port["vif_port"].vif_mac) + self.ovs_dhcp.int_br.add_dhcp_ipv6_flow.assert_called_once_with( + port['port_id'], + port["vif_port"].ofport, + port["vif_port"].vif_mac) + self.assertIsNotNone(self.ovs_dhcp.VIF_PORT_CACHE.get(port['port_id'])) + + def _test_delete_port(self, with_vif_port=False): + vif_port = ovs_lib.VifPort("tap-p1", 1, "p1", + "aa:aa:aa:aa:aa:aa", "br-int") + port1 = {"port_id": "p1", + "fixed_ips": [{"ip_address": "1.1.1.1"}], + "vif_port": vif_port, + "device_owner": "compute:test"} + self.ovs_dhcp.handle_port(self.context, port1) + with mock.patch.object(self.ovs_dhcp.int_br, "get_vif_ports", + return_value=[]): + if with_vif_port: + port2 = {"port_id": "p1", + "vif_port": vif_port} + else: + port2 = {"port_id": "p1"} + self.ovs_dhcp.delete_port(self.context, port2) + self.ovs_dhcp.int_br.del_dhcp_flow.assert_called_once_with( + port1["vif_port"].ofport, + port1["vif_port"].vif_mac) + # verify the cache + self.assertNotIn("p1", self.ovs_dhcp.VIF_PORT_CACHE.keys()) + + def test_delete_port_without_vif_port(self): + self._test_delete_port() + + def test_delete_port_with_vif_port(self): + self._test_delete_port(with_vif_port=True) diff --git a/neutron/tests/unit/plugins/ml2/drivers/openvswitch/agent/openflow/native/test_br_int.py b/neutron/tests/unit/plugins/ml2/drivers/openvswitch/agent/openflow/native/test_br_int.py index 0c479dd656e..89e9daa0b0a 100644 --- a/neutron/tests/unit/plugins/ml2/drivers/openvswitch/agent/openflow/native/test_br_int.py +++ b/neutron/tests/unit/plugins/ml2/drivers/openvswitch/agent/openflow/native/test_br_int.py @@ -33,7 +33,8 @@ class OVSIntegrationBridgeTest(ovs_bridge_test_base.OVSBridgeTestBase): self.stamp = self.br.default_cookie def test_setup_default_table(self): - self.br.setup_default_table() + self.br.setup_default_table(enable_openflow_dhcp=True, + enable_dhcpv6=True) (dp, ofp, ofpp) = self._get_dp() expected = [ call._send_msg(ofpp.OFPFlowMod(dp, @@ -64,6 +65,42 @@ class OVSIntegrationBridgeTest(ovs_bridge_test_base.OVSBridgeTestBase): priority=3, table_id=60), active_bundle=None), + call._send_msg(ofpp.OFPFlowMod(dp, + cookie=self.stamp, + instructions=[ofpp.OFPInstructionGotoTable(table_id=77)], + match=ofpp.OFPMatch(eth_type=self.ether_types.ETH_TYPE_IP, + ip_proto=self.in_proto.IPPROTO_UDP, + ipv4_dst="255.255.255.255", + udp_dst=67, + udp_src=68), + priority=101, + table_id=60), + active_bundle=None), + call._send_msg(ofpp.OFPFlowMod(dp, + cookie=self.stamp, + instructions=[], + match=ofpp.OFPMatch(), + priority=0, + table_id=77), + active_bundle=None), + call._send_msg(ofpp.OFPFlowMod(dp, + cookie=self.stamp, + instructions=[ofpp.OFPInstructionGotoTable(table_id=78)], + match=ofpp.OFPMatch(eth_type=self.ether_types.ETH_TYPE_IPV6, + ip_proto=self.in_proto.IPPROTO_UDP, + ipv6_dst="ff02::1:2", + udp_dst=547, + udp_src=546), + priority=101, + table_id=60), + active_bundle=None), + call._send_msg(ofpp.OFPFlowMod(dp, + cookie=self.stamp, + instructions=[], + match=ofpp.OFPMatch(), + priority=0, + table_id=78), + active_bundle=None), call._send_msg(ofpp.OFPFlowMod(dp, cookie=self.stamp, instructions=[], diff --git a/neutron/tests/unit/plugins/ml2/drivers/openvswitch/agent/test_ovs_neutron_agent.py b/neutron/tests/unit/plugins/ml2/drivers/openvswitch/agent/test_ovs_neutron_agent.py index c368d5971f2..0f612175a58 100644 --- a/neutron/tests/unit/plugins/ml2/drivers/openvswitch/agent/test_ovs_neutron_agent.py +++ b/neutron/tests/unit/plugins/ml2/drivers/openvswitch/agent/test_ovs_neutron_agent.py @@ -159,6 +159,7 @@ class TestOvsNeutronAgent(object): mock.patch('neutron.agent.rpc.PluginReportStateAPI.' 'has_alive_neutron_server'): ext_manager = mock.Mock() + ext_manager.names = mock.Mock(return_value=[]) agent = self.mod_agent.OVSNeutronAgent(self._bridge_classes(), ext_manager, cfg.CONF) agent.tun_br = self.br_tun_cls(br_name='br-tun') @@ -237,6 +238,7 @@ class TestOvsNeutronAgent(object): expected, group='OVS') ext_manager = mock.Mock() + ext_manager.names = mock.Mock(return_value=[]) self.agent = self.mod_agent.OVSNeutronAgent(self._bridge_classes(), ext_manager, cfg.CONF) self.assertEqual(expected, self.agent.int_br.datapath_type) @@ -2904,6 +2906,7 @@ class AncillaryBridgesTest(object): mock.patch('neutron.agent.rpc.PluginReportStateAPI.' 'has_alive_neutron_server'): ext_manager = mock.Mock() + ext_manager.names = mock.Mock(return_value=[]) self.agent = self.mod_agent.OVSNeutronAgent(self._bridge_classes(), ext_manager, cfg.CONF) self.assertEqual(len(ancillary), len(self.agent.ancillary_brs)) @@ -2944,6 +2947,7 @@ class AncillaryBridgesTest(object): mock.patch('neutron.agent.rpc.PluginReportStateAPI.' 'has_alive_neutron_server'): ext_manager = mock.Mock() + ext_manager.names = mock.Mock(return_value=[]) self.agent = self.mod_agent.OVSNeutronAgent(self._bridge_classes(), ext_manager, cfg.CONF) return self.agent.scan_ancillary_ports(registered_ports, sync) @@ -3017,6 +3021,7 @@ class TestOvsDvrNeutronAgent(object): mock.patch('neutron.agent.rpc.PluginReportStateAPI.' 'has_alive_neutron_server'): ext_manager = mock.Mock() + ext_manager.names = mock.Mock(return_value=[]) self.agent = self.mod_agent.OVSNeutronAgent(self._bridge_classes(), ext_manager, cfg.CONF) self.agent.tun_br = self.br_tun_cls(br_name='br-tun') diff --git a/neutron/tests/unit/plugins/ml2/drivers/openvswitch/agent/test_ovs_tunnel.py b/neutron/tests/unit/plugins/ml2/drivers/openvswitch/agent/test_ovs_tunnel.py index c4a685ce3d5..a452cf40374 100644 --- a/neutron/tests/unit/plugins/ml2/drivers/openvswitch/agent/test_ovs_tunnel.py +++ b/neutron/tests/unit/plugins/ml2/drivers/openvswitch/agent/test_ovs_tunnel.py @@ -210,7 +210,8 @@ class TunnelTest(object): mock.call.set_secure_mode(), mock.call.setup_controllers(mock.ANY), mock.call.set_igmp_snooping_state(igmp_snooping), - mock.call.setup_default_table(), + mock.call.setup_default_table(enable_openflow_dhcp=False, + enable_dhcpv6=False), ] self.mock_map_tun_bridge_expected = [ @@ -314,11 +315,13 @@ class TunnelTest(object): cfg.CONF.set_override('tunnel_types', ['gre'], 'AGENT') cfg.CONF.set_override('veth_mtu', self.VETH_MTU, 'AGENT') cfg.CONF.set_override('minimize_polling', False, 'AGENT') + cfg.CONF.set_override('enable_ipv6', False, 'DHCP') for k, v in config_opts_agent.items(): cfg.CONF.set_override(k, v, 'AGENT') ext_mgr = mock.Mock() + ext_mgr.names = mock.Mock(return_value=[]) agent = self.mod_agent.OVSNeutronAgent( bridge_classes, ext_mgr, cfg.CONF) mock.patch.object(agent.ovs.ovsdb, 'idl_monitor').start() diff --git a/releasenotes/notes/dhcp_ext_for_ovs_agent-ee895747687c7b58.yaml b/releasenotes/notes/dhcp_ext_for_ovs_agent-ee895747687c7b58.yaml new file mode 100644 index 00000000000..6c7f603b43a --- /dev/null +++ b/releasenotes/notes/dhcp_ext_for_ovs_agent-ee895747687c7b58.yaml @@ -0,0 +1,9 @@ +--- +features: + - | + Added a new OVS agent extension ``dhcp`` to support distributed + DHCP for VMs in compute nodes directly. To enable this just + set ``extensions=dhcp`` to OVS agent config file under ``[agent]`` + section. We also add a new config section ``[dhcp]`` which + has options ``enable_ipv6 = True/False`` for indicating whether + enable the DHCPv6 for VM ports. diff --git a/setup.cfg b/setup.cfg index 9e78c5f29d7..dc272f0fb7a 100644 --- a/setup.cfg +++ b/setup.cfg @@ -126,6 +126,7 @@ neutron.agent.l2.extensions = qos = neutron.agent.l2.extensions.qos:QosAgentExtension fdb = neutron.agent.l2.extensions.fdb_population:FdbPopulationAgentExtension log = neutron.services.logapi.agent.log_extension:LoggingExtension + dhcp = neutron.agent.l2.extensions.dhcp.extension:DHCPAgentExtension neutron.agent.l3.extensions = fip_qos = neutron.agent.l3.extensions.qos.fip:FipQosAgentExtension gateway_ip_qos = neutron.agent.l3.extensions.qos.gateway_ip:RouterGatewayIPQosAgentExtension