summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorViacheslav Hletenko <v.gletenko@vyos.io>2026-01-09 14:55:52 +0200
committerGitHub <noreply@github.com>2026-01-09 14:55:52 +0200
commit33f1763bf6434497ad0f7904122d5cb943d20138 (patch)
tree8f221183536bdfc2879c74f1c81c82cde730295b
parentf80aae18439fe93a32101dfe45ef65812cf91e45 (diff)
parenta4b3cd33ca11aba579510456941f404839135af4 (diff)
downloadvyos-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.j23
-rw-r--r--data/templates/frr/zebra.traffic_engineering.frr.j237
-rw-r--r--interface-definitions/include/isis/protocol-common-config.xml.i6
-rw-r--r--interface-definitions/protocols_traffic_engineering.xml.in108
-rw-r--r--python/vyos/frrender.py47
-rwxr-xr-xsmoketest/scripts/cli/test_protocols_traffic-engineering.py123
-rwxr-xr-xsrc/conf_mode/protocols_traffic_engineering.py97
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)