FRR: OSPF ipv6 influence interface cost via carp (#2929)

add event handler for ospf6 carp demotion including required interface fields (carp_depend_on and cost_demoted).
while here polish some small usability issues, knowing:

o Interface networktype and interfacename should be single dropdown boxes
o diagnostics / bgp - fix search in grid, should only use a formatter for presentation purposes.
o carp_frr_ospf6 rc.carp_service_status.d event handler
o add "CARP demote" to model, form and template
o bugfix previous, missing IFS= (internal field seperator)
o missing ospf6d in log event handler
This commit is contained in:
Ad Schellevis
2022-04-21 11:33:59 +02:00
committed by GitHub
parent 58ec526297
commit 8d45144ad4
12 changed files with 198 additions and 8 deletions
+7
View File
@@ -11,6 +11,13 @@ switching and routing, Internet access routers, and Internet peering.
Plugin Changelog
================
1.28
* OSPF6 Interface change networktype and interfacename to dropdown in stead of multi select
* Diagnostics / BGP - fix search in grid, should only use a formatter for presentation purposes.
* Add ospf6 carp demotion event handler
1.27
* Add BGP password authentication
+25
View File
@@ -0,0 +1,25 @@
#!/bin/sh
if [ -f /etc/rc.conf.d/frr ]; then
. /etc/rc.conf.d/frr
fi
if [ "$frr_enable" == "YES" ] && (`echo "$frr_daemons" | /usr/bin/grep -F -q -w "ospf6d"`) &&
(`echo "$frr_carp_demote" | /usr/bin/grep -F -q -w "ospf6d"`) ; then
# OSPF enabled
OSPF_NEIGHBOR=`echo "show ipv6 ospf6 neighbor" | /usr/local/bin/vtysh 2>&1` IFS=
if [ "$?" -eq 0 ]; then
# running, check if we can find any neighbors
IFS=
neighbors_count=`echo $OSPF_NEIGHBOR | grep "Full/" | wc -l`
unset IFS
if [ "$neighbors_count" -eq 0 ]; then
# no neighbors in state Full/* found
exit 2
else
exit 0
fi
else
# not running
exit 1
fi
fi
@@ -7,7 +7,7 @@
<field>
<id>interface.interfacename</id>
<label>Interface</label>
<type>select_multiple</type>
<type>dropdown</type>
<help>Select an interface where this settings apply to.</help>
</field>
<field>
@@ -26,6 +26,18 @@
<label>Cost</label>
<type>text</type>
</field>
<field>
<id>interface.cost_demoted</id>
<label>Cost (when demoted)</label>
<type>text</type>
</field>
<field>
<id>interface.carp_depend_on</id>
<label>Depend on (carp)</label>
<type>dropdown</type>
<help>The carp VHID to depend on, when this virtual address is not in master state,
the interface cost will be set to the demoted cost (specified above).</help>
</field>
<field>
<id>interface.hellointerval</id>
<label>Hello Interval</label>
@@ -54,6 +66,6 @@
<field>
<id>interface.networktype</id>
<label>Network Type</label>
<type>select_multiple</type>
<type>dropdown</type>
</field>
</form>
@@ -5,6 +5,16 @@
<type>checkbox</type>
<help>This will activate the OSPFv3 service if routing protocols are enabled in "General".</help>
</field>
<field>
<id>ospf6.carp_demote</id>
<label>CARP demote</label>
<type>checkbox</type>
<help>
Register CARP status monitor, when no neighbors are found, consider this node less attractive.
This feature needs syslog enabled using "Debugging" logging to catch all relevant status events.
This option is not compatible with "Enable CARP Failover".
</help>
</field>
<field>
<id>ospf6.redistribute</id>
<label>Route Redistribution</label>
@@ -7,6 +7,10 @@
<default>0</default>
<Required>Y</Required>
</enabled>
<carp_demote type="BooleanField">
<default>0</default>
<Required>Y</Required>
</carp_demote>
<redistribute type="OptionField">
<Required>N</Required>
<multiple>Y</multiple>
@@ -52,6 +56,17 @@
<MaximumValue>4294967295</MaximumValue>
<ValidationMessage>Cost must be between 0 and 4294967295.</ValidationMessage>
</cost>
<cost_demoted type="IntegerField">
<default>65535</default>
<MinimumValue>1</MinimumValue>
<Required>N</Required>
<MaximumValue>65535</MaximumValue>
<ValidationMessage>Cost must be between 1 and 65535.</ValidationMessage>
</cost_demoted>
<carp_depend_on type="VirtualIPField">
<type>carp</type>
<Required>N</Required>
</carp_depend_on>
<hellointerval type="IntegerField">
<default></default>
<MinimumValue>0</MinimumValue>
@@ -101,7 +101,6 @@ $(document).ready(function() {
content = _.template($('#routestpl').html())(data['response']);
$('#routing').html(content);
$('#routing table').bootgrid({
converters: dataconverters,
formatters: dataformatters
});
});
@@ -110,7 +109,6 @@ $(document).ready(function() {
content = _.template($('#routestpl').html())(data['response']);
$('#routing6').html(content);
$('#routing6 table').bootgrid({
converters: dataconverters,
formatters: dataformatters
});
});
@@ -0,0 +1,107 @@
"""
Copyright (c) 2022 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 Ospf6dEventHandler(BaseEventHandler):
_config = '/usr/local/etc/frr/ospf6d_carp.conf'
@property
def should_run(self):
return self.vtysh.is_running('ospf6d')
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):
# parse ospf6 interface data, keep structure similar to what ospf offers when using json output
ospf_interfaces = {
'interfaces': {}
}
this_interface = None
for line in self.vtysh.execute('show ipv6 ospf6 interface', translate=None).decode().split('\n'):
if len(line) > 0 and line[0] != ' ':
this_interface = line.split()[0]
ospf_interfaces['interfaces'][this_interface] = {}
elif this_interface is not None:
if line.find('Area ID') > 0 and line.split()[-1].isdigit():
# Area ID X.X.X.X, Cost XXXX
ospf_interfaces['interfaces'][this_interface]['cost'] = int(line.split()[-1])
config_interfaces = self._read_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 ospf6 interface
conf_cost = config_interfaces[intf]['default_cost']
if conf_cost is None:
syslog.syslog(
syslog.LOG_NOTICE, 'ospf6d promote interface %s (no default cost configured).' % intf
)
self.vtysh.execute(
['interface %s' % intf, 'no ipv6 ospf6 cost'], translate=None, configure=True
)
elif conf_cost != ospf_intf_cost:
syslog.syslog(
syslog.LOG_NOTICE, 'ospf6d promote interface %s (cost %d).' % (intf, conf_cost)
)
self.vtysh.execute(
['interface %s' % intf, 'ipv6 ospf6 cost %d' % conf_cost],
translate=None, configure=True
)
elif not is_intf_master and not is_ospf_dem:
# demote ospf6 interface
conf_cost = config_interfaces[intf]['demoted_cost']
syslog.syslog(
syslog.LOG_NOTICE, 'ospf6d demote interface %s (cost %d).' % (intf, conf_cost)
)
self.vtysh.execute(
['interface %s' % intf, 'ipv6 ospf6 cost %d' % conf_cost],
translate=None, configure=True
)
@@ -59,8 +59,6 @@ class OspfdEventHandler(BaseEventHandler):
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']
@@ -3,6 +3,7 @@ bgpd.conf:/usr/local/etc/frr/bgpd.conf
ospfd.conf:/usr/local/etc/frr/ospfd.conf
ospfd_carp.conf:/usr/local/etc/frr/ospfd_carp.conf
ospf6d.conf:/usr/local/etc/frr/ospf6d.conf
ospf6d_carp.conf:/usr/local/etc/frr/ospf6d_carp.conf
ripd.conf:/usr/local/etc/frr/ripd.conf
sa_policies.conf:/usr/local/etc/frr/sa_policies.conf
frr:/etc/rc.conf.d/frr
@@ -12,7 +12,10 @@ if helpers.exists('OPNsense.quagga.bgp.enabled') and OPNsense.quagga.bgp.enabled
if helpers.exists('OPNsense.quagga.ospf6.enabled') and OPNsense.quagga.ospf6.enabled == '1' %} ospf6d{% endif %}{%
if helpers.exists('OPNsense.quagga.ripng.enabled') and OPNsense.quagga.ripng.enabled == '1' %} ripngd{% endif %}{%
if helpers.exists('OPNsense.quagga.isis.enabled') and OPNsense.quagga.isis.enabled == '1' %} isisd{% endif %}"
frr_carp_demote="{% if not helpers.empty('OPNsense.quagga.ospf.carp_demote') %} ospfd{% endif %}"
frr_carp_demote="{%
if not helpers.empty('OPNsense.quagga.ospf.carp_demote') %} ospfd{% endif %}{%
if not helpers.empty('OPNsense.quagga.ospf6.carp_demote') %} ospf6d{% endif
%}"
start_postcmd="/usr/local/opnsense/scripts/frr/carp_event_handler"
{% else %}
frr_enable="NO"
@@ -0,0 +1,13 @@
{% from 'OPNsense/Macros/interface.macro' import physical_interface %}
{% if helpers.exists('OPNsense.quagga.ospf6.interfaces.interface') %}
{% for interface in helpers.toList('OPNsense.quagga.ospf6.interfaces.interface') %}
{% if interface.enabled == '1' %}
[{{ 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 %}
{% endfor %}
{% endif %}
@@ -6,7 +6,8 @@ destination d_frr_event {
};
filter f_frr_ospf {
program("ospfd") and (
(program("ospfd") or program("ospf6d"))
and (
(
level("info") or level("notice")
) or (