"""BGP implementation for FAUCET."""
# Copyright (C) 2013 Nippon Telegraph and Telephone Corporation.
# Copyright (C) 2015 Brad Cowie, Christopher Lorier and Joe Stringer.
# Copyright (C) 2015 Research and Education Advanced Network New Zealand Ltd.
# Copyright (C) 2015--2019 The Contributors
#
# 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 ipaddress
from os_ken.lib import hub
from beka.beka import Beka
from faucet.valve_util import kill_on_exception
[docs]
class BgpSpeakerKey:
"""Uniquely describe a BGP speaker."""
def __init__(self, dp_id, vlan_vid, ipv):
self.dp_id = dp_id
self.vlan_vid = vlan_vid
self.ipv = ipv
def __str__(self):
return "BGP speaker key DP ID: %u, VLAN VID: %u, IP version: %u" % (
self.dp_id,
self.vlan_vid,
self.ipv,
)
def __repr__(self):
return self.__str__()
def __hash__(self):
return hash(self.__str__())
def __eq__(self, other):
return self.__hash__() == other.__hash__()
[docs]
class FaucetBgp:
"""Wrapper for Ryu BGP speaker."""
exc_logname = None
def __init__(self, logger, exc_logname, metrics, send_flow_msgs):
self.logger = logger
self.exc_logname = exc_logname
self.metrics = metrics
self._send_flow_msgs = send_flow_msgs
self._dp_bgp_speakers = {}
self._dp_bgp_rib = {}
self._valves = None
self.thread = None
def _valve_vlan(self, dp_id, vlan_vid):
valve = None
vlan = None
if dp_id in self._valves:
if vlan_vid in self._valves[dp_id].dp.vlans:
valve = self._valves[dp_id]
vlan = valve.dp.vlans[vlan_vid]
return (valve, vlan)
@staticmethod
def _neighbor_states(bgp_speaker):
"""Return state of each neighbor for a BGP speaker as a list."""
neighbor_states = []
if bgp_speaker is not None:
neighbor_states = bgp_speaker.neighbor_states()
return neighbor_states
@kill_on_exception(exc_logname)
def _bgp_up_handler(self, remote_ip, remote_as):
self.logger.info("BGP peer router ID %s AS %s up" % (remote_ip, remote_as))
@kill_on_exception(exc_logname)
def _bgp_down_handler(self, remote_ip, remote_as):
self.logger.info("BGP peer router ID %s AS %s down" % (remote_ip, remote_as))
# TODO: delete RIB routes for down peer.
@kill_on_exception(exc_logname)
def _bgp_route_handler(self, path_change, bgp_speaker_key):
"""Handle a BGP change event.
Args:
path_change (ryu.services.protocols.bgp.bgpspeaker.EventPrefix): path change
"""
dp_id = bgp_speaker_key.dp_id
vlan_vid = bgp_speaker_key.vlan_vid
valve, vlan = self._valve_vlan(dp_id, vlan_vid)
if vlan is None:
return
prefix = ipaddress.ip_network(str(path_change.prefix))
route_str = "BGP route %s" % prefix
if path_change.next_hop:
nexthop = ipaddress.ip_address(str(path_change.next_hop))
route_str = "BGP route %s nexthop %s" % (prefix, nexthop)
if vlan.is_faucet_vip(nexthop):
self.logger.error(
"Skipping %s because nexthop cannot be us" % route_str
)
return
if valve.router_vlan_for_ip_gw(vlan, nexthop) is None:
self.logger.info(
"Skipping %s because nexthop not in %s" % (route_str, vlan)
)
return
if bgp_speaker_key not in self._dp_bgp_rib:
self._dp_bgp_rib[bgp_speaker_key] = {}
flowmods = []
if path_change.is_withdraw:
self.logger.info("withdraw %s", route_str)
if prefix in self._dp_bgp_rib[bgp_speaker_key]:
del self._dp_bgp_rib[bgp_speaker_key][prefix]
flowmods = valve.del_route(vlan, prefix)
else:
self.logger.info("add %s", route_str)
self._dp_bgp_rib[bgp_speaker_key][prefix] = nexthop
flowmods = valve.add_route(vlan, nexthop, prefix)
if flowmods:
self._send_flow_msgs(valve, flowmods)
@staticmethod
def _vlan_prefixes_by_ipv(vlan, ipv):
vlan_prefixes = [
(str(faucet_vip), str(faucet_vip.ip))
for faucet_vip in vlan.faucet_vips_by_ipv(ipv)
]
vlan_prefixes.extend(
[
(str(ip_dst), str(ip_gw))
for ip_dst, ip_gw in vlan.routes_by_ipv(ipv).items()
]
)
return vlan_prefixes
def _create_bgp_speaker_for_vlan(self, bgp_speaker_key, bgp_router):
"""Set up BGP speaker for an individual VLAN if required.
Args:
bgp_speaker_key (BgpSpeakerKey): BGP speaker key.
bgp_router: Router.
Returns:
ryu.services.protocols.bgp.bgpspeaker.BGPSpeaker: BGP speaker.
"""
server_address = sorted(
bgp_router.bgp_server_addresses_by_ipv(bgp_speaker_key.ipv)
)[0]
beka = Beka(
local_address=str(server_address),
bgp_port=bgp_router.bgp_port(),
local_as=bgp_router.bgp_as(),
router_id=bgp_router.bgp_routerid(),
peer_up_handler=self._bgp_up_handler,
peer_down_handler=self._bgp_down_handler,
route_handler=lambda x: self._bgp_route_handler(x, bgp_speaker_key),
error_handler=self.logger.warning,
)
for ip_dst, ip_gw in self._vlan_prefixes_by_ipv(
bgp_router.bgp_vlan(), bgp_speaker_key.ipv
):
beka.add_route(prefix=str(ip_dst), next_hop=str(ip_gw))
for bgp_neighbor_address in bgp_router.bgp_neighbor_addresses_by_ipv(
bgp_speaker_key.ipv
):
beka.add_neighbor(
connect_mode=bgp_router.bgp_connect_mode(),
peer_ip=str(bgp_neighbor_address),
peer_as=bgp_router.bgp_neighbor_as(),
)
self.thread = hub.spawn(beka.run)
self.thread.name = "beka"
return beka
[docs]
def shutdown_bgp_speakers(self):
"""Shutdown any active BGP speakers."""
for bgp_speaker in self._dp_bgp_speakers.values():
bgp_speaker.shutdown()
self._dp_bgp_speakers = {}
def _add_bgp_speaker(self, valve, bgp_speaker_key, bgp_router):
if bgp_speaker_key in self._dp_bgp_speakers:
self.logger.info(
"Skipping re/configuration of existing %s" % bgp_speaker_key
)
bgp_speaker = self._dp_bgp_speakers[bgp_speaker_key]
if bgp_speaker_key in self._dp_bgp_rib:
# Re-add routes (to avoid flapping BGP even when VLAN cold starts).
for prefix, nexthop in self._dp_bgp_rib[bgp_speaker_key].items():
self.logger.info("Re-adding %s via %s" % (prefix, nexthop))
bgp_vlan = bgp_router.bgp_vlan()
flowmods = valve.add_route(bgp_vlan, nexthop, prefix)
if flowmods:
self._send_flow_msgs(valve, flowmods)
else:
self.logger.info("Adding %s" % bgp_speaker_key)
bgp_speaker = self._create_bgp_speaker_for_vlan(bgp_speaker_key, bgp_router)
return {bgp_speaker_key: bgp_speaker}
def _add_valve_bgp_speakers(self, valve):
bgp_speakers = {}
bgp_routers = valve.dp.bgp_routers()
if bgp_routers:
dp_id = valve.dp.dp_id
for bgp_router in bgp_routers:
bgp_vlan = bgp_router.bgp_vlan()
vlan_vid = bgp_vlan.vid
for ipv in bgp_router.bgp_ipvs():
bgp_speaker_key = BgpSpeakerKey(dp_id, vlan_vid, ipv)
bgp_speakers.update(
self._add_bgp_speaker(valve, bgp_speaker_key, bgp_router)
)
return bgp_speakers
[docs]
def reset(self, valves):
"""Set up a BGP speaker for every VLAN that requires it."""
# TODO: port status changes should cause us to withdraw a route.
new_dp_bgp_speakers = {}
if valves:
for valve in valves.values():
new_dp_bgp_speakers.update(self._add_valve_bgp_speakers(valve))
# TODO: shutdown and remove deconfigured BGP speakers.
for bgp_speaker_key, old_bgp_speaker in self._dp_bgp_speakers.items():
if bgp_speaker_key not in new_dp_bgp_speakers:
new_dp_bgp_speakers[bgp_speaker_key] = old_bgp_speaker
self._dp_bgp_speakers = new_dp_bgp_speakers
self._valves = valves
[docs]
def update_metrics(self, _now):
"""Update BGP metrics."""
for bgp_speaker_key, bgp_speaker in self._dp_bgp_speakers.items():
dp_id = bgp_speaker_key.dp_id
vlan_vid = bgp_speaker_key.vlan_vid
ipv = bgp_speaker_key.ipv
valve, vlan = self._valve_vlan(dp_id, vlan_vid)
if vlan is None:
continue
neighbor_states = self._neighbor_states(bgp_speaker)
for neighbor, neighbor_state in neighbor_states:
neighbor_labels = dict(
valve.dp.base_prom_labels(), vlan=vlan.vid, neighbor=neighbor
)
self.metrics.bgp_neighbor_uptime_seconds.labels( # pylint: disable=no-member
**neighbor_labels
).set(
neighbor_state["info"]["uptime"]
)
self.metrics.bgp_neighbor_routes.labels( # pylint: disable=no-member
**dict(neighbor_labels, ipv=ipv)
).set(vlan.route_count_by_ipv(ipv))