diff options
| author | Christian Breunig <christian@breunig.cc> | 2023-05-04 15:32:43 +0200 | 
|---|---|---|
| committer | GitHub <noreply@github.com> | 2023-05-04 15:32:43 +0200 | 
| commit | ba81c15185d7a59ab0ec6705b53b311b4dda721d (patch) | |
| tree | 669b9092b127dae24bbc28eb8ffa5c138bfba613 | |
| parent | d6364153436505ff6c25ce8b438ad204c927a1d1 (diff) | |
| parent | eac5dc2b1f7be06f0d7a4e35f8850a0c1f514fa0 (diff) | |
| download | vyos-1x-ba81c15185d7a59ab0ec6705b53b311b4dda721d.tar.gz vyos-1x-ba81c15185d7a59ab0ec6705b53b311b4dda721d.zip | |
Merge pull request #1973 from sever-sever/T5171
T5171: Use XML for loadbalancing wan instead of old templates
| -rw-r--r-- | Makefile | 3 | ||||
| -rw-r--r-- | data/templates/load-balancing/wlb.conf.j2 | 130 | ||||
| -rw-r--r-- | interface-definitions/load-balancing-wan.xml.in | 11 | ||||
| -rwxr-xr-x | src/conf_mode/load-balancing-wan.py | 131 | 
4 files changed, 264 insertions, 11 deletions
| @@ -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() | 
