summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Makefile3
-rw-r--r--data/templates/load-balancing/wlb.conf.j2130
-rw-r--r--interface-definitions/load-balancing-wan.xml.in11
-rwxr-xr-xsrc/conf_mode/load-balancing-wan.py131
4 files changed, 264 insertions, 11 deletions
diff --git a/Makefile b/Makefile
index e130bec70..4400cbfdc 100644
--- a/Makefile
+++ b/Makefile
@@ -38,9 +38,6 @@ interface_definitions: $(config_xml_obj)
# T2773 - EIGRP support for VRF
rm -rf $(TMPL_DIR)/vrf/name/node.tag/protocols/eigrp
- # T4518, T4470 Load-balancing wan
- rm -rf $(TMPL_DIR)/load-balancing
-
# XXX: test if there are empty node.def files - this is not allowed as these
# could mask help strings or mandatory priority statements
find $(TMPL_DIR) -name node.def -type f -empty -exec false {} + || sh -c 'echo "There are empty node.def files! Check your interface definitions." && exit 1'
diff --git a/data/templates/load-balancing/wlb.conf.j2 b/data/templates/load-balancing/wlb.conf.j2
new file mode 100644
index 000000000..d3326b6b8
--- /dev/null
+++ b/data/templates/load-balancing/wlb.conf.j2
@@ -0,0 +1,130 @@
+# Generated by /usr/libexec/vyos/conf_mode/load-balancing-wan.py
+
+{% if disable_source_nat is vyos_defined %}
+disable-source-nat
+{% endif %}
+{% if enable_local_traffic is vyos_defined %}
+enable-local-traffic
+{% endif %}
+{% if sticky_connections is vyos_defined %}
+sticky-connections inbound
+{% endif %}
+{% if flush_connections is vyos_defined %}
+flush-conntrack
+{% endif %}
+{% if hook is vyos_defined %}
+hook "{{ hook }}"
+{% endif %}
+{% if interface_health is vyos_defined %}
+health {
+{% for interface, interface_config in interface_health.items() %}
+ interface {{ interface }} {
+{% if interface_config.failure_count is vyos_defined %}
+ failure-ct {{ interface_config.failure_count }}
+{% endif %}
+{% if interface_config.success_count is vyos_defined %}
+ success-ct {{ interface_config.success_count }}
+{% endif %}
+{% if interface_config.nexthop is vyos_defined %}
+ nexthop {{ interface_config.nexthop }}
+{% endif %}
+{% if interface_config.test is vyos_defined %}
+{% for test_rule, test_config in interface_config.test.items() %}
+ rule {{ test_rule }} {
+{% if test_config.type is vyos_defined %}
+{% set type_translate = {'ping': 'ping', 'ttl': 'udp', 'user-defined': 'user-defined'} %}
+ type {{ type_translate[test_config.type] }} {
+{% if test_config.ttl_limit is vyos_defined and test_config.type == 'ttl' %}
+ ttl {{ test_config.ttl_limit }}
+{% endif %}
+{% if test_config.test_script is vyos_defined and test_config.type == 'user-defined' %}
+ test-script {{ test_config.test_script }}
+{% endif %}
+{% if test_config.target is vyos_defined %}
+ target {{ test_config.target }}
+{% endif %}
+ resp-time {{ test_config.resp_time | int * 1000 }}
+ }
+{% endif %}
+ }
+{% endfor %}
+{% endif %}
+ }
+{% endfor %}
+}
+{% endif %}
+
+{% if rule is vyos_defined %}
+{% for rule, rule_config in rule.items() %}
+rule {{ rule }} {
+{% if rule_config.exclude is vyos_defined %}
+ exclude
+{% endif %}
+{% if rule_config.failover is vyos_defined %}
+ failover
+{% endif %}
+{% if rule_config.limit is vyos_defined %}
+ limit {
+{% if rule_config.limit.burst is vyos_defined %}
+ burst {{ rule_config.limit.burst }}
+{% endif %}
+{% if rule_config.limit.rate is vyos_defined %}
+ rate {{ rule_config.limit.rate }}
+{% endif %}
+{% if rule_config.limit.period is vyos_defined %}
+ period {{ rule_config.limit.period }}
+{% endif %}
+{% if rule_config.limit.threshold is vyos_defined %}
+ thresh {{ rule_config.limit.threshold }}
+{% endif %}
+ }
+{% endif %}
+{% if rule_config.per_packet_balancing is vyos_defined %}
+ per-packet-balancing
+{% endif %}
+{% if rule_config.protocol is vyos_defined %}
+ protocol {{ rule_config.protocol }}
+{% endif %}
+{% if rule_config.destination is vyos_defined %}
+ destination {
+{% if rule_config.destination.address is vyos_defined %}
+ address "{{ rule_config.destination.address }}"
+{% endif %}
+{% if rule_config.destination.port is vyos_defined %}
+{% if '-' in rule_config.destination.port %}
+ port-ipt "-m multiport --dports {{ rule_config.destination.port | replace('-', ':') }}"
+{% else %}
+ port-ipt " --dport {{ rule_config.destination.port }}"
+{% endif %}
+{% endif %}
+ }
+{% endif %}
+{% if rule_config.source is vyos_defined %}
+ source {
+{% if rule_config.source.address is vyos_defined %}
+ address "{{ rule_config.source.address }}"
+{% endif %}
+{% if rule_config.source.port is vyos_defined %}
+{% if '-' in rule_config.source.port %}
+ port-ipt "-m multiport --sports {{ rule_config.source.port | replace('-', ':') }}"
+{% else %}
+ port.ipt " --sport {{ rule_config.source.port }}"
+{% endif %}
+{% endif %}
+ }
+{% endif %}
+{% if rule_config.inbound_interface is vyos_defined %}
+ inbound-interface {{ rule_config.inbound_interface }}
+{% endif %}
+{% if rule_config.interface is vyos_defined %}
+{% for interface, interface_config in rule_config.interface.items() %}
+ interface {{ interface }} {
+{% if interface_config.weight is vyos_defined %}
+ weight {{ interface_config.weight }}
+{% endif %}
+ }
+{% endfor %}
+{% endif %}
+}
+{% endfor %}
+{% endif %}
diff --git a/interface-definitions/load-balancing-wan.xml.in b/interface-definitions/load-balancing-wan.xml.in
index c1d7e2c67..3a2c111ac 100644
--- a/interface-definitions/load-balancing-wan.xml.in
+++ b/interface-definitions/load-balancing-wan.xml.in
@@ -3,6 +3,7 @@
<node name="load-balancing">
<properties>
<help>Configure load-balancing</help>
+ <priority>900</priority>
</properties>
<children>
<node name="wan" owner="${vyos_conf_scripts_dir}/load-balancing-wan.py">
@@ -59,6 +60,7 @@
<validator name="numeric" argument="--range 1-10"/>
</constraint>
</properties>
+ <defaultValue>1</defaultValue>
</leafNode>
<leafNode name="nexthop">
<properties>
@@ -91,6 +93,7 @@
<validator name="numeric" argument="--range 1-10"/>
</constraint>
</properties>
+ <defaultValue>1</defaultValue>
</leafNode>
<tagNode name="test">
<properties>
@@ -115,6 +118,7 @@
<validator name="numeric" argument="--range 1-30"/>
</constraint>
</properties>
+ <defaultValue>5</defaultValue>
</leafNode>
<leafNode name="target">
<properties>
@@ -151,6 +155,7 @@
<validator name="numeric" argument="--range 1-254"/>
</constraint>
</properties>
+ <defaultValue>1</defaultValue>
</leafNode>
<leafNode name="type">
<properties>
@@ -242,6 +247,7 @@
</constraint>
<constraintErrorMessage>Weight must be between 1 and 255</constraintErrorMessage>
</properties>
+ <defaultValue>1</defaultValue>
</leafNode>
</children>
</tagNode>
@@ -261,6 +267,7 @@
<validator name="numeric" argument="--range 0-4294967295"/>
</constraint>
</properties>
+ <defaultValue>5</defaultValue>
</leafNode>
<leafNode name="period">
<properties>
@@ -284,6 +291,7 @@
<regex>(hour|minute|second)</regex>
</constraint>
</properties>
+ <defaultValue>second</defaultValue>
</leafNode>
<leafNode name="rate">
<properties>
@@ -296,6 +304,7 @@
<validator name="numeric" argument="--range 0-4294967295"/>
</constraint>
</properties>
+ <defaultValue>5</defaultValue>
</leafNode>
<leafNode name="threshold">
<properties>
@@ -315,6 +324,7 @@
<regex>(above|below)</regex>
</constraint>
</properties>
+ <defaultValue>below</defaultValue>
</leafNode>
</children>
</node>
@@ -355,6 +365,7 @@
<validator name="ip-protocol"/>
</constraint>
</properties>
+ <defaultValue>all</defaultValue>
</leafNode>
<node name="source">
<properties>
diff --git a/src/conf_mode/load-balancing-wan.py b/src/conf_mode/load-balancing-wan.py
index 11840249f..2f0cf1293 100755
--- a/src/conf_mode/load-balancing-wan.py
+++ b/src/conf_mode/load-balancing-wan.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2022 VyOS maintainers and contributors
+# Copyright (C) 2023 VyOS maintainers and contributors
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2 or later as
@@ -14,17 +14,24 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
+import os
from sys import exit
+from shutil import rmtree
+from vyos.base import Warning
from vyos.config import Config
-from vyos.configdict import node_changed
-from vyos.util import call
+from vyos.configdict import dict_merge
+from vyos.util import cmd
+from vyos.template import render
+from vyos.xml import defaults
from vyos import ConfigError
-from pprint import pprint
from vyos import airbag
airbag.enable()
+load_balancing_dir = '/run/load-balance'
+load_balancing_conf_file = f'{load_balancing_dir}/wlb.conf'
+
def get_config(config=None):
if config:
@@ -33,27 +40,135 @@ def get_config(config=None):
conf = Config()
base = ['load-balancing', 'wan']
- lb = conf.get_config_dict(base, get_first_key=True,
- no_tag_node_value_mangle=True)
+ lb = conf.get_config_dict(base,
+ get_first_key=True,
+ key_mangling=('-', '_'),
+ no_tag_node_value_mangle=True)
+
+ # We have gathered the dict representation of the CLI, but there are default
+ # options which we need to update into the dictionary retrived.
+ default_values = defaults(base)
+ # lb base default values can not be merged here - remove and add them later
+ if 'interface_health' in default_values:
+ del default_values['interface_health']
+ if 'rule' in default_values:
+ del default_values['rule']
+ lb = dict_merge(default_values, lb)
+
+ if 'interface_health' in lb:
+ for iface in lb.get('interface_health'):
+ default_values_iface = defaults(base + ['interface-health'])
+ if 'test' in default_values_iface:
+ del default_values_iface['test']
+ lb['interface_health'][iface] = dict_merge(
+ default_values_iface, lb['interface_health'][iface])
+ if 'test' in lb['interface_health'][iface]:
+ for node_test in lb['interface_health'][iface]['test']:
+ default_values_test = defaults(base +
+ ['interface-health', 'test'])
+ lb['interface_health'][iface]['test'][node_test] = dict_merge(
+ default_values_test,
+ lb['interface_health'][iface]['test'][node_test])
+
+ if 'rule' in lb:
+ for rule in lb.get('rule'):
+ default_values_rule = defaults(base + ['rule'])
+ if 'interface' in default_values_rule:
+ del default_values_rule['interface']
+ lb['rule'][rule] = dict_merge(default_values_rule, lb['rule'][rule])
+ if not conf.exists(base + ['rule', rule, 'limit']):
+ del lb['rule'][rule]['limit']
+ if 'interface' in lb['rule'][rule]:
+ for iface in lb['rule'][rule]['interface']:
+ default_values_rule_iface = defaults(base + ['rule', 'interface'])
+ lb['rule'][rule]['interface'][iface] = dict_merge(default_values_rule_iface, lb['rule'][rule]['interface'][iface])
- pprint(lb)
return lb
+
def verify(lb):
- return None
+ if not lb:
+ return None
+
+ if 'interface_health' not in lb:
+ raise ConfigError(
+ 'A valid WAN load-balance configuration requires an interface with a nexthop!'
+ )
+
+ for interface, interface_config in lb['interface_health'].items():
+ if 'nexthop' not in interface_config:
+ raise ConfigError(
+ f'interface-health {interface} nexthop must be specified!')
+
+ if 'test' in interface_config:
+ for test_rule, test_config in interface_config['test'].items():
+ if 'type' in test_config:
+ if test_config['type'] == 'user-defined' and 'test_script' not in test_config:
+ raise ConfigError(
+ f'test {test_rule} script must be defined for test-script!'
+ )
+
+ if 'rule' not in lb:
+ Warning(
+ 'At least one rule with an (outbound) interface must be defined for WAN load balancing to be active!'
+ )
+ else:
+ for rule, rule_config in lb['rule'].items():
+ if 'inbound_interface' not in rule_config:
+ raise ConfigError(f'rule {rule} inbound-interface must be specified!')
+ if {'failover', 'exclude'} <= set(rule_config):
+ raise ConfigError(f'rule {rule} failover cannot be configured with exclude!')
+ if {'limit', 'exclude'} <= set(rule_config):
+ raise ConfigError(f'rule {rule} limit cannot be used with exclude!')
+ if 'interface' not in rule_config:
+ if 'exclude' not in rule_config:
+ Warning(
+ f'rule {rule} will be inactive because no (outbound) interfaces have been defined for this rule'
+ )
+ for direction in {'source', 'destination'}:
+ if direction in rule_config:
+ if 'protocol' in rule_config and 'port' in rule_config[
+ direction]:
+ if rule_config['protocol'] not in {'tcp', 'udp'}:
+ raise ConfigError('ports can only be specified when protocol is "tcp" or "udp"')
def generate(lb):
if not lb:
+ # Delete /run/load-balance/wlb.conf
+ if os.path.isfile(load_balancing_conf_file):
+ os.unlink(load_balancing_conf_file)
+ # Delete old directories
+ if os.path.isdir(load_balancing_dir):
+ rmtree(load_balancing_dir, ignore_errors=True)
+ if os.path.exists('/var/run/load-balance/wlb.out'):
+ os.unlink('/var/run/load-balance/wlb.out')
+
return None
+ # Create load-balance dir
+ if not os.path.isdir(load_balancing_dir):
+ os.mkdir(load_balancing_dir)
+
+ render(load_balancing_conf_file, 'load-balancing/wlb.conf.j2', lb)
+
return None
def apply(lb):
+ if not lb:
+ try:
+ cmd('sudo /opt/vyatta/sbin/vyatta-wanloadbalance.init stop')
+ except Exception as e:
+ print(f"Error message: {e}")
+
+ else:
+ cmd('sudo sysctl -w net.netfilter.nf_conntrack_acct=1')
+ cmd(f'sudo /opt/vyatta/sbin/vyatta-wanloadbalance.init restart {load_balancing_conf_file}')
return None
+
if __name__ == '__main__':
try:
c = get_config()