FRR: work in progress carp event handler + ospfd implementation for https://github.com/opnsense/plugins/issues/2091

This commit is contained in:
Ad Schellevis
2020-11-16 16:53:02 +01:00
committed by Ad Schellevis
parent b49b29bc9d
commit f29d8ddf0b
6 changed files with 160 additions and 12 deletions
@@ -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.')
@@ -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
+1 -1
View File
@@ -25,7 +25,7 @@
"""
class baseEventHandler:
class BaseEventHandler:
def __init__(self, ifstatus, vtysh):
self.ifstatus = ifstatus
self.vtysh = vtysh
@@ -0,0 +1,44 @@
"""
Copyright (c) 2020 Ad Schellevis <ad@opnsense.org>
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
@@ -0,0 +1,96 @@
"""
Copyright (c) 2020 Ad Schellevis <ad@opnsense.org>
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
)
@@ -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 %}