diff options
| author | Christian Poessinger <christian@poessinger.com> | 2022-06-16 21:20:39 +0200 | 
|---|---|---|
| committer | Christian Poessinger <christian@poessinger.com> | 2023-01-01 08:14:31 +0100 | 
| commit | f89a6806d90fd11e0e1e5e922ef95332ad8bfeb8 (patch) | |
| tree | 3b99f40da30a23df0f9677974236127de38b44a5 | |
| parent | 38e9bcfd496c4203ab6b2551af1f9627a4119191 (diff) | |
| download | vyos-1x-f89a6806d90fd11e0e1e5e922ef95332ad8bfeb8.tar.gz vyos-1x-f89a6806d90fd11e0e1e5e922ef95332ad8bfeb8.zip | |
qos: T4284: first implementation introducing a new vyos.qos module
31 files changed, 2080 insertions, 296 deletions
| diff --git a/debian/control b/debian/control index 5d90b79e6..696f8902d 100644 --- a/debian/control +++ b/debian/control @@ -69,7 +69,7 @@ Depends:    ipaddrcheck,    iperf,    iperf3, -  iproute2, +  iproute2 (>= 6.0.0),    iputils-arping,    isc-dhcp-client,    isc-dhcp-relay, diff --git a/interface-definitions/include/interface/mirror.xml.i b/interface-definitions/include/interface/mirror.xml.i index 2959551f0..74a172b50 100644 --- a/interface-definitions/include/interface/mirror.xml.i +++ b/interface-definitions/include/interface/mirror.xml.i @@ -1,23 +1,31 @@  <!-- include start from interface/mirror.xml.i -->  <node name="mirror">    <properties> -    <help>Incoming/outgoing packet mirroring destination</help> +    <help>Mirror ingress/egress packets</help>    </properties>    <children>      <leafNode name="ingress">        <properties> -        <help>Mirror the ingress traffic of the interface to the destination interface</help> +        <help>Mirror ingress traffic to destination interface</help>          <completionHelp> -            <script>${vyos_completion_dir}/list_interfaces.py</script> +          <script>${vyos_completion_dir}/list_interfaces.py</script>          </completionHelp> +        <valueHelp> +          <format>txt</format> +          <description>Destination interface name</description> +        </valueHelp>        </properties>      </leafNode>      <leafNode name="egress">        <properties> -        <help>Mirror the egress traffic of the interface to the destination interface</help> +        <help>Mirror egress traffic to destination interface</help>          <completionHelp> -            <script>${vyos_completion_dir}/list_interfaces.py</script> +          <script>${vyos_completion_dir}/list_interfaces.py</script>          </completionHelp> +        <valueHelp> +          <format>txt</format> +          <description>Destination interface name</description> +        </valueHelp>        </properties>      </leafNode>    </children> diff --git a/interface-definitions/include/interface/redirect.xml.i b/interface-definitions/include/interface/redirect.xml.i index 8df8957ac..b01e486ce 100644 --- a/interface-definitions/include/interface/redirect.xml.i +++ b/interface-definitions/include/interface/redirect.xml.i @@ -1,13 +1,13 @@  <!-- include start from interface/redirect.xml.i -->  <leafNode name="redirect">    <properties> -    <help>Incoming packet redirection destination</help> +    <help>Redirect incoming packet to destination</help>      <completionHelp>        <script>${vyos_completion_dir}/list_interfaces.py</script>      </completionHelp>      <valueHelp>        <format>txt</format> -      <description>Interface name</description> +      <description>Destination interface name</description>      </valueHelp>      <constraint>        #include <include/constraint/interface-name.xml.in> diff --git a/interface-definitions/include/qos/bandwidth-auto.xml.i b/interface-definitions/include/qos/bandwidth-auto.xml.i new file mode 100644 index 000000000..3780b7444 --- /dev/null +++ b/interface-definitions/include/qos/bandwidth-auto.xml.i @@ -0,0 +1,43 @@ +<!-- include start from qos/bandwidth-auto.xml.i --> +<leafNode name="bandwidth"> +  <properties> +    <help>Available bandwidth for this policy</help> +    <completionHelp> +      <list>auto</list> +    </completionHelp> +    <valueHelp> +      <format>auto</format> +      <description>Rate matches interface speed</description> +    </valueHelp> +    <valueHelp> +      <format><number></format> +      <description>Bits per second</description> +    </valueHelp> +    <valueHelp> +      <format><number>bit</format> +      <description>Bits per second</description> +    </valueHelp> +    <valueHelp> +      <format><number>kbit</format> +      <description>Kilobits per second</description> +    </valueHelp> +    <valueHelp> +      <format><number>mbit</format> +      <description>Megabits per second</description> +    </valueHelp> +    <valueHelp> +      <format><number>gbit</format> +      <description>Gigabits per second</description> +    </valueHelp> +    <valueHelp> +      <format><number>tbit</format> +      <description>Terabits per second</description> +    </valueHelp> +    <constraint> +      <validator name="numeric" argument="--positive"/> +      <regex>\d+(bit|kbit|mbit|gbit|tbit)</regex> +    </constraint> +  </properties> +  <defaultValue>auto</defaultValue> +</leafNode> +<!-- include end --> diff --git a/interface-definitions/include/qos/bandwidth.xml.i b/interface-definitions/include/qos/bandwidth.xml.i index 82af22f42..62ea93b67 100644 --- a/interface-definitions/include/qos/bandwidth.xml.i +++ b/interface-definitions/include/qos/bandwidth.xml.i @@ -1,15 +1,35 @@  <!-- include start from qos/bandwidth.xml.i -->  <leafNode name="bandwidth">    <properties> -    <help>Traffic-limit used for this class</help> +    <help>Available bandwidth for this policy</help>      <valueHelp>        <format><number></format> -      <description>Rate in kbit (kilobit per second)</description> +      <description>Bits per second</description>      </valueHelp>      <valueHelp> -      <format><number><suffix></format> -      <description>Rate with scaling suffix (mbit, mbps, ...)</description> +      <format><number>bit</format> +      <description>Bits per second</description>      </valueHelp> +    <valueHelp> +      <format><number>kbit</format> +      <description>Kilobits per second</description> +    </valueHelp> +    <valueHelp> +      <format><number>mbit</format> +      <description>Megabits per second</description> +    </valueHelp> +    <valueHelp> +      <format><number>gbit</format> +      <description>Gigabits per second</description> +    </valueHelp> +    <valueHelp> +      <format><number>tbit</format> +      <description>Terabits per second</description> +    </valueHelp> +    <constraint> +      <validator name="numeric" argument="--positive"/> +      <regex>\d+(bit|kbit|mbit|gbit|tbit)</regex> +    </constraint>    </properties>  </leafNode>  <!-- include end --> diff --git a/interface-definitions/include/qos/class-match-ipv4-address.xml.i b/interface-definitions/include/qos/class-match-ipv4-address.xml.i new file mode 100644 index 000000000..8e84c988a --- /dev/null +++ b/interface-definitions/include/qos/class-match-ipv4-address.xml.i @@ -0,0 +1,19 @@ +<!-- include start from qos/class-match-ipv4-address.xml.i --> +<leafNode name="address"> +  <properties> +    <help>IPv4 destination address for this match</help> +    <valueHelp> +      <format>ipv4</format> +      <description>IPv4 address</description> +    </valueHelp> +    <valueHelp> +      <format>ipv4net</format> +      <description>IPv4 prefix</description> +    </valueHelp> +    <constraint> +      <validator name="ipv4-address"/> +      <validator name="ipv4-prefix"/> +    </constraint> +  </properties> +</leafNode> +<!-- include end --> diff --git a/interface-definitions/include/qos/class-match-ipv6-address.xml.i b/interface-definitions/include/qos/class-match-ipv6-address.xml.i new file mode 100644 index 000000000..fd7388127 --- /dev/null +++ b/interface-definitions/include/qos/class-match-ipv6-address.xml.i @@ -0,0 +1,14 @@ +<!-- include start from qos/class-match-ipv6-address.xml.i --> +<leafNode name="address"> +  <properties> +    <help>IPv6 destination address for this match</help> +    <valueHelp> +      <format>ipv6net</format> +      <description>IPv6 address and prefix length</description> +    </valueHelp> +    <constraint> +      <validator name="ipv6"/> +    </constraint> +  </properties> +</leafNode> +<!-- include end --> diff --git a/interface-definitions/include/qos/match.xml.i b/interface-definitions/include/qos/class-match.xml.i index 7d89e4460..d9c35731d 100644 --- a/interface-definitions/include/qos/match.xml.i +++ b/interface-definitions/include/qos/class-match.xml.i @@ -1,4 +1,4 @@ -<!-- include start from qos/match.xml.i --> +<!-- include start from qos/class-match.xml.i -->  <tagNode name="match">    <properties>      <help>Class matching rule name</help> @@ -99,22 +99,11 @@              <help>Match on destination port or address</help>            </properties>            <children> -            <leafNode name="address"> -              <properties> -                <help>IPv4 destination address for this match</help> -                <valueHelp> -                  <format>ipv4net</format> -                  <description>IPv4 address and prefix length</description> -                </valueHelp> -                <constraint> -                  <validator name="ipv4"/> -                </constraint> -              </properties> -            </leafNode> +            #include <include/qos/class-match-ipv4-address.xml.i>              #include <include/port-number.xml.i>            </children>          </node> -        #include <include/qos/dscp.xml.i> +        #include <include/qos/match-dscp.xml.i>          #include <include/qos/max-length.xml.i>          #include <include/ip-protocol.xml.i>          <node name="source"> @@ -122,18 +111,7 @@              <help>Match on source port or address</help>            </properties>            <children> -            <leafNode name="address"> -              <properties> -                <help>IPv4 source address for this match</help> -                <valueHelp> -                  <format>ipv4net</format> -                  <description>IPv4 address and prefix length</description> -                </valueHelp> -                <constraint> -                  <validator name="ipv4"/> -                </constraint> -              </properties> -            </leafNode> +            #include <include/qos/class-match-ipv4-address.xml.i>              #include <include/port-number.xml.i>            </children>          </node> @@ -150,22 +128,11 @@              <help>Match on destination port or address</help>            </properties>            <children> -            <leafNode name="address"> -              <properties> -                <help>IPv6 destination address for this match</help> -                <valueHelp> -                  <format>ipv6net</format> -                  <description>IPv6 address and prefix length</description> -                </valueHelp> -                <constraint> -                  <validator name="ipv6"/> -                </constraint> -              </properties> -            </leafNode> +            #include <include/qos/class-match-ipv6-address.xml.i>              #include <include/port-number.xml.i>            </children>          </node> -        #include <include/qos/dscp.xml.i> +        #include <include/qos/match-dscp.xml.i>          #include <include/qos/max-length.xml.i>          #include <include/ip-protocol.xml.i>          <node name="source"> @@ -173,18 +140,7 @@              <help>Match on source port or address</help>            </properties>            <children> -            <leafNode name="address"> -              <properties> -                <help>IPv6 source address for this match</help> -                <valueHelp> -                  <format>ipv6net</format> -                  <description>IPv6 address and prefix length</description> -                </valueHelp> -                <constraint> -                  <validator name="ipv6"/> -                </constraint> -              </properties> -            </leafNode> +            #include <include/qos/class-match-ipv6-address.xml.i>              #include <include/port-number.xml.i>            </children>          </node> diff --git a/interface-definitions/include/qos/limiter-actions.xml.i b/interface-definitions/include/qos/class-police-exceed.xml.i index a993423aa..ee2ce16a8 100644 --- a/interface-definitions/include/qos/limiter-actions.xml.i +++ b/interface-definitions/include/qos/class-police-exceed.xml.i @@ -1,13 +1,13 @@ -<!-- include start from qos/limiter-actions.xml.i --> -<leafNode name="exceed-action"> +<!-- include start from qos/police.xml.i --> +<leafNode name="exceed">    <properties> -    <help>Default action for packets exceeding the limiter (default: drop)</help> +    <help>Default action for packets exceeding the limiter</help>      <completionHelp>        <list>continue drop ok reclassify pipe</list>      </completionHelp>      <valueHelp>        <format>continue</format> -      <description>Don't do anything, just continue with the next action in line</description> +      <description>Do not do anything, just continue with the next action in line</description>      </valueHelp>      <valueHelp>        <format>drop</format> @@ -31,15 +31,15 @@    </properties>    <defaultValue>drop</defaultValue>  </leafNode> -<leafNode name="notexceed-action"> +<leafNode name="not-exceed">    <properties> -    <help>Default action for packets not exceeding the limiter (default: ok)</help> +    <help>Default action for packets not exceeding the limiter</help>      <completionHelp>        <list>continue drop ok reclassify pipe</list>      </completionHelp>      <valueHelp>        <format>continue</format> -      <description>Don't do anything, just continue with the next action in line</description> +      <description>Do not do anything, just continue with the next action in line</description>      </valueHelp>      <valueHelp>        <format>drop</format> diff --git a/interface-definitions/include/qos/class-priority.xml.i b/interface-definitions/include/qos/class-priority.xml.i new file mode 100644 index 000000000..3fd848c93 --- /dev/null +++ b/interface-definitions/include/qos/class-priority.xml.i @@ -0,0 +1,15 @@ +<!-- include start from qos/class-priority.xml.i --> +<leafNode name="priority"> +  <properties> +    <help>Priority for rule evaluation</help> +    <valueHelp> +      <format>u32:0-20</format> +      <description>Priority for match rule evaluation</description> +    </valueHelp> +    <constraint> +      <validator name="numeric" argument="--range 0-20"/> +    </constraint> +    <constraintErrorMessage>Priority must be between 0 and 20</constraintErrorMessage> +  </properties> +</leafNode> +<!-- include end --> diff --git a/interface-definitions/include/qos/dscp.xml.i b/interface-definitions/include/qos/match-dscp.xml.i index bb90850ac..1323fc033 100644 --- a/interface-definitions/include/qos/dscp.xml.i +++ b/interface-definitions/include/qos/match-dscp.xml.i @@ -1,9 +1,9 @@ -<!-- include start from qos/dscp.xml.i --> +<!-- include start from qos/match-dscp.xml.i -->  <leafNode name="dscp">    <properties>      <help>Match on Differentiated Services Codepoint (DSCP)</help>      <completionHelp> -      <list>default reliability throughput lowdelay priority immediate flash flash-override critical internet network AF11 AF12 AF13 AF21 AF22 AF23 AF31 AF32 AF33 AF41 AF42 AF43 CS1 CS2 CS3 CS4 CS5 CS6 CS7 EF</list> +      <list>default reliability throughput lowdelay priority immediate flash flash-override critical internet network af11 af12 af13 af21 af22 af23 af31 af32 af33 af41 af42 af43 cs1 cs2 cs3 cs4 cs5 cs6 cs7 ef</list>      </completionHelp>      <valueHelp>        <format>u32:0-63</format> @@ -54,90 +54,89 @@        <description>match DSCP (111000)</description>      </valueHelp>      <valueHelp> -      <format>AF11</format> +      <format>af11</format>        <description>High-throughput data</description>      </valueHelp>      <valueHelp> -      <format>AF12</format> +      <format>af12</format>        <description>High-throughput data</description>      </valueHelp>      <valueHelp> -      <format>AF13</format> +      <format>af13</format>        <description>High-throughput data</description>      </valueHelp>      <valueHelp> -      <format>AF21</format> +      <format>af21</format>        <description>Low-latency data</description>      </valueHelp>      <valueHelp> -      <format>AF22</format> +      <format>af22</format>        <description>Low-latency data</description>      </valueHelp>      <valueHelp> -      <format>AF23</format> +      <format>af23</format>        <description>Low-latency data</description>      </valueHelp>      <valueHelp> -      <format>AF31</format> +      <format>af31</format>        <description>Multimedia streaming</description>      </valueHelp>      <valueHelp> -      <format>AF32</format> +      <format>af32</format>        <description>Multimedia streaming</description>      </valueHelp>      <valueHelp> -      <format>AF33</format> +      <format>af33</format>        <description>Multimedia streaming</description>      </valueHelp>      <valueHelp> -      <format>AF41</format> +      <format>af41</format>        <description>Multimedia conferencing</description>      </valueHelp>      <valueHelp> -      <format>AF42</format> +      <format>af42</format>        <description>Multimedia conferencing</description>      </valueHelp>      <valueHelp> -      <format>AF43</format> +      <format>af43</format>        <description>Multimedia conferencing</description>      </valueHelp>      <valueHelp> -      <format>CS1</format> +      <format>cs1</format>        <description>Low-priority data</description>      </valueHelp>      <valueHelp> -      <format>CS2</format> +      <format>cs2</format>        <description>OAM</description>      </valueHelp>      <valueHelp> -      <format>CS3</format> +      <format>cs3</format>        <description>Broadcast video</description>      </valueHelp>      <valueHelp> -      <format>CS4</format> +      <format>cs4</format>        <description>Real-time interactive</description>      </valueHelp>      <valueHelp> -      <format>CS5</format> +      <format>cs5</format>        <description>Signaling</description>      </valueHelp>      <valueHelp> -      <format>CS6</format> +      <format>cs6</format>        <description>Network control</description>      </valueHelp>      <valueHelp> -      <format>CS7</format> +      <format>cs7</format>        <description></description>      </valueHelp>      <valueHelp> -      <format>EF</format> +      <format>ef</format>        <description>Expedited Forwarding</description>      </valueHelp>      <constraint>        <validator name="numeric" argument="--range 0-63"/> -      <regex>(default|reliability|throughput|lowdelay|priority|immediate|flash|flash-override|critical|internet|network|AF11|AF12|AF13|AF21|AF22|AF23|AF31|AF32|AF33|AF41|AF42|AF43|CS1|CS2|CS3|CS4|CS5|CS6|CS7|EF)</regex> +      <regex>(default|reliability|throughput|lowdelay|priority|immediate|flash|flash-override|critical|internet|network|af11|af12|af13|af21|af22|af23|af31|af32|af33|af41|af42|af43|cs1|cs2|cs3|cs4|cs5|cs6|cs7|ef)</regex>      </constraint> -    <constraintErrorMessage>Priority must be between 0 and 63</constraintErrorMessage>    </properties>  </leafNode>  <!-- include end --> diff --git a/interface-definitions/include/qos/max-length.xml.i b/interface-definitions/include/qos/max-length.xml.i index 4cc20f8c4..64cdd02ec 100644 --- a/interface-definitions/include/qos/max-length.xml.i +++ b/interface-definitions/include/qos/max-length.xml.i @@ -1,15 +1,15 @@  <!-- include start from qos/max-length.xml.i -->  <leafNode name="max-length">    <properties> -    <help>Maximum packet length (ipv4)</help> +    <help>Maximum packet length</help>      <valueHelp> -      <format>u32:0-65535</format> +      <format>u32:1-65535</format>        <description>Maximum packet/payload length</description>      </valueHelp>      <constraint> -      <validator name="numeric" argument="--range 0-65535"/> +      <validator name="numeric" argument="--range 1-65535"/>      </constraint> -    <constraintErrorMessage>Maximum IPv4 total packet length is 65535</constraintErrorMessage> +    <constraintErrorMessage>Maximum packet length is 65535</constraintErrorMessage>    </properties>  </leafNode>  <!-- include end --> diff --git a/interface-definitions/include/qos/queue-type.xml.i b/interface-definitions/include/qos/queue-type.xml.i index 634f61024..c7d4cde82 100644 --- a/interface-definitions/include/qos/queue-type.xml.i +++ b/interface-definitions/include/qos/queue-type.xml.i @@ -3,28 +3,31 @@    <properties>      <help>Queue type for default traffic</help>      <completionHelp> -      <list>fq-codel fair-queue drop-tail random-detect</list> +      <list>drop-tail fair-queue fq-codel priority random-detect</list>      </completionHelp>      <valueHelp> -      <format>fq-codel</format> -      <description>Fair Queue Codel</description> +      <format>drop-tail</format> +      <description>First-In-First-Out (FIFO)</description>      </valueHelp>      <valueHelp>        <format>fair-queue</format>        <description>Stochastic Fair Queue (SFQ)</description>      </valueHelp>      <valueHelp> -      <format>drop-tail</format> -      <description>First-In-First-Out (FIFO)</description> +      <format>fq-codel</format> +      <description>Fair Queue Codel</description> +    </valueHelp> +    <valueHelp> +      <format>priority</format> +      <description>Priority queuing</description>      </valueHelp>      <valueHelp>        <format>random-detect</format>        <description>Random Early Detection (RED)</description>      </valueHelp>      <constraint> -      <regex>(fq-codel|fair-queue|drop-tail|random-detect)</regex> +      <regex>(drop-tail|fair-queue|fq-codel|priority|random-detect)</regex>      </constraint>    </properties> -  <defaultValue>drop-tail</defaultValue>  </leafNode>  <!-- include end --> diff --git a/interface-definitions/include/version/qos-version.xml.i b/interface-definitions/include/version/qos-version.xml.i index e4d139349..c67e61e91 100644 --- a/interface-definitions/include/version/qos-version.xml.i +++ b/interface-definitions/include/version/qos-version.xml.i @@ -1,3 +1,3 @@  <!-- include start from include/version/qos-version.xml.i --> -<syntaxVersion component='qos' version='1'></syntaxVersion> +<syntaxVersion component='qos' version='2'></syntaxVersion>  <!-- include end --> diff --git a/interface-definitions/qos.xml.in b/interface-definitions/qos.xml.in index 546c138c6..c243ad8fe 100644 --- a/interface-definitions/qos.xml.in +++ b/interface-definitions/qos.xml.in @@ -3,6 +3,7 @@    <node name="qos" owner="${vyos_conf_scripts_dir}/qos.py">      <properties>        <help>Quality of Service (QoS)</help> +      <priority>900</priority>      </properties>      <children>        <tagNode name="interface"> @@ -24,17 +25,7 @@              <properties>                <help>Interface ingress traffic policy</help>                <completionHelp> -                <path>qos policy drop-tail</path> -                <path>qos policy fair-queue</path> -                <path>qos policy fq-codel</path>                  <path>qos policy limiter</path> -                <path>qos policy network-emulator</path> -                <path>qos policy priority-queue</path> -                <path>qos policy random-detect</path> -                <path>qos policy rate-control</path> -                <path>qos policy round-robin</path> -                <path>qos policy shaper</path> -                <path>qos policy shaper-hfsc</path>                </completionHelp>                <valueHelp>                  <format>txt</format> @@ -46,10 +37,10 @@              <properties>                <help>Interface egress traffic policy</help>                <completionHelp> +                <path>qos policy cake</path>                  <path>qos policy drop-tail</path>                  <path>qos policy fair-queue</path>                  <path>qos policy fq-codel</path> -                <path>qos policy limiter</path>                  <path>qos policy network-emulator</path>                  <path>qos policy priority-queue</path>                  <path>qos policy random-detect</path> @@ -66,12 +57,97 @@            </leafNode>          </children>        </tagNode> -      <node name="policy" owner="${vyos_conf_scripts_dir}/qos.py"> +      <node name="policy">          <properties>            <help>Service Policy definitions</help> -          <priority>900</priority>          </properties>          <children> +          <tagNode name="cake"> +            <properties> +              <help>Common Applications Kept Enhanced (CAKE)</help> +              <valueHelp> +                <format>txt</format> +                <description>Policy name</description> +              </valueHelp> +              <constraint> +                <regex>[[:alnum:]][-_[:alnum:]]*</regex> +              </constraint> +              <constraintErrorMessage>Only alpha-numeric policy name allowed</constraintErrorMessage> +            </properties> +            <children> +              #include <include/generic-description.xml.i> +              #include <include/qos/bandwidth.xml.i> +              <node name="flow-isolation"> +                <properties> +                  <help>Flow isolation settings</help> +                </properties> +                <children> +                  <leafNode name="blind"> +                    <properties> +                      <help>Disables flow isolation, all traffic passes through a single queue</help> +                      <valueless/> +                    </properties> +                  </leafNode> +                  <leafNode name="src-host"> +                    <properties> +                      <help>Flows are defined only by source address</help> +                      <valueless/> +                    </properties> +                  </leafNode> +                  <leafNode name="dst-host"> +                    <properties> +                      <help>Flows are defined only by destination address</help> +                      <valueless/> +                    </properties> +                  </leafNode> +                  <leafNode name="host"> +                    <properties> +                      <help>Flows are defined by source-destination host pairs</help> +                      <valueless/> +                    </properties> +                  </leafNode> +                  <leafNode name="flow"> +                    <properties> +                      <help>Flows are defined by the entire 5-tuple</help> +                      <valueless/> +                    </properties> +                  </leafNode> +                  <leafNode name="dual-src-host"> +                    <properties> +                      <help>Flows are defined by the 5-tuple, and fairness is applied first over source addresses, then over individual flows</help> +                      <valueless/> +                    </properties> +                  </leafNode> +                  <leafNode name="dual-dst-host"> +                    <properties> +                      <help>Flows are defined by the 5-tuple, and fairness is applied first over destination addresses, then over individual flows</help> +                      <valueless/> +                    </properties> +                  </leafNode> +                  <leafNode name="nat"> +                    <properties> +                      <help>Perform NAT lookup before applying flow-isolation rules</help> +                      <valueless/> +                    </properties> +                  </leafNode> +                </children> +              </node> +              <leafNode name="rtt"> +                <properties> +                  <help>Round-Trip-Time for Active Queue Management (AQM)</help> +                  <valueHelp> +                    <format>u32:1-3600000</format> +                    <description>RTT in ms</description> +                  </valueHelp> +                  <constraint> +                    <validator name="numeric" argument="--range 1-3600000"/> +                  </constraint> +                  <constraintErrorMessage>RTT must be in range 1 to 3600000 milli-seconds</constraintErrorMessage> +                </properties> +                <defaultValue>100</defaultValue> +              </leafNode> +            </children> +          </tagNode>            <tagNode name="drop-tail">              <properties>                <help>Packet limited First In, First Out queue</help> @@ -171,6 +247,7 @@                <constraintErrorMessage>Only alpha-numeric policy name allowed</constraintErrorMessage>              </properties>              <children> +              #include <include/generic-description.xml.i>                <tagNode name="class">                  <properties>                    <help>Class ID</help> @@ -184,23 +261,13 @@                    <constraintErrorMessage>Class identifier must be between 1 and 4090</constraintErrorMessage>                  </properties>                  <children> +                  #include <include/generic-description.xml.i>                    #include <include/qos/bandwidth.xml.i>                    #include <include/qos/burst.xml.i> -                  #include <include/generic-description.xml.i> -                  #include <include/qos/match.xml.i> -                  #include <include/qos/limiter-actions.xml.i> +                  #include <include/qos/class-police-exceed.xml.i> +                  #include <include/qos/class-match.xml.i> +                  #include <include/qos/class-priority.xml.i>                    <leafNode name="priority"> -                    <properties> -                      <help>Priority for rule evaluation</help> -                      <valueHelp> -                        <format>u32:0-20</format> -                        <description>Priority for match rule evaluation</description> -                      </valueHelp> -                      <constraint> -                        <validator name="numeric" argument="--range 0-20"/> -                      </constraint> -                      <constraintErrorMessage>Priority must be between 0 and 20</constraintErrorMessage> -                    </properties>                      <defaultValue>20</defaultValue>                    </leafNode>                  </children> @@ -212,10 +279,9 @@                  <children>                    #include <include/qos/bandwidth.xml.i>                    #include <include/qos/burst.xml.i> -                  #include <include/qos/limiter-actions.xml.i> +                  #include <include/qos/class-police-exceed.xml.i>                  </children>                </node> -              #include <include/generic-description.xml.i>              </children>            </tagNode>            <tagNode name="network-emulator"> @@ -231,10 +297,9 @@                <constraintErrorMessage>Only alpha-numeric policy name allowed</constraintErrorMessage>              </properties>              <children> -              #include <include/qos/bandwidth.xml.i> -              #include <include/qos/burst.xml.i>                #include <include/generic-description.xml.i> -              <leafNode name="network-delay"> +              #include <include/qos/bandwidth.xml.i> +              <leafNode name="delay">                  <properties>                    <help>Adds delay to packets outgoing to chosen network interface</help>                    <valueHelp> @@ -247,7 +312,7 @@                    <constraintErrorMessage>Priority must be between 0 and 65535</constraintErrorMessage>                  </properties>                </leafNode> -              <leafNode name="packet-corruption"> +              <leafNode name="corruption">                  <properties>                    <help>Introducing error in a random position for chosen percent of packets</help>                    <valueHelp> @@ -260,9 +325,9 @@                    <constraintErrorMessage>Priority must be between 0 and 100</constraintErrorMessage>                  </properties>                </leafNode> -              <leafNode name="packet-loss"> +              <leafNode name="duplicate">                  <properties> -                  <help>Add independent loss probability to the packets outgoing to chosen network interface</help> +                  <help>Cosen percent of packets is duplicated before queuing them</help>                    <valueHelp>                      <format><number></format>                      <description>Percentage of packets affected</description> @@ -270,10 +335,10 @@                    <constraint>                      <validator name="numeric" argument="--range 0-100"/>                    </constraint> -                  <constraintErrorMessage>Must be between 0 and 100</constraintErrorMessage> +                  <constraintErrorMessage>Priority must be between 0 and 100</constraintErrorMessage>                  </properties>                </leafNode> -              <leafNode name="packet-loss"> +              <leafNode name="loss">                  <properties>                    <help>Add independent loss probability to the packets outgoing to chosen network interface</help>                    <valueHelp> @@ -286,9 +351,9 @@                    <constraintErrorMessage>Must be between 0 and 100</constraintErrorMessage>                  </properties>                </leafNode> -              <leafNode name="packet-loss"> +              <leafNode name="reordering">                  <properties> -                  <help>Packet reordering percentage</help> +                  <help>Emulated packet reordering percentage</help>                    <valueHelp>                      <format><number></format>                      <description>Percentage of packets affected</description> @@ -315,6 +380,7 @@                <constraintErrorMessage>Only alpha-numeric policy name allowed</constraintErrorMessage>              </properties>              <children> +              #include <include/generic-description.xml.i>                <tagNode name="class">                  <properties>                    <help>Class Handle</help> @@ -332,10 +398,13 @@                    #include <include/qos/codel-quantum.xml.i>                    #include <include/qos/flows.xml.i>                    #include <include/qos/interval.xml.i> -                  #include <include/qos/match.xml.i> -                  #include <include/qos/queue-limit-2-10999.xml.i> -                  #include <include/qos/target.xml.i> +                  #include <include/qos/class-match.xml.i> +                  #include <include/qos/queue-limit-1-4294967295.xml.i>                    #include <include/qos/queue-type.xml.i> +                  <leafNode name="queue-type"> +                    <defaultValue>drop-tail</defaultValue> +                  </leafNode> +                  #include <include/qos/target.xml.i>                  </children>                </tagNode>                <node name="default"> @@ -343,16 +412,17 @@                    <help>Default policy</help>                  </properties>                  <children> -                  #include <include/generic-description.xml.i>                    #include <include/qos/codel-quantum.xml.i>                    #include <include/qos/flows.xml.i>                    #include <include/qos/interval.xml.i> -                  #include <include/qos/queue-limit-2-10999.xml.i> -                  #include <include/qos/target.xml.i> +                  #include <include/qos/queue-limit-1-4294967295.xml.i>                    #include <include/qos/queue-type.xml.i> +                  <leafNode name="queue-type"> +                    <defaultValue>drop-tail</defaultValue> +                  </leafNode> +                  #include <include/qos/target.xml.i>                  </children>                </node> -              #include <include/generic-description.xml.i>              </children>            </tagNode>            <tagNode name="random-detect"> @@ -368,11 +438,8 @@                <constraintErrorMessage>Only alpha-numeric policy name allowed</constraintErrorMessage>              </properties>              <children> -              #include <include/qos/bandwidth.xml.i> -              <leafNode name="bandwidth"> -                <defaultValue>auto</defaultValue> -              </leafNode>                #include <include/generic-description.xml.i> +              #include <include/qos/bandwidth-auto.xml.i>                <tagNode name="precedence">                  <properties>                    <help>IP precedence</help> @@ -413,6 +480,7 @@                        </constraint>                        <constraintErrorMessage>Mark probability must be greater than 0</constraintErrorMessage>                      </properties> +                    <defaultValue>10</defaultValue>                    </leafNode>                    <leafNode name="maximum-threshold">                      <properties> @@ -426,6 +494,7 @@                        </constraint>                        <constraintErrorMessage>Threshold must be between 0 and 4096</constraintErrorMessage>                      </properties> +                    <defaultValue>18</defaultValue>                    </leafNode>                    <leafNode name="minimum-threshold">                      <properties> @@ -457,8 +526,8 @@                <constraintErrorMessage>Only alpha-numeric policy name allowed</constraintErrorMessage>              </properties>              <children> -              #include <include/qos/bandwidth.xml.i>                #include <include/generic-description.xml.i> +              #include <include/qos/bandwidth.xml.i>                #include <include/qos/burst.xml.i>                <leafNode name="latency">                  <properties> @@ -478,7 +547,7 @@            </tagNode>            <tagNode name="round-robin">              <properties> -              <help>Round-Robin based policy</help> +              <help>Deficit Round Robin Scheduler</help>                <valueHelp>                  <format>txt</format>                  <description>Policy name</description> @@ -503,11 +572,11 @@                    <constraintErrorMessage>Class identifier must be between 1 and 4095</constraintErrorMessage>                  </properties>                  <children> -                  #include <include/qos/codel-quantum.xml.i>                    #include <include/generic-description.xml.i> +                  #include <include/qos/codel-quantum.xml.i>                    #include <include/qos/flows.xml.i>                    #include <include/qos/interval.xml.i> -                  #include <include/qos/match.xml.i> +                  #include <include/qos/class-match.xml.i>                    <leafNode name="quantum">                      <properties>                        <help>Packet scheduling quantum</help> @@ -523,111 +592,26 @@                    </leafNode>                    #include <include/qos/queue-limit-1-4294967295.xml.i>                    #include <include/qos/queue-type.xml.i> +                  <leafNode name="queue-type"> +                    <defaultValue>drop-tail</defaultValue> +                  </leafNode>                    #include <include/qos/target.xml.i>                  </children>                </tagNode> -            </children> -          </tagNode> -          <tagNode name="shaper-hfsc"> -            <properties> -              <help>Hierarchical Fair Service Curve's policy</help> -              <valueHelp> -                <format>txt</format> -                <description>Policy name</description> -              </valueHelp> -              <constraint> -                <regex>[[:alnum:]][-_[:alnum:]]*</regex> -              </constraint> -              <constraintErrorMessage>Only alpha-numeric policy name allowed</constraintErrorMessage> -            </properties> -            <children> -              #include <include/qos/bandwidth.xml.i> -              <leafNode name="bandwidth"> -                <defaultValue>auto</defaultValue> -              </leafNode> -              #include <include/generic-description.xml.i> -              <tagNode name="class"> -                <properties> -                  <help>Class ID</help> -                  <valueHelp> -                    <format>u32:1-4095</format> -                    <description>Class Identifier</description> -                  </valueHelp> -                  <constraint> -                    <validator name="numeric" argument="--range 1-4095"/> -                  </constraint> -                  <constraintErrorMessage>Class identifier must be between 1 and 4095</constraintErrorMessage> -                </properties> -                <children> -                  #include <include/generic-description.xml.i> -                  <node name="linkshare"> -                    <properties> -                      <help>Linkshare class settings</help> -                    </properties> -                    <children> -                      #include <include/qos/hfsc-d.xml.i> -                      #include <include/qos/hfsc-m1.xml.i> -                      #include <include/qos/hfsc-m2.xml.i> -                    </children> -                  </node> -                  #include <include/qos/match.xml.i> -                  <node name="realtime"> -                    <properties> -                      <help>Realtime class settings</help> -                    </properties> -                    <children> -                      #include <include/qos/hfsc-d.xml.i> -                      #include <include/qos/hfsc-m1.xml.i> -                      #include <include/qos/hfsc-m2.xml.i> -                    </children> -                  </node> -                  <node name="upperlimit"> -                    <properties> -                      <help>Upperlimit class settings</help> -                    </properties> -                    <children> -                      #include <include/qos/hfsc-d.xml.i> -                      #include <include/qos/hfsc-m1.xml.i> -                      #include <include/qos/hfsc-m2.xml.i> -                    </children> -                  </node> -                </children> -              </tagNode>                <node name="default">                  <properties>                    <help>Default policy</help>                  </properties>                  <children> -                  <node name="linkshare"> -                    <properties> -                      <help>Linkshare class settings</help> -                    </properties> -                    <children> -                      #include <include/qos/hfsc-d.xml.i> -                      #include <include/qos/hfsc-m1.xml.i> -                      #include <include/qos/hfsc-m2.xml.i> -                    </children> -                  </node> -                  <node name="realtime"> -                    <properties> -                      <help>Realtime class settings</help> -                    </properties> -                    <children> -                      #include <include/qos/hfsc-d.xml.i> -                      #include <include/qos/hfsc-m1.xml.i> -                      #include <include/qos/hfsc-m2.xml.i> -                    </children> -                  </node> -                  <node name="upperlimit"> -                    <properties> -                      <help>Upperlimit class settings</help> -                    </properties> -                    <children> -                      #include <include/qos/hfsc-d.xml.i> -                      #include <include/qos/hfsc-m1.xml.i> -                      #include <include/qos/hfsc-m2.xml.i> -                    </children> -                  </node> +                  #include <include/qos/codel-quantum.xml.i> +                  #include <include/qos/flows.xml.i> +                  #include <include/qos/interval.xml.i> +                  #include <include/qos/queue-limit-1-4294967295.xml.i> +                  #include <include/qos/queue-type.xml.i> +                  <leafNode name="queue-type"> +                    <defaultValue>fair-queue</defaultValue> +                  </leafNode> +                  #include <include/qos/target.xml.i>                  </children>                </node>              </children> @@ -645,10 +629,8 @@                <constraintErrorMessage>Only alpha-numeric policy name allowed</constraintErrorMessage>              </properties>              <children> -              #include <include/qos/bandwidth.xml.i> -              <leafNode name="bandwidth"> -                <defaultValue>auto</defaultValue> -              </leafNode> +              #include <include/generic-description.xml.i> +              #include <include/qos/bandwidth-auto.xml.i>                <tagNode name="class">                  <properties>                    <help>Class ID</help> @@ -662,10 +644,8 @@                    <constraintErrorMessage>Class identifier must be between 2 and 4095</constraintErrorMessage>                  </properties>                  <children> -                  #include <include/qos/bandwidth.xml.i> -                  <leafNode name="bandwidth"> -                    <defaultValue>100%</defaultValue> -                  </leafNode> +                  #include <include/generic-description.xml.i> +                  #include <include/qos/bandwidth-auto.xml.i>                    #include <include/qos/burst.xml.i>                    <leafNode name="ceiling">                      <properties> @@ -697,31 +677,19 @@                      </properties>                    </leafNode>                    #include <include/qos/codel-quantum.xml.i> -                  #include <include/generic-description.xml.i>                    #include <include/qos/flows.xml.i>                    #include <include/qos/interval.xml.i> -                  #include <include/qos/match.xml.i> -                  <leafNode name="priority"> -                    <properties> -                      <help>Priority for usage of excess bandwidth</help> -                      <valueHelp> -                        <format>u32:0-7</format> -                        <description>Priority order for bandwidth pool</description> -                      </valueHelp> -                      <constraint> -                        <validator name="numeric" argument="--range 0-7"/> -                      </constraint> -                      <constraintErrorMessage>Priority must be between 0 and 7</constraintErrorMessage> -                    </properties> -                    <defaultValue>20</defaultValue> -                  </leafNode> +                  #include <include/qos/class-match.xml.i> +                  #include <include/qos/class-priority.xml.i>                    #include <include/qos/queue-limit-1-4294967295.xml.i>                    #include <include/qos/queue-type.xml.i> +                  <leafNode name="queue-type"> +                    <defaultValue>fair-queue</defaultValue> +                  </leafNode>                    #include <include/qos/set-dscp.xml.i>                    #include <include/qos/target.xml.i>                  </children>                </tagNode> -              #include <include/generic-description.xml.i>                <node name="default">                  <properties>                    <help>Default policy</help> @@ -759,7 +727,6 @@                      </properties>                    </leafNode>                    #include <include/qos/codel-quantum.xml.i> -                  #include <include/generic-description.xml.i>                    #include <include/qos/flows.xml.i>                    #include <include/qos/interval.xml.i>                    <leafNode name="priority"> @@ -778,12 +745,116 @@                    </leafNode>                    #include <include/qos/queue-limit-1-4294967295.xml.i>                    #include <include/qos/queue-type.xml.i> +                  <leafNode name="queue-type"> +                    <defaultValue>fair-queue</defaultValue> +                  </leafNode>                    #include <include/qos/set-dscp.xml.i>                    #include <include/qos/target.xml.i>                  </children>                </node>              </children>            </tagNode> +          <tagNode name="shaper-hfsc"> +            <properties> +              <help>Hierarchical Fair Service Curve's policy</help> +              <valueHelp> +                <format>txt</format> +                <description>Policy name</description> +              </valueHelp> +              <constraint> +                <regex>[[:alnum:]][-_[:alnum:]]*</regex> +              </constraint> +              <constraintErrorMessage>Only alpha-numeric policy name allowed</constraintErrorMessage> +            </properties> +            <children> +              #include <include/generic-description.xml.i> +              #include <include/qos/bandwidth-auto.xml.i> +              <tagNode name="class"> +                <properties> +                  <help>Class ID</help> +                  <valueHelp> +                    <format>u32:1-4095</format> +                    <description>Class Identifier</description> +                  </valueHelp> +                  <constraint> +                    <validator name="numeric" argument="--range 1-4095"/> +                  </constraint> +                  <constraintErrorMessage>Class identifier must be between 1 and 4095</constraintErrorMessage> +                </properties> +                <children> +                  #include <include/generic-description.xml.i> +                  <node name="linkshare"> +                    <properties> +                      <help>Linkshare class settings</help> +                    </properties> +                    <children> +                      #include <include/qos/hfsc-d.xml.i> +                      #include <include/qos/hfsc-m1.xml.i> +                      #include <include/qos/hfsc-m2.xml.i> +                    </children> +                  </node> +                  #include <include/qos/class-match.xml.i> +                  <node name="realtime"> +                    <properties> +                      <help>Realtime class settings</help> +                    </properties> +                    <children> +                      #include <include/qos/hfsc-d.xml.i> +                      #include <include/qos/hfsc-m1.xml.i> +                      #include <include/qos/hfsc-m2.xml.i> +                    </children> +                  </node> +                  <node name="upperlimit"> +                    <properties> +                      <help>Upperlimit class settings</help> +                    </properties> +                    <children> +                      #include <include/qos/hfsc-d.xml.i> +                      #include <include/qos/hfsc-m1.xml.i> +                      #include <include/qos/hfsc-m2.xml.i> +                    </children> +                  </node> +                </children> +              </tagNode> +              <node name="default"> +                <properties> +                  <help>Default policy</help> +                </properties> +                <children> +                  <node name="linkshare"> +                    <properties> +                      <help>Linkshare class settings</help> +                    </properties> +                    <children> +                      #include <include/qos/hfsc-d.xml.i> +                      #include <include/qos/hfsc-m1.xml.i> +                      #include <include/qos/hfsc-m2.xml.i> +                    </children> +                  </node> +                  <node name="realtime"> +                    <properties> +                      <help>Realtime class settings</help> +                    </properties> +                    <children> +                      #include <include/qos/hfsc-d.xml.i> +                      #include <include/qos/hfsc-m1.xml.i> +                      #include <include/qos/hfsc-m2.xml.i> +                    </children> +                  </node> +                  <node name="upperlimit"> +                    <properties> +                      <help>Upperlimit class settings</help> +                    </properties> +                    <children> +                      #include <include/qos/hfsc-d.xml.i> +                      #include <include/qos/hfsc-m1.xml.i> +                      #include <include/qos/hfsc-m2.xml.i> +                    </children> +                  </node> +                </children> +              </node> +            </children> +          </tagNode>          </children>        </node>      </children> diff --git a/python/vyos/qos/__init__.py b/python/vyos/qos/__init__.py new file mode 100644 index 000000000..a2980ccde --- /dev/null +++ b/python/vyos/qos/__init__.py @@ -0,0 +1,28 @@ +# Copyright 2022 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 +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library 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 +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library.  If not, see <http://www.gnu.org/licenses/>. + +from vyos.qos.base import QoSBase +from vyos.qos.cake import CAKE +from vyos.qos.droptail import DropTail +from vyos.qos.fairqueue import FairQueue +from vyos.qos.fqcodel import FQCodel +from vyos.qos.limiter import Limiter +from vyos.qos.netem import NetEm +from vyos.qos.priority import Priority +from vyos.qos.randomdetect import RandomDetect +from vyos.qos.ratelimiter import RateLimiter +from vyos.qos.roundrobin import RoundRobin +from vyos.qos.trafficshaper import TrafficShaper +from vyos.qos.trafficshaper import TrafficShaperHFSC diff --git a/python/vyos/qos/base.py b/python/vyos/qos/base.py new file mode 100644 index 000000000..d039bbb0f --- /dev/null +++ b/python/vyos/qos/base.py @@ -0,0 +1,276 @@ +# Copyright 2022 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 +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library 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 +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library.  If not, see <http://www.gnu.org/licenses/>. + +from vyos.base import Warning +from vyos.util import cmd +from vyos.util import dict_search +from vyos.util import read_file + +class QoSBase: +    _debug = True +    _direction = ['egress'] +    _parent = 0xffff + +    def __init__(self, interface): +        self._interface = interface + +    def _cmd(self, command): +        if self._debug: +            print(f'DEBUG/QoS: {command}') +        return cmd(command) + +    def get_direction(self) -> list: +        return self._direction + +    def _get_class_max_id(self, config) -> int: +        if 'class' in config: +            tmp = list(config['class'].keys()) +            tmp.sort(key=lambda ii: int(ii)) +            return tmp[-1] +        return None + +    def _tmp_qdisc(self, config : dict, cls_id : int): +        """ +        Add/replace qdisc for every class (also default is a class). This is +        a genetic method which need an implementation "per" queue-type. + +        This matches the old mapping as defined in Perl here: +        https://github.com/vyos/vyatta-cfg-qos/blob/equuleus/lib/Vyatta/Qos/ShaperClass.pm#L223-L229 +        """ +        queue_type = dict_search('queue_type', config) +        default_tc = f'tc qdisc replace dev {self._interface} parent {self._parent}:{cls_id:x}' + +        if queue_type == 'priority': +            handle = 0x4000 + cls_id +            default_tc += f' handle {handle:x}: prio' +            self._cmd(default_tc) + +            queue_limit = dict_search('queue_limit', config) +            for ii in range(1, 4): +                tmp = f'tc qdisc replace dev {self._interface} parent {handle:x}:{ii:x} pfifo limit {queue_limit}' +                self._cmd(tmp) + +        elif queue_type == 'fair-queue': +            default_tc += f' sfq' + +            tmp = dict_search('queue_limit', config) +            if tmp: default_tc += f' limit {tmp}' + +            self._cmd(default_tc) + +        elif queue_type == 'fq-codel': +            default_tc += f' fq_codel' +            tmp = dict_search('codel_quantum', config) +            if tmp: default_tc += f' quantum {tmp}' + +            tmp = dict_search('flows', config) +            if tmp: default_tc += f' flows {tmp}' + +            tmp = dict_search('interval', config) +            if tmp: default_tc += f' interval {tmp}' + +            tmp = dict_search('interval', config) +            if tmp: default_tc += f' interval {tmp}' + +            tmp = dict_search('queue_limit', config) +            if tmp: default_tc += f' limit {tmp}' + +            tmp = dict_search('target', config) +            if tmp: default_tc += f' target {tmp}' + +            default_tc += f' noecn' + +            self._cmd(default_tc) + +        elif queue_type == 'random-detect': +            default_tc += f' red' + +            self._cmd(default_tc) + +        elif queue_type == 'drop-tail': +            default_tc += f' pfifo' + +            tmp = dict_search('queue_limit', config) +            if tmp: default_tc += f' limit {tmp}' + +            self._cmd(default_tc) + +    def _rate_convert(self, rate) -> int: +        rates = { +            'bit'   : 1, +            'kbit'  : 1000, +            'mbit'  : 1000000, +            'gbit'  : 1000000000, +            'tbit'  : 1000000000000, +        } + +        if rate == 'auto': +            speed = read_file(f'/sys/class/net/{self._interface}/speed') +            if not speed.isnumeric(): +                Warning('Interface speed cannot be determined (assuming 10 Mbit/s)') +                speed = 10 +            return int(speed) *1000000 # convert to MBit/s + +        rate_numeric = int(''.join([n for n in rate if n.isdigit()])) +        rate_scale   = ''.join([n for n in rate if not n.isdigit()]) + +        if int(rate_numeric) <= 0: +            raise ValueError(f'{rate_numeric} is not a valid bandwidth <= 0') + +        if rate_scale: +            return int(rate_numeric * rates[rate_scale]) +        else: +            # No suffix implies Kbps just as Cisco IOS +            return int(rate_numeric * 1000) + +    def update(self, config, direction, priority=None): +        """ method must be called from derived class after it has completed qdisc setup """ + +        if 'class' in config: +            for cls, cls_config in config['class'].items(): + +                self._tmp_qdisc(cls_config, int(cls)) + +                if 'match' in cls_config: +                    for match, match_config in cls_config['match'].items(): +                        for af in ['ip', 'ipv6']: +                            # every match criteria has it's tc instance +                            filter_cmd = f'tc filter replace dev {self._interface} parent {self._parent:x}:' + +                            if priority: +                                filter_cmd += f' prio {cls}' +                            elif 'priority' in cls_config: +                                prio = cls_config['priority'] +                                filter_cmd += f' prio {prio}' + +                            filter_cmd += ' protocol all u32' + +                            tc_af = af +                            if af == 'ipv6': +                                tc_af = 'ip6' + +                            if af in match_config: +                                tmp = dict_search(f'{af}.source.address', match_config) +                                if tmp: filter_cmd += f' match {tc_af} src {tmp}' + +                                tmp = dict_search(f'{af}.source.port', match_config) +                                if tmp: filter_cmd += f' match {tc_af} sport {tmp} 0xffff' + +                                tmp = dict_search(f'{af}.destination.address', match_config) +                                if tmp: filter_cmd += f' match {tc_af} dst {tmp}' + +                                tmp = dict_search(f'{af}.destination.port', match_config) +                                if tmp: filter_cmd += f' match {tc_af} dport {tmp} 0xffff' + +                                tmp = dict_search(f'{af}.protocol', match_config) +                                if tmp: filter_cmd += f' match {tc_af} protocol {tmp} 0xff' + +                                # Will match against total length of an IPv4 packet and +                                # payload length of an IPv6 packet. +                                # +                                # IPv4 : match u16 0x0000 ~MAXLEN at 2 +                                # IPv6 : match u16 0x0000 ~MAXLEN at 4 +                                tmp = dict_search(f'{af}.max_length', match_config) +                                if tmp: +                                    # We need the 16 bit two's complement of the maximum +                                    # packet length +                                    tmp = hex(0xffff & ~int(tmp)) + +                                    if af == 'ip': +                                        filter_cmd += f' match u16 0x0000 {tmp} at 2' +                                    elif af == 'ipv6': +                                        filter_cmd += f' match u16 0x0000 {tmp} at 4' + +                                # We match against specific TCP flags - we assume the IPv4 +                                # header length is 20 bytes and assume the IPv6 packet is +                                # not using extension headers (hence a ip header length of 40 bytes) +                                # TCP Flags are set on byte 13 of the TCP header. +                                # IPv4 : match u8 X X at 33 +                                # IPv6 : match u8 X X at 53 +                                # with X = 0x02 for SYN and X = 0x10 for ACK +                                tmp = dict_search(f'{af}.tcp', match_config) +                                if tmp: +                                    mask = 0 +                                    if 'ack' in tmp: +                                        mask |= 0x10 +                                    if 'syn' in tmp: +                                        mask |= 0x02 +                                    mask = hex(mask) + +                                    if af == 'ip': +                                        filter_cmd += f' match u8 {mask} {mask} at 33' +                                    elif af == 'ipv6': +                                        filter_cmd += f' match u8 {mask} {mask} at 53' + +                                # The police block allows limiting of the byte or packet rate of +                                # traffic matched by the filter it is attached to. +                                # https://man7.org/linux/man-pages/man8/tc-police.8.html +                                if any(tmp in ['exceed', 'bandwidth', 'burst'] for tmp in cls_config): +                                    filter_cmd += f' action police' + +                                if 'exceed' in cls_config: +                                    action = cls_config['exceed'] +                                    filter_cmd += f' conform-exceed {action}' +                                    if 'not_exceed' in cls_config: +                                        action = cls_config['not_exceed'] +                                        filter_cmd += f'/{action}' + +                                if 'bandwidth' in cls_config: +                                    rate = self._rate_convert(cls_config['bandwidth']) +                                    filter_cmd += f' rate {rate}' + +                                if 'burst' in cls_config: +                                    burst = cls_config['burst'] +                                    filter_cmd += f' burst {burst}' + +                                cls = int(cls) +                                filter_cmd += f' flowid {self._parent:x}:{cls:x}' +                                self._cmd(filter_cmd) + +        if 'default' in config: +            class_id_max = self._get_class_max_id(config) +            default_cls_id = int(class_id_max) +1 + +            if 'default' in config: +                self._tmp_qdisc(config['default'], default_cls_id) + +            filter_cmd = f'tc filter replace dev {self._interface} parent {self._parent:x}: ' +            filter_cmd += 'prio 255 protocol all basic' + +            # The police block allows limiting of the byte or packet rate of +            # traffic matched by the filter it is attached to. +            # https://man7.org/linux/man-pages/man8/tc-police.8.html +            if any(tmp in ['exceed', 'bandwidth', 'burst'] for tmp in cls_config): +                filter_cmd += f' action police' + +            if 'exceed' in cls_config: +                action = cls_config['exceed'] +                filter_cmd += f' conform-exceed {action}' +                if 'not_exceed' in cls_config: +                    action = cls_config['not_exceed'] +                    filter_cmd += f'/{action}' + +            if 'bandwidth' in cls_config: +                rate = self._rate_convert(cls_config['bandwidth']) +                filter_cmd += f' rate {rate}' + +            if 'burst' in cls_config: +                burst = cls_config['burst'] +                filter_cmd += f' burst {burst}' + + +            filter_cmd += f' flowid {self._parent:x}:{default_cls_id:x}' +            self._cmd(filter_cmd) + diff --git a/python/vyos/qos/cake.py b/python/vyos/qos/cake.py new file mode 100644 index 000000000..a89b1de1e --- /dev/null +++ b/python/vyos/qos/cake.py @@ -0,0 +1,55 @@ +# Copyright 2022 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 +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library 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 +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library.  If not, see <http://www.gnu.org/licenses/>. + +from vyos.qos.base import QoSBase + +class CAKE(QoSBase): +    _direction = ['egress'] + +    # https://man7.org/linux/man-pages/man8/tc-cake.8.html +    def update(self, config, direction): +        tmp = f'tc qdisc add dev {self._interface} root handle 1: cake {direction}' +        if 'bandwidth' in config: +            bandwidth = self._rate_convert(config['bandwidth']) +            tmp += f' bandwidth {bandwidth}' + +        if 'rtt' in config: +            rtt = config['rtt'] +            tmp += f' rtt {rtt}ms' + +        if 'flow_isolation' in config: +            if 'blind' in config['flow_isolation']: +                tmp += f' flowblind' +            if 'dst_host' in config['flow_isolation']: +                tmp += f' dsthost' +            if 'dual_dst_host' in config['flow_isolation']: +                tmp += f' dual-dsthost' +            if 'dual_src_host' in config['flow_isolation']: +                tmp += f' dual-srchost' +            if 'flow' in config['flow_isolation']: +                tmp += f' flows' +            if 'host' in config['flow_isolation']: +                tmp += f' hosts' +            if 'nat' in config['flow_isolation']: +                tmp += f' nat' +            if 'src_host' in config['flow_isolation']: +                tmp += f' srchost ' +        else: +            tmp += f' nonat' + +        self._cmd(tmp) + +        # call base class +        super().update(config, direction) diff --git a/python/vyos/qos/droptail.py b/python/vyos/qos/droptail.py new file mode 100644 index 000000000..427d43d19 --- /dev/null +++ b/python/vyos/qos/droptail.py @@ -0,0 +1,28 @@ +# Copyright 2022 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 +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library 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 +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library.  If not, see <http://www.gnu.org/licenses/>. + +from vyos.qos.base import QoSBase + +class DropTail(QoSBase): +    # https://man7.org/linux/man-pages/man8/tc-pfifo.8.html +    def update(self, config, direction): +        tmp = f'tc qdisc add dev {self._interface} root pfifo' +        if 'queue_limit' in config: +            limit = config["queue_limit"] +            tmp += f' limit {limit}' +        self._cmd(tmp) + +        # call base class +        super().update(config, direction) diff --git a/python/vyos/qos/fairqueue.py b/python/vyos/qos/fairqueue.py new file mode 100644 index 000000000..f41d098fb --- /dev/null +++ b/python/vyos/qos/fairqueue.py @@ -0,0 +1,31 @@ +# Copyright 2022 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 +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library 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 +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library.  If not, see <http://www.gnu.org/licenses/>. + +from vyos.qos.base import QoSBase + +class FairQueue(QoSBase): +    # https://man7.org/linux/man-pages/man8/tc-sfq.8.html +    def update(self, config, direction): +        tmp = f'tc qdisc add dev {self._interface} root sfq' + +        if 'hash_interval' in config: +            tmp += f' perturb {config["hash_interval"]}' +        if 'queue_limit' in config: +            tmp += f' limit {config["queue_limit"]}' + +        self._cmd(tmp) + +        # call base class +        super().update(config, direction) diff --git a/python/vyos/qos/fqcodel.py b/python/vyos/qos/fqcodel.py new file mode 100644 index 000000000..cd2340aa2 --- /dev/null +++ b/python/vyos/qos/fqcodel.py @@ -0,0 +1,40 @@ +# Copyright 2022 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 +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library 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 +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library.  If not, see <http://www.gnu.org/licenses/>. + +from vyos.qos.base import QoSBase + +class FQCodel(QoSBase): +    # https://man7.org/linux/man-pages/man8/tc-fq_codel.8.html +    def update(self, config, direction): +        tmp = f'tc qdisc add dev {self._interface} root fq_codel' + +        if 'codel_quantum' in config: +            tmp += f' quantum {config["codel_quantum"]}' +        if 'flows' in config: +            tmp += f' flows {config["flows"]}' +        if 'interval' in config: +            interval = int(config['interval']) * 1000 +            tmp += f' interval {interval}' +        if 'queue_limit' in config: +            tmp += f' limit {config["queue_limit"]}' +        if 'target' in config: +            target = int(config['target']) * 1000 +            tmp += f' target {target}' + +        tmp += f' noecn' +        self._cmd(tmp) + +        # call base class +        super().update(config, direction) diff --git a/python/vyos/qos/limiter.py b/python/vyos/qos/limiter.py new file mode 100644 index 000000000..ace0c0b6c --- /dev/null +++ b/python/vyos/qos/limiter.py @@ -0,0 +1,27 @@ +# Copyright 2022 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 +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library 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 +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library.  If not, see <http://www.gnu.org/licenses/>. + +from vyos.qos.base import QoSBase + +class Limiter(QoSBase): +    _direction = ['ingress'] + +    def update(self, config, direction): +        tmp = f'tc qdisc add dev {self._interface} handle {self._parent:x}: {direction}' +        self._cmd(tmp) + +        # base class must be called last +        super().update(config, direction) + diff --git a/python/vyos/qos/netem.py b/python/vyos/qos/netem.py new file mode 100644 index 000000000..8bdef300b --- /dev/null +++ b/python/vyos/qos/netem.py @@ -0,0 +1,53 @@ +# Copyright 2022 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 +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library 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 +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library.  If not, see <http://www.gnu.org/licenses/>. + +from vyos.qos.base import QoSBase + +class NetEm(QoSBase): +    # https://man7.org/linux/man-pages/man8/tc-netem.8.html +    def update(self, config, direction): +        tmp = f'tc qdisc add dev {self._interface} root netem' +        if 'bandwidth' in config: +            rate = self._rate_convert(config["bandwidth"]) +            tmp += f' rate {rate}' + +        if 'queue_limit' in config: +            limit = config["queue_limit"] +            tmp += f' limit {limit}' + +        if 'delay' in config: +            delay = config["delay"] +            tmp += f' delay {delay}ms' + +        if 'loss' in config: +            drop  = config["loss"] +            tmp += f' drop {drop}%' + +        if 'corruption' in config: +            corrupt = config["corruption"] +            tmp += f' corrupt {corrupt}%' + +        if 'reordering' in config: +            reorder = config["reordering"] +            tmp += f' reorder {reorder}%' + +        if 'duplicate' in config: +            duplicate = config["duplicate"] +            tmp += f' duplicate {duplicate}%' + +        self._cmd(tmp) + +        # call base class +        super().update(config, direction) diff --git a/python/vyos/qos/priority.py b/python/vyos/qos/priority.py new file mode 100644 index 000000000..72092b7ef --- /dev/null +++ b/python/vyos/qos/priority.py @@ -0,0 +1,40 @@ +# Copyright 2022 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 +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library 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 +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library.  If not, see <http://www.gnu.org/licenses/>. + +from vyos.qos.base import QoSBase +from vyos.util import dict_search + +class Priority(QoSBase): +    _parent = 1 + +    # https://man7.org/linux/man-pages/man8/tc-prio.8.html +    def update(self, config, direction): +        if 'class' in config: +            class_id_max = self._get_class_max_id(config) +            bands = int(class_id_max) +1 + +            tmp = f'tc qdisc add dev {self._interface} root handle {self._parent:x}: prio bands {bands} priomap ' \ +                  f'{class_id_max} {class_id_max} {class_id_max} {class_id_max} ' \ +                  f'{class_id_max} {class_id_max} {class_id_max} {class_id_max} ' \ +                  f'{class_id_max} {class_id_max} {class_id_max} {class_id_max} ' \ +                  f'{class_id_max} {class_id_max} {class_id_max} {class_id_max} ' +            self._cmd(tmp) + +            for cls in config['class']: +                tmp = f'tc qdisc add dev {self._interface} parent {self._parent:x}:{cls:x} pfifo' +                self._cmd(tmp) + +        # base class must be called last +        super().update(config, direction, priority=True) diff --git a/python/vyos/qos/randomdetect.py b/python/vyos/qos/randomdetect.py new file mode 100644 index 000000000..d7d84260f --- /dev/null +++ b/python/vyos/qos/randomdetect.py @@ -0,0 +1,54 @@ +# Copyright 2022 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 +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library 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 +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library.  If not, see <http://www.gnu.org/licenses/>. + +from vyos.qos.base import QoSBase + +class RandomDetect(QoSBase): +    _parent = 1 + +    # https://man7.org/linux/man-pages/man8/tc.8.html +    def update(self, config, direction): + +        tmp = f'tc qdisc add dev {self._interface} root handle {self._parent}:0 dsmark indices 8 set_tc_index' +        self._cmd(tmp) + +        tmp = f'tc filter add dev {self._interface} parent {self._parent}:0 protocol ip prio 1 tcindex mask 0xe0 shift 5' +        self._cmd(tmp) + +        # Generalized Random Early Detection +        handle = self._parent +1 +        tmp = f'tc qdisc add dev {self._interface} parent {self._parent}:0 handle {handle}:0 gred setup DPs 8 default 0 grio' +        self._cmd(tmp) + +        bandwidth = self._rate_convert(config['bandwidth']) + +        # set VQ (virtual queue) parameters +        for precedence, precedence_config in config['precedence'].items(): +            precedence = int(precedence) +            avg_pkt = int(precedence_config['average_packet']) +            limit = int(precedence_config['queue_limit']) * avg_pkt +            min_val = int(precedence_config['minimum_threshold']) * avg_pkt +            max_val = int(precedence_config['maximum_threshold']) * avg_pkt + +            tmp  = f'tc qdisc change dev {self._interface} handle {handle}:0 gred limit {limit} min {min_val} max {max_val} avpkt {avg_pkt} ' + +            burst = (2 * int(precedence_config['minimum_threshold']) + int(precedence_config['maximum_threshold'])) // 3 +            probability = 1 / int(precedence_config['mark_probability']) +            tmp += f'burst {burst} bandwidth {bandwidth} probability {probability} DP {precedence} prio {8 - precedence:x}' + +            self._cmd(tmp) + +        # call base class +        super().update(config, direction) diff --git a/python/vyos/qos/ratelimiter.py b/python/vyos/qos/ratelimiter.py new file mode 100644 index 000000000..a4f80a1be --- /dev/null +++ b/python/vyos/qos/ratelimiter.py @@ -0,0 +1,37 @@ +# Copyright 2022 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 +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library 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 +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library.  If not, see <http://www.gnu.org/licenses/>. + +from vyos.qos.base import QoSBase + +class RateLimiter(QoSBase): +    # https://man7.org/linux/man-pages/man8/tc-tbf.8.html +    def update(self, config, direction): +        # call base class +        super().update(config, direction) + +        tmp = f'tc qdisc add dev {self._interface} root tbf' +        if 'bandwidth' in config: +            rate = self._rate_convert(config['bandwidth']) +            tmp += f' rate {rate}' + +        if 'burst' in config: +            burst = config['burst'] +            tmp += f' burst {burst}' + +        if 'latency' in config: +            latency = config['latency'] +            tmp += f' latency {latency}ms' + +        self._cmd(tmp) diff --git a/python/vyos/qos/roundrobin.py b/python/vyos/qos/roundrobin.py new file mode 100644 index 000000000..4a0cb18aa --- /dev/null +++ b/python/vyos/qos/roundrobin.py @@ -0,0 +1,44 @@ +# Copyright 2022 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 +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library 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 +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library.  If not, see <http://www.gnu.org/licenses/>. + +from vyos.qos.base import QoSBase + +class RoundRobin(QoSBase): +    _parent = 1 + +    # https://man7.org/linux/man-pages/man8/tc-drr.8.html +    def update(self, config, direction): +        tmp = f'tc qdisc add dev {self._interface} root handle 1: drr' +        self._cmd(tmp) + +        if 'class' in config: +            for cls in config['class']: +                cls = int(cls) +                tmp = f'tc class add dev {self._interface} parent 1:1 classid 1:{cls:x} drr' +                self._cmd(tmp) + +                tmp = f'tc qdisc add dev {self._interface} parent 1:{cls:x} pfifo' +                self._cmd(tmp) + +        if 'default' in config: +            class_id_max = self._get_class_max_id(config) +            default_cls_id = int(class_id_max) +1 + +            # class ID via CLI is in range 1-4095, thus 1000 hex = 4096 +            tmp = f'tc class add dev {self._interface} parent 1:1 classid 1:{default_cls_id:x} drr' +            self._cmd(tmp) + +        # call base class +        super().update(config, direction, priority=True) diff --git a/python/vyos/qos/trafficshaper.py b/python/vyos/qos/trafficshaper.py new file mode 100644 index 000000000..6d465b38e --- /dev/null +++ b/python/vyos/qos/trafficshaper.py @@ -0,0 +1,104 @@ +# Copyright 2022 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 +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library 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 +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library.  If not, see <http://www.gnu.org/licenses/>. + +from math import ceil +from vyos.qos.base import QoSBase + +# Kernel limits on quantum (bytes) +MAXQUANTUM = 200000 +MINQUANTUM = 1000 + +class TrafficShaper(QoSBase): +    _parent = 1 + +    # https://man7.org/linux/man-pages/man8/tc-htb.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) +            print(r2q) +        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 add dev {self._interface} root handle {self._parent:x}: htb r2q {r2q} default {default_minor_id:x}' # default is in hex +        self._cmd(tmp) + +        tmp = f'tc class add dev {self._interface} parent {self._parent:x}: classid {self._parent:x}:1 htb 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) + +                # bandwidth is a mandatory CLI node +                rate = self._rate_convert(cls_config['bandwidth']) +                burst = cls_config['burst'] +                quantum = cls_config['codel_quantum'] + +                tmp = f'tc class add dev {self._interface} parent {self._parent:x}:1 classid {self._parent:x}:{cls:x} htb rate {rate} burst {burst} quantum {quantum}' +                if 'priority' in cls_config: +                    priority = cls_config['priority'] +                    tmp += f' prio {priority}' +                self._cmd(tmp) + +                tmp = f'tc qdisc add dev {self._interface} parent {self._parent:x}:{cls:x} sfq' +                self._cmd(tmp) + +        if 'default' in config: +                tmp = f'tc class add dev {self._interface} parent {self._parent:x}:1 classid {self._parent:x}:{default_minor_id:x} htb rate {rate} burst {burst} quantum {quantum}' +                if 'priority' in config['default']: +                    priority = config['default']['priority'] +                    tmp += f' prio {priority}' +                self._cmd(tmp) + +                tmp = f'tc qdisc add dev {self._interface} parent {self._parent:x}:{default_minor_id:x} sfq' +                self._cmd(tmp) + +        # call base class +        super().update(config, direction) + +class TrafficShaperHFSC(TrafficShaper): +    def update(self, config, direction): +        # call base class +        super().update(config, direction) + diff --git a/smoketest/scripts/cli/test_qos.py b/smoketest/scripts/cli/test_qos.py new file mode 100755 index 000000000..d1fa3d07b --- /dev/null +++ b/smoketest/scripts/cli/test_qos.py @@ -0,0 +1,548 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2022 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 os +import unittest + +from json import loads +from base_vyostest_shim import VyOSUnitTestSHIM + +from vyos.configsession import ConfigSessionError +from vyos.ifconfig import Section +from vyos.util import cmd + +base_path = ['qos'] + +def get_tc_qdisc_json(interface) -> dict: +    tmp = cmd(f'tc -detail -json qdisc show dev {interface}') +    tmp = loads(tmp) +    return next(iter(tmp)) + +def get_tc_filter_json(interface, direction) -> list: +    if direction not in ['ingress', 'egress']: +        raise ValueError() +    tmp = cmd(f'tc -detail -json filter show dev {interface} {direction}') +    tmp = loads(tmp) +    return tmp + +class TestQoS(VyOSUnitTestSHIM.TestCase): +    @classmethod +    def setUpClass(cls): +        super(TestQoS, cls).setUpClass() + +        # ensure we can also run this test on a live system - so lets clean +        # out the current configuration :) +        cls.cli_delete(cls, base_path) + +        # We only test on physical interfaces and not VLAN (sub-)interfaces +        cls._interfaces = [] +        if 'TEST_ETH' in os.environ: +            tmp = os.environ['TEST_ETH'].split() +            cls._interfaces = tmp +        else: +            for tmp in Section.interfaces('ethernet', vlan=False): +                cls._interfaces.append(tmp) + +    def tearDown(self): +        # delete testing SSH config +        self.cli_delete(base_path) +        self.cli_commit() + +    def test_01_cake(self): +        bandwidth = 1000000 +        rtt = 200 + +        for interface in self._interfaces: +            policy_name = f'qos-policy-{interface}' +            self.cli_set(base_path + ['interface', interface, 'egress', policy_name]) +            self.cli_set(base_path + ['policy', 'cake', policy_name, 'bandwidth', str(bandwidth)]) +            self.cli_set(base_path + ['policy', 'cake', policy_name, 'rtt', str(rtt)]) +            self.cli_set(base_path + ['policy', 'cake', policy_name, 'flow-isolation', 'dual-src-host']) + +            bandwidth += 1000000 +            rtt += 20 + +        # commit changes +        self.cli_commit() + +        bandwidth = 1000000 +        rtt = 200 +        for interface in self._interfaces: +            tmp = get_tc_qdisc_json(interface) + +            self.assertEqual('cake', tmp['kind']) +            # TC store rates as a 32-bit unsigned integer in bps (Bytes per second) +            self.assertEqual(int(bandwidth *125), tmp['options']['bandwidth']) +            # RTT internally is in us +            self.assertEqual(int(rtt *1000), tmp['options']['rtt']) +            self.assertEqual('dual-srchost', tmp['options']['flowmode']) +            self.assertFalse(tmp['options']['ingress']) +            self.assertFalse(tmp['options']['nat']) +            self.assertTrue(tmp['options']['raw']) + +            bandwidth += 1000000 +            rtt += 20 + +    def test_02_drop_tail(self): +        queue_limit = 50 + +        first = True +        for interface in self._interfaces: +            policy_name = f'qos-policy-{interface}' + +            if first: +                self.cli_set(base_path + ['interface', interface, 'ingress', policy_name]) +                # verify() - selected QoS policy on interface only supports egress +                with self.assertRaises(ConfigSessionError): +                    self.cli_commit() +                self.cli_delete(base_path + ['interface', interface, 'ingress', policy_name]) +                first = False + +            self.cli_set(base_path + ['interface', interface, 'egress', policy_name]) +            self.cli_set(base_path + ['policy', 'drop-tail', policy_name, 'queue-limit', str(queue_limit)]) + +            queue_limit += 10 + +        # commit changes +        self.cli_commit() + +        queue_limit = 50 +        for interface in self._interfaces: +            tmp = get_tc_qdisc_json(interface) + +            self.assertEqual('pfifo', tmp['kind']) +            self.assertEqual(queue_limit, tmp['options']['limit']) + +            queue_limit += 10 + +    def test_03_fair_queue(self): +        hash_interval = 10 +        queue_limit = 50 +        policy_type = 'fair-queue' + +        first = True +        for interface in self._interfaces: +            policy_name = f'qos-policy-{interface}' + +            if first: +                self.cli_set(base_path + ['interface', interface, 'ingress', policy_name]) +                # verify() - selected QoS policy on interface only supports egress +                with self.assertRaises(ConfigSessionError): +                    self.cli_commit() +                self.cli_delete(base_path + ['interface', interface, 'ingress', policy_name]) +                first = False + +            self.cli_set(base_path + ['interface', interface, 'egress', policy_name]) +            self.cli_set(base_path + ['policy', policy_type, policy_name, 'hash-interval', str(hash_interval)]) +            self.cli_set(base_path + ['policy', policy_type, policy_name, 'queue-limit', str(queue_limit)]) + +            hash_interval += 1 +            queue_limit += 10 + +        # commit changes +        self.cli_commit() + +        hash_interval = 10 +        queue_limit = 50 +        for interface in self._interfaces: +            tmp = get_tc_qdisc_json(interface) + +            self.assertEqual('sfq', tmp['kind']) +            self.assertEqual(hash_interval, tmp['options']['perturb']) +            self.assertEqual(queue_limit, tmp['options']['limit']) + +            hash_interval += 1 +            queue_limit += 10 + +    def test_04_fq_codel(self): +        policy_type = 'fq-codel' +        codel_quantum = 1500 +        flows = 512 +        interval = 100 +        queue_limit = 2048 +        target = 5 + +        first = True +        for interface in self._interfaces: +            policy_name = f'qos-policy-{interface}' + +            if first: +                self.cli_set(base_path + ['interface', interface, 'ingress', policy_name]) +                # verify() - selected QoS policy on interface only supports egress +                with self.assertRaises(ConfigSessionError): +                    self.cli_commit() +                self.cli_delete(base_path + ['interface', interface, 'ingress', policy_name]) +                first = False + +            self.cli_set(base_path + ['interface', interface, 'egress', policy_name]) +            self.cli_set(base_path + ['policy', policy_type, policy_name, 'codel-quantum', str(codel_quantum)]) +            self.cli_set(base_path + ['policy', policy_type, policy_name, 'flows', str(flows)]) +            self.cli_set(base_path + ['policy', policy_type, policy_name, 'interval', str(interval)]) +            self.cli_set(base_path + ['policy', policy_type, policy_name, 'queue-limit', str(queue_limit)]) +            self.cli_set(base_path + ['policy', policy_type, policy_name, 'target', str(target)]) + +            codel_quantum += 10 +            flows += 2 +            interval += 10 +            queue_limit += 512 +            target += 1 + +        # commit changes +        self.cli_commit() + +        codel_quantum = 1500 +        flows = 512 +        interval = 100 +        queue_limit = 2048 +        target = 5 +        for interface in self._interfaces: +            tmp = get_tc_qdisc_json(interface) + +            self.assertEqual('fq_codel', tmp['kind']) +            self.assertEqual(codel_quantum, tmp['options']['quantum']) +            self.assertEqual(flows, tmp['options']['flows']) +            self.assertEqual(queue_limit, tmp['options']['limit']) + +            # due to internal rounding we need to substract 1 from interval and target after converting to milliseconds +            # configuration of: +            # tc qdisc add dev eth0 root fq_codel quantum 1500 flows 512 interval 100ms limit 2048 target 5ms noecn +            # results in: tc -j qdisc show dev eth0 +            # [{"kind":"fq_codel","handle":"8046:","root":true,"refcnt":3,"options":{"limit":2048,"flows":512, +            #   "quantum":1500,"target":4999,"interval":99999,"memory_limit":33554432,"drop_batch":64}}] +            self.assertEqual(interval *1000 -1, tmp['options']['interval']) +            self.assertEqual(target *1000 -1, tmp['options']['target']) + +            codel_quantum += 10 +            flows += 2 +            interval += 10 +            queue_limit += 512 +            target += 1 + +    def test_05_limiter(self): +        qos_config = { +            '1' : { +                'bandwidth' : '100', +                'match4' : { +                    'ssh'   : { 'dport' : '22', }, +                    }, +                }, +            '2' : { +                'bandwidth' : '100', +                'match6' : { +                    'ssh'   : { 'dport' : '22', }, +                    }, +                }, +            } + +        first = True +        for interface in self._interfaces: +            policy_name = f'qos-policy-{interface}' + +            if first: +                self.cli_set(base_path + ['interface', interface, 'egress', policy_name]) +                # verify() - selected QoS policy on interface only supports egress +                with self.assertRaises(ConfigSessionError): +                    self.cli_commit() +                self.cli_delete(base_path + ['interface', interface, 'egress', policy_name]) +                first = False + +            self.cli_set(base_path + ['interface', interface, 'ingress', policy_name]) + + +            for qos_class, qos_class_config in qos_config.items(): +                qos_class_base = base_path + ['policy', 'limiter', policy_name, 'class', qos_class] + +                if 'match4' in qos_class_config: +                    for match, match_config in qos_class_config['match4'].items(): +                        if 'dport' in match_config: +                            self.cli_set(qos_class_base + ['match', match, 'ip', 'destination', 'port', match_config['dport']]) + +                if 'match6' in qos_class_config: +                    for match, match_config in qos_class_config['match6'].items(): +                        if 'dport' in match_config: +                            self.cli_set(qos_class_base + ['match', match, 'ipv6', 'destination', 'port', match_config['dport']]) + +                if 'bandwidth' in qos_class_config: +                    self.cli_set(qos_class_base + ['bandwidth', qos_class_config['bandwidth']]) + + +        # commit changes +        self.cli_commit() + +        self.skipTest('iproute2 bug - invalid JSON') + +        for interface in self._interfaces: +            for filter in get_tc_filter_json(interface, 'ingress'): +                # bail out early if filter has no attached action +                if 'options' not in filter or 'actions' not in filter['options']: +                    continue + +                for qos_class, qos_class_config in qos_config.items(): +                    # Every flowid starts with ffff and we encopde the class number after the colon +                    if 'flowid' not in filter['options'] or filter['options']['flowid'] != f'ffff:{qos_class}': +                        continue + +                    ip_hdr_offset = 20 +                    if 'match6' in qos_class_config: +                        ip_hdr_offset = 40 + +                    self.assertEqual(ip_hdr_offset, filter['options']['match']['off']) +                    if 'dport' in match_config: +                        dport = int(match_config['dport']) +                        self.assertEqual(f'{dport:x}', filter['options']['match']['value']) + +    def test_06_network_emulator(self): +        policy_type = 'network-emulator' + +        bandwidth = 1000000 +        corruption = 1 +        delay = 2 +        duplicate = 3 +        loss = 4 +        queue_limit = 5 +        reordering = 6 + +        first = True +        for interface in self._interfaces: +            policy_name = f'qos-policy-{interface}' + +            if first: +                self.cli_set(base_path + ['interface', interface, 'ingress', policy_name]) +                # verify() - selected QoS policy on interface only supports egress +                with self.assertRaises(ConfigSessionError): +                    self.cli_commit() +                self.cli_delete(base_path + ['interface', interface, 'ingress', policy_name]) +                first = False + +            self.cli_set(base_path + ['interface', interface, 'egress', policy_name]) + +            self.cli_set(base_path + ['policy', policy_type, policy_name, 'bandwidth', str(bandwidth)]) +            self.cli_set(base_path + ['policy', policy_type, policy_name, 'corruption', str(corruption)]) +            self.cli_set(base_path + ['policy', policy_type, policy_name, 'delay', str(delay)]) +            self.cli_set(base_path + ['policy', policy_type, policy_name, 'duplicate', str(duplicate)]) +            self.cli_set(base_path + ['policy', policy_type, policy_name, 'loss', str(loss)]) +            self.cli_set(base_path + ['policy', policy_type, policy_name, 'queue-limit', str(queue_limit)]) +            self.cli_set(base_path + ['policy', policy_type, policy_name, 'reordering', str(reordering)]) + +            bandwidth += 1000000 +            corruption += 1 +            delay += 1 +            duplicate +=1 +            loss += 1 +            queue_limit += 1 +            reordering += 1 + +        # commit changes +        self.cli_commit() + +        bandwidth = 1000000 +        corruption = 1 +        delay = 2 +        duplicate = 3 +        loss = 4 +        queue_limit = 5 +        reordering = 6 +        for interface in self._interfaces: +            tmp = get_tc_qdisc_json(interface) +            self.assertEqual('netem', tmp['kind']) + +            self.assertEqual(int(bandwidth *125), tmp['options']['rate']['rate']) +            # values are in % +            self.assertEqual(corruption/100, tmp['options']['corrupt']['corrupt']) +            self.assertEqual(duplicate/100, tmp['options']['duplicate']['duplicate']) +            self.assertEqual(loss/100, tmp['options']['loss-random']['loss']) +            self.assertEqual(reordering/100, tmp['options']['reorder']['reorder']) +            self.assertEqual(delay/1000, tmp['options']['delay']['delay']) + +            self.assertEqual(queue_limit, tmp['options']['limit']) + +            bandwidth += 1000000 +            corruption += 1 +            delay += 1 +            duplicate += 1 +            loss += 1 +            queue_limit += 1 +            reordering += 1 + +    def test_07_priority_queue(self): +        priorities = ['1', '2', '3', '4', '5'] + +        first = True +        for interface in self._interfaces: +            policy_name = f'qos-policy-{interface}' + +            if first: +                self.cli_set(base_path + ['interface', interface, 'ingress', policy_name]) +                # verify() - selected QoS policy on interface only supports egress +                with self.assertRaises(ConfigSessionError): +                    self.cli_commit() +                self.cli_delete(base_path + ['interface', interface, 'ingress', policy_name]) +                first = False + +            self.cli_set(base_path + ['interface', interface, 'egress', policy_name]) +            self.cli_set(base_path + ['policy', 'priority-queue', policy_name, 'default', 'queue-limit', '10']) + +            for priority in priorities: +                prio_base = base_path + ['policy', 'priority-queue', policy_name, 'class', priority] +                self.cli_set(prio_base + ['match', f'prio-{priority}', 'ip', 'destination', 'port', str(1000 + int(priority))]) + +        # commit changes +        self.cli_commit() + +    def test_08_random_detect(self): +        self.skipTest('tc returns invalid JSON here - needs iproute2 fix') +        bandwidth = 5000 + +        first = True +        for interface in self._interfaces: +            policy_name = f'qos-policy-{interface}' + +            if first: +                self.cli_set(base_path + ['interface', interface, 'ingress', policy_name]) +                # verify() - selected QoS policy on interface only supports egress +                with self.assertRaises(ConfigSessionError): +                    self.cli_commit() +                self.cli_delete(base_path + ['interface', interface, 'ingress', policy_name]) +                first = False + +            self.cli_set(base_path + ['interface', interface, 'egress', policy_name]) +            self.cli_set(base_path + ['policy', 'random-detect', policy_name, 'bandwidth', str(bandwidth)]) + +            bandwidth += 1000 + +        # commit changes +        self.cli_commit() + +        bandwidth = 5000 +        for interface in self._interfaces: +            tmp = get_tc_qdisc_json(interface) +            import pprint +            pprint.pprint(tmp) + +    def test_09_rate_control(self): +        bandwidth = 5000 +        burst = 20 +        latency = 5 + +        first = True +        for interface in self._interfaces: +            policy_name = f'qos-policy-{interface}' + +            if first: +                self.cli_set(base_path + ['interface', interface, 'ingress', policy_name]) +                # verify() - selected QoS policy on interface only supports egress +                with self.assertRaises(ConfigSessionError): +                    self.cli_commit() +                self.cli_delete(base_path + ['interface', interface, 'ingress', policy_name]) +                first = False + +            self.cli_set(base_path + ['interface', interface, 'egress', policy_name]) +            self.cli_set(base_path + ['policy', 'rate-control', policy_name, 'bandwidth', str(bandwidth)]) +            self.cli_set(base_path + ['policy', 'rate-control', policy_name, 'burst', str(burst)]) +            self.cli_set(base_path + ['policy', 'rate-control', policy_name, 'latency', str(latency)]) + +            bandwidth += 1000 +            burst += 5 +            latency += 1 +        # commit changes +        self.cli_commit() + +        bandwidth = 5000 +        burst = 20 +        latency = 5 +        for interface in self._interfaces: +            tmp = get_tc_qdisc_json(interface) + +            self.assertEqual('tbf', tmp['kind']) +            self.assertEqual(0, tmp['options']['mpu']) +            # TC store rates as a 32-bit unsigned integer in bps (Bytes per second) +            self.assertEqual(int(bandwidth * 125), tmp['options']['rate']) + +            bandwidth += 1000 +            burst += 5 +            latency += 1 + +    def test_10_round_robin(self): +        qos_config = { +            '1' : { +                'match4' : { +                    'ssh'   : { 'dport' : '22', }, +                    }, +                }, +            '2' : { +                'match6' : { +                    'ssh'   : { 'dport' : '22', }, +                    }, +                }, +            } + +        first = True +        for interface in self._interfaces: +            policy_name = f'qos-policy-{interface}' + +            if first: +                self.cli_set(base_path + ['interface', interface, 'ingress', policy_name]) +                # verify() - selected QoS policy on interface only supports egress +                with self.assertRaises(ConfigSessionError): +                    self.cli_commit() +                self.cli_delete(base_path + ['interface', interface, 'ingress', policy_name]) +                first = False + +            self.cli_set(base_path + ['interface', interface, 'egress', policy_name]) + +            for qos_class, qos_class_config in qos_config.items(): +                qos_class_base = base_path + ['policy', 'round-robin', policy_name, 'class', qos_class] + +                if 'match4' in qos_class_config: +                    for match, match_config in qos_class_config['match4'].items(): +                        if 'dport' in match_config: +                            self.cli_set(qos_class_base + ['match', match, 'ip', 'destination', 'port', match_config['dport']]) + +                if 'match6' in qos_class_config: +                    for match, match_config in qos_class_config['match6'].items(): +                        if 'dport' in match_config: +                            self.cli_set(qos_class_base + ['match', match, 'ipv6', 'destination', 'port', match_config['dport']]) + + +        # commit changes +        self.cli_commit() + +        for interface in self._interfaces: +            import pprint +            tmp = get_tc_qdisc_json(interface) +            self.assertEqual('drr', tmp['kind']) + +            for filter in get_tc_filter_json(interface, 'ingress'): +                # bail out early if filter has no attached action +                if 'options' not in filter or 'actions' not in filter['options']: +                    continue + +                for qos_class, qos_class_config in qos_config.items(): +                    # Every flowid starts with ffff and we encopde the class number after the colon +                    if 'flowid' not in filter['options'] or filter['options']['flowid'] != f'ffff:{qos_class}': +                        continue + +                    ip_hdr_offset = 20 +                    if 'match6' in qos_class_config: +                        ip_hdr_offset = 40 + +                    self.assertEqual(ip_hdr_offset, filter['options']['match']['off']) +                    if 'dport' in match_config: +                        dport = int(match_config['dport']) +                        self.assertEqual(f'{dport:x}', filter['options']['match']['value']) + +if __name__ == '__main__': +    unittest.main(verbosity=2, failfast=True) diff --git a/src/conf_mode/qos.py b/src/conf_mode/qos.py index dbe3be225..7e94e95bf 100755 --- a/src/conf_mode/qos.py +++ b/src/conf_mode/qos.py @@ -15,14 +15,59 @@  # along with this program. If not, see <http://www.gnu.org/licenses/>.  from sys import exit +from netifaces import interfaces  from vyos.config import Config  from vyos.configdict import dict_merge +from vyos.configverify import verify_interface_exists +from vyos.qos import CAKE +from vyos.qos import DropTail +from vyos.qos import FairQueue +from vyos.qos import FQCodel +from vyos.qos import Limiter +from vyos.qos import NetEm +from vyos.qos import Priority +from vyos.qos import RandomDetect +from vyos.qos import RateLimiter +from vyos.qos import RoundRobin +from vyos.qos import TrafficShaper +from vyos.qos import TrafficShaperHFSC +from vyos.util import call +from vyos.util import dict_search_recursive  from vyos.xml import defaults  from vyos import ConfigError  from vyos import airbag  airbag.enable() +map_vyops_tc = { +    'cake'             : CAKE, +    'drop_tail'        : DropTail, +    'fair_queue'       : FairQueue, +    'fq_codel'         : FQCodel, +    'limiter'          : Limiter, +    'network_emulator' : NetEm, +    'priority_queue'   : Priority, +    'random_detect'    : RandomDetect, +    'rate_control'     : RateLimiter, +    'round_robin'      : RoundRobin, +    'shaper'           : TrafficShaper, +    'shaper_hfsc'      : TrafficShaperHFSC, +} + +def get_shaper(qos, interface_config, direction): +    policy_name = interface_config[direction] +    # An interface might have a QoS configuration, search the used +    # configuration referenced by this. Path will hold the dict element +    # referenced by the config, as this will be of sort: +    # +    # ['policy', 'drop_tail', 'foo-dtail'] <- we are only interested in +    # drop_tail as the policy/shaper type +    _, path = next(dict_search_recursive(qos, policy_name)) +    shaper_type = path[1] +    shaper_config = qos['policy'][shaper_type][policy_name] + +    return (map_vyops_tc[shaper_type], shaper_config) +  def get_config(config=None):      if config:          conf = config @@ -32,48 +77,167 @@ def get_config(config=None):      if not conf.exists(base):          return None -    qos = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True) +    qos = conf.get_config_dict(base, key_mangling=('-', '_'), +                               get_first_key=True, +                               no_tag_node_value_mangle=True)      if 'policy' in qos:          for policy in qos['policy']: -            # CLI mangles - to _ for better Jinja2 compatibility - do we need -            # Jinja2 here? -            policy = policy.replace('-','_') +            # when calling defaults() we need to use the real CLI node, thus we +            # need a hyphen +            policy_hyphen = policy.replace('_', '-') + +            if policy in ['random_detect']: +                for rd_name, rd_config in qos['policy'][policy].items(): +                    # There are eight precedence levels - ensure all are present +                    # to be filled later down with the appropriate default values +                    default_precedence = {'precedence' : { '0' : {}, '1' : {}, '2' : {}, '3' : {}, +                                                           '4' : {}, '5' : {}, '6' : {}, '7' : {} }} +                    qos['policy']['random_detect'][rd_name] = dict_merge( +                        default_precedence, qos['policy']['random_detect'][rd_name]) + +            for p_name, p_config in qos['policy'][policy].items(): +                default_values = defaults(base + ['policy', policy_hyphen]) -            default_values = defaults(base + ['policy', policy]) +                if policy in ['priority_queue']: +                    if 'default' not in p_config: +                        raise ConfigError(f'QoS policy {p_name} misses "default" class!') -            # class is another tag node which requires individual handling -            class_default_values = defaults(base + ['policy', policy, 'class']) -            if 'class' in default_values: -                del default_values['class'] +                # XXX: T2665: we can not safely rely on the defaults() when there are +                # tagNodes in place, it is better to blend in the defaults manually. +                if 'class' in default_values: +                    del default_values['class'] +                if 'precedence' in default_values: +                    del default_values['precedence'] -            for p_name, p_config in qos['policy'][policy].items():                  qos['policy'][policy][p_name] = dict_merge(                      default_values, qos['policy'][policy][p_name]) +                # class is another tag node which requires individual handling                  if 'class' in p_config: +                    default_values = defaults(base + ['policy', policy_hyphen, 'class'])                      for p_class in p_config['class']:                          qos['policy'][policy][p_name]['class'][p_class] = dict_merge( -                            class_default_values, qos['policy'][policy][p_name]['class'][p_class]) +                            default_values, qos['policy'][policy][p_name]['class'][p_class]) + +                if 'precedence' in p_config: +                    default_values = defaults(base + ['policy', policy_hyphen, 'precedence']) +                    # precedence values are a bit more complex as they are calculated +                    # under specific circumstances - thus we need to iterate two times. +                    # first blend in the defaults from XML / CLI +                    for precedence in p_config['precedence']: +                        qos['policy'][policy][p_name]['precedence'][precedence] = dict_merge( +                            default_values, qos['policy'][policy][p_name]['precedence'][precedence]) +                    # second calculate defaults based on actual dictionary +                    for precedence in p_config['precedence']: +                        max_thr = int(qos['policy'][policy][p_name]['precedence'][precedence]['maximum_threshold']) +                        if 'minimum_threshold' not in qos['policy'][policy][p_name]['precedence'][precedence]: +                            qos['policy'][policy][p_name]['precedence'][precedence]['minimum_threshold'] = str( +                                int((9 + int(precedence)) * max_thr) // 18); + +                        if 'queue_limit' not in qos['policy'][policy][p_name]['precedence'][precedence]: +                            qos['policy'][policy][p_name]['precedence'][precedence]['queue_limit'] = \ +                                str(int(4 * max_thr))      import pprint      pprint.pprint(qos) +      return qos  def verify(qos): -    if not qos: +    if not qos or 'interface' not in qos:          return None      # network policy emulator      # reorder rerquires delay to be set +    if 'policy' in qos: +        for policy_type in qos['policy']: +            for policy, policy_config in qos['policy'][policy_type].items(): +                # a policy with it's given name is only allowed to exist once +                # on the system. This is because an interface selects a policy +                # for ingress/egress traffic, and thus there can only be one +                # policy with a given name. +                # +                # We check if the policy name occurs more then once - error out +                # if this is true +                counter = 0 +                for _, path in dict_search_recursive(qos['policy'], policy): +                    counter += 1 +                    if counter > 1: +                        raise ConfigError(f'Conflicting policy name "{policy}", already in use!') + +                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']: +                            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(): +                            if {'ip', 'ipv6'} <= set(match_config): +                                 raise ConfigError(f'Can not use both IPv6 and IPv4 in one match ({match})!') + +                if policy_type in ['random_detect']: +                    if 'precedence' in policy_config: +                        for precedence, precedence_config in policy_config['precedence'].items(): +                            max_tr = int(precedence_config['maximum_threshold']) +                            if {'maximum_threshold', 'minimum_threshold'} <= set(precedence_config): +                                min_tr = int(precedence_config['minimum_threshold']) +                                if min_tr >= max_tr: +                                    raise ConfigError(f'Policy "{policy}" uses min-threshold "{min_tr}" >= max-threshold "{max_tr}"!') + +                            if {'maximum_threshold', 'queue_limit'} <= set(precedence_config): +                                queue_lim = int(precedence_config['queue_limit']) +                                if queue_lim < max_tr: +                                    raise ConfigError(f'Policy "{policy}" uses queue-limit "{queue_lim}" < max-threshold "{max_tr}"!') + + +    # we should check interface ingress/egress configuration after verifying that +    # the policy name is used only once - this makes the logic easier! +    for interface, interface_config in qos['interface'].items(): +        verify_interface_exists(interface) + +        for direction in ['egress', 'ingress']: +            # bail out early if shaper for given direction is not used at all +            if direction not in interface_config: +                continue + +            policy_name = interface_config[direction] +            if 'policy' not in qos or list(dict_search_recursive(qos['policy'], policy_name)) == []: +                raise ConfigError(f'Selected QoS policy "{policy_name}" does not exist!') + +            shaper_type, shaper_config = get_shaper(qos, interface_config, direction) +            tmp = shaper_type(interface).get_direction() +            if direction not in tmp: +                raise ConfigError(f'Selected QoS policy on interface "{interface}" only supports "{tmp}"!') -    raise ConfigError('123')      return None  def generate(qos): +    if not qos or 'interface' not in qos: +        return None +      return None  def apply(qos): +    # Always delete "old" shapers first +    for interface in interfaces(): +        # Ignore errors (may have no qdisc) +        call(f'tc qdisc del dev {interface} parent ffff:') +        call(f'tc qdisc del dev {interface} root') + +    if not qos or 'interface' not in qos: +        return None + +    for interface, interface_config in qos['interface'].items(): +        for direction in ['egress', 'ingress']: +            # bail out early if shaper for given direction is not used at all +            if direction not in interface_config: +                continue + +            shaper_type, shaper_config = get_shaper(qos, interface_config, direction) +            tmp = shaper_type(interface) +            tmp.update(shaper_config, direction) +      return None  if __name__ == '__main__': diff --git a/src/migration-scripts/qos/1-to-2 b/src/migration-scripts/qos/1-to-2 new file mode 100755 index 000000000..240777574 --- /dev/null +++ b/src/migration-scripts/qos/1-to-2 @@ -0,0 +1,107 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2022 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/>. + +from sys import argv,exit +from vyos.configtree import ConfigTree + +if (len(argv) < 1): +    print("Must specify file name!") +    exit(1) + +file_name = argv[1] + +with open(file_name, 'r') as f: +    config_file = f.read() + +base = ['traffic-policy'] +config = ConfigTree(config_file) + +if not config.exists(base): +    # Nothing to do +    exit(0) + +iface_config = {} + +if config.exists(['interfaces']): +    def get_qos(config, interface, interface_base): +        if config.exists(interface_base): +            tmp = { interface : {} } +            if config.exists(interface_base + ['in']): +                tmp[interface]['ingress'] = config.return_value(interface_base + ['in']) +            if config.exists(interface_base + ['out']): +                tmp[interface]['egress'] = config.return_value(interface_base + ['out']) +            config.delete(interface_base) +            return tmp +        return None + +    # Migrate "interface ethernet eth0 traffic-policy in|out" to "qos interface eth0 ingress|egress" +    for type in config.list_nodes(['interfaces']): +        for interface in config.list_nodes(['interfaces', type]): +            interface_base = ['interfaces', type, interface, 'traffic-policy'] +            tmp = get_qos(config, interface, interface_base) +            if tmp: iface_config.append(tmp) + +            vif_path = ['interfaces', type, interface, 'vif'] +            if config.exists(vif_path): +                for vif in config.list_nodes(vif_path): +                    vif_interface_base = vif_path + [vif, 'traffic-policy'] +                    ifname = f'{interface}.{vif}' +                    tmp = get_qos(config, ifname, vif_interface_base) +                    if tmp: iface_config.update(tmp) + +            vif_s_path = ['interfaces', type, interface, 'vif-s'] +            if config.exists(vif_s_path): +                for vif_s in config.list_nodes(vif_s_path): +                    vif_s_interface_base = vif_s_path + [vif_s, 'traffic-policy'] +                    ifname = f'{interface}.{vif_s}' +                    tmp = get_qos(config, ifname, vif_s_interface_base) +                    if tmp: iface_config.update(tmp) + +                    # vif-c interfaces MUST be migrated before their parent vif-s +                    # interface as the migrate_*() functions delete the path! +                    vif_c_path = ['interfaces', type, interface, 'vif-s', vif_s, 'vif-c'] +                    if config.exists(vif_c_path): +                        for vif_c in config.list_nodes(vif_c_path): +                            vif_c_interface_base = vif_c_path + [vif_c, 'traffic-policy'] +                            ifname = f'{interface}.{vif_s}.{vif_c}' +                            tmp = get_qos(config, ifname, vif_s_interface_base) +                            if tmp: iface_config.update(tmp) + + +# Now we have the information which interface uses which QoS policy. +# Interface binding will be moved to the qos CLi tree +config.set(['qos']) +config.copy(base, ['qos', 'policy']) +config.delete(base) + + +# TODO +# - remove burst from network emulator +# - convert rates to bits/s + +# Now map the interface policy binding to the new CLI syntax +for interface, interface_config in iface_config.items(): +    if 'ingress' in interface_config: +        config.set(['qos', 'interface', interface, 'ingress'], value=interface_config['ingress']) +    if 'egress' in interface_config: +        config.set(['qos', 'interface', interface, 'egress'], value=interface_config['egress']) + +try: +    with open(file_name, 'w') as f: +        f.write(config.to_string()) +except OSError as e: +    print("Failed to save the modified config: {}".format(e)) +    exit(1) | 
