From 4b329c345c7820ff12bf25a91228cdfbf99500df Mon Sep 17 00:00:00 2001 From: John Davidge Date: Wed, 24 Jun 2015 14:52:13 +0100 Subject: [PATCH] L3 agent changes and reference implementation for IPv6 PD This patch adds the common framework to be used by specific implementations of the DHCPv6 protocol for Prefix Delegation. It also includes a reference implementation based on the Dibbler DHCPv6 client. Dibbler version 1.0.1 or greater is required. Sanity tests are included to verify the installed version. A patch for admin/user documentation is up for review here: https://review.openstack.org/#/c/178739 Video guides for configuring and using this feature are available on YouTube: https://www.youtube.com/watch?v=wI830s881HQ https://www.youtube.com/watch?v=zfsFyS01Fn0 Co-Authored-By: Baodong (Robert) Li Co-Authored-By: Sam Betts Change-Id: Id94acbbe96c717f68f318b2d715dd9cb9cc7fe4f Implements: blueprint ipv6-prefix-delegation --- etc/l3_agent.ini | 5 + etc/neutron/rootwrap.d/dibbler.filters | 16 + neutron/agent/l3/agent.py | 27 ++ neutron/agent/l3/config.py | 7 + neutron/agent/l3/router_info.py | 60 ++- neutron/agent/l3/router_processing_queue.py | 2 + neutron/agent/linux/dibbler.py | 181 ++++++++ neutron/agent/linux/external_process.py | 17 +- neutron/agent/linux/interface.py | 29 ++ neutron/agent/linux/pd.py | 351 +++++++++++++++ neutron/agent/linux/pd_driver.py | 65 +++ neutron/cmd/pd_notify.py | 38 ++ neutron/cmd/sanity/checks.py | 17 + neutron/cmd/sanity_check.py | 11 + neutron/common/constants.py | 3 + neutron/common/utils.py | 19 + neutron/tests/common/l3_test_common.py | 28 ++ .../tests/functional/sanity/test_sanity.py | 3 + neutron/tests/unit/agent/l3/test_agent.py | 408 +++++++++++++++++- .../tests/unit/agent/linux/test_interface.py | 79 ++++ setup.cfg | 3 + 21 files changed, 1336 insertions(+), 33 deletions(-) create mode 100644 etc/neutron/rootwrap.d/dibbler.filters create mode 100644 neutron/agent/linux/dibbler.py create mode 100644 neutron/agent/linux/pd.py create mode 100644 neutron/agent/linux/pd_driver.py create mode 100644 neutron/cmd/pd_notify.py diff --git a/etc/l3_agent.ini b/etc/l3_agent.ini index 310b6b59e..29a20de95 100644 --- a/etc/l3_agent.ini +++ b/etc/l3_agent.ini @@ -50,6 +50,11 @@ # and not through this parameter. # ipv6_gateway = +# (StrOpt) Driver used for ipv6 prefix delegation. This needs to be +# an entry point defined in the neutron.agent.linux.pd_drivers namespace. See +# setup.cfg for entry points included with the neutron source. +# prefix_delegation_driver = dibbler + # Indicates that this L3 agent should also handle routers that do not have # an external network gateway configured. This option should be True only # for a single agent in a Neutron deployment, and may be False for all agents diff --git a/etc/neutron/rootwrap.d/dibbler.filters b/etc/neutron/rootwrap.d/dibbler.filters new file mode 100644 index 000000000..eea55252f --- /dev/null +++ b/etc/neutron/rootwrap.d/dibbler.filters @@ -0,0 +1,16 @@ +# neutron-rootwrap command filters for nodes on which neutron is +# expected to control network +# +# This file should be owned by (and only-writeable by) the root user + +# format seems to be +# cmd-name: filter-name, raw-command, user, args + +[Filters] + +# Filters for the dibbler-based reference implementation of the pluggable +# Prefix Delegation driver. Other implementations using an alternative agent +# should include a similar filter in this folder. + +# prefix_delegation_agent +dibbler-client: CommandFilter, dibbler-client, root diff --git a/neutron/agent/l3/agent.py b/neutron/agent/l3/agent.py index 3bfcee9e4..99921846c 100644 --- a/neutron/agent/l3/agent.py +++ b/neutron/agent/l3/agent.py @@ -36,6 +36,7 @@ from neutron.agent.l3 import router_info as rinf from neutron.agent.l3 import router_processing_queue as queue from neutron.agent.linux import external_process from neutron.agent.linux import ip_lib +from neutron.agent.linux import pd from neutron.agent.metadata import driver as metadata_driver from neutron.agent import rpc as agent_rpc from neutron.callbacks import events @@ -78,6 +79,7 @@ class L3PluginApi(object): 1.4 - Added L3 HA update_router_state. This method was reworked in to update_ha_routers_states 1.5 - Added update_ha_routers_states + 1.6 - Added process_prefix_update """ @@ -131,6 +133,12 @@ class L3PluginApi(object): return cctxt.call(context, 'update_ha_routers_states', host=self.host, states=states) + def process_prefix_update(self, context, prefix_update): + """Process prefix update whenever prefixes get changed.""" + cctxt = self.client.prepare(version='1.6') + return cctxt.call(context, 'process_prefix_update', + subnets=prefix_update) + class L3NATAgent(firewall_l3_agent.FWaaSL3AgentRpcCallback, ha.AgentMixin, @@ -218,6 +226,12 @@ class L3NATAgent(firewall_l3_agent.FWaaSL3AgentRpcCallback, self.target_ex_net_id = None self.use_ipv6 = ipv6_utils.is_enabled() + self.pd = pd.PrefixDelegation(self.context, self.process_monitor, + self.driver, + self.plugin_rpc.process_prefix_update, + self.create_pd_router_update, + self.conf) + def _check_config_params(self): """Check items in configuration files. @@ -440,6 +454,9 @@ class L3NATAgent(firewall_l3_agent.FWaaSL3AgentRpcCallback, for rp, update in self._queue.each_update_to_next_router(): LOG.debug("Starting router update for %s, action %s, priority %s", update.id, update.action, update.priority) + if update.action == queue.PD_UPDATE: + self.pd.process_prefix_update() + continue router = update.router if update.action != queue.DELETE_ROUTER and not router: try: @@ -574,6 +591,14 @@ class L3NATAgent(firewall_l3_agent.FWaaSL3AgentRpcCallback, # When L3 agent is ready, we immediately do a full sync self.periodic_sync_routers_task(self.context) + def create_pd_router_update(self): + router_id = None + update = queue.RouterUpdate(router_id, + queue.PRIORITY_PD_UPDATE, + timestamp=timeutils.utcnow(), + action=queue.PD_UPDATE) + self._queue.add(update) + class L3NATAgentWithStateReport(L3NATAgent): @@ -646,6 +671,8 @@ class L3NATAgentWithStateReport(L3NATAgent): # When L3 agent is ready, we immediately do a full sync self.periodic_sync_routers_task(self.context) + self.pd.after_start() + def agent_updated(self, context, payload): """Handle the agent_updated notification event.""" self.fullsync = True diff --git a/neutron/agent/l3/config.py b/neutron/agent/l3/config.py index edb5c5c90..dfb72bf1d 100644 --- a/neutron/agent/l3/config.py +++ b/neutron/agent/l3/config.py @@ -74,6 +74,13 @@ OPTS = [ "next-hop using a global unique address (GUA) is " "desired, it needs to be done via a subnet allocated " "to the network and not through this parameter. ")), + cfg.StrOpt('prefix_delegation_driver', + default='dibbler', + help=_('Driver used for ipv6 prefix delegation. This needs to ' + 'be an entry point defined in the ' + 'neutron.agent.linux.pd_drivers namespace. See ' + 'setup.cfg for entry points included with the neutron ' + 'source.')), cfg.BoolOpt('enable_metadata_proxy', default=True, help=_("Allow running metadata proxy.")), cfg.BoolOpt('router_delete_namespaces', default=True, diff --git a/neutron/agent/l3/router_info.py b/neutron/agent/l3/router_info.py index ba20be41e..70cc880cf 100644 --- a/neutron/agent/l3/router_info.py +++ b/neutron/agent/l3/router_info.py @@ -22,6 +22,7 @@ from neutron.agent.linux import iptables_manager from neutron.agent.linux import ra from neutron.common import constants as l3_constants from neutron.common import exceptions as n_exc +from neutron.common import ipv6_utils from neutron.common import utils as common_utils from neutron.i18n import _LW @@ -267,6 +268,23 @@ class RouterInfo(object): if self.router_namespace: self.router_namespace.delete() + def _internal_network_updated(self, port, subnet_id, prefix, old_prefix, + updated_cidrs): + interface_name = self.get_internal_device_name(port['id']) + if prefix != l3_constants.PROVISIONAL_IPV6_PD_PREFIX: + fixed_ips = port['fixed_ips'] + for fixed_ip in fixed_ips: + if fixed_ip['subnet_id'] == subnet_id: + v6addr = common_utils.ip_to_cidr(fixed_ip['ip_address'], + fixed_ip.get('prefixlen')) + if v6addr not in updated_cidrs: + self.driver.add_ipv6_addr(interface_name, v6addr, + self.ns_name) + else: + self.driver.delete_ipv6_addr_with_prefix(interface_name, + old_prefix, + self.ns_name) + def _internal_network_added(self, ns_name, network_id, port_id, fixed_ips, mac_address, interface_name, prefix): @@ -330,7 +348,8 @@ class RouterInfo(object): def _port_has_ipv6_subnet(port): if 'subnets' in port: for subnet in port['subnets']: - if netaddr.IPNetwork(subnet['cidr']).version == 6: + if (netaddr.IPNetwork(subnet['cidr']).version == 6 and + subnet['cidr'] != l3_constants.PROVISIONAL_IPV6_PD_PREFIX): return True def enable_radvd(self, internal_ports=None): @@ -348,7 +367,7 @@ class RouterInfo(object): self.driver.init_l3(interface_name, ip_cidrs=ip_cidrs, namespace=self.ns_name) - def _process_internal_ports(self): + def _process_internal_ports(self, pd): existing_port_ids = set(p['id'] for p in self.internal_ports) internal_ports = self.router.get(l3_constants.INTERFACE_KEY, []) @@ -368,13 +387,23 @@ class RouterInfo(object): LOG.debug("appending port %s to internal_ports cache", p) self.internal_ports.append(p) enable_ra = enable_ra or self._port_has_ipv6_subnet(p) + for subnet in p['subnets']: + if ipv6_utils.is_ipv6_pd_enabled(subnet): + interface_name = self.get_internal_device_name(p['id']) + pd.enable_subnet(self.router_id, subnet['id'], + subnet['cidr'], + interface_name, p['mac_address']) for p in old_ports: self.internal_network_removed(p) LOG.debug("removing port %s from internal_ports cache", p) self.internal_ports.remove(p) enable_ra = enable_ra or self._port_has_ipv6_subnet(p) + for subnet in p['subnets']: + if ipv6_utils.is_ipv6_pd_enabled(subnet): + pd.disable_subnet(self.router_id, subnet['id']) + updated_cidrs = [] if updated_ports: for index, p in enumerate(internal_ports): if not updated_ports.get(p['id']): @@ -383,9 +412,25 @@ class RouterInfo(object): interface_name = self.get_internal_device_name(p['id']) ip_cidrs = common_utils.fixed_ip_cidrs(p['fixed_ips']) LOG.debug("updating internal network for port %s", p) + updated_cidrs += ip_cidrs self.internal_network_updated(interface_name, ip_cidrs) enable_ra = enable_ra or self._port_has_ipv6_subnet(p) + # Check if there is any pd prefix update + for p in internal_ports: + if p['id'] in (set(current_port_ids) & set(existing_port_ids)): + for subnet in p.get('subnets', []): + if ipv6_utils.is_ipv6_pd_enabled(subnet): + old_prefix = pd.update_subnet(self.router_id, + subnet['id'], + subnet['cidr']) + if old_prefix: + self._internal_network_updated(p, subnet['id'], + subnet['cidr'], + old_prefix, + updated_cidrs) + enable_ra = True + # Enable RA if enable_ra: self.enable_radvd(internal_ports) @@ -399,6 +444,7 @@ class RouterInfo(object): for stale_dev in stale_devs: LOG.debug('Deleting stale internal router device: %s', stale_dev) + pd.remove_stale_ri_ifname(self.router_id, stale_dev) self.driver.unplug(stale_dev, namespace=self.ns_name, prefix=INTERNAL_DEV_PREFIX) @@ -494,7 +540,7 @@ class RouterInfo(object): def _gateway_ports_equal(port1, port2): return port1 == port2 - def _process_external_gateway(self, ex_gw_port): + def _process_external_gateway(self, ex_gw_port, pd): # TODO(Carl) Refactor to clarify roles of ex_gw_port vs self.ex_gw_port ex_gw_port_id = (ex_gw_port and ex_gw_port['id'] or self.ex_gw_port and self.ex_gw_port['id']) @@ -505,10 +551,12 @@ class RouterInfo(object): if ex_gw_port: if not self.ex_gw_port: self.external_gateway_added(ex_gw_port, interface_name) + pd.add_gw_interface(self.router['id'], interface_name) elif not self._gateway_ports_equal(ex_gw_port, self.ex_gw_port): self.external_gateway_updated(ex_gw_port, interface_name) elif not ex_gw_port and self.ex_gw_port: self.external_gateway_removed(self.ex_gw_port, interface_name) + pd.remove_gw_interface(self.router['id']) existing_devices = self._get_existing_devices() stale_devs = [dev for dev in existing_devices @@ -516,6 +564,7 @@ class RouterInfo(object): and dev != interface_name] for stale_dev in stale_devs: LOG.debug('Deleting stale external router device: %s', stale_dev) + pd.remove_gw_interface(self.router['id']) self.driver.unplug(stale_dev, bridge=self.agent_conf.external_network_bridge, namespace=self.ns_name, @@ -592,7 +641,7 @@ class RouterInfo(object): try: with self.iptables_manager.defer_apply(): ex_gw_port = self.get_ex_gw_port() - self._process_external_gateway(ex_gw_port) + self._process_external_gateway(ex_gw_port, agent.pd) if not ex_gw_port: return @@ -624,7 +673,8 @@ class RouterInfo(object): :param agent: Passes the agent in order to send RPC messages. """ LOG.debug("process router updates") - self._process_internal_ports() + self._process_internal_ports(agent.pd) + agent.pd.sync_router(self.router['id']) self.process_external(agent) # Process static routes for router self.routes_updated() diff --git a/neutron/agent/l3/router_processing_queue.py b/neutron/agent/l3/router_processing_queue.py index a46177005..a0b3fa1d6 100644 --- a/neutron/agent/l3/router_processing_queue.py +++ b/neutron/agent/l3/router_processing_queue.py @@ -21,7 +21,9 @@ from oslo_utils import timeutils # Lower value is higher priority PRIORITY_RPC = 0 PRIORITY_SYNC_ROUTERS_TASK = 1 +PRIORITY_PD_UPDATE = 2 DELETE_ROUTER = 1 +PD_UPDATE = 2 class RouterUpdate(object): diff --git a/neutron/agent/linux/dibbler.py b/neutron/agent/linux/dibbler.py new file mode 100644 index 000000000..3a97f620e --- /dev/null +++ b/neutron/agent/linux/dibbler.py @@ -0,0 +1,181 @@ +# Copyright 2015 Cisco Systems +# 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 jinja2 +import os +from oslo_config import cfg +import shutil +import six + +from neutron.agent.linux import external_process +from neutron.agent.linux import pd +from neutron.agent.linux import pd_driver +from neutron.agent.linux import utils +from neutron.common import constants +from oslo_log import log as logging + + +LOG = logging.getLogger(__name__) + +PD_SERVICE_NAME = 'dibbler' +CONFIG_TEMPLATE = jinja2.Template(""" +# Config for dibbler-client. + +# Use enterprise number based duid +duid-type duid-en {{ enterprise_number }} {{ va_id }} + +# 8 (Debug) is most verbose. 7 (Info) is usually the best option +log-level 8 + +# No automatic downlink address assignment +downlink-prefix-ifaces "none" + +# Use script to notify l3_agent of assigned prefix +script {{ script_path }} + +# Ask for prefix over the external gateway interface +iface {{ interface_name }} { +# Bind to generated LLA +bind-to-address {{ bind_address }} +# ask for address + pd 1 +} +""") + +# The first line must be #!/usr/bin/env bash +SCRIPT_TEMPLATE = jinja2.Template("""#!/usr/bin/env bash + +exec neutron-pd-notify $1 {{ prefix_path }} {{ l3_agent_pid }} +""") + + +class PDDibbler(pd_driver.PDDriverBase): + def __init__(self, router_id, subnet_id, ri_ifname): + super(PDDibbler, self).__init__(router_id, subnet_id, ri_ifname) + self.requestor_id = "%s:%s:%s" % (self.router_id, + self.subnet_id, + self.ri_ifname) + self.dibbler_client_working_area = "%s/%s" % (cfg.CONF.pd_confs, + self.requestor_id) + self.prefix_path = "%s/prefix" % self.dibbler_client_working_area + self.pid_path = "%s/client.pid" % self.dibbler_client_working_area + self.converted_subnet_id = self.subnet_id.replace('-', '') + + def _is_dibbler_client_running(self): + return utils.get_value_from_file(self.pid_path) + + def _generate_dibbler_conf(self, ex_gw_ifname, lla): + dcwa = self.dibbler_client_working_area + script_path = utils.get_conf_file_name(dcwa, 'notify', 'sh', True) + buf = six.StringIO() + buf.write('%s' % SCRIPT_TEMPLATE.render( + prefix_path=self.prefix_path, + l3_agent_pid=os.getpid())) + utils.replace_file(script_path, buf.getvalue()) + os.chmod(script_path, 0o744) + + dibbler_conf = utils.get_conf_file_name(dcwa, 'client', 'conf', False) + buf = six.StringIO() + buf.write('%s' % CONFIG_TEMPLATE.render( + enterprise_number=cfg.CONF.vendor_pen, + va_id='0x%s' % self.converted_subnet_id, + script_path='"%s/notify.sh"' % dcwa, + interface_name='"%s"' % ex_gw_ifname, + bind_address='%s' % lla)) + + utils.replace_file(dibbler_conf, buf.getvalue()) + return dcwa + + def _spawn_dibbler(self, pmon, router_ns, dibbler_conf): + def callback(pid_file): + dibbler_cmd = ['dibbler-client', + 'start', + '-w', '%s' % dibbler_conf] + return dibbler_cmd + + pm = external_process.ProcessManager( + uuid=self.requestor_id, + default_cmd_callback=callback, + namespace=router_ns, + service=PD_SERVICE_NAME, + conf=cfg.CONF, + pid_file=self.pid_path) + pm.enable(reload_cfg=False) + pmon.register(uuid=self.requestor_id, + service_name=PD_SERVICE_NAME, + monitored_process=pm) + + def enable(self, pmon, router_ns, ex_gw_ifname, lla): + LOG.debug("Enable IPv6 PD for router %s subnet %s ri_ifname %s", + self.router_id, self.subnet_id, self.ri_ifname) + if not self._is_dibbler_client_running(): + dibbler_conf = self._generate_dibbler_conf(ex_gw_ifname, lla) + self._spawn_dibbler(pmon, router_ns, dibbler_conf) + LOG.debug("dibbler client enabled for router %s subnet %s" + " ri_ifname %s", + self.router_id, self.subnet_id, self.ri_ifname) + + def disable(self, pmon, router_ns): + LOG.debug("Disable IPv6 PD for router %s subnet %s ri_ifname %s", + self.router_id, self.subnet_id, self.ri_ifname) + dcwa = self.dibbler_client_working_area + + def callback(pid_file): + dibbler_cmd = ['dibbler-client', + 'stop', + '-w', '%s' % dcwa] + return dibbler_cmd + + pmon.unregister(uuid=self.requestor_id, + service_name=PD_SERVICE_NAME) + pm = external_process.ProcessManager( + uuid=self.requestor_id, + namespace=router_ns, + service=PD_SERVICE_NAME, + conf=cfg.CONF, + pid_file=self.pid_path) + pm.disable(get_stop_command=callback) + shutil.rmtree(dcwa, ignore_errors=True) + LOG.debug("dibbler client disabled for router %s subnet %s " + "ri_ifname %s", + self.router_id, self.subnet_id, self.ri_ifname) + + def get_prefix(self): + prefix = utils.get_value_from_file(self.prefix_path) + if not prefix: + prefix = constants.PROVISIONAL_IPV6_PD_PREFIX + return prefix + + @staticmethod + def get_sync_data(): + try: + requestor_ids = os.listdir(cfg.CONF.pd_confs) + except OSError: + return [] + + sync_data = [] + requestors = (r.split(':') for r in requestor_ids if r.count(':') == 2) + for router_id, subnet_id, ri_ifname in requestors: + pd_info = pd.PDInfo() + pd_info.router_id = router_id + pd_info.subnet_id = subnet_id + pd_info.ri_ifname = ri_ifname + pd_info.driver = PDDibbler(router_id, subnet_id, ri_ifname) + pd_info.client_started = ( + pd_info.driver._is_dibbler_client_running()) + pd_info.prefix = pd_info.driver.get_prefix() + sync_data.append(pd_info) + + return sync_data diff --git a/neutron/agent/linux/external_process.py b/neutron/agent/linux/external_process.py index 4cf287218..2bccdf67c 100644 --- a/neutron/agent/linux/external_process.py +++ b/neutron/agent/linux/external_process.py @@ -96,15 +96,20 @@ class ProcessManager(MonitoredProcess): def reload_cfg(self): self.disable('HUP') - def disable(self, sig='9'): + def disable(self, sig='9', get_stop_command=None): pid = self.pid if self.active: - cmd = ['kill', '-%s' % (sig), pid] - utils.execute(cmd, run_as_root=True) - # In the case of shutting down, remove the pid file - if sig == '9': - fileutils.delete_if_exists(self.get_pid_file_name()) + if get_stop_command: + cmd = get_stop_command(self.get_pid_file_name()) + ip_wrapper = ip_lib.IPWrapper(namespace=self.namespace) + ip_wrapper.netns.execute(cmd, addl_env=self.cmd_addl_env) + else: + cmd = ['kill', '-%s' % (sig), pid] + utils.execute(cmd, run_as_root=True) + # In the case of shutting down, remove the pid file + if sig == '9': + fileutils.delete_if_exists(self.get_pid_file_name()) elif pid: LOG.debug('Process for %(uuid)s pid %(pid)d is stale, ignoring ' 'signal %(signal)s', {'uuid': self.uuid, 'pid': pid, diff --git a/neutron/agent/linux/interface.py b/neutron/agent/linux/interface.py index c76278bb2..d44b82da8 100644 --- a/neutron/agent/linux/interface.py +++ b/neutron/agent/linux/interface.py @@ -142,6 +142,35 @@ class LinuxInterfaceDriver(object): LOG.debug("deleting onlink route(%s)", route) device.route.delete_onlink_route(route) + def add_ipv6_addr(self, device_name, v6addr, namespace, scope='global'): + device = ip_lib.IPDevice(device_name, + namespace=namespace) + net = netaddr.IPNetwork(v6addr) + device.addr.add(str(net), scope) + + def delete_ipv6_addr(self, device_name, v6addr, namespace): + device = ip_lib.IPDevice(device_name, + namespace=namespace) + device.delete_addr_and_conntrack_state(v6addr) + + def delete_ipv6_addr_with_prefix(self, device_name, prefix, namespace): + """Delete the first listed IPv6 address that falls within a given + prefix. + """ + device = ip_lib.IPDevice(device_name, namespace=namespace) + net = netaddr.IPNetwork(prefix) + for address in device.addr.list(scope='global', filters=['permanent']): + ip_address = netaddr.IPNetwork(address['cidr']) + if ip_address in net: + device.delete_addr_and_conntrack_state(address['cidr']) + break + + def get_ipv6_llas(self, device_name, namespace): + device = ip_lib.IPDevice(device_name, + namespace=namespace) + + return device.addr.list(scope='link', ip_version=6) + def check_bridge_exists(self, bridge): if not ip_lib.device_exists(bridge): raise exceptions.BridgeDoesNotExist(bridge=bridge) diff --git a/neutron/agent/linux/pd.py b/neutron/agent/linux/pd.py new file mode 100644 index 000000000..b9289286f --- /dev/null +++ b/neutron/agent/linux/pd.py @@ -0,0 +1,351 @@ +# Copyright 2015 Cisco Systems +# 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 eventlet +import functools +import signal +import six + +from stevedore import driver + +from oslo_config import cfg +from oslo_log import log as logging + +from neutron.agent.linux import utils as linux_utils +from neutron.callbacks import events +from neutron.callbacks import registry +from neutron.callbacks import resources +from neutron.common import constants as l3_constants +from neutron.common import ipv6_utils +from neutron.common import utils + +LOG = logging.getLogger(__name__) + +OPTS = [ + cfg.StrOpt('pd_dhcp_driver', + default='dibbler', + help=_('Service to handle DHCPv6 Prefix delegation.')), +] + +cfg.CONF.register_opts(OPTS) + + +class PrefixDelegation(object): + def __init__(self, context, pmon, intf_driver, notifier, pd_update_cb, + agent_conf): + self.context = context + self.pmon = pmon + self.intf_driver = intf_driver + self.notifier = notifier + self.routers = {} + self.pd_update_cb = pd_update_cb + self.agent_conf = agent_conf + self.pd_dhcp_driver = driver.DriverManager( + namespace='neutron.agent.linux.pd_drivers', + name=agent_conf.prefix_delegation_driver, + ).driver + registry.subscribe(add_router, + resources.ROUTER, + events.BEFORE_CREATE) + registry.subscribe(remove_router, + resources.ROUTER, + events.AFTER_DELETE) + self._get_sync_data() + + @utils.synchronized("l3-agent-pd") + def enable_subnet(self, router_id, subnet_id, prefix, ri_ifname, mac): + router = self.routers.get(router_id) + if router is None: + return + + pd_info = router['subnets'].get(subnet_id) + if not pd_info: + pd_info = PDInfo(ri_ifname=ri_ifname, mac=mac) + router['subnets'][subnet_id] = pd_info + + pd_info.bind_lla = self._get_lla(mac) + if pd_info.sync: + pd_info.mac = mac + pd_info.old_prefix = prefix + else: + self._add_lla(router, pd_info.get_bind_lla_with_mask()) + + def _delete_pd(self, router, pd_info): + self._delete_lla(router, pd_info.get_bind_lla_with_mask()) + if pd_info.client_started: + pd_info.driver.disable(self.pmon, router['ns_name']) + + @utils.synchronized("l3-agent-pd") + def disable_subnet(self, router_id, subnet_id): + prefix_update = {} + router = self.routers.get(router_id) + if not router: + return + pd_info = router['subnets'].get(subnet_id) + if not pd_info: + return + self._delete_pd(router, pd_info) + prefix_update[subnet_id] = l3_constants.PROVISIONAL_IPV6_PD_PREFIX + del router['subnets'][subnet_id] + LOG.debug("Update server with prefixes: %s", prefix_update) + self.notifier(self.context, prefix_update) + + @utils.synchronized("l3-agent-pd") + def update_subnet(self, router_id, subnet_id, prefix): + router = self.routers.get(router_id) + if router is not None: + pd_info = router['subnets'].get(subnet_id) + if pd_info and pd_info.old_prefix != prefix: + old_prefix = pd_info.old_prefix + pd_info.old_prefix = prefix + return old_prefix + + @utils.synchronized("l3-agent-pd") + def add_gw_interface(self, router_id, gw_ifname): + router = self.routers.get(router_id) + prefix_update = {} + if not router: + return + router['gw_interface'] = gw_ifname + for subnet_id, pd_info in six.iteritems(router['subnets']): + # gateway is added after internal router ports. + # If a PD is being synced, and if the prefix is available, + # send update if prefix out of sync; If not available, + # start the PD client + bind_lla_with_mask = pd_info.get_bind_lla_with_mask() + if pd_info.sync: + pd_info.sync = False + if pd_info.client_started: + if pd_info.prefix != pd_info.old_prefix: + prefix_update['subnet_id'] = pd_info.prefix + else: + self._delete_lla(router, bind_lla_with_mask) + self._add_lla(router, bind_lla_with_mask) + else: + self._add_lla(router, bind_lla_with_mask) + if prefix_update: + LOG.debug("Update server with prefixes: %s", prefix_update) + self.notifier(self.context, prefix_update) + + def delete_router_pd(self, router): + prefix_update = {} + for subnet_id, pd_info in six.iteritems(router['subnets']): + self._delete_lla(router, pd_info.get_bind_lla_with_mask()) + if pd_info.client_started: + pd_info.driver.disable(self.pmon, router['ns_name']) + pd_info.prefix = None + pd_info.client_started = False + prefix = l3_constants.PROVISIONAL_IPV6_PD_PREFIX + prefix_update[subnet_id] = prefix + if prefix_update: + LOG.debug("Update server with prefixes: %s", prefix_update) + self.notifier(self.context, prefix_update) + + @utils.synchronized("l3-agent-pd") + def remove_gw_interface(self, router_id): + router = self.routers.get(router_id) + if router is not None: + router['gw_interface'] = None + self.delete_router_pd(router) + + @utils.synchronized("l3-agent-pd") + def sync_router(self, router_id): + router = self.routers.get(router_id) + if router is not None and router['gw_interface'] is None: + self.delete_router_pd(router) + + @utils.synchronized("l3-agent-pd") + def remove_stale_ri_ifname(self, router_id, stale_ifname): + router = self.routers.get(router_id) + if router is not None: + for subnet_id, pd_info in router['subnets'].items(): + if pd_info.ri_ifname == stale_ifname: + self._delete_pd(router, pd_info) + del router['subnets'][subnet_id] + + @staticmethod + def _get_lla(mac): + lla = ipv6_utils.get_ipv6_addr_by_EUI64(l3_constants.IPV6_LLA_PREFIX, + mac) + return lla + + def _get_llas(self, gw_ifname, ns_name): + try: + return self.intf_driver.get_ipv6_llas(gw_ifname, ns_name) + except RuntimeError: + # The error message was printed as part of the driver call + # This could happen if the gw_ifname was removed + # simply return and exit the thread + return + + def _add_lla(self, router, lla_with_mask): + if router['gw_interface']: + self.intf_driver.add_ipv6_addr(router['gw_interface'], + lla_with_mask, + router['ns_name'], + 'link') + # There is a delay before the LLA becomes active. + # This is because the kernal runs DAD to make sure LLA uniqueness + # Spawn a thread to wait for the interface to be ready + self._spawn_lla_thread(router['gw_interface'], + router['ns_name'], + lla_with_mask) + + def _spawn_lla_thread(self, gw_ifname, ns_name, lla_with_mask): + eventlet.spawn_n(self._ensure_lla_task, + gw_ifname, + ns_name, + lla_with_mask) + + def _delete_lla(self, router, lla_with_mask): + if lla_with_mask and router['gw_interface']: + try: + self.intf_driver.delete_ipv6_addr(router['gw_interface'], + lla_with_mask, + router['ns_name']) + except RuntimeError: + # Ignore error if the lla doesn't exist + pass + + def _ensure_lla_task(self, gw_ifname, ns_name, lla_with_mask): + # It would be insane for taking so long unless DAD test failed + # In that case, the subnet would never be assigned a prefix. + linux_utils.wait_until_true(functools.partial(self._lla_available, + gw_ifname, + ns_name, + lla_with_mask), + timeout=l3_constants.LLA_TASK_TIMEOUT, + sleep=2) + + def _lla_available(self, gw_ifname, ns_name, lla_with_mask): + llas = self._get_llas(gw_ifname, ns_name) + if self._is_lla_active(lla_with_mask, llas): + LOG.debug("LLA %s is active now" % lla_with_mask) + self.pd_update_cb() + return True + + @staticmethod + def _is_lla_active(lla_with_mask, llas): + for lla in llas: + if lla_with_mask == lla['cidr']: + return not lla['tentative'] + return False + + @utils.synchronized("l3-agent-pd") + def process_prefix_update(self): + LOG.debug("Processing IPv6 PD Prefix Update") + + prefix_update = {} + for router_id, router in six.iteritems(self.routers): + if not router['gw_interface']: + continue + + llas = None + for subnet_id, pd_info in six.iteritems(router['subnets']): + if pd_info.client_started: + prefix = pd_info.driver.get_prefix() + if prefix != pd_info.prefix: + pd_info.prefix = prefix + prefix_update[subnet_id] = prefix + else: + if not llas: + llas = self._get_llas(router['gw_interface'], + router['ns_name']) + + if self._is_lla_active(pd_info.get_bind_lla_with_mask(), + llas): + if not pd_info.driver: + pd_info.driver = self.pd_dhcp_driver( + router_id, subnet_id, pd_info.ri_ifname) + pd_info.driver.enable(self.pmon, router['ns_name'], + router['gw_interface'], + pd_info.bind_lla) + pd_info.client_started = True + + if prefix_update: + LOG.debug("Update server with prefixes: %s", prefix_update) + self.notifier(self.context, prefix_update) + + def after_start(self): + LOG.debug('SIGHUP signal handler set') + signal.signal(signal.SIGHUP, self._handle_sighup) + + def _handle_sighup(self, signum, frame): + # The external DHCPv6 client uses SIGHUP to notify agent + # of prefix changes. + self.pd_update_cb() + + def _get_sync_data(self): + sync_data = self.pd_dhcp_driver.get_sync_data() + for pd_info in sync_data: + router_id = pd_info.router_id + if not self.routers.get(router_id): + self.routers[router_id] = {'gw_interface': None, + 'ns_name': None, + 'subnets': {}} + new_pd_info = PDInfo(pd_info=pd_info) + subnets = self.routers[router_id]['subnets'] + subnets[pd_info.subnet_id] = new_pd_info + + +@utils.synchronized("l3-agent-pd") +def remove_router(resource, event, l3_agent, **kwargs): + router = l3_agent.pd.routers.get(kwargs['router'].router_id) + l3_agent.pd.delete_router_pd(router) + del l3_agent.pd.routers[router['id']]['subnets'] + del l3_agent.pd.routers[router['id']] + + +@utils.synchronized("l3-agent-pd") +def add_router(resource, event, l3_agent, **kwargs): + added_router = kwargs['router'] + router = l3_agent.pd.routers.get(added_router.router_id) + if not router: + l3_agent.pd.routers[added_router.router_id] = { + 'gw_interface': None, + 'ns_name': added_router.ns_name, + 'subnets': {}} + else: + # This will happen during l3 agent restart + router['ns_name'] = added_router.ns_name + + +class PDInfo(object): + """A class to simplify storing and passing of information relevant to + Prefix Delegation operations for a given subnet. + """ + def __init__(self, pd_info=None, ri_ifname=None, mac=None): + if pd_info is None: + self.prefix = l3_constants.PROVISIONAL_IPV6_PD_PREFIX + self.old_prefix = l3_constants.PROVISIONAL_IPV6_PD_PREFIX + self.ri_ifname = ri_ifname + self.mac = mac + self.bind_lla = None + self.sync = False + self.driver = None + self.client_started = False + else: + self.prefix = pd_info.prefix + self.old_prefix = None + self.ri_ifname = pd_info.ri_ifname + self.mac = None + self.bind_lla = None + self.sync = True + self.driver = pd_info.driver + self.client_started = pd_info.client_started + + def get_bind_lla_with_mask(self): + bind_lla_with_mask = '%s/64' % self.bind_lla + return bind_lla_with_mask diff --git a/neutron/agent/linux/pd_driver.py b/neutron/agent/linux/pd_driver.py new file mode 100644 index 000000000..8f11e817c --- /dev/null +++ b/neutron/agent/linux/pd_driver.py @@ -0,0 +1,65 @@ +# Copyright 2015 Cisco Systems +# 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 abc +import six + +from oslo_config import cfg + +OPTS = [ + cfg.StrOpt('pd_confs', + default='$state_path/pd', + help=_('Location to store IPv6 PD files.')), + cfg.StrOpt('vendor_pen', + default='8888', + help=_("A decimal value as Vendor's Registered Private " + "Enterprise Number as required by RFC3315 DUID-EN.")), +] + +cfg.CONF.register_opts(OPTS) + + +@six.add_metaclass(abc.ABCMeta) +class PDDriverBase(object): + + def __init__(self, router_id, subnet_id, ri_ifname): + self.router_id = router_id + self.subnet_id = subnet_id + self.ri_ifname = ri_ifname + + @abc.abstractmethod + def enable(self, pmon, router_ns, ex_gw_ifname, lla): + """Enable IPv6 Prefix Delegation for this PDDriver on the given + external interface, with the given link local address + """ + + @abc.abstractmethod + def disable(self, pmon, router_ns): + """Disable IPv6 Prefix Delegation for this PDDriver + """ + + @abc.abstractmethod + def get_prefix(self): + """Get the current assigned prefix for this PDDriver from the PD agent. + If no prefix is currently assigned, return + constants.PROVISIONAL_IPV6_PD_PREFIX + """ + + @staticmethod + @abc.abstractmethod + def get_sync_data(): + """Get the latest router_id, subnet_id, and ri_ifname from the PD agent + so that the PDDriver can be kept up to date + """ diff --git a/neutron/cmd/pd_notify.py b/neutron/cmd/pd_notify.py new file mode 100644 index 000000000..02f5fdcfe --- /dev/null +++ b/neutron/cmd/pd_notify.py @@ -0,0 +1,38 @@ +# Copyright (c) 2015 Cisco Systems. +# 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 os +import signal +import sys + +from neutron.common import utils + + +def main(): + """Expected arguments: + sys.argv[1] - The add/update/delete operation performed by the PD agent + sys.argv[2] - The file where the new prefix should be written + sys.argv[3] - The process ID of the L3 agent to be notified of this change + """ + operation = sys.argv[1] + prefix_fname = sys.argv[2] + agent_pid = sys.argv[3] + prefix = os.getenv('PREFIX1', "::") + + if operation == "add" or operation == "update": + utils.replace_file(prefix_fname, "%s/64" % prefix) + elif operation == "delete": + utils.replace_file(prefix_fname, "::/64") + os.kill(int(agent_pid), signal.SIGHUP) diff --git a/neutron/cmd/sanity/checks.py b/neutron/cmd/sanity/checks.py index 659c02e67..484438e05 100644 --- a/neutron/cmd/sanity/checks.py +++ b/neutron/cmd/sanity/checks.py @@ -42,6 +42,7 @@ LOG = logging.getLogger(__name__) MINIMUM_DNSMASQ_VERSION = 2.67 +MINIMUM_DIBBLER_VERSION = '1.0.1' def ovs_vxlan_supported(from_ip='192.0.2.1', to_ip='192.0.2.2'): @@ -323,3 +324,19 @@ def ebtables_supported(): LOG.debug("Exception while checking for installed ebtables. " "Exception: %s", e) return False + + +def get_minimal_dibbler_version_supported(): + return MINIMUM_DIBBLER_VERSION + + +def dibbler_version_supported(): + try: + cmd = ['dibbler-client', + 'help'] + out = agent_utils.execute(cmd) + return '-w' in out + except (OSError, RuntimeError, IndexError, ValueError) as e: + LOG.debug("Exception while checking minimal dibbler version. " + "Exception: %s", e) + return False diff --git a/neutron/cmd/sanity_check.py b/neutron/cmd/sanity_check.py index 90895e234..123db3edb 100644 --- a/neutron/cmd/sanity_check.py +++ b/neutron/cmd/sanity_check.py @@ -116,6 +116,15 @@ def check_keepalived_ipv6_support(): return result +def check_dibbler_version(): + result = checks.dibbler_version_supported() + if not result: + LOG.error(_LE('The installed version of dibbler-client is too old. ' + 'Please update to at least version %s.'), + checks.get_minimal_dibbler_version_supported()) + return result + + def check_nova_notify(): result = checks.nova_notify_supported() if not result: @@ -194,6 +203,8 @@ OPTS = [ help=_('Check ebtables installation')), BoolOptCallback('keepalived_ipv6_support', check_keepalived_ipv6_support, help=_('Check keepalived IPv6 support')), + BoolOptCallback('dibbler_version', check_dibbler_version, + help=_('Check minimal dibbler version')), ] diff --git a/neutron/common/constants.py b/neutron/common/constants.py index e9424b237..5bec47c9e 100644 --- a/neutron/common/constants.py +++ b/neutron/common/constants.py @@ -148,6 +148,9 @@ IPV6_PD_POOL_ID = 'prefix_delegation' # Special provisional prefix for IPv6 Prefix Delegation PROVISIONAL_IPV6_PD_PREFIX = '::/64' +# Timeout in seconds for getting an IPv6 LLA +LLA_TASK_TIMEOUT = 40 + # Linux interface max length DEVICE_NAME_MAX_LEN = 15 diff --git a/neutron/common/utils.py b/neutron/common/utils.py index f62890471..2eb31a836 100644 --- a/neutron/common/utils.py +++ b/neutron/common/utils.py @@ -29,6 +29,7 @@ import os import random import signal import socket +import tempfile import uuid from eventlet.green import subprocess @@ -449,3 +450,21 @@ def round_val(val): # versions (2.x vs. 3.x) return int(decimal.Decimal(val).quantize(decimal.Decimal('1'), rounding=decimal.ROUND_HALF_UP)) + + +def replace_file(file_name, data): + """Replaces the contents of file_name with data in a safe manner. + + First write to a temp file and then rename. Since POSIX renames are + atomic, the file is unlikely to be corrupted by competing writes. + + We create the tempfile on the same device to ensure that it can be renamed. + """ + + base_dir = os.path.dirname(os.path.abspath(file_name)) + with tempfile.NamedTemporaryFile('w+', + dir=base_dir, + delete=False) as tmp_file: + tmp_file.write(data) + os.chmod(tmp_file.name, 0o644) + os.rename(tmp_file.name, file_name) diff --git a/neutron/tests/common/l3_test_common.py b/neutron/tests/common/l3_test_common.py index 6045f56bb..1c3a9f36d 100644 --- a/neutron/tests/common/l3_test_common.py +++ b/neutron/tests/common/l3_test_common.py @@ -244,6 +244,34 @@ def router_append_subnet(router, count=1, ip_version=4, router[l3_constants.INTERFACE_KEY] = interfaces +def router_append_pd_enabled_subnet(router, count=1): + interfaces = router[l3_constants.INTERFACE_KEY] + current = sum(netaddr.IPNetwork(subnet['cidr']).version == 6 + for p in interfaces for subnet in p['subnets']) + + mac_address = netaddr.EUI('ca:fe:de:ad:be:ef') + mac_address.dialect = netaddr.mac_unix + pd_intfs = [] + for i in range(current, current + count): + subnet_id = _uuid() + intf = {'id': _uuid(), + 'network_id': _uuid(), + 'admin_state_up': True, + 'fixed_ips': [{'ip_address': '::1', + 'prefixlen': 64, + 'subnet_id': subnet_id}], + 'mac_address': str(mac_address), + 'subnets': [{'id': subnet_id, + 'cidr': l3_constants.PROVISIONAL_IPV6_PD_PREFIX, + 'gateway_ip': '::1', + 'ipv6_ra_mode': l3_constants.IPV6_SLAAC, + 'subnetpool_id': l3_constants.IPV6_PD_POOL_ID}]} + interfaces.append(intf) + pd_intfs.append(intf) + mac_address.value += 1 + return pd_intfs + + def prepare_ext_gw_test(context, ri, dual_stack=False): subnet_id = _uuid() fixed_ips = [{'subnet_id': subnet_id, diff --git a/neutron/tests/functional/sanity/test_sanity.py b/neutron/tests/functional/sanity/test_sanity.py index b65de687a..f6029e8ed 100644 --- a/neutron/tests/functional/sanity/test_sanity.py +++ b/neutron/tests/functional/sanity/test_sanity.py @@ -35,6 +35,9 @@ class SanityTestCase(base.BaseTestCase): def test_dnsmasq_version(self): checks.dnsmasq_version_supported() + def test_dibbler_version(self): + checks.dibbler_version_supported() + class SanityTestCaseRoot(functional_base.BaseSudoTestCase): """Sanity checks that require root access. diff --git a/neutron/tests/unit/agent/l3/test_agent.py b/neutron/tests/unit/agent/l3/test_agent.py index b4921692d..50131a440 100644 --- a/neutron/tests/unit/agent/l3/test_agent.py +++ b/neutron/tests/unit/agent/l3/test_agent.py @@ -23,6 +23,7 @@ import netaddr from oslo_log import log import oslo_messaging from oslo_utils import uuidutils +import six from testtools import matchers from neutron.agent.common import config as agent_config @@ -35,8 +36,10 @@ from neutron.agent.l3 import legacy_router from neutron.agent.l3 import link_local_allocator as lla from neutron.agent.l3 import namespaces from neutron.agent.l3 import router_info as l3router +from neutron.agent.linux import dibbler from neutron.agent.linux import external_process from neutron.agent.linux import interface +from neutron.agent.linux import pd from neutron.agent.linux import ra from neutron.agent.metadata import driver as metadata_driver from neutron.agent import rpc as agent_rpc @@ -1149,14 +1152,18 @@ class TestBasicRouterOperations(BasicRouterOperationsFramework): self.assertFalse(nat_rules_delta) return ri - def _expected_call_lookup_ri_process(self, ri, process): - """Expected call if a process is looked up in a router instance.""" - return [mock.call(uuid=ri.router['id'], - service=process, + def _radvd_expected_call_external_process(self, ri, enable=True): + expected_calls = [mock.call(uuid=ri.router['id'], + service='radvd', default_cmd_callback=mock.ANY, namespace=ri.ns_name, conf=mock.ANY, run_as_root=True)] + if enable: + expected_calls.append(mock.call().enable(reload_cfg=True)) + else: + expected_calls.append(mock.call().disable()) + return expected_calls def _process_router_ipv6_subnet_added( self, router, ipv6_subnet_modes=None): @@ -1175,24 +1182,20 @@ class TestBasicRouterOperations(BasicRouterOperationsFramework): self._process_router_instance_for_agent(agent, ri, router) return ri - def _assert_ri_process_enabled(self, ri, process): + def _assert_ri_process_enabled(self, ri): """Verify that process was enabled for a router instance.""" - expected_calls = self._expected_call_lookup_ri_process( - ri, process) - expected_calls.append(mock.call().enable(reload_cfg=True)) + expected_calls = self._radvd_expected_call_external_process(ri) self.assertEqual(expected_calls, self.external_process.mock_calls) - def _assert_ri_process_disabled(self, ri, process): + def _assert_ri_process_disabled(self, ri): """Verify that process was disabled for a router instance.""" - expected_calls = self._expected_call_lookup_ri_process( - ri, process) - expected_calls.append(mock.call().disable()) + expected_calls = self._radvd_expected_call_external_process(ri, False) self.assertEqual(expected_calls, self.external_process.mock_calls) def test_process_router_ipv6_interface_added(self): router = l3_test_common.prepare_router_data() ri = self._process_router_ipv6_interface_added(router) - self._assert_ri_process_enabled(ri, 'radvd') + self._assert_ri_process_enabled(ri) # Expect radvd configured without prefix self.assertNotIn('prefix', self.utils_replace_file.call_args[0][1].split()) @@ -1201,7 +1204,7 @@ class TestBasicRouterOperations(BasicRouterOperationsFramework): router = l3_test_common.prepare_router_data() ri = self._process_router_ipv6_interface_added( router, ra_mode=l3_constants.IPV6_SLAAC) - self._assert_ri_process_enabled(ri, 'radvd') + self._assert_ri_process_enabled(ri) # Expect radvd configured with prefix self.assertIn('prefix', self.utils_replace_file.call_args[0][1].split()) @@ -1215,7 +1218,7 @@ class TestBasicRouterOperations(BasicRouterOperationsFramework): 'address_mode': l3_constants.DHCPV6_STATELESS}, {'ra_mode': l3_constants.DHCPV6_STATEFUL, 'address_mode': l3_constants.DHCPV6_STATEFUL}]) - self._assert_ri_process_enabled(ri, 'radvd') + self._assert_ri_process_enabled(ri) radvd_config = self.utils_replace_file.call_args[0][1].split() # Assert we have a prefix from IPV6_SLAAC and a prefix from # DHCPV6_STATELESS on one interface @@ -1235,7 +1238,7 @@ class TestBasicRouterOperations(BasicRouterOperationsFramework): {'ra_mode': l3_constants.IPV6_SLAAC, 'address_mode': l3_constants.IPV6_SLAAC}]) self._process_router_instance_for_agent(agent, ri, router) - self._assert_ri_process_enabled(ri, 'radvd') + self._assert_ri_process_enabled(ri) radvd_config = self.utils_replace_file.call_args[0][1].split() self.assertEqual(1, len(ri.internal_ports[1]['subnets'])) self.assertEqual(1, len(ri.internal_ports[1]['fixed_ips'])) @@ -1257,7 +1260,7 @@ class TestBasicRouterOperations(BasicRouterOperationsFramework): self._process_router_instance_for_agent(agent, ri, router) # radvd should have been enabled again and the interface # should have two prefixes - self._assert_ri_process_enabled(ri, 'radvd') + self._assert_ri_process_enabled(ri) radvd_config = self.utils_replace_file.call_args[0][1].split() self.assertEqual(2, len(ri.internal_ports[1]['subnets'])) self.assertEqual(2, len(ri.internal_ports[1]['fixed_ips'])) @@ -1276,7 +1279,7 @@ class TestBasicRouterOperations(BasicRouterOperationsFramework): l3_test_common.router_append_interface(router, count=1, ip_version=6) # Reassign the router object to RouterInfo self._process_router_instance_for_agent(agent, ri, router) - self._assert_ri_process_enabled(ri, 'radvd') + self._assert_ri_process_enabled(ri) def test_process_router_interface_removed(self): agent = l3_agent.L3NATAgent(HOSTNAME, self.conf) @@ -1302,14 +1305,14 @@ class TestBasicRouterOperations(BasicRouterOperationsFramework): # Add an IPv6 interface and reprocess l3_test_common.router_append_interface(router, count=1, ip_version=6) self._process_router_instance_for_agent(agent, ri, router) - self._assert_ri_process_enabled(ri, 'radvd') + self._assert_ri_process_enabled(ri) # Reset the calls so we can check for disable radvd self.external_process.reset_mock() self.process_monitor.reset_mock() # Remove the IPv6 interface and reprocess del router[l3_constants.INTERFACE_KEY][1] self._process_router_instance_for_agent(agent, ri, router) - self._assert_ri_process_disabled(ri, 'radvd') + self._assert_ri_process_disabled(ri) def test_process_router_ipv6_subnet_removed(self): agent = l3_agent.L3NATAgent(HOSTNAME, self.conf) @@ -1324,7 +1327,7 @@ class TestBasicRouterOperations(BasicRouterOperationsFramework): 'address_mode': l3_constants.IPV6_SLAAC}] * 2)) self._process_router_instance_for_agent(agent, ri, router) - self._assert_ri_process_enabled(ri, 'radvd') + self._assert_ri_process_enabled(ri) # Reset mocks to check for modified radvd config self.utils_replace_file.reset_mock() self.external_process.reset_mock() @@ -1336,7 +1339,7 @@ class TestBasicRouterOperations(BasicRouterOperationsFramework): self._process_router_instance_for_agent(agent, ri, router) # Assert radvd was enabled again and that we only have one # prefix on the interface - self._assert_ri_process_enabled(ri, 'radvd') + self._assert_ri_process_enabled(ri) radvd_config = self.utils_replace_file.call_args[0][1].split() self.assertEqual(1, len(ri.internal_ports[1]['subnets'])) self.assertEqual(1, len(ri.internal_ports[1]['fixed_ips'])) @@ -2121,3 +2124,364 @@ class TestBasicRouterOperations(BasicRouterOperationsFramework): self.utils_replace_file.call_args[0][1]) assertFlag(managed_flag)('AdvManagedFlag on;', self.utils_replace_file.call_args[0][1]) + + def _pd_expected_call_external_process(self, requestor, ri, enable=True): + expected_calls = [] + if enable: + expected_calls.append(mock.call(uuid=requestor, + service='dibbler', + default_cmd_callback=mock.ANY, + namespace=ri.ns_name, + conf=mock.ANY, + pid_file=mock.ANY)) + expected_calls.append(mock.call().enable(reload_cfg=False)) + else: + expected_calls.append(mock.call(uuid=requestor, + service='dibbler', + namespace=ri.ns_name, + conf=mock.ANY, + pid_file=mock.ANY)) + expected_calls.append(mock.call().disable( + get_stop_command=mock.ANY)) + return expected_calls + + def _pd_setup_agent_router(self): + router = l3_test_common.prepare_router_data() + ri = l3router.RouterInfo(router['id'], router, **self.ri_kwargs) + agent = l3_agent.L3NATAgent(HOSTNAME, self.conf) + agent.external_gateway_added = mock.Mock() + ri.process(agent) + agent._router_added(router['id'], router) + # Make sure radvd monitor is created + if not ri.radvd: + ri.radvd = ra.DaemonMonitor(router['id'], + ri.ns_name, + agent.process_monitor, + ri.get_internal_device_name) + return agent, router, ri + + def _pd_remove_gw_interface(self, intfs, agent, router, ri): + expected_pd_update = {} + expected_calls = [] + for intf in intfs: + requestor_id = self._pd_get_requestor_id(intf, router, ri) + expected_calls += (self._pd_expected_call_external_process( + requestor_id, ri, False)) + for subnet in intf['subnets']: + expected_pd_update[subnet['id']] = ( + l3_constants.PROVISIONAL_IPV6_PD_PREFIX) + + # Implement the prefix update notifier + # Keep track of the updated prefix + self.pd_update = {} + + def pd_notifier(context, prefix_update): + self.pd_update = prefix_update + for subnet_id, prefix in six.iteritems(prefix_update): + for intf in intfs: + for subnet in intf['subnets']: + if subnet['id'] == subnet_id: + # Update the prefix + subnet['cidr'] = prefix + break + + # Remove the gateway interface + agent.pd.notifier = pd_notifier + agent.pd.remove_gw_interface(router['id']) + + self._pd_assert_dibbler_calls(expected_calls, + self.external_process.mock_calls[-len(expected_calls):]) + self.assertEqual(expected_pd_update, self.pd_update) + + def _pd_remove_interfaces(self, intfs, agent, router, ri): + expected_pd_update = [] + expected_calls = [] + for intf in intfs: + # Remove the router interface + router[l3_constants.INTERFACE_KEY].remove(intf) + requestor_id = self._pd_get_requestor_id(intf, router, ri) + expected_calls += (self._pd_expected_call_external_process( + requestor_id, ri, False)) + for subnet in intf['subnets']: + expected_pd_update += [{subnet['id']: + l3_constants.PROVISIONAL_IPV6_PD_PREFIX}] + + # Implement the prefix update notifier + # Keep track of the updated prefix + self.pd_update = [] + + def pd_notifier(context, prefix_update): + self.pd_update.append(prefix_update) + for intf in intfs: + for subnet in intf['subnets']: + if subnet['id'] == prefix_update.keys()[0]: + # Update the prefix + subnet['cidr'] = prefix_update.values()[0] + + # Process the router for removed interfaces + agent.pd.notifier = pd_notifier + ri.process(agent) + + # The number of external process calls takes radvd into account. + # This is because there is no ipv6 interface any more after removing + # the interfaces, and radvd will be killed because of that + self._pd_assert_dibbler_calls(expected_calls, + self.external_process.mock_calls[-len(expected_calls) - 2:]) + self._pd_assert_radvd_calls(ri, False) + self.assertEqual(expected_pd_update, self.pd_update) + + def _pd_get_requestor_id(self, intf, router, ri): + ifname = ri.get_internal_device_name(intf['id']) + for subnet in intf['subnets']: + return dibbler.PDDibbler(router['id'], + subnet['id'], ifname).requestor_id + + def _pd_assert_dibbler_calls(self, expected, actual): + '''Check the external process calls for dibbler are expected + + in the case of multiple pd-enabled router ports, the exact sequence + of these calls are not deterministic. It's known, though, that each + external_process call is followed with either an enable() or disable() + ''' + + num_ext_calls = len(expected) / 2 + expected_ext_calls = [] + actual_ext_calls = [] + expected_action_calls = [] + actual_action_calls = [] + for c in range(num_ext_calls): + expected_ext_calls.append(expected[c * 2]) + actual_ext_calls.append(actual[c * 2]) + expected_action_calls.append(expected[c * 2 + 1]) + actual_action_calls.append(actual[c * 2 + 1]) + + self.assertEqual(expected_action_calls, actual_action_calls) + for exp in expected_ext_calls: + for act in actual_ext_calls: + if exp == act: + break + else: + msg = "Unexpected dibbler external process call." + self.fail(msg) + + def _pd_assert_radvd_calls(self, ri, enable=True): + exp_calls = self._radvd_expected_call_external_process(ri, enable) + self.assertEqual(exp_calls, + self.external_process.mock_calls[-len(exp_calls):]) + + def _pd_get_prefixes(self, agent, router, ri, + existing_intfs, new_intfs, mock_get_prefix): + # First generate the prefixes that will be used for each interface + prefixes = {} + expected_pd_update = {} + expected_calls = [] + for ifno, intf in enumerate(existing_intfs + new_intfs): + requestor_id = self._pd_get_requestor_id(intf, router, ri) + prefixes[requestor_id] = "2001:cafe:cafe:%d::/64" % ifno + if intf in new_intfs: + subnet_id = (intf['subnets'][0]['id'] if intf['subnets'] + else None) + expected_pd_update[subnet_id] = prefixes[requestor_id] + expected_calls += ( + self._pd_expected_call_external_process(requestor_id, ri)) + + # Implement the prefix update notifier + # Keep track of the updated prefix + self.pd_update = {} + + def pd_notifier(context, prefix_update): + self.pd_update = prefix_update + for subnet_id, prefix in six.iteritems(prefix_update): + for intf in new_intfs: + for subnet in intf['subnets']: + if subnet['id'] == subnet_id: + # Update the prefix + subnet['cidr'] = prefix + break + + # Start the dibbler client + agent.pd.notifier = pd_notifier + agent.pd.process_prefix_update() + + # Get the prefix and check that the neutron server is notified + def get_prefix(pdo): + key = '%s:%s:%s' % (pdo.router_id, pdo.subnet_id, pdo.ri_ifname) + return prefixes[key] + mock_get_prefix.side_effect = get_prefix + agent.pd.process_prefix_update() + + # Make sure that the updated prefixes are expected + self._pd_assert_dibbler_calls(expected_calls, + self.external_process.mock_calls[-len(expected_calls):]) + self.assertEqual(expected_pd_update, self.pd_update) + + def _pd_add_gw_interface(self, agent, router, ri): + gw_ifname = ri.get_external_device_name(router['gw_port']['id']) + agent.pd.add_gw_interface(router['id'], gw_ifname) + + @mock.patch.object(dibbler.PDDibbler, 'get_prefix', autospec=True) + @mock.patch.object(dibbler.os, 'getpid', return_value=1234) + @mock.patch.object(pd.PrefixDelegation, '_is_lla_active', + return_value=True) + @mock.patch.object(dibbler.os, 'chmod') + @mock.patch.object(dibbler.shutil, 'rmtree') + @mock.patch.object(pd.PrefixDelegation, '_get_sync_data') + def test_pd_add_remove_subnet(self, mock1, mock2, mock3, mock4, + mock_getpid, mock_get_prefix): + '''Add and remove one pd-enabled subnet + Remove the interface by deleting it from the router + ''' + # Initial setup + agent, router, ri = self._pd_setup_agent_router() + + # Create one pd-enabled subnet and add router interface + intfs = l3_test_common.router_append_pd_enabled_subnet(router) + ri.process(agent) + + # No client should be started since there is no gateway port + self.assertFalse(self.external_process.call_count) + self.assertFalse(mock_get_prefix.call_count) + + # Add the gateway interface + self._pd_add_gw_interface(agent, router, ri) + + # Get one prefix + self._pd_get_prefixes(agent, router, ri, [], intfs, mock_get_prefix) + + # Update the router with the new prefix + ri.process(agent) + + # Check that radvd is started and the router port is configured + # with the new prefix + self._pd_assert_radvd_calls(ri) + + # Now remove the interface + self._pd_remove_interfaces(intfs, agent, router, ri) + + @mock.patch.object(dibbler.PDDibbler, 'get_prefix', autospec=True) + @mock.patch.object(dibbler.os, 'getpid', return_value=1234) + @mock.patch.object(pd.PrefixDelegation, '_is_lla_active', + return_value=True) + @mock.patch.object(dibbler.os, 'chmod') + @mock.patch.object(dibbler.shutil, 'rmtree') + @mock.patch.object(pd.PrefixDelegation, '_get_sync_data') + def test_pd_remove_gateway(self, mock1, mock2, mock3, mock4, + mock_getpid, mock_get_prefix): + '''Add one pd-enabled subnet and remove the gateway port + Remove the gateway port and check the prefix is removed + ''' + # Initial setup + agent, router, ri = self._pd_setup_agent_router() + + # Create one pd-enabled subnet and add router interface + intfs = l3_test_common.router_append_pd_enabled_subnet(router) + ri.process(agent) + + # Add the gateway interface + self._pd_add_gw_interface(agent, router, ri) + + # Get one prefix + self._pd_get_prefixes(agent, router, ri, [], intfs, mock_get_prefix) + + # Update the router with the new prefix + ri.process(agent) + + # Check that radvd is started + self._pd_assert_radvd_calls(ri) + + # Now remove the gw interface + self._pd_remove_gw_interface(intfs, agent, router, ri) + + # There will be a router update + ri.process(agent) + + @mock.patch.object(dibbler.PDDibbler, 'get_prefix', autospec=True) + @mock.patch.object(dibbler.os, 'getpid', return_value=1234) + @mock.patch.object(pd.PrefixDelegation, '_is_lla_active', + return_value=True) + @mock.patch.object(dibbler.os, 'chmod') + @mock.patch.object(dibbler.shutil, 'rmtree') + @mock.patch.object(pd.PrefixDelegation, '_get_sync_data') + def test_pd_add_remove_2_subnets(self, mock1, mock2, mock3, mock4, + mock_getpid, mock_get_prefix): + '''Add and remove two pd-enabled subnets + Remove the interfaces by deleting them from the router + ''' + # Initial setup + agent, router, ri = self._pd_setup_agent_router() + + # Create 2 pd-enabled subnets and add router interfaces + intfs = l3_test_common.router_append_pd_enabled_subnet(router, count=2) + ri.process(agent) + + # No client should be started + self.assertFalse(self.external_process.call_count) + self.assertFalse(mock_get_prefix.call_count) + + # Add the gateway interface + self._pd_add_gw_interface(agent, router, ri) + + # Get prefixes + self._pd_get_prefixes(agent, router, ri, [], intfs, mock_get_prefix) + + # Update the router with the new prefix + ri.process(agent) + + # Check that radvd is started and the router port is configured + # with the new prefix + self._pd_assert_radvd_calls(ri) + + # Now remove the interface + self._pd_remove_interfaces(intfs, agent, router, ri) + + @mock.patch.object(dibbler.PDDibbler, 'get_prefix', autospec=True) + @mock.patch.object(dibbler.os, 'getpid', return_value=1234) + @mock.patch.object(pd.PrefixDelegation, '_is_lla_active', + return_value=True) + @mock.patch.object(dibbler.os, 'chmod') + @mock.patch.object(dibbler.shutil, 'rmtree') + @mock.patch.object(pd.PrefixDelegation, '_get_sync_data') + def test_pd_remove_gateway_2_subnets(self, mock1, mock2, mock3, mock4, + mock_getpid, mock_get_prefix): + '''Add one pd-enabled subnet, followed by adding another one + Remove the gateway port and check the prefix is removed + ''' + # Initial setup + agent, router, ri = self._pd_setup_agent_router() + + # Add the gateway interface + self._pd_add_gw_interface(agent, router, ri) + + # Create 1 pd-enabled subnet and add router interface + intfs = l3_test_common.router_append_pd_enabled_subnet(router, count=1) + ri.process(agent) + + # Get prefixes + self._pd_get_prefixes(agent, router, ri, [], intfs, mock_get_prefix) + + # Update the router with the new prefix + ri.process(agent) + + # Check that radvd is started + self._pd_assert_radvd_calls(ri) + + # Now add another interface + # Create one pd-enabled subnet and add router interface + intfs1 = l3_test_common.router_append_pd_enabled_subnet(router, + count=1) + ri.process(agent) + + # Get prefixes + self._pd_get_prefixes(agent, router, ri, intfs, + intfs1, mock_get_prefix) + + # Update the router with the new prefix + ri.process(agent) + + # Check that radvd is notified for the new prefix + self._pd_assert_radvd_calls(ri) + + # Now remove the gw interface + self._pd_remove_gw_interface(intfs + intfs1, agent, router, ri) + + ri.process(agent) diff --git a/neutron/tests/unit/agent/linux/test_interface.py b/neutron/tests/unit/agent/linux/test_interface.py index a46354a1a..11a0aa97d 100644 --- a/neutron/tests/unit/agent/linux/test_interface.py +++ b/neutron/tests/unit/agent/linux/test_interface.py @@ -249,6 +249,85 @@ class TestABCDriver(TestBase): namespace=ns) self.assertFalse(self.ip_dev().addr.add.called) + def test_add_ipv6_addr(self): + device_name = 'tap0' + cidr = '2001:db8::/64' + ns = '12345678-1234-5678-90ab-ba0987654321' + bc = BaseChild(self.conf) + + bc.add_ipv6_addr(device_name, cidr, ns) + + self.ip_dev.assert_has_calls( + [mock.call(device_name, namespace=ns), + mock.call().addr.add(cidr, 'global')]) + + def test_delete_ipv6_addr(self): + device_name = 'tap0' + cidr = '2001:db8::/64' + ns = '12345678-1234-5678-90ab-ba0987654321' + bc = BaseChild(self.conf) + + bc.delete_ipv6_addr(device_name, cidr, ns) + + self.ip_dev.assert_has_calls( + [mock.call(device_name, namespace=ns), + mock.call().delete_addr_and_conntrack_state(cidr)]) + + def test_delete_ipv6_addr_with_prefix(self): + device_name = 'tap0' + prefix = '2001:db8::/48' + in_cidr = '2001:db8::/64' + out_cidr = '2001:db7::/64' + ns = '12345678-1234-5678-90ab-ba0987654321' + in_addresses = [dict(scope='global', + dynamic=False, + cidr=in_cidr)] + out_addresses = [dict(scope='global', + dynamic=False, + cidr=out_cidr)] + # Initially set the address list to be empty + self.ip_dev().addr.list = mock.Mock(return_value=[]) + + bc = BaseChild(self.conf) + + # Call delete_v6addr_with_prefix when the address list is empty + bc.delete_ipv6_addr_with_prefix(device_name, prefix, ns) + # Assert that delete isn't called + self.assertFalse(self.ip_dev().delete_addr_and_conntrack_state.called) + + # Set the address list to contain only an address outside of the range + # of the given prefix + self.ip_dev().addr.list = mock.Mock(return_value=out_addresses) + bc.delete_ipv6_addr_with_prefix(device_name, prefix, ns) + # Assert that delete isn't called + self.assertFalse(self.ip_dev().delete_addr_and_conntrack_state.called) + + # Set the address list to contain only an address inside of the range + # of the given prefix + self.ip_dev().addr.list = mock.Mock(return_value=in_addresses) + bc.delete_ipv6_addr_with_prefix(device_name, prefix, ns) + # Assert that delete is called + self.ip_dev.assert_has_calls( + [mock.call(device_name, namespace=ns), + mock.call().addr.list(scope='global', filters=['permanent']), + mock.call().delete_addr_and_conntrack_state(in_cidr)]) + + def test_get_ipv6_llas(self): + ns = '12345678-1234-5678-90ab-ba0987654321' + addresses = [dict(scope='link', + dynamic=False, + cidr='fe80:cafe::/64')] + self.ip_dev().addr.list = mock.Mock(return_value=addresses) + device_name = self.ip_dev().name + bc = BaseChild(self.conf) + + llas = bc.get_ipv6_llas(device_name, ns) + + self.assertEqual(addresses, llas) + self.ip_dev.assert_has_calls( + [mock.call(device_name, namespace=ns), + mock.call().addr.list(scope='link', ip_version=6)]) + class TestOVSInterfaceDriver(TestBase): diff --git a/setup.cfg b/setup.cfg index 63ce1645c..41d190bfa 100644 --- a/setup.cfg +++ b/setup.cfg @@ -99,6 +99,7 @@ console_scripts = neutron-nvsd-agent = neutron.plugins.oneconvergence.agent.nvsd_neutron_agent:main neutron-openvswitch-agent = neutron.cmd.eventlet.plugins.ovs_neutron_agent:main neutron-ovs-cleanup = neutron.cmd.ovs_cleanup:main + neutron-pd-notify = neutron.cmd.pd_notify:main neutron-restproxy-agent = neutron.plugins.bigswitch.agent.restproxy_agent:main neutron-server = neutron.cmd.eventlet.server:main neutron-rootwrap = oslo_rootwrap.cmd:main @@ -188,6 +189,8 @@ neutron.agent.l2.extensions = neutron.qos.agent_drivers = ovs = neutron.plugins.ml2.drivers.openvswitch.agent.extension_drivers.qos_driver:QosOVSAgentDriver sriov = neutron.plugins.ml2.drivers.mech_sriov.agent.extension_drivers.qos_driver:QosSRIOVAgentDriver +neutron.agent.linux.pd_drivers = + dibbler = neutron.agent.linux.dibbler:PDDibbler # These are for backwards compat with Icehouse notification_driver configuration values oslo.messaging.notify.drivers = neutron.openstack.common.notifier.log_notifier = oslo_messaging.notify._impl_log:LogDriver -- 2.45.2