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
This commit is contained in:
LIU Yulong 2019-06-28 10:55:01 +08:00
parent ad2bc847ab
commit 56e8498a4d
13 changed files with 426 additions and 7 deletions

View File

@ -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)

View File

@ -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.")),

View File

@ -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,

View File

@ -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)

View File

@ -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

View File

@ -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.'''

View File

@ -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)

View File

@ -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)

View File

@ -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=[],

View File

@ -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')

View File

@ -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()

View File

@ -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.

View File

@ -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