From 422bf9ac15a12572c9f8384010e2b4d8240b55b0 Mon Sep 17 00:00:00 2001 From: Blake Harnden <32446120+bharnden@users.noreply.github.com> Date: Thu, 23 Jan 2020 11:03:56 -0800 Subject: [PATCH] initial work to add support for quagga services as config services --- daemon/core/configservice/base.py | 52 +++--- .../configservices/nrlservices/services.py | 40 +---- .../configservices/quaggaservices/__init__.py | 0 .../configservices/quaggaservices/services.py | 169 ++++++++++++++++++ .../quaggaservices/templates/Quagga.conf | 23 +++ .../quaggaservices/templates/quaggaboot.sh | 92 ++++++++++ .../quaggaservices/templates/vtysh.conf | 1 + 7 files changed, 312 insertions(+), 65 deletions(-) create mode 100644 daemon/core/configservices/quaggaservices/__init__.py create mode 100644 daemon/core/configservices/quaggaservices/services.py create mode 100644 daemon/core/configservices/quaggaservices/templates/Quagga.conf create mode 100644 daemon/core/configservices/quaggaservices/templates/quaggaboot.sh create mode 100644 daemon/core/configservices/quaggaservices/templates/vtysh.conf diff --git a/daemon/core/configservice/base.py b/daemon/core/configservice/base.py index fa8f0ad3..8db4d7f5 100644 --- a/daemon/core/configservice/base.py +++ b/daemon/core/configservice/base.py @@ -42,6 +42,10 @@ class ConfigService(abc.ABC): configs = self.default_configs[:] self._define_config(configs) + @staticmethod + def clean_text(text: str) -> str: + return inspect.cleandoc(text) + @property @abc.abstractmethod def name(self) -> str: @@ -147,12 +151,12 @@ class ConfigService(abc.ABC): basename = pathlib.Path(name).name if name in self.custom_templates: template = self.custom_templates[name] - template = inspect.cleandoc(template) + template = self.clean_text(template) elif self.templates.has_template(basename): template = self.templates.get_template(basename).source else: template = self.get_text(name) - template = inspect.cleandoc(template) + template = self.clean_text(template) templates[name] = template return templates @@ -162,14 +166,22 @@ class ConfigService(abc.ABC): basename = pathlib.Path(name).name if name in self.custom_templates: text = self.custom_templates[name] - text = inspect.cleandoc(text) - self.render_text(name, text, data) + text = self.clean_text(text) + rendered = self.render_text(text, data) elif self.templates.has_template(basename): - self.render_template(name, basename, data) + rendered = self.render_template(basename, data) else: text = self.get_text(name) - text = inspect.cleandoc(text) - self.render_text(name, text, data) + text = self.clean_text(text) + rendered = self.render_text(text, data) + logging.info( + "node(%s) service(%s) template(%s): \n%s", + self.node.name, + self.name, + name, + rendered, + ) + self.node.nodefile(name, rendered) def run_startup(self) -> None: for cmd in self.startup: @@ -205,44 +217,30 @@ class ConfigService(abc.ABC): f"failed to validate" ) - def _render( - self, name: str, template: Template, data: Dict[str, Any] = None - ) -> None: + def _render(self, template: Template, data: Dict[str, Any] = None) -> str: if data is None: data = {} - rendered = template.render_unicode( + return template.render_unicode( node=self.node, config=self.render_config(), **data ) - logging.info( - "node(%s) service(%s) template(%s): \n%s", - self.node.name, - self.name, - name, - rendered, - ) - self.node.nodefile(name, rendered) - def render_text(self, name: str, text: str, data: Dict[str, Any] = None) -> None: + def render_text(self, text: str, data: Dict[str, Any] = None) -> str: try: template = Template(text) - self._render(name, template, data) + return self._render(template, data) except Exception: raise CoreError( f"node({self.node.name}) service({self.name}) " - f"error rendering template({name}): " f"{exceptions.text_error_template().render_unicode()}" ) - def render_template( - self, name: str, basename: str, data: Dict[str, Any] = None - ) -> None: + def render_template(self, basename: str, data: Dict[str, Any] = None) -> str: try: template = self.templates.get_template(basename) - self._render(name, template, data) + return self._render(template, data) except Exception: raise CoreError( f"node({self.node.name}) service({self.name}) " - f"error rendering template({name}): " f"{exceptions.text_error_template().render_template()}" ) diff --git a/daemon/core/configservices/nrlservices/services.py b/daemon/core/configservices/nrlservices/services.py index 98df2ceb..a67fac53 100644 --- a/daemon/core/configservices/nrlservices/services.py +++ b/daemon/core/configservices/nrlservices/services.py @@ -8,42 +8,6 @@ from core.configservice.base import ConfigService, ConfigServiceMode GROUP = "ProtoSvc" -class NrlService(ConfigService): - name = "NrlBase" - group = GROUP - directories = [] - files = [] - executables = [] - dependencies = [] - startup = [] - validate = [] - shutdown = [] - validation_mode = ConfigServiceMode.BLOCKING - default_configs = [] - modes = {} - - @classmethod - def generate_config(cls, node, filename): - return "" - - @staticmethod - def firstipv4prefix(node, prefixlen=24): - """ - Similar to QuaggaService.routerid(). Helper to return the first IPv4 - prefix of a node, using the supplied prefix length. This ignores the - interface's prefix length, so e.g. '/32' can turn into '/24'. - """ - for ifc in node.netifs(): - if hasattr(ifc, "control") and ifc.control is True: - continue - for a in ifc.addrlist: - a = a.split("/")[0] - if netaddr.valid_ipv4(a): - return f"{a}/{prefixlen}" - # raise ValueError, "no IPv4 address found" - return "0.0.0.0/%s" % prefixlen - - class MgenSinkService(ConfigService): name = "MGEN_Sink" group = GROUP @@ -66,7 +30,7 @@ class MgenSinkService(ConfigService): return dict(ifnames=ifnames) -class NrlNhdp(NrlService): +class NrlNhdp(ConfigService): name = "NHDP" group = GROUP directories = [] @@ -204,7 +168,7 @@ class OlsrOrg(ConfigService): return dict(has_smf=has_smf, ifnames=ifnames) -class MgenActor(NrlService): +class MgenActor(ConfigService): name = "MgenActor" group = GROUP directories = [] diff --git a/daemon/core/configservices/quaggaservices/__init__.py b/daemon/core/configservices/quaggaservices/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/daemon/core/configservices/quaggaservices/services.py b/daemon/core/configservices/quaggaservices/services.py new file mode 100644 index 00000000..ef36b932 --- /dev/null +++ b/daemon/core/configservices/quaggaservices/services.py @@ -0,0 +1,169 @@ +import abc +from typing import Any, Dict + +import netaddr + +from core import constants +from core.configservice.base import ConfigService, ConfigServiceMode +from core.nodes.base import CoreNodeBase +from core.nodes.interface import CoreInterface + +GROUP = "Quagga" + + +def has_mtu_mismatch(ifc: CoreInterface) -> bool: + """ + Helper to detect MTU mismatch and add the appropriate OSPF + 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_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 hasattr(ifc, "control") and ifc.control is True: + continue + for a in ifc.addrlist: + a = a.split("/")[0] + if netaddr.valid_ipv4(a): + return a + return "0.0.0.0" + + +class Zebra(ConfigService): + name = "zebra" + group = GROUP + directories = ["/usr/local/etc/quagga", "/var/run/quagga"] + files = [ + "/usr/local/etc/quagga/Quagga.conf", + "quaggaboot.sh", + "/usr/local/etc/quagga/vtysh.conf", + ] + executables = ["zebra"] + dependencies = [] + startup = ["sh quaggaboot.sh zebra"] + validate = ["pidof zebra"] + shutdown = ["killall zebra"] + validation_mode = ConfigServiceMode.BLOCKING + default_configs = [] + modes = {} + + def data(self) -> Dict[str, Any]: + quagga_bin_search = self.node.session.options.get_config( + "quagga_bin_search", default="/usr/local/bin /usr/bin /usr/lib/quagga" + ) + quagga_sbin_search = self.node.session.options.get_config( + "quagga_sbin_search", default="/usr/local/sbin /usr/sbin /usr/lib/quagga" + ) + quagga_state_dir = constants.QUAGGA_STATE_DIR + quagga_conf = self.files[0] + + 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( + quagga_bin_search=quagga_bin_search, + quagga_sbin_search=quagga_sbin_search, + quagga_state_dir=quagga_state_dir, + quagga_conf=quagga_conf, + interfaces=interfaces, + want_ip4=want_ip4, + want_ip6=want_ip6, + ) + + +class QuaggaService(abc.ABC): + group = GROUP + directories = [] + files = [] + executables = [] + dependencies = ["zebra"] + startup = [] + validate = [] + shutdown = [] + validation_mode = ConfigServiceMode.BLOCKING + default_configs = [] + modes = {} + ipv4_routing = False + ipv6_routing = False + + @abc.abstractmethod + def quagga_interface_config(self, ifc: CoreInterface) -> str: + raise NotImplementedError + + @abc.abstractmethod + def quagga_config(self) -> str: + raise NotImplementedError + + +class Ospfv2(QuaggaService, 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 Quagga.conf file. + """ + + name = "OSPFv2" + validate = ["pidof ospfd"] + shutdown = ["killall ospfd"] + ipv4_routing = True + + def quagga_interface_config(self, ifc: CoreInterface) -> str: + if has_mtu_mismatch(ifc): + return "ip ospf mtu-ignore" + else: + return "" + + def quagga_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) diff --git a/daemon/core/configservices/quaggaservices/templates/Quagga.conf b/daemon/core/configservices/quaggaservices/templates/Quagga.conf new file mode 100644 index 00000000..138f4288 --- /dev/null +++ b/daemon/core/configservices/quaggaservices/templates/Quagga.conf @@ -0,0 +1,23 @@ +% for ifc, ip4s, ip6s 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 is_control: + % for service in services: + ${service.quagga_interface_config(ifc)} + % endfor + % endif +! +% endfor + +% for service in services: + ${service.quagga_config()} +% endfor diff --git a/daemon/core/configservices/quaggaservices/templates/quaggaboot.sh b/daemon/core/configservices/quaggaservices/templates/quaggaboot.sh new file mode 100644 index 00000000..c22fdd5f --- /dev/null +++ b/daemon/core/configservices/quaggaservices/templates/quaggaboot.sh @@ -0,0 +1,92 @@ +#!/bin/sh +# auto-generated by zebra service (quagga.py) +QUAGGA_CONF="${quagga_conf}" +QUAGGA_SBIN_SEARCH="${quagga_sbin_search}" +QUAGGA_BIN_SEARCH="${quagga_bin_search}" +QUAGGA_STATE_DIR="${quagga_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 $QUAGGA_CONF` + # if /etc/quagga exists, point /etc/quagga/Quagga.conf -> CONF_DIR + if [ "$CONF_DIR" != "/etc/quagga" ] && [ -d /etc/quagga ] && [ ! -e /etc/quagga/Quagga.conf ]; then + ln -s $CONF_DIR/Quagga.conf /etc/quagga/Quagga.conf + fi + # if /etc/quagga exists, point /etc/quagga/vtysh.conf -> CONF_DIR + if [ "$CONF_DIR" != "/etc/quagga" ] && [ -d /etc/quagga ] && [ ! -e /etc/quagga/vtysh.conf ]; then + ln -s $CONF_DIR/vtysh.conf /etc/quagga/vtysh.conf + fi +} + +bootdaemon() +{ + QUAGGA_SBIN_DIR=$(searchforprog $1 $QUAGGA_SBIN_SEARCH) + if [ "z$QUAGGA_SBIN_DIR" = "z" ]; then + echo "ERROR: Quagga's '$1' daemon not found in search path:" + echo " $QUAGGA_SBIN_SEARCH" + return 1 + fi + + flags="" + + if [ "$1" = "xpimd" ] && \\ + grep -E -q '^[[:space:]]*router[[:space:]]+pim6[[:space:]]*$' $QUAGGA_CONF; then + flags="$flags -6" + fi + + $QUAGGA_SBIN_DIR/$1 $flags -d + if [ "$?" != "0" ]; then + echo "ERROR: Quagga's '$1' daemon failed to start!:" + return 1 + fi +} + +bootquagga() +{ + QUAGGA_BIN_DIR=$(searchforprog 'vtysh' $QUAGGA_BIN_SEARCH) + if [ "z$QUAGGA_BIN_DIR" = "z" ]; then + echo "ERROR: Quagga's 'vtysh' program not found in search path:" + echo " $QUAGGA_BIN_SEARCH" + return 1 + fi + + # fix /var/run/quagga permissions + id -u quagga 2>/dev/null >/dev/null + if [ "$?" = "0" ]; then + chown quagga $QUAGGA_STATE_DIR + fi + + bootdaemon "zebra" + for r in rip ripng ospf6 ospf bgp babel; do + if grep -q "^router \\<$${}{r}\\>" $QUAGGA_CONF; then + bootdaemon "$${}{r}d" + fi + done + + if grep -E -q '^[[:space:]]*router[[:space:]]+pim6?[[:space:]]*$' $QUAGGA_CONF; then + bootdaemon "xpimd" + fi + + $QUAGGA_BIN_DIR/vtysh -b +} + +if [ "$1" != "zebra" ]; then + echo "WARNING: '$1': all Quagga daemons are launched by the 'zebra' service!" + exit 1 +fi +confcheck +bootquagga diff --git a/daemon/core/configservices/quaggaservices/templates/vtysh.conf b/daemon/core/configservices/quaggaservices/templates/vtysh.conf new file mode 100644 index 00000000..e0ab9cb6 --- /dev/null +++ b/daemon/core/configservices/quaggaservices/templates/vtysh.conf @@ -0,0 +1 @@ +service integrated-vtysh-config