From 8362b19bbfac3cc41d3fe5b85e43a0d9374b98a8 Mon Sep 17 00:00:00 2001
From: Viacheslav Hletenko <v.gletenko@vyos.io>
Date: Thu, 18 Jan 2024 19:26:18 +0000
Subject: T5958: QoS add basic implementation of policy shaper-hfsc

QoS policy shaper-hfsc was not implemented after rewriting the
traffic-policy to qos policy. We had CLI but it  does not use the
correct class. Add a basic implementation of policy shaper-hfsc.
Write the class `TrafficShaperHFS`

(cherry picked from commit f6b6ee636e34f98d336ee53599666afd1f395d78)
---
 interface-definitions/include/qos/hfsc-m1.xml.i |  2 +-
 interface-definitions/include/qos/hfsc-m2.xml.i |  2 +-
 python/vyos/qos/trafficshaper.py                | 89 ++++++++++++++++++++++++-
 src/conf_mode/qos.py                            |  4 +-
 4 files changed, 90 insertions(+), 7 deletions(-)

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/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/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
-- 
cgit v1.2.3