initial work to add support for quagga services as config services

This commit is contained in:
Blake Harnden 2020-01-23 11:03:56 -08:00
parent 8f03c9c975
commit 422bf9ac15
7 changed files with 312 additions and 65 deletions

View file

@ -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()}"
)

View file

@ -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 = []

View file

@ -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)

View file

@ -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

View file

@ -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

View file

@ -0,0 +1 @@
service integrated-vtysh-config