neutron/neutron/agent/linux/pd.py

424 lines
16 KiB
Python

# 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