diff options
-rw-r--r-- | data/op-mode-standardized.json | 6 | ||||
-rw-r--r-- | interface-definitions/include/qos/hfsc-m1.xml.i | 2 | ||||
-rw-r--r-- | interface-definitions/include/qos/hfsc-m2.xml.i | 2 | ||||
-rw-r--r-- | op-mode-definitions/multicast-group.xml.in | 63 | ||||
-rw-r--r-- | op-mode-definitions/show-ip-multicast.xml.in | 3 | ||||
-rw-r--r-- | python/vyos/qos/trafficshaper.py | 89 | ||||
-rwxr-xr-x | smoketest/scripts/cli/test_interfaces_ethernet.py | 13 | ||||
-rwxr-xr-x | src/conf_mode/qos.py | 4 | ||||
-rwxr-xr-x | src/op_mode/multicast.py | 72 |
9 files changed, 237 insertions, 17 deletions
diff --git a/data/op-mode-standardized.json b/data/op-mode-standardized.json index dfb7b023f..d3685caaf 100644 --- a/data/op-mode-standardized.json +++ b/data/op-mode-standardized.json @@ -9,20 +9,22 @@ "dhcp.py", "dns.py", "interfaces.py", +"ipsec.py", "lldp.py", "log.py", "memory.py", +"multicast.py", "nat.py", "neighbor.py", "nhrp.py", "openconnect.py", "openvpn.py", +"otp.py", "reset_vpn.py", "reverseproxy.py", "route.py", -"system.py", -"ipsec.py", "storage.py", +"system.py", "uptime.py", "version.py", "vrf.py" diff --git a/interface-definitions/include/qos/hfsc-m1.xml.i b/interface-definitions/include/qos/hfsc-m1.xml.i index 677d817ba..21b9c4f32 100644 --- a/interface-definitions/include/qos/hfsc-m1.xml.i +++ b/interface-definitions/include/qos/hfsc-m1.xml.i @@ -27,6 +27,6 @@ <description>bps(8),kbps(8*10^3),mbps(8*10^6), gbps, tbps - Byte/sec</description> </valueHelp> </properties> - <defaultValue>100%%</defaultValue> + <defaultValue>0bit</defaultValue> </leafNode> <!-- include end --> diff --git a/interface-definitions/include/qos/hfsc-m2.xml.i b/interface-definitions/include/qos/hfsc-m2.xml.i index 7690df4b0..24e8f5d63 100644 --- a/interface-definitions/include/qos/hfsc-m2.xml.i +++ b/interface-definitions/include/qos/hfsc-m2.xml.i @@ -27,6 +27,6 @@ <description>bps(8),kbps(8*10^3),mbps(8*10^6), gbps, tbps - Byte/sec</description> </valueHelp> </properties> - <defaultValue>100%%</defaultValue> + <defaultValue>100%</defaultValue> </leafNode> <!-- include end --> diff --git a/op-mode-definitions/multicast-group.xml.in b/op-mode-definitions/multicast-group.xml.in new file mode 100644 index 000000000..39b4e347c --- /dev/null +++ b/op-mode-definitions/multicast-group.xml.in @@ -0,0 +1,63 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="show"> + <children> + <node name="ip"> + <children> + <node name="multicast"> + <properties> + <help>Show IP multicast</help> + </properties> + <children> + <node name="group"> + <properties> + <help>Show IP multicast group membership</help> + </properties> + <command>${vyos_op_scripts_dir}/multicast.py show_group --family inet</command> + <children> + <tagNode name="interface"> + <properties> + <help>Show IP multicast group membership of specific interface</help> + <completionHelp> + <script>${vyos_completion_dir}/list_interfaces</script> + </completionHelp> + </properties> + <command>${vyos_op_scripts_dir}/multicast.py show_group --family inet --interface "$6"</command> + </tagNode> + </children> + </node> + </children> + </node> + </children> + </node> + <node name="ipv6"> + <children> + <node name="multicast"> + <properties> + <help>Show IPv6 multicast</help> + </properties> + <children> + <node name="group"> + <properties> + <help>Show IPv6 multicast group membership</help> + </properties> + <command>${vyos_op_scripts_dir}/multicast.py show_group --family inet6</command> + <children> + <tagNode name="interface"> + <properties> + <help>Show IP multicast group membership of specific interface</help> + <completionHelp> + <script>${vyos_completion_dir}/list_interfaces</script> + </completionHelp> + </properties> + <command>${vyos_op_scripts_dir}/multicast.py show_group --family inet6 --interface "$6"</command> + </tagNode> + </children> + </node> + </children> + </node> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/op-mode-definitions/show-ip-multicast.xml.in b/op-mode-definitions/show-ip-multicast.xml.in index 605d61e8d..00a4704c7 100644 --- a/op-mode-definitions/show-ip-multicast.xml.in +++ b/op-mode-definitions/show-ip-multicast.xml.in @@ -5,9 +5,6 @@ <node name="ip"> <children> <node name="multicast"> - <properties> - <help>Show IP multicast</help> - </properties> <children> <leafNode name="interface"> <properties> diff --git a/python/vyos/qos/trafficshaper.py b/python/vyos/qos/trafficshaper.py index 0d5f9a8a1..1f3b03680 100644 --- a/python/vyos/qos/trafficshaper.py +++ b/python/vyos/qos/trafficshaper.py @@ -1,4 +1,4 @@ -# Copyright 2022-2023 VyOS maintainers and contributors <maintainers@vyos.io> +# Copyright 2022-2024 VyOS maintainers and contributors <maintainers@vyos.io> # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public @@ -117,8 +117,91 @@ class TrafficShaper(QoSBase): # call base class super().update(config, direction) -class TrafficShaperHFSC(TrafficShaper): +class TrafficShaperHFSC(QoSBase): + _parent = 1 + qostype = 'shaper_hfsc' + + # https://man7.org/linux/man-pages/man8/tc-hfsc.8.html def update(self, config, direction): + class_id_max = 0 + if 'class' in config: + tmp = list(config['class']) + tmp.sort() + class_id_max = tmp[-1] + + r2q = 10 + # bandwidth is a mandatory CLI node + speed = self._rate_convert(config['bandwidth']) + speed_bps = int(speed) // 8 + + # need a bigger r2q if going fast than 16 mbits/sec + if (speed_bps // r2q) >= MAXQUANTUM: # integer division + r2q = ceil(speed_bps // MAXQUANTUM) + else: + # if there is a slow class then may need smaller value + if 'class' in config: + min_speed = speed_bps + for cls, cls_options in config['class'].items(): + # find class with the lowest bandwidth used + if 'bandwidth' in cls_options: + bw_bps = int(self._rate_convert(cls_options['bandwidth'])) // 8 # bandwidth in bytes per second + if bw_bps < min_speed: + min_speed = bw_bps + + while (r2q > 1) and (min_speed // r2q) < MINQUANTUM: + tmp = r2q -1 + if (speed_bps // tmp) >= MAXQUANTUM: + break + r2q = tmp + + default_minor_id = int(class_id_max) +1 + tmp = f'tc qdisc replace dev {self._interface} root handle {self._parent:x}: hfsc default {default_minor_id:x}' # default is in hex + self._cmd(tmp) + + tmp = f'tc class replace dev {self._interface} parent {self._parent:x}: classid {self._parent:x}:1 hfsc sc rate {speed} ul rate {speed}' + self._cmd(tmp) + + if 'class' in config: + for cls, cls_config in config['class'].items(): + # class id is used later on and passed as hex, thus this needs to be an int + cls = int(cls) + # ls m1 + if cls_config.get('linkshare', {}).get('m1').endswith('%'): + percent = cls_config['linkshare']['m1'].rstrip('%') + m_one_rate = self._rate_convert(config['bandwidth']) * int(percent) // 100 + else: + m_one_rate = cls_config['linkshare']['m1'] + # ls m2 + if cls_config.get('linkshare', {}).get('m2').endswith('%'): + percent = cls_config['linkshare']['m2'].rstrip('%') + m_two_rate = self._rate_convert(config['bandwidth']) * int(percent) // 100 + else: + m_two_rate = self._rate_convert(cls_config['linkshare']['m2']) + + tmp = f'tc class replace dev {self._interface} parent {self._parent:x}:1 classid {self._parent:x}:{cls:x} hfsc ls m1 {m_one_rate} m2 {m_two_rate} ' + self._cmd(tmp) + + tmp = f'tc qdisc replace dev {self._interface} parent {self._parent:x}:{cls:x} sfq perturb 10' + self._cmd(tmp) + + if 'default' in config: + # ls m1 + if config.get('default', {}).get('linkshare', {}).get('m1').endswith('%'): + percent = config['default']['linkshare']['m1'].rstrip('%') + m_one_rate = self._rate_convert(config['default']['linkshare']['m1']) * int(percent) // 100 + else: + m_one_rate = config['default']['linkshare']['m1'] + # ls m2 + if config.get('default', {}).get('linkshare', {}).get('m2').endswith('%'): + percent = config['default']['linkshare']['m2'].rstrip('%') + m_two_rate = self._rate_convert(config['default']['linkshare']['m2']) * int(percent) // 100 + else: + m_two_rate = self._rate_convert(config['default']['linkshare']['m2']) + tmp = f'tc class replace dev {self._interface} parent {self._parent:x}:1 classid {self._parent:x}:{default_minor_id:x} hfsc ls m1 {m_one_rate} m2 {m_two_rate} ' + self._cmd(tmp) + + tmp = f'tc qdisc replace dev {self._interface} parent {self._parent:x}:{default_minor_id:x} sfq perturb 10' + self._cmd(tmp) + # call base class super().update(config, direction) - diff --git a/smoketest/scripts/cli/test_interfaces_ethernet.py b/smoketest/scripts/cli/test_interfaces_ethernet.py index a39b81348..e414f18cb 100755 --- a/smoketest/scripts/cli/test_interfaces_ethernet.py +++ b/smoketest/scripts/cli/test_interfaces_ethernet.py @@ -141,15 +141,18 @@ class EthernetInterfaceTest(BasicInterfaceTest.TestCase): # Verify that no address remains on the system as this is an eternal # interface. - for intf in self._interfaces: - self.assertNotIn(AF_INET, ifaddresses(intf)) + for interface in self._interfaces: + self.assertNotIn(AF_INET, ifaddresses(interface)) # required for IPv6 link-local address - self.assertIn(AF_INET6, ifaddresses(intf)) - for addr in ifaddresses(intf)[AF_INET6]: + self.assertIn(AF_INET6, ifaddresses(interface)) + for addr in ifaddresses(interface)[AF_INET6]: # checking link local addresses makes no sense if is_ipv6_link_local(addr['addr']): continue - self.assertFalse(is_intf_addr_assigned(intf, addr['addr'])) + self.assertFalse(is_intf_addr_assigned(interface, addr['addr'])) + # Ensure no VLAN interfaces are left behind + tmp = [x for x in Section.interfaces('ethernet') if x.startswith(f'{interface}.')] + self.assertListEqual(tmp, []) def test_offloading_rps(self): # enable RPS on all available CPUs, RPS works with a CPU bitmask, diff --git a/src/conf_mode/qos.py b/src/conf_mode/qos.py index ad4121a49..40d7a6c16 100755 --- a/src/conf_mode/qos.py +++ b/src/conf_mode/qos.py @@ -149,7 +149,7 @@ def verify(qos): if 'class' in policy_config: for cls, cls_config in policy_config['class'].items(): # bandwidth is not mandatory for priority-queue - that is why this is on the exception list - if 'bandwidth' not in cls_config and policy_type not in ['priority_queue', 'round_robin']: + if 'bandwidth' not in cls_config and policy_type not in ['priority_queue', 'round_robin', 'shaper_hfsc']: raise ConfigError(f'Bandwidth must be defined for policy "{policy}" class "{cls}"!') if 'match' in cls_config: for match, match_config in cls_config['match'].items(): @@ -173,7 +173,7 @@ def verify(qos): if 'default' not in policy_config: raise ConfigError(f'Policy {policy} misses "default" class!') if 'default' in policy_config: - if 'bandwidth' not in policy_config['default'] and policy_type not in ['priority_queue', 'round_robin']: + if 'bandwidth' not in policy_config['default'] and policy_type not in ['priority_queue', 'round_robin', 'shaper_hfsc']: raise ConfigError('Bandwidth not defined for default traffic!') # we should check interface ingress/egress configuration after verifying that diff --git a/src/op_mode/multicast.py b/src/op_mode/multicast.py new file mode 100755 index 000000000..0666f8af3 --- /dev/null +++ b/src/op_mode/multicast.py @@ -0,0 +1,72 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2024 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 +# 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 json +import sys +import typing + +from tabulate import tabulate +from vyos.utils.process import cmd + +import vyos.opmode + +ArgFamily = typing.Literal['inet', 'inet6'] + +def _get_raw_data(family, interface=None): + tmp = 'ip -4' + if family == 'inet6': + tmp = 'ip -6' + tmp = f'{tmp} -j maddr show' + if interface: + tmp = f'{tmp} dev {interface}' + output = cmd(tmp) + data = json.loads(output) + if not data: + return [] + return data + +def _get_formatted_output(raw_data): + data_entries = [] + + # sort result by interface name + for interface in sorted(raw_data, key=lambda x: x['ifname']): + for address in interface['maddr']: + tmp = [] + tmp.append(interface['ifname']) + tmp.append(address['family']) + tmp.append(address['address']) + + data_entries.append(tmp) + + headers = ["Interface", "Family", "Address"] + output = tabulate(data_entries, headers, numalign="left") + return output + +def show_group(raw: bool, family: ArgFamily, interface: typing.Optional[str]): + multicast_data = _get_raw_data(family=family, interface=interface) + if raw: + return multicast_data + else: + return _get_formatted_output(multicast_data) + +if __name__ == "__main__": + try: + res = vyos.opmode.run(sys.modules[__name__]) + if res: + print(res) + except (ValueError, vyos.opmode.Error) as e: + print(e) + sys.exit(1) |