# 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 functools import signal import eventlet from neutron_lib.callbacks import events from neutron_lib.callbacks import registry from neutron_lib.callbacks import resources from neutron_lib import constants as n_const from neutron_lib.utils import runtime from oslo_log import log as logging from oslo_utils import netutils from stevedore import driver from neutron.agent.linux import ip_lib from neutron.common import utils LOG = logging.getLogger(__name__) 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(update_router, resources.ROUTER, events.AFTER_UPDATE) registry.subscribe(remove_router, resources.ROUTER, events.AFTER_DELETE) self._get_sync_data() def _is_pd_master_router(self, router): return router['master'] @runtime.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 elif self._is_pd_master_router(router): self._add_lla(router, pd_info.get_bind_lla_with_mask()) def _delete_pd(self, router, pd_info): if not self._is_pd_master_router(router): return 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']) @runtime.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) if self._is_pd_master_router(router): prefix_update[subnet_id] = n_const.PROVISIONAL_IPV6_PD_PREFIX LOG.debug("Update server with prefixes: %s", prefix_update) self.notifier(self.context, prefix_update) del router['subnets'][subnet_id] @runtime.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 pd_info.prefix = prefix return old_prefix @runtime.synchronized("l3-agent-pd") def add_gw_interface(self, router_id, gw_ifname): router = self.routers.get(router_id) if not router: return router['gw_interface'] = gw_ifname if not self._is_pd_master_router(router): return prefix_update = {} for pd_info in router['subnets'].values(): # 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): if not self._is_pd_master_router(router): return prefix_update = {} for subnet_id, pd_info in router['subnets'].items(): 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 = n_const.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) @runtime.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) @runtime.synchronized("l3-agent-pd") def get_preserve_ips(self, router_id): preserve_ips = [] router = self.routers.get(router_id) if router is not None: for pd_info in router['subnets'].values(): preserve_ips.append(pd_info.get_bind_lla_with_mask()) return preserve_ips @runtime.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) @runtime.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: subnet_to_delete = None for subnet_id, pd_info in router['subnets'].items(): if pd_info.ri_ifname == stale_ifname: self._delete_pd(router, pd_info) subnet_to_delete = subnet_id break if subnet_to_delete: del router['subnets'][subnet_to_delete] @staticmethod def _get_lla(mac): lla = netutils.get_ipv6_addr_by_EUI64(n_const.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']: try: 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 kernel 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) except ip_lib.IpAddressAlreadyExists: pass 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. utils.wait_until_true(functools.partial(self._lla_available, gw_ifname, ns_name, lla_with_mask), timeout=n_const.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 @runtime.synchronized("l3-agent-pd") def process_ha_state(self, router_id, master): router = self.routers.get(router_id) if router is None or router['master'] == master: return router['master'] = master if master: for pd_info in router['subnets'].values(): bind_lla_with_mask = pd_info.get_bind_lla_with_mask() self._add_lla(router, bind_lla_with_mask) else: for pd_info in router['subnets'].values(): 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'], switch_over=True) pd_info.client_started = False @runtime.synchronized("l3-agent-pd") def process_prefix_update(self): LOG.debug("Processing IPv6 PD Prefix Update") prefix_update = {} for router_id, router in self.routers.items(): if not (self._is_pd_master_router(router) and router['gw_interface']): continue llas = None for subnet_id, pd_info in router['subnets'].items(): 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) prefix = None if (pd_info.prefix != n_const.PROVISIONAL_IPV6_PD_PREFIX): prefix = pd_info.prefix pd_info.driver.enable(self.pmon, router['ns_name'], router['gw_interface'], pd_info.bind_lla, prefix) 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('SIGUSR1 signal handler set') signal.signal(signal.SIGUSR1, self._handle_sigusr1) def _handle_sigusr1(self, signum, frame): """Update PD on receiving SIGUSR1. The external DHCPv6 client uses SIGUSR1 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] = {'master': True, '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 @runtime.synchronized("l3-agent-pd") def remove_router(resource, event, l3_agent, **kwargs): router_id = kwargs['router'].router_id router = l3_agent.pd.routers.get(router_id) l3_agent.pd.delete_router_pd(router) del l3_agent.pd.routers[router_id]['subnets'] del l3_agent.pd.routers[router_id] def get_router_entry(ns_name, master): return {'master': master, 'gw_interface': None, 'ns_name': ns_name, 'subnets': {}} @runtime.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) gw_ns_name = added_router.get_gw_ns_name() master = added_router.is_router_master() if not router: l3_agent.pd.routers[added_router.router_id] = ( get_router_entry(gw_ns_name, master)) else: # This will happen during l3 agent restart router['ns_name'] = gw_ns_name router['master'] = master @runtime.synchronized("l3-agent-pd") def update_router(resource, event, l3_agent, **kwargs): updated_router = kwargs['router'] router = l3_agent.pd.routers.get(updated_router.router_id) if not router: LOG.exception("Router to be updated is not in internal routers " "list: %s", updated_router.router_id) else: router['ns_name'] = updated_router.get_gw_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 = n_const.PROVISIONAL_IPV6_PD_PREFIX self.old_prefix = n_const.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