diff options
| author | Viacheslav Hletenko <v.gletenko@vyos.io> | 2026-01-09 14:55:52 +0200 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-01-09 14:55:52 +0200 |
| commit | 33f1763bf6434497ad0f7904122d5cb943d20138 (patch) | |
| tree | 8f221183536bdfc2879c74f1c81c82cde730295b | |
| parent | f80aae18439fe93a32101dfe45ef65812cf91e45 (diff) | |
| parent | a4b3cd33ca11aba579510456941f404839135af4 (diff) | |
| download | vyos-1x-33f1763bf6434497ad0f7904122d5cb943d20138.tar.gz vyos-1x-33f1763bf6434497ad0f7904122d5cb943d20138.zip | |
Merge pull request #4912 from hedrok/T8046-add-link-params-cli
T8046: traffic-engineering: support link-params
| -rw-r--r-- | data/templates/frr/isisd.frr.j2 | 3 | ||||
| -rw-r--r-- | data/templates/frr/zebra.traffic_engineering.frr.j2 | 37 | ||||
| -rw-r--r-- | interface-definitions/include/isis/protocol-common-config.xml.i | 6 | ||||
| -rw-r--r-- | interface-definitions/protocols_traffic_engineering.xml.in | 108 | ||||
| -rw-r--r-- | python/vyos/frrender.py | 47 | ||||
| -rwxr-xr-x | smoketest/scripts/cli/test_protocols_traffic-engineering.py | 123 | ||||
| -rwxr-xr-x | src/conf_mode/protocols_traffic_engineering.py | 97 |
7 files changed, 418 insertions, 3 deletions
diff --git a/data/templates/frr/isisd.frr.j2 b/data/templates/frr/isisd.frr.j2 index 6691517b4..a7e614e3e 100644 --- a/data/templates/frr/isisd.frr.j2 +++ b/data/templates/frr/isisd.frr.j2 @@ -153,6 +153,9 @@ advertise-passive-only {% endif %} mpls-te inter-as{{ level }} {% endif %} +{% if traffic_engineering.export is vyos_defined %} + mpls-te export +{% endif %} {% if segment_routing is vyos_defined %} {% if segment_routing.maximum_label_depth is vyos_defined %} segment-routing node-msd {{ segment_routing.maximum_label_depth }} diff --git a/data/templates/frr/zebra.traffic_engineering.frr.j2 b/data/templates/frr/zebra.traffic_engineering.frr.j2 new file mode 100644 index 000000000..fee41a898 --- /dev/null +++ b/data/templates/frr/zebra.traffic_engineering.frr.j2 @@ -0,0 +1,37 @@ +{% if interface is vyos_defined %} +! +{% set ns = namespace(mask_by_admin_group=dict()) %} +{% if admin_group is vyos_defined %} +{% for ag in admin_group %} +{% set _ = ns.mask_by_admin_group.update({ag: 1.__lshift__(admin_group[ag]['bit_position'] | int)}) %} +{% endfor %} +{% endif %} +{% set mbit_per_second_2_byte_per_second = 1024 * 1024 / 8 %} +{% for ifname, data in interface.items() %} +interface {{ ifname }} + link-params +{% if data.metric is vyos_defined %} + metric {{ data.metric }} +{% endif %} +{% if data.admin_group is vyos_defined %} +{% set val = namespace(x=0) %} +{% for ag in data.admin_group %} +{% set val.x = (val.x | int).__or__(ns.mask_by_admin_group[ag]) %} +{% endfor %} + admin-grp {{ '0x{:x}'.format(val.x) }} +{% endif %} +{% if data.max_bandwidth is vyos_defined %} + max-bw {{ (data.max_bandwidth | int) * mbit_per_second_2_byte_per_second }} +{% endif %} +{% if data.max_reservable_bandwidth is vyos_defined %} +{% set v = (data.max_reservable_bandwidth | int) * mbit_per_second_2_byte_per_second %} + max-rsv-bw {{ v }} +{% for i in range(8) %} + unrsv-bw {{ i }} {{ v }} +{% endfor %} +{% endif %} + exit-link-params +exit +! +{% endfor %} +{% endif %} diff --git a/interface-definitions/include/isis/protocol-common-config.xml.i b/interface-definitions/include/isis/protocol-common-config.xml.i index 459a048a2..359dbc3ce 100644 --- a/interface-definitions/include/isis/protocol-common-config.xml.i +++ b/interface-definitions/include/isis/protocol-common-config.xml.i @@ -266,6 +266,12 @@ </constraint> </properties> </leafNode> + <leafNode name="export"> + <properties> + <help>Export Traffic Engineering Database, see options under protocols traffic-engineering</help> + <valueless/> + </properties> + </leafNode> </children> </node> <node name="segment-routing"> diff --git a/interface-definitions/protocols_traffic_engineering.xml.in b/interface-definitions/protocols_traffic_engineering.xml.in new file mode 100644 index 000000000..32e1793b1 --- /dev/null +++ b/interface-definitions/protocols_traffic_engineering.xml.in @@ -0,0 +1,108 @@ +<?xml version="1.0" encoding="utf-8"?> +<interfaceDefinition> + <node name="protocols"> + <children> + <node name="traffic-engineering" owner="${vyos_conf_scripts_dir}/protocols_traffic_engineering.py"> + <properties> + <help>Traffic Engineering link parameters</help> + <priority>605</priority> + </properties> + <children> + <tagNode name="admin-group"> + <properties> + <help>Configure administrative groups that can be used in interface configuration</help> + <valueHelp> + <format>txt</format> + <description>Administrative group name</description> + </valueHelp> + <constraint> + <regex>[-a-zA-Z0-9]+</regex> + </constraint> + <constraintErrorMessage>Administrative group must be alphanumeric and can contain hyphens</constraintErrorMessage> + </properties> + <children> + <leafNode name="bit-position"> + <properties> + <help>Specify bit position of the admin group</help> + <valueHelp> + <format>u8:0-31</format> + <description>Admin group bit position</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 0-31"/> + </constraint> + </properties> + </leafNode> + </children> + </tagNode> + <tagNode name="interface"> + <properties> + <help>Traffic engineering parameters for interface</help> + <completionHelp> + <script>${vyos_completion_dir}/list_interfaces</script> + </completionHelp> + <valueHelp> + <format>txt</format> + <description>Interface to configure name</description> + </valueHelp> + <constraint> + #include <include/constraint/interface-name.xml.i> + </constraint> + </properties> + <children> + <leafNode name="metric"> + <properties> + <help>TE metric</help> + <valueHelp> + <format>u32:1-4294967295</format> + <description>TE Metric (different from the OSPF or ISIS metric)</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-4294967295"/> + </constraint> + </properties> + </leafNode> + <leafNode name="max-bandwidth"> + <properties> + <help>Maximum bandwidth (interface speed by default)</help> + <valueHelp> + <format>u32:1-4294967295</format> + <description>Maximum bandwidth in Mbits/sec</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-4294967295"/> + </constraint> + </properties> + </leafNode> + <leafNode name="max-reservable-bandwidth"> + <properties> + <help>Maximum reservable bandwidth</help> + <valueHelp> + <format>u32:1-4294967295</format> + <description>Maximum reservable bandwidth in Mbits/sec</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-4294967295"/> + </constraint> + </properties> + </leafNode> + <leafNode name="admin-group"> + <properties> + <help>Admin groups of interface</help> + <valueHelp> + <format>txt</format> + <description>Admin group of interface</description> + </valueHelp> + <completionHelp> + <path>protocols traffic-engineering admin-group</path> + </completionHelp> + <multi/> + </properties> + </leafNode> + </children> + </tagNode> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/python/vyos/frrender.py b/python/vyos/frrender.py index f79e6c0f0..1aed31f96 100644 --- a/python/vyos/frrender.py +++ b/python/vyos/frrender.py @@ -51,9 +51,26 @@ ERROR_RELOAD_TEST: str = 'The system encountered an error while rendering the ' 'new routing daemon configuration. To ensure network stability and avoid ' \ 'potential connectivity disruptions, the configuration was not applied!' -frr_protocols = ['babel', 'bfd', 'bgp', 'eigrp', 'isis', 'mpls', 'nhrp', - 'openfabric', 'ospf', 'ospfv3', 'pim', 'pim6', 'rip', - 'ripng', 'rpki', 'segment_routing', 'static'] +frr_protocols = [ + 'babel', + 'bfd', + 'bgp', + 'eigrp', + 'isis', + 'mpls', + 'nhrp', + 'openfabric', + 'ospf', + 'ospfv3', + 'pim', + 'pim6', + 'rip', + 'ripng', + 'rpki', + 'segment_routing', + 'static', + 'traffic_engineering', +] babel_daemon = 'babeld' bfd_daemon = 'bfdd' @@ -426,6 +443,21 @@ def get_frrender_dict(conf: Config, argv=None) -> dict: elif conf.exists_effective(sr_cli_path): dict.update({'segment_routing' : deleted_protocol}) + # We need to check the CLI if the Traffic Engineering node is present and thus load in + # all the default values present on the CLI - that's why we have if conf.exists() + te_cli_path = ['protocols', 'traffic-engineering'] + if conf.exists(te_cli_path): + te = conf.get_config_dict( + te_cli_path, + key_mangling=('-', '_'), + get_first_key=True, + no_tag_node_value_mangle=True, + with_recursive_defaults=True, + ) + dict.update({'traffic_engineering': te}) + elif conf.exists_effective(te_cli_path): + dict.update({'traffic_engineering': deleted_protocol}) + # We need to check the CLI if the static node is present and thus load in # all the default values present on the CLI - that's why we have if conf.exists() static_cli_path = ['protocols', 'static'] @@ -722,6 +754,15 @@ class FRRender: if 'segment_routing' in config_dict and 'deleted' not in config_dict['segment_routing']: output += render_to_string('frr/zebra.segment_routing.frr.j2', config_dict['segment_routing']) output += '\n' + if ( + 'traffic_engineering' in config_dict + and 'deleted' not in config_dict['traffic_engineering'] + ): + output += render_to_string( + 'frr/zebra.traffic_engineering.frr.j2', + config_dict['traffic_engineering'], + ) + output += '\n' if 'static' in config_dict and 'deleted' not in config_dict['static']: output += render_to_string('frr/staticd.frr.j2', config_dict['static']) output += '\n' diff --git a/smoketest/scripts/cli/test_protocols_traffic-engineering.py b/smoketest/scripts/cli/test_protocols_traffic-engineering.py new file mode 100755 index 000000000..e1e9d8475 --- /dev/null +++ b/smoketest/scripts/cli/test_protocols_traffic-engineering.py @@ -0,0 +1,123 @@ +#!/usr/bin/env python3 +# +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> +# +# 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 +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# 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 unittest + +from base_vyostest_shim import VyOSUnitTestSHIM + +from vyos.configsession import ConfigSessionError +from vyos.frrender import zebra_daemon +from vyos.utils.process import process_named_running + +base_path = ['protocols', 'traffic-engineering'] + +dummy_if1 = 'dum2191' +dummy_if2 = 'dum2192' + + +class TestProtocolsTrafficEngineering(VyOSUnitTestSHIM.TestCase): + @classmethod + def setUpClass(cls): + # call base-classes classmethod + super(TestProtocolsTrafficEngineering, cls).setUpClass() + # Retrieve FRR daemon PID - it is not allowed to crash, thus PID must remain the same + cls.daemon_pid = process_named_running(zebra_daemon) + # ensure we can also run this test on a live system - so lets clean + # out the current configuration :) + cls.cli_delete(cls, base_path) + + cls.cli_set(cls, ['interfaces', 'dummy', dummy_if1]) + cls.cli_set(cls, ['interfaces', 'dummy', dummy_if2]) + cls.cli_commit(cls) + + @classmethod + def tearDownClass(cls): + cls.cli_delete(cls, ['interfaces', 'dummy', dummy_if2]) + cls.cli_delete(cls, ['interfaces', 'dummy', dummy_if1]) + cls.cli_commit(cls) + + super(TestProtocolsTrafficEngineering, cls).tearDownClass() + + def tearDown(self): + self.cli_delete(base_path) + self.cli_commit() + + # check process health and continuity + self.assertEqual(self.daemon_pid, process_named_running(zebra_daemon)) + # always forward to base class + super().tearDown() + + def test_te_normal(self): + self.cli_set(base_path + ['admin-group', 'cyan', 'bit-position', '1']) + self.cli_set(base_path + ['admin-group', 'magenta', 'bit-position', '3']) + + self.cli_set(base_path + ['interface', dummy_if1, 'admin-group', 'magenta']) + self.cli_set(base_path + ['interface', dummy_if1, 'max-bandwidth', '1024']) + self.cli_set( + base_path + ['interface', dummy_if1, 'max-reservable-bandwidth', '2048'] + ) + self.cli_set(base_path + ['interface', dummy_if1, 'metric', '74837']) + + self.cli_set(base_path + ['interface', dummy_if2, 'admin-group', 'cyan']) + self.cli_set(base_path + ['interface', dummy_if2, 'admin-group', 'magenta']) + + self.cli_commit() + + frrconfig = self.getFRRconfig(f'^interface {dummy_if1}', stop_section='^exit') + self.assertIn('link-params', frrconfig) + self.assertIn('metric 74837', frrconfig) + self.assertIn('admin-grp 0x8', frrconfig) + self.assertIn('max-bw 1.34218e+08', frrconfig) + self.assertIn('max-rsv-bw 2.68435e+08', frrconfig) + self.assertIn('unrsv-bw 0 2.68435e+08', frrconfig) + self.assertIn('unrsv-bw 1 2.68435e+08', frrconfig) + self.assertIn('unrsv-bw 2 2.68435e+08', frrconfig) + self.assertIn('unrsv-bw 3 2.68435e+08', frrconfig) + self.assertIn('unrsv-bw 4 2.68435e+08', frrconfig) + self.assertIn('unrsv-bw 5 2.68435e+08', frrconfig) + self.assertIn('unrsv-bw 6 2.68435e+08', frrconfig) + self.assertIn('unrsv-bw 7 2.68435e+08', frrconfig) + + frrconfig = self.getFRRconfig(f'^interface {dummy_if2}', stop_section='^exit') + self.assertIn('link-params', frrconfig) + self.assertIn('admin-grp 0xa', frrconfig) + + def test_te_verify(self): + self.cli_set(base_path + ['interface', dummy_if1, 'admin-group', 'cyan']) + + # Unknown group + with self.assertRaises(ConfigSessionError): + self.cli_commit() + + self.cli_set(base_path + ['admin-group', 'cyan', 'bit-position', '0']) + self.cli_set(base_path + ['admin-group', 'magenta', 'bit-position', '4']) + + # Now group is known + self.cli_commit() + + self.cli_set(base_path + ['admin-group', 'red', 'bit-position', '4']) + + # Same bit position as other group + with self.assertRaises(ConfigSessionError): + self.cli_commit() + + self.cli_set(base_path + ['admin-group', 'red', 'bit-position', '2']) + # Now should be ok + self.cli_commit() + + +if __name__ == '__main__': + unittest.main(verbosity=2, failfast=VyOSUnitTestSHIM.TestCase.debug_on()) diff --git a/src/conf_mode/protocols_traffic_engineering.py b/src/conf_mode/protocols_traffic_engineering.py new file mode 100755 index 000000000..925585158 --- /dev/null +++ b/src/conf_mode/protocols_traffic_engineering.py @@ -0,0 +1,97 @@ +#!/usr/bin/env python3 +# +# Copyright VyOS maintainers and contributors <maintainers@vyos.io> +# +# 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 +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +from sys import exit + +from vyos.config import Config +from vyos.configverify import has_frr_protocol_in_dict +from vyos.frrender import FRRender +from vyos.frrender import get_frrender_dict +from vyos.utils.process import is_systemd_service_running +from vyos import ConfigError +from vyos import airbag + +airbag.enable() + + +def get_config(config=None): + if config: + conf = config + else: + conf = Config() + + return get_frrender_dict(conf) + + +def verify(config_dict): + if not has_frr_protocol_in_dict(config_dict, 'traffic_engineering'): + return None + + te = config_dict['traffic_engineering'] + + group_by_bit_position = {} + if 'admin_group' in te: + for admin_group, admin_group_data in te['admin_group'].items(): + if 'bit_position' not in admin_group_data: + raise ConfigError( + f'Missing required "bit-position" in group {admin_group}' + ) + if admin_group_data['bit_position'] in group_by_bit_position: + other = group_by_bit_position[admin_group_data['bit_position']] + raise ConfigError( + f'Two admin-groups cannot have same bit positions! Conflicting groups: {admin_group} and {other}' + ) + group_by_bit_position[admin_group_data['bit_position']] = admin_group + + all_groups = group_by_bit_position.values() + + if 'interface' in te: + for interface, interface_data in te['interface'].items(): + if 'admin_group' not in interface_data: + continue + for grp in interface_data['admin_group']: + if grp not in all_groups: + raise ConfigError( + f'Unknown admin-group "{grp}" set for interface "{interface}"' + ) + + return None + + +def generate(config_dict): + if config_dict and not is_systemd_service_running('vyos-configd.service'): + FRRender().generate(config_dict) + return None + + +def apply(config_dict): + if not has_frr_protocol_in_dict(config_dict, 'traffic_engineering'): + return None + + if config_dict and not is_systemd_service_running('vyos-configd.service'): + FRRender().apply() + return None + + +if __name__ == '__main__': + try: + c = get_config() + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + exit(1) |
