From: John Davidge Date: Wed, 24 Jun 2015 13:52:13 +0000 (+0100) Subject: L3 agent changes and reference implementation for IPv6 PD X-Git-Url: https://review.fuel-infra.org/gitweb?a=commitdiff_plain;h=4b329c345c7820ff12bf25a91228cdfbf99500df;p=openstack-build%2Fneutron-build.git 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 --- 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