From f29d8ddf0b4efdea9ab7a115695f2642bf6262ff Mon Sep 17 00:00:00 2001 From: Ad Schellevis Date: Wed, 11 Nov 2020 18:04:34 +0100 Subject: [PATCH] FRR: work in progress carp event handler + ospfd implementation for https://github.com/opnsense/plugins/issues/2091 --- .../opnsense/scripts/frr/carp_event_handler | 18 ++-- .../src/opnsense/scripts/frr/lib/__init__.py | 4 +- net/frr/src/opnsense/scripts/frr/lib/base.py | 2 +- .../scripts/frr/lib/events/__init__.py | 44 +++++++++ .../opnsense/scripts/frr/lib/events/ospfd.py | 96 +++++++++++++++++++ .../templates/OPNsense/Quagga/ospfd_carp.conf | 8 +- 6 files changed, 160 insertions(+), 12 deletions(-) create mode 100644 net/frr/src/opnsense/scripts/frr/lib/events/__init__.py create mode 100644 net/frr/src/opnsense/scripts/frr/lib/events/ospfd.py diff --git a/net/frr/src/opnsense/scripts/frr/carp_event_handler b/net/frr/src/opnsense/scripts/frr/carp_event_handler index ba490b1b1..3256478b8 100755 --- a/net/frr/src/opnsense/scripts/frr/carp_event_handler +++ b/net/frr/src/opnsense/scripts/frr/carp_event_handler @@ -25,15 +25,21 @@ POSSIBILITY OF SUCH DAMAGE. """ +import sys +import syslog +import lib.events from lib import InterfaceStatus, VtySH - if __name__ == '__main__': + syslog.openlog('frr_carp', logoption=syslog.LOG_DAEMON, facility=syslog.LOG_LOCAL1) + syslog.syslog(syslog.LOG_NOTICE, 'FRR received carp configuration event.') ifstatus = InterfaceStatus() vtysh = VtySH() if vtysh.is_active: - print (ifstatus.address_status('10.111.112.113')) - if vtysh.is_running('ospfd'): - ospf_interfaces = vtysh.execute('show ip ospf interface json') - vtysh.execute(['interface le1', 'ip ospf cost 65535'], translate=None, configure=True) - print(ospf_interfaces) + for event in lib.events.get_events(): + event_object = event(ifstatus=ifstatus, vtysh=vtysh) + if event_object.should_run: + syslog.syslog(syslog.LOG_NOTICE, 'FRR trigger %s event.' % event_object.__class__.__name__) + event_object.execute() + else: + syslog.syslog(syslog.LOG_ERR, 'no frr deamons active.') diff --git a/net/frr/src/opnsense/scripts/frr/lib/__init__.py b/net/frr/src/opnsense/scripts/frr/lib/__init__.py index 85753b0df..bada445ec 100644 --- a/net/frr/src/opnsense/scripts/frr/lib/__init__.py +++ b/net/frr/src/opnsense/scripts/frr/lib/__init__.py @@ -69,8 +69,8 @@ class VtySH: self.init() def init(self): - # wait maximum 30 seconds for daemon to startup - for i in range(30): + # wait a maximum of 5 seconds for daemon to startup + for i in range(5): try: self._daemons = self.execute('show daemons', lambda x: x.decode().split()) break diff --git a/net/frr/src/opnsense/scripts/frr/lib/base.py b/net/frr/src/opnsense/scripts/frr/lib/base.py index c584bcc0f..76efb5db8 100644 --- a/net/frr/src/opnsense/scripts/frr/lib/base.py +++ b/net/frr/src/opnsense/scripts/frr/lib/base.py @@ -25,7 +25,7 @@ """ -class baseEventHandler: +class BaseEventHandler: def __init__(self, ifstatus, vtysh): self.ifstatus = ifstatus self.vtysh = vtysh diff --git a/net/frr/src/opnsense/scripts/frr/lib/events/__init__.py b/net/frr/src/opnsense/scripts/frr/lib/events/__init__.py new file mode 100644 index 000000000..b9dfe7d8b --- /dev/null +++ b/net/frr/src/opnsense/scripts/frr/lib/events/__init__.py @@ -0,0 +1,44 @@ +""" + Copyright (c) 2020 Ad Schellevis + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, + INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, + OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + POSSIBILITY OF SUCH DAMAGE. + +""" +import glob +import importlib +import sys +import os +from ..base import BaseEventHandler + + +def get_events(): + """ iterate event handlers + """ + for filename in glob.glob("%s/*.py" % os.path.dirname(__file__)): + importlib.import_module(".%s" % os.path.splitext(os.path.basename(filename))[0], __name__) + + for module_name in dir(sys.modules[__name__]): + for attribute_name in dir(getattr(sys.modules[__name__], module_name)): + cls = getattr(getattr(sys.modules[__name__], module_name), attribute_name) + if isinstance(cls, type) and issubclass(cls, BaseEventHandler) and cls != BaseEventHandler: + yield cls diff --git a/net/frr/src/opnsense/scripts/frr/lib/events/ospfd.py b/net/frr/src/opnsense/scripts/frr/lib/events/ospfd.py new file mode 100644 index 000000000..d69274708 --- /dev/null +++ b/net/frr/src/opnsense/scripts/frr/lib/events/ospfd.py @@ -0,0 +1,96 @@ +""" + Copyright (c) 2020 Ad Schellevis + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, + INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, + OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + POSSIBILITY OF SUCH DAMAGE. + +""" +import os +import syslog +from configparser import ConfigParser +from ..base import BaseEventHandler + + +class OspfdEventHandler(BaseEventHandler): + _config = '/usr/local/etc/frr/ospfd_carp.conf' + + @property + def should_run(self): + return self.vtysh.is_running('ospfd') + + def _read_config(self): + result = dict() + if os.path.isfile(self._config): + cnf = ConfigParser() + cnf.read(self._config) + not_empty = lambda x, y: cnf.has_option(x, y) and cnf.get(x, y) != '' and cnf.get(x, y) != '0' + for section in cnf.sections(): + if not_empty(section, 'interface') and not_empty(section, 'interface') \ + and not_empty(section, 'demoted_cost') and not_empty(section, 'carp_depend_on'): + default_cost = cnf.getint(section, 'default_cost') if not_empty(section, 'default_cost') else None + result[cnf.get(section, 'interface')] = { + 'demoted_cost': cnf.getint(section, 'demoted_cost'), + 'carp_depend_on': cnf.get(section, 'carp_depend_on'), + 'default_cost': default_cost, + } + + return result + + def execute(self): + if os.path.isfile(self._config): + ospf_interfaces = self.vtysh.execute('show ip ospf interface json') + config_interfaces = self._read_config() + cnf = ConfigParser() + cnf.read(self._config) + for intf in config_interfaces: + if 'interfaces' in ospf_interfaces and intf in ospf_interfaces['interfaces']: + ospf_intf_cost = ospf_interfaces['interfaces'][intf]['cost'] + is_intf_master = self.ifstatus.address_status(config_interfaces[intf]['carp_depend_on']) == 'master' + is_ospf_dem = ospf_intf_cost == config_interfaces[intf]['demoted_cost'] + if is_intf_master and is_ospf_dem: + # promote ospf interface + conf_cost = config_interfaces[intf]['default_cost'] + if conf_cost is None: + syslog.syslog( + syslog.LOG_NOTICE, 'ospfd promote interface %s (no default cost configured).' % intf + ) + self.vtysh.execute( + ['interface %s' % intf, 'no ip ospf cost'], translate=None, configure=True + ) + elif conf_cost != ospf_intf_cost: + syslog.syslog( + syslog.LOG_NOTICE, 'ospfd promote interface %s (cost %d).' % (intf, conf_cost) + ) + self.vtysh.execute( + ['interface %s' % intf, 'ip ospf cost %d' % conf_cost], + translate=None, configure=True + ) + elif not is_intf_master and not is_ospf_dem: + # demote ospf interface + conf_cost = config_interfaces[intf]['demoted_cost'] + syslog.syslog( + syslog.LOG_NOTICE, 'ospfd demote interface %s (cost %d).' % (intf, conf_cost) + ) + self.vtysh.execute( + ['interface %s' % intf, 'ip ospf cost %d' % conf_cost], + translate=None, configure=True + ) diff --git a/net/frr/src/opnsense/service/templates/OPNsense/Quagga/ospfd_carp.conf b/net/frr/src/opnsense/service/templates/OPNsense/Quagga/ospfd_carp.conf index 90e2149e2..724d7cb3c 100644 --- a/net/frr/src/opnsense/service/templates/OPNsense/Quagga/ospfd_carp.conf +++ b/net/frr/src/opnsense/service/templates/OPNsense/Quagga/ospfd_carp.conf @@ -1,9 +1,11 @@ +{% from 'OPNsense/Macros/interface.macro' import physical_interface %} {% if helpers.exists('OPNsense.quagga.ospf.interfaces.interface') %} {% for interface in helpers.toList('OPNsense.quagga.ospf.interfaces.interface') %} {% if interface.enabled == '1' %} -[interface_interface.interfacename] -interface={{interface.interfacename}} -default_cost={{interface.cost|default('10')}} +[{{ interface['@uuid'] }}] +enabled={{interface.enabled|default('0')}} +interface={{physical_interface(interface.interfacename)}} +default_cost={{interface.cost|default('')}} demoted_cost={{interface.cost_demoted|default('')}} carp_depend_on={{interface.carp_depend_on|default('')}} {% endif %}