From 531b55e1e7f998a0a205fde1976453cab972fb3d Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 23 Jan 2020 22:06:30 -0800 Subject: [PATCH] small update to quagga config services, converted frr services to config services --- .../configservices/frrservices/__init__.py | 0 .../configservices/frrservices/services.py | 391 ++++++++++++++++++ .../frrservices/templates/daemons | 59 +++ .../frrservices/templates/frr.conf | 25 ++ .../frrservices/templates/frrboot.sh | 95 +++++ .../frrservices/templates/vtysh.conf | 1 + .../configservices/quaggaservices/services.py | 9 + 7 files changed, 580 insertions(+) create mode 100644 daemon/core/configservices/frrservices/__init__.py create mode 100644 daemon/core/configservices/frrservices/services.py create mode 100644 daemon/core/configservices/frrservices/templates/daemons create mode 100644 daemon/core/configservices/frrservices/templates/frr.conf create mode 100644 daemon/core/configservices/frrservices/templates/frrboot.sh create mode 100644 daemon/core/configservices/frrservices/templates/vtysh.conf diff --git a/daemon/core/configservices/frrservices/__init__.py b/daemon/core/configservices/frrservices/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/daemon/core/configservices/frrservices/services.py b/daemon/core/configservices/frrservices/services.py new file mode 100644 index 00000000..c4502f86 --- /dev/null +++ b/daemon/core/configservices/frrservices/services.py @@ -0,0 +1,391 @@ +import abc +from typing import Any, Dict + +import netaddr + +from core import constants +from core.configservice.base import ConfigService, ConfigServiceMode +from core.emane.nodes import EmaneNet +from core.nodes.base import CoreNodeBase +from core.nodes.interface import CoreInterface +from core.nodes.network import WlanNode + +GROUP = "FRR" + + +def has_mtu_mismatch(ifc: CoreInterface) -> bool: + """ + Helper to detect MTU mismatch and add the appropriate FRR + mtu-ignore command. This is needed when e.g. a node is linked via a + GreTap device. + """ + if ifc.mtu != 1500: + return True + if not ifc.net: + return False + for i in ifc.net.netifs(): + if i.mtu != ifc.mtu: + return True + return False + + +def get_min_mtu(ifc): + """ + Helper to discover the minimum MTU of interfaces linked with the + given interface. + """ + mtu = ifc.mtu + if not ifc.net: + return mtu + for i in ifc.net.netifs(): + if i.mtu < mtu: + mtu = i.mtu + return mtu + + +def get_router_id(node: CoreNodeBase) -> str: + """ + Helper to return the first IPv4 address of a node as its router ID. + """ + for ifc in node.netifs(): + if getattr(ifc, "control", False): + continue + for a in ifc.addrlist: + a = a.split("/")[0] + if netaddr.valid_ipv4(a): + return a + return "0.0.0.0" + + +class FRRZebra(ConfigService): + name = "FRRzebra" + group = GROUP + directories = ["/usr/local/etc/frr", "/var/run/frr", "/var/log/frr"] + files = [ + "/usr/local/etc/frr/frr.conf", + "frrboot.sh", + "/usr/local/etc/frr/vtysh.conf", + "/usr/local/etc/frr/daemons", + ] + executables = ["zebra"] + dependencies = [] + startup = ["sh frrboot.sh zebra"] + validate = ["pidof zebra"] + shutdown = ["killall zebra"] + validation_mode = ConfigServiceMode.BLOCKING + default_configs = [] + modes = {} + + def data(self) -> Dict[str, Any]: + frr_conf = self.files[0] + frr_bin_search = self.node.session.options.get_config( + "frr_bin_search", default="/usr/local/bin /usr/bin /usr/lib/frr" + ).strip('"') + frr_sbin_search = self.node.session.options.get_config( + "frr_sbin_search", default="/usr/local/sbin /usr/sbin /usr/lib/frr" + ).strip('"') + + services = [] + want_ip4 = False + want_ip6 = False + for service in self.node.config_services.values(): + if self.name not in service.dependencies: + continue + if service.ipv4_routing: + want_ip4 = True + if service.ipv6_routing: + want_ip6 = True + services.append(service) + + interfaces = [] + for ifc in self.node.netifs(): + ip4s = [] + ip6s = [] + for x in ifc.addrlist: + addr = x.split("/")[0] + if netaddr.valid_ipv4(addr): + ip4s.append(x) + else: + ip6s.append(x) + is_control = getattr(ifc, "control", False) + interfaces.append((ifc, ip4s, ip6s, is_control)) + + return dict( + frr_conf=frr_conf, + frr_sbin_search=frr_sbin_search, + frr_bin_search=frr_bin_search, + frr_state_dir=constants.FRR_STATE_DIR, + interfaces=interfaces, + want_ip4=want_ip4, + want_ip6=want_ip6, + services=services, + ) + + +class FrrService(abc.ABC): + group = GROUP + directories = [] + files = [] + executables = [] + dependencies = ["FRRzebra"] + startup = [] + validate = [] + shutdown = [] + validation_mode = ConfigServiceMode.BLOCKING + default_configs = [] + modes = {} + ipv4_routing = False + ipv6_routing = False + + @abc.abstractmethod + def frr_interface_config(self, ifc: CoreInterface) -> str: + raise NotImplementedError + + @abc.abstractmethod + def frr_config(self) -> str: + raise NotImplementedError + + +class FRROspfv2(FrrService, ConfigService): + """ + The OSPFv2 service provides IPv4 routing for wired networks. It does + not build its own configuration file but has hooks for adding to the + unified frr.conf file. + """ + + name = "FRROSPFv2" + startup = () + shutdown = ["killall ospfd"] + validate = ["pidof ospfd"] + ipv4_routing = True + + def frr_config(self) -> str: + router_id = get_router_id(self.node) + addresses = [] + for ifc in self.node.netifs(): + if getattr(ifc, "control", False): + continue + for a in ifc.addrlist: + addr = a.split("/")[0] + if netaddr.valid_ipv4(addr): + addresses.append(a) + data = dict(router_id=router_id, addresses=addresses) + text = """ + router ospf + router-id ${router_id} + % for addr in addresses: + network ${addr} area 0 + % endfor + ! + """ + return self.render_text(text, data) + + def frr_interface_config(self, ifc: CoreInterface) -> str: + if has_mtu_mismatch(ifc): + return "ip ospf mtu-ignore" + else: + return "" + + +class FRROspfv3(FrrService, ConfigService): + """ + The OSPFv3 service provides IPv6 routing for wired networks. It does + not build its own configuration file but has hooks for adding to the + unified frr.conf file. + """ + + name = "FRROSPFv3" + shutdown = ["killall ospf6d"] + validate = ["pidof ospf6d"] + ipv4_routing = True + ipv6_routing = True + + def frr_config(self) -> str: + router_id = get_router_id(self.node) + ifnames = [] + for ifc in self.node.netifs(): + if getattr(ifc, "control", False): + continue + ifnames.append(ifc.name) + data = dict(router_id=router_id, ifnames=ifnames) + text = """ + router ospf6 + router-id ${router_id} + % for ifname in ifnames: + interface ${ifname} area 0.0.0.0 + % endfor + ! + """ + return self.render_text(text, data) + + def frr_interface_config(self, ifc: CoreInterface) -> str: + mtu = get_min_mtu(ifc) + if mtu < ifc.mtu: + return f"ipv6 ospf6 ifmtu {mtu}" + else: + return "" + + +class FRRBgp(FrrService, ConfigService): + """ + The BGP service provides interdomain routing. + Peers must be manually configured, with a full mesh for those + having the same AS number. + """ + + name = "FRRBGP" + shutdown = ["killall bgpd"] + validate = ["pidof bgpd"] + custom_needed = True + ipv4_routing = True + ipv6_routing = True + + def frr_config(self) -> str: + router_id = get_router_id(self.node) + text = f""" + ! BGP configuration + ! You should configure the AS number below + ! along with this router's peers. + router bgp {self.node.id} + bgp router-id {router_id} + redistribute connected + !neighbor 1.2.3.4 remote-as 555 + ! + """ + return self.clean_text(text) + + def frr_interface_config(self, ifc: CoreInterface) -> str: + return "" + + +class FRRRip(FrrService, ConfigService): + """ + The RIP service provides IPv4 routing for wired networks. + """ + + name = "FRRRIP" + shutdown = ["killall ripd"] + validate = ["pidof ripd"] + ipv4_routing = True + + def frr_config(self) -> str: + text = """ + router rip + redistribute static + redistribute connected + redistribute ospf + network 0.0.0.0/0 + ! + """ + return self.clean_text(text) + + def frr_interface_config(self, ifc: CoreInterface) -> str: + return "" + + +class FRRRipng(FrrService, ConfigService): + """ + The RIP NG service provides IPv6 routing for wired networks. + """ + + name = "FRRRIPNG" + shutdown = ["killall ripngd"] + validate = ["pidof ripngd"] + ipv6_routing = True + + def frr_config(self) -> str: + text = """ + router ripng + redistribute static + redistribute connected + redistribute ospf6 + network ::/0 + ! + """ + return self.clean_text(text) + + def frr_interface_config(self, ifc: CoreInterface) -> str: + return "" + + +class FRRBabel(FrrService, ConfigService): + """ + The Babel service provides a loop-avoiding distance-vector routing + protocol for IPv6 and IPv4 with fast convergence properties. + """ + + name = "FRRBabel" + shutdown = ["killall babeld"] + validate = ["pidof babeld"] + ipv6_routing = True + + def frr_config(self) -> str: + ifnames = [] + for ifc in self.node.netifs(): + if getattr(ifc, "control", False): + continue + ifnames.append(ifc.name) + text = """ + router babel + % for ifname in ifnames: + network ${ifname} + % endfor + redistribute static + redistribute ipv4 connected + ! + """ + data = dict(ifnames=ifnames) + return self.render_text(text, data) + + def frr_interface_config(self, ifc: CoreInterface) -> str: + if isinstance(ifc.net, (WlanNode, EmaneNet)): + text = """ + babel wireless + no babel split-horizon + """ + else: + text = """ + babel wired + babel split-horizon + """ + return self.clean_text(text) + + +class FRRpimd(FrrService, ConfigService): + """ + PIM multicast routing based on XORP. + """ + + name = "FRRpimd" + shutdown = ["killall pimd"] + validate = ["pidof pimd"] + ipv4_routing = True + + def frr_config(self) -> str: + ifname = "eth0" + for ifc in self.node.netifs(): + if ifc.name != "lo": + ifname = ifc.name + break + + text = f""" + router mfea + ! + router igmp + ! + router pim + !ip pim rp-address 10.0.0.1 + ip pim bsr-candidate {ifname} + ip pim rp-candidate {ifname} + !ip pim spt-threshold interval 10 bytes 80000 + ! + """ + return self.clean_text(text) + + def frr_interface_config(self, ifc: CoreInterface) -> str: + text = """ + ip mfea + ip igmp + ip pim + """ + return self.clean_text(text) diff --git a/daemon/core/configservices/frrservices/templates/daemons b/daemon/core/configservices/frrservices/templates/daemons new file mode 100644 index 00000000..0f6bda53 --- /dev/null +++ b/daemon/core/configservices/frrservices/templates/daemons @@ -0,0 +1,59 @@ +# +# When activation a daemon at the first time, a config file, even if it is +# empty, has to be present *and* be owned by the user and group "frr", else +# the daemon will not be started by /etc/init.d/frr. The permissions should +# be u=rw,g=r,o=. +# When using "vtysh" such a config file is also needed. It should be owned by +# group "frrvty" and set to ug=rw,o= though. Check /etc/pam.d/frr, too. +# +# The watchfrr and zebra daemons are always started. +# +bgpd=yes +ospfd=yes +ospf6d=yes +ripd=yes +ripngd=yes +isisd=yes +pimd=yes +ldpd=yes +nhrpd=yes +eigrpd=yes +babeld=yes +sharpd=yes +pbrd=yes +bfdd=yes +fabricd=yes + +# +# If this option is set the /etc/init.d/frr script automatically loads +# the config via "vtysh -b" when the servers are started. +# Check /etc/pam.d/frr if you intend to use "vtysh"! +# +vtysh_enable=yes +zebra_options=" -A 127.0.0.1 -s 90000000" +bgpd_options=" -A 127.0.0.1" +ospfd_options=" -A 127.0.0.1" +ospf6d_options=" -A ::1" +ripd_options=" -A 127.0.0.1" +ripngd_options=" -A ::1" +isisd_options=" -A 127.0.0.1" +pimd_options=" -A 127.0.0.1" +ldpd_options=" -A 127.0.0.1" +nhrpd_options=" -A 127.0.0.1" +eigrpd_options=" -A 127.0.0.1" +babeld_options=" -A 127.0.0.1" +sharpd_options=" -A 127.0.0.1" +pbrd_options=" -A 127.0.0.1" +staticd_options="-A 127.0.0.1" +bfdd_options=" -A 127.0.0.1" +fabricd_options="-A 127.0.0.1" + +# The list of daemons to watch is automatically generated by the init script. +#watchfrr_options="" + +# for debugging purposes, you can specify a "wrap" command to start instead +# of starting the daemon directly, e.g. to use valgrind on ospfd: +# ospfd_wrap="/usr/bin/valgrind" +# or you can use "all_wrap" for all daemons, e.g. to use perf record: +# all_wrap="/usr/bin/perf record --call-graph -" +# the normal daemon command is added to this at the end. diff --git a/daemon/core/configservices/frrservices/templates/frr.conf b/daemon/core/configservices/frrservices/templates/frr.conf new file mode 100644 index 00000000..748c8692 --- /dev/null +++ b/daemon/core/configservices/frrservices/templates/frr.conf @@ -0,0 +1,25 @@ +% for ifc, ip4s, ip6s, is_control in interfaces: +interface ${ifc.name} + % if want_ip4: + % for addr in ip4s: + ip address ${addr} + % endfor + % endif + % if want_ip6: + % for addr in ip6s: + ipv6 address ${addr} + % endfor + % endif + % if not is_control: + % for service in services: + % for line in service.frr_interface_config(ifc).split("\n"): + ${line} + % endfor + % endfor + % endif +! +% endfor + +% for service in services: +${service.frr_config()} +% endfor diff --git a/daemon/core/configservices/frrservices/templates/frrboot.sh b/daemon/core/configservices/frrservices/templates/frrboot.sh new file mode 100644 index 00000000..5a6a0e3d --- /dev/null +++ b/daemon/core/configservices/frrservices/templates/frrboot.sh @@ -0,0 +1,95 @@ +#!/bin/sh +# auto-generated by zebra service (frr.py) +FRR_CONF="${frr_conf}" +FRR_SBIN_SEARCH="${frr_sbin_search}" +FRR_BIN_SEARCH="${frr_bin_search}" +FRR_STATE_DIR="${frr_state_dir}" + +searchforprog() +{ + prog=$1 + searchpath=$@ + ret= + for p in $searchpath; do + if [ -x $p/$prog ]; then + ret=$p + break + fi + done + echo $ret +} + +confcheck() +{ + CONF_DIR=`dirname $FRR_CONF` + # if /etc/frr exists, point /etc/frr/frr.conf -> CONF_DIR + if [ "$CONF_DIR" != "/etc/frr" ] && [ -d /etc/frr ] && [ ! -e /etc/frr/frr.conf ]; then + ln -s $CONF_DIR/frr.conf /etc/frr/frr.conf + fi + # if /etc/frr exists, point /etc/frr/vtysh.conf -> CONF_DIR + if [ "$CONF_DIR" != "/etc/frr" ] && [ -d /etc/frr ] && [ ! -e /etc/frr/vtysh.conf ]; then + ln -s $CONF_DIR/vtysh.conf /etc/frr/vtysh.conf + fi +} + +bootdaemon() +{ + FRR_SBIN_DIR=$(searchforprog $1 $FRR_SBIN_SEARCH) + if [ "z$FRR_SBIN_DIR" = "z" ]; then + echo "ERROR: FRR's '$1' daemon not found in search path:" + echo " $FRR_SBIN_SEARCH" + return 1 + fi + + flags="" + + if [ "$1" = "pimd" ] && \\ + grep -E -q '^[[:space:]]*router[[:space:]]+pim6[[:space:]]*$' $FRR_CONF; then + flags="$flags -6" + fi + + #force FRR to use CORE generated conf file + flags="$flags -d -f $FRR_CONF" + $FRR_SBIN_DIR/$1 $flags + + if [ "$?" != "0" ]; then + echo "ERROR: FRR's '$1' daemon failed to start!:" + return 1 + fi +} + +bootfrr() +{ + FRR_BIN_DIR=$(searchforprog 'vtysh' $FRR_BIN_SEARCH) + if [ "z$FRR_BIN_DIR" = "z" ]; then + echo "ERROR: FRR's 'vtysh' program not found in search path:" + echo " $FRR_BIN_SEARCH" + return 1 + fi + + # fix /var/run/frr permissions + id -u frr 2>/dev/null >/dev/null + if [ "$?" = "0" ]; then + chown frr $FRR_STATE_DIR + fi + + bootdaemon "zebra" + for r in rip ripng ospf6 ospf bgp babel; do + if grep -q "^router \\<$${}{r}\\>" $FRR_CONF; then + bootdaemon "$${}{r}d" + fi + done + + if grep -E -q '^[[:space:]]*router[[:space:]]+pim6?[[:space:]]*$' $FRR_CONF; then + bootdaemon "pimd" + fi + + $FRR_BIN_DIR/vtysh -b +} + +if [ "$1" != "zebra" ]; then + echo "WARNING: '$1': all FRR daemons are launched by the 'zebra' service!" + exit 1 +fi +confcheck +bootfrr diff --git a/daemon/core/configservices/frrservices/templates/vtysh.conf b/daemon/core/configservices/frrservices/templates/vtysh.conf new file mode 100644 index 00000000..e0ab9cb6 --- /dev/null +++ b/daemon/core/configservices/frrservices/templates/vtysh.conf @@ -0,0 +1 @@ +service integrated-vtysh-config diff --git a/daemon/core/configservices/quaggaservices/services.py b/daemon/core/configservices/quaggaservices/services.py index bc937038..32ce99be 100644 --- a/daemon/core/configservices/quaggaservices/services.py +++ b/daemon/core/configservices/quaggaservices/services.py @@ -274,6 +274,9 @@ class Bgp(QuaggaService, ConfigService): ipv4_routing = True ipv6_routing = True + def quagga_config(self) -> str: + return "" + def quagga_interface_config(self, ifc: CoreInterface) -> str: router_id = get_router_id(self.node) text = f""" @@ -310,6 +313,9 @@ class Rip(QuaggaService, ConfigService): """ return self.clean_text(text) + def quagga_interface_config(self, ifc: CoreInterface) -> str: + return "" + class Ripng(QuaggaService, ConfigService): """ @@ -332,6 +338,9 @@ class Ripng(QuaggaService, ConfigService): """ return self.clean_text(text) + def quagga_interface_config(self, ifc: CoreInterface) -> str: + return "" + class Babel(QuaggaService, ConfigService): """