diff --git a/board/aarch64/freescale-imx8mp-evk/rootfs/usr/share/product/fsl,imx8mp-evk/etc/product/interface-quirks.json b/board/aarch64/freescale-imx8mp-evk/rootfs/usr/share/product/fsl,imx8mp-evk/etc/product/interface-quirks.json index f90721134..70098394c 100644 --- a/board/aarch64/freescale-imx8mp-evk/rootfs/usr/share/product/fsl,imx8mp-evk/etc/product/interface-quirks.json +++ b/board/aarch64/freescale-imx8mp-evk/rootfs/usr/share/product/fsl,imx8mp-evk/etc/product/interface-quirks.json @@ -1,8 +1,5 @@ { "eth0": { "phy-detached-when-down": true - }, - "eth1": { - "broken-mqprio": true } } diff --git a/board/aarch64/linux_defconfig b/board/aarch64/linux_defconfig index b7c4fe051..c23012847 100644 --- a/board/aarch64/linux_defconfig +++ b/board/aarch64/linux_defconfig @@ -245,13 +245,22 @@ CONFIG_VLAN_8021Q_GVRP=y CONFIG_VLAN_8021Q_MVRP=y CONFIG_NET_SCHED=y CONFIG_NET_SCH_MQPRIO=y +CONFIG_NET_SCH_ETS=y +CONFIG_NET_SCH_TBF=y +CONFIG_NET_SCH_INGRESS=y CONFIG_NET_CLS_BASIC=y CONFIG_NET_CLS_BPF=y CONFIG_NET_CLS_FLOWER=y +CONFIG_NET_CLS_MATCHALL=y +CONFIG_NET_EMATCH=y +CONFIG_NET_EMATCH_CMP=y +CONFIG_NET_EMATCH_META=y CONFIG_NET_CLS_ACT=y CONFIG_NET_ACT_GACT=y CONFIG_NET_ACT_MIRRED=y CONFIG_NET_ACT_SKBEDIT=y +CONFIG_NET_ACT_PEDIT=y +CONFIG_NET_ACT_CSUM=y CONFIG_DCB=y CONFIG_NETLINK_DIAG=y CONFIG_MPLS=y diff --git a/board/aarch64/microchip-ev23x71a/README.md b/board/aarch64/microchip-ev23x71a/README.md index 195d5534c..b66bf32cb 100644 --- a/board/aarch64/microchip-ev23x71a/README.md +++ b/board/aarch64/microchip-ev23x71a/README.md @@ -26,7 +26,9 @@ Supported: Not yet done: - TSN queueing. PSFP and time aware shaping need `NET_SCH_TAPRIO`, - `NET_ACT_GATE`, `NET_SCH_ETS`, and `NET_SCH_CBS`, all currently off + `NET_ACT_GATE`, and `NET_SCH_CBS`, all currently off. Ingress + classification (`dcb`) and traffic classes (`mqprio`) are configurable + from the QoS model but not yet verified on this board - HSR/PRP offload, see above - MAC addresses. With no environment in flash the board falls back to `lan969x_otp_get_mac()`, which derives 30 addresses for this board diff --git a/board/arm/linux_defconfig b/board/arm/linux_defconfig index 3c0639ad7..1ed746af3 100644 --- a/board/arm/linux_defconfig +++ b/board/arm/linux_defconfig @@ -229,13 +229,22 @@ CONFIG_VLAN_8021Q_GVRP=y CONFIG_VLAN_8021Q_MVRP=y CONFIG_NET_SCHED=y CONFIG_NET_SCH_MQPRIO=y +CONFIG_NET_SCH_ETS=y +CONFIG_NET_SCH_TBF=y +CONFIG_NET_SCH_INGRESS=y CONFIG_NET_CLS_BASIC=y CONFIG_NET_CLS_BPF=y CONFIG_NET_CLS_FLOWER=y +CONFIG_NET_CLS_MATCHALL=y +CONFIG_NET_EMATCH=y +CONFIG_NET_EMATCH_CMP=y +CONFIG_NET_EMATCH_META=y CONFIG_NET_CLS_ACT=y CONFIG_NET_ACT_GACT=y CONFIG_NET_ACT_MIRRED=y CONFIG_NET_ACT_SKBEDIT=y +CONFIG_NET_ACT_PEDIT=y +CONFIG_NET_ACT_CSUM=y CONFIG_DCB=y CONFIG_NETLINK_DIAG=y CONFIG_MPLS=y diff --git a/board/common/rootfs/usr/libexec/infix/init.d/25-mqprio b/board/common/rootfs/usr/libexec/infix/init.d/25-mqprio deleted file mode 100755 index 875dcc0b9..000000000 --- a/board/common/rootfs/usr/libexec/infix/init.d/25-mqprio +++ /dev/null @@ -1,60 +0,0 @@ -#!/bin/sh -# Opportunistically look for interfaces with multiple transmit queues -# and hardware support for the mqprio queuing discipline. For every -# matching interface, set up mappings from kernel-internal packet -# priorities, via traffic classes, to transmit queues such that as -# many high priorities as possible are scheduled on separate queues. - -set -e - -map() -{ - case "$1" in - 2) - echo "map 0 0 0 0 0 0 1 1";; - 3) - echo "map 0 0 0 0 1 1 2 2";; - 4) - echo "map 0 0 1 1 2 2 3 3";; - 5) - echo "map 0 0 1 1 2 2 3 4";; - 6) - echo "map 0 0 1 1 2 3 4 5";; - 7) - echo "map 0 0 1 2 3 4 5 6";; - 8) - echo "map 0 1 2 3 4 5 6 7";; - esac -} - -queues() -{ - out="queues " - for tc in $(seq 0 $(($1 - 1))); do - out="$out 1@$tc" - done - - echo "$out" -} - -quirks= -[ -f /etc/product/interface-quirks.json ] && quirks=yes - -set $(ip -j -d link show | jq -r '.[] | .ifname, .num_tx_queues') -while [ "$1" ]; do - iface="$1" - txqs="$2" - shift 2 - - [ "$quirks" ] && [ $(/usr/libexec/infix/has-quirk "broken-mqprio" "$iface") = "true" ] && echo "Skipping $iface, does not support mqprio" && continue - [ $txqs -lt 2 ] && continue - [ $txqs -gt 8 ] && txqs=8 - - output=$(tc qdisc add dev $iface root mqprio hw 1 \ - num_tc $txqs $(map $txqs) $(queues $txqs) 2>&1) || true - if echo "$output" | grep -q "does not support hardware offload"; then - echo "Skipping $iface, hardware offload not supported." - elif [ -n "$output" ]; then - echo "$output" - fi -done diff --git a/board/riscv64/linux_defconfig b/board/riscv64/linux_defconfig index 080eca13e..c5938d6d1 100644 --- a/board/riscv64/linux_defconfig +++ b/board/riscv64/linux_defconfig @@ -193,13 +193,22 @@ CONFIG_VLAN_8021Q_GVRP=y CONFIG_VLAN_8021Q_MVRP=y CONFIG_NET_SCHED=y CONFIG_NET_SCH_MQPRIO=y +CONFIG_NET_SCH_ETS=y +CONFIG_NET_SCH_TBF=y +CONFIG_NET_SCH_INGRESS=y CONFIG_NET_CLS_BASIC=y CONFIG_NET_CLS_BPF=y CONFIG_NET_CLS_FLOWER=y +CONFIG_NET_CLS_MATCHALL=y +CONFIG_NET_EMATCH=y +CONFIG_NET_EMATCH_CMP=y +CONFIG_NET_EMATCH_META=y CONFIG_NET_CLS_ACT=y CONFIG_NET_ACT_GACT=y CONFIG_NET_ACT_MIRRED=y CONFIG_NET_ACT_SKBEDIT=y +CONFIG_NET_ACT_PEDIT=y +CONFIG_NET_ACT_CSUM=y CONFIG_DCB=y CONFIG_NETLINK_DIAG=y CONFIG_MPLS=y diff --git a/board/x86_64/linux_defconfig b/board/x86_64/linux_defconfig index 4b9f5d050..3a2e89380 100644 --- a/board/x86_64/linux_defconfig +++ b/board/x86_64/linux_defconfig @@ -219,9 +219,23 @@ CONFIG_VLAN_8021Q=y CONFIG_VLAN_8021Q_GVRP=y CONFIG_VLAN_8021Q_MVRP=y CONFIG_NET_SCHED=y +CONFIG_NET_SCH_MQPRIO=y +CONFIG_NET_SCH_ETS=y +CONFIG_NET_SCH_TBF=y +CONFIG_NET_SCH_INGRESS=y CONFIG_NET_CLS_BASIC=y CONFIG_NET_CLS_BPF=y CONFIG_NET_CLS_FLOWER=y +CONFIG_NET_CLS_MATCHALL=y +CONFIG_NET_EMATCH=y +CONFIG_NET_EMATCH_CMP=y +CONFIG_NET_EMATCH_META=y +CONFIG_NET_CLS_ACT=y +CONFIG_NET_ACT_GACT=y +CONFIG_NET_ACT_SKBEDIT=y +CONFIG_NET_ACT_PEDIT=y +CONFIG_NET_ACT_CSUM=y +CONFIG_DCB=y CONFIG_NETLINK_DIAG=y CONFIG_MPLS=y CONFIG_NET_MPLS_GSO=y diff --git a/doc/ChangeLog.md b/doc/ChangeLog.md index d6d9fa15a..c4e3f7731 100644 --- a/doc/ChangeLog.md +++ b/doc/ChangeLog.md @@ -16,6 +16,24 @@ All notable changes to the project are documented in this file. - Document the release and maintenance policy: which versions receive updates, what may go into a patch release, and the levels of long-term maintenance available, see [Releases & Support][relsup] +- Add per-interface Quality of Service configuration: which header field + a port trusts (PCP, DSCP, or both in order), the default priority, PCP + and DSCP to priority maps with standard presets, an egress traffic + class table with strict-priority or weighted transmission selection per + class, and PCP and DSCP remarking on transmit. The defaults are on out + of the box on every port: trust PCP, one traffic class per + transmit queue mapped per IEEE 802.1Q-2022 Table 8-5, replacing the + fixed boot-time queue setup, see [QoS](qos.md) +- Add a per-port egress rate limit under + `/interfaces/interface/qos/egress/rate-limit`, one token bucket above + the traffic classes, see [QoS](qos.md) +- Transmission selection is rendered with the `ets` scheduler on every + port, so drivers that offload it take the class algorithms and weights + into the switch fabric, not only the priority to class map +- Marvell LinkStreet 88E6390 and 88E6393X switch ports offload QoS + classification, trust order, remarking, transmission selection and the + egress rate limit to the switch, so they apply to hardware forwarded + traffic as well ### Added diff --git a/doc/extra.css b/doc/extra.css index 3e9c836df..7df85d675 100644 --- a/doc/extra.css +++ b/doc/extra.css @@ -102,14 +102,21 @@ body { --md-primary-bg-color: #5c5f5c; } -/* Center Markdown Tables (requires md_in_html extension) */ -.center-table { +/* Center all Markdown tables, like figures */ +.md-typeset .md-typeset__scrollwrap { text-align: center; } -.md-typeset .center-table :is(td,th):not([align]) { - /* Reset alignment for table cells */ - text-align: initial; +/* Material forces a 5rem minimum width on header cells, which makes + * narrow numeric tables sprawl past the page. Fit headers to content. */ +.md-typeset table:not([class]) th { + min-width: 0; +} + +/* Material's 1.4 line height leaves gaps between box-drawing characters + * in CLI output and diagrams. Applies to code blocks as a whole. */ +.md-typeset pre { + line-height: 1.15; } /* CLI terminal output styling */ diff --git a/doc/img/qos-pipeline.svg b/doc/img/qos-pipeline.svg new file mode 100644 index 000000000..88c256ac0 --- /dev/null +++ b/doc/img/qos-pipeline.svg @@ -0,0 +1,4 @@ + + + +
Forwarding
Bridge FDB lookup and MAC learning, VLAN membership, STP state, ACLs, multicast replication.
→ egress port(s)
INGRESS receiving port
EGRESS transmitting port
1 Classify
Trust order picks PCP or DSCP, else the port default.
→ priority + drop precedence
2 Match flows
TCAM match on a 5-tuple may override the priority and drop precedence.
3 Police and gate
Policers, storm control,
PSFP (IEEE 802.1Qci)
4 Map to traffic class and queue
IEEE 802.1Q 8.6.6. The egress port’s table decides
5 Transmission selection
IEEE 802.1Q 8.6.8. TAS gates,
then strict priority, ETS or CBS
6 Egress processing
Remark PCP, DEI and DSCP
Port cap
frame
wire
\ No newline at end of file diff --git a/doc/qos.md b/doc/qos.md index d937104f6..48ae70ee7 100644 --- a/doc/qos.md +++ b/doc/qos.md @@ -2,93 +2,509 @@ Quality of Service ================== On occasion, most networks will experience congestion due to some -extraordinary load being placed upon it. If the load is transient, +extraordinary load being placed upon it. If the load is transient, switches and routers may be able to absorb such bursts of traffic by -queuing packets in internal memories. However, if the load is -sustained over long periods of time, queues will fill up and packets -will start to be dropped. When such situations arise, it is the job of -the network's Quality of Service (QoS) policy to define _which_ -packets to drop and which ones to prioritize, such that critical -services remain operational. +queuing packets in internal memories. However, if the load is sustained +over long periods of time, queues will fill up and packets will start to +be dropped. When such situations arise, it is the job of the network's +Quality of Service (QoS) policy to define _which_ packets to drop and +which ones to prioritize, such that critical services remain operational. +QoS is configured per interface, under `/interfaces/interface[name='eth0']/qos/`. +Every interface type is accepted; what the hardware can offload varies +and is reported back in the operational datastore, see [Hardware +Offload](#hardware-offload). -## Hardware Forwarded Traffic -The default QoS policy for flows which are offloaded to a switching -ASIC is defined by the hardware defaults of the device in question. +## Terminology + +QoS vocabulary comes from two families of standards. IEEE 802.1Q[^1] +defines priorities and traffic classes for bridged Ethernet, the IETF +Differentiated Services (DiffServ) RFCs define how IP packets are marked. + +**Priority** is the internal value, 0 to 7, that every frame carries +through the system from classification to transmission. IEEE 802.1Q +Annex I names a traffic type for each priority. Higher is better, +except that priority 1 is meant for traffic that may yield to best +effort: + +| Priority | Traffic type | +|---------:|-----------------------| +| 0 | Best Effort (default) | +| 1 | Background | +| 2 | Excellent Effort | +| 3 | Critical Applications | +| 4 | Video | +| 5 | Voice | +| 6 | Internetwork Control | +| 7 | Network Control | +/// table-caption +IEEE 802.1Q-2022 Table I-2, traffic types. +/// + +**PCP and DEI** are the Priority Code Point and Drop Eligible Indicator, +the three-bit priority and one-bit drop flag in a VLAN tag[^2]. A frame +without a VLAN tag has neither. + +**DSCP** is the six-bit Differentiated Services Code Point in the IP +header[^3]. Named codepoints group into _per-hop behaviours_ (PHB), the +forwarding treatment a router or switch gives them[^4]: + +| Name | Codepoints | Reference | +|--------------------------|--------------------------------|----------------| +| Default Forwarding, DF | 0, also written CS0 | RFC 4594 1.5.1 | +| Class Selector, CS1..7 | 8, 16, 24, 32, 40, 48, 56 | RFC 4594 1.5.4 | +| Assured Forwarding, AFxy | 10..14, 18..22, 26..30, 34..38 | RFC 4594 1.5.2 | +| Expedited Forwarding, EF | 46 | RFC 4594 1.5.3 | +/// table-caption +DiffServ per-hop behaviours and their codepoints. +/// + +The class selectors carry the old IP precedence in the top three bits, +so CS_n_ maps naturally to priority _n_. Assured forwarding has four +classes, AF1 to AF4, each with three drop precedences, e.g. AF11, AF12, +AF13. RFC 4594 groups the codepoints into service classes, telephony, +signaling, multimedia and so on, with a recommended treatment for +each[^5]. + +**Traffic class** is a transmit queue on the egress port. A port has 1 +to 8 of them, numbered so that the highest class is served first, and a +table binds each of the eight priorities to one class[^6]. + +**Transmission selection** is how the port picks the next class to send +from[^7]. _Strict priority_ always serves the highest non-empty class. +_Enhanced Transmission Selection_ (ETS)[^8] shares bandwidth between +classes in proportion to a bandwidth share. + +**Stream Reservation (SR) classes** are the two priorities reserved for +time-sensitive audio and video streams in AVB and TSN networks[^9]: +SR class A on priority 3 and SR class B on priority 2. + +**DCB**, Data Center Bridging[^10], is the Linux kernel interface through +which per-port priority tables are programmed into switch hardware. + + +## The Pipeline + +![QoS pipeline](img/qos-pipeline.svg){ width=460 } +/// figure-caption +The QoS pipeline: ingress climbs to forwarding, egress descends to the wire. +/// + +A frame passes six stages, three on the port it arrives on and three on +the port it leaves by: + +1. **Classify** assigns the priority from the PCP of the VLAN tag or the + DSCP of the IP header, depending on what the port trusts. Frames + matching neither get the port's default priority. +2. **Match flows** may override the classification for traffic picked out + by, e.g., source address and port. +3. **Police and gate** limits the rate of a port, of broadcast and + multicast, or of an individual stream. +4. **Map to traffic class** on the egress port binds the eight priorities + to the port's traffic classes. +5. **Transmission selection** decides which class transmits next. +6. **Egress processing** rewrites PCP and DSCP from the priority and caps + the port rate. + +Stages 1, 4 and 5, and the remarking half of stage 6, are configurable +today and described below. The others show where later features +attach; the figure is the intended pipeline, not a promise of hardware +support for every stage. + + +## Ingress Classification + +Configured under `qos ingress`: + +| Setting | Description | +|--------------------|------------------------------------------------| +| `trust` | Trusted fields in order, default `pcp` | +| `default-priority` | Priority when no trusted field matches | +| `pcp-map` | PCP to priority, preset or custom table | +| `dscp-map` | DSCP to priority, preset or custom table | + +The first trusted field that yields a result wins, and a field only +yields a result for frames that carry it. So the five `trust` values +differ on frames that have one field but not the other: + +| Frame | `pcp` | `pcp-dscp` | `dscp` | `dscp-pcp` | `none` | +|-----------------|---------|------------|---------|------------|---------| +| Tagged IP | PCP | PCP | DSCP | DSCP | default | +| Tagged, not IP | PCP | PCP | default | PCP | default | +| Untagged IP | default | DSCP | DSCP | DSCP | default | +| Untagged, other | default | default | default | default | default | +/// table-caption +Which field sets the priority, per frame type and trust setting. +/// + +"default" is `default-priority`, 0 unless set. The default `trust` is +`pcp`: a bridge trusts the tag, as IEEE 802.1Q has it, and classification +is on out of the box for tagged traffic. + +The usual arrangement classifies once, where traffic enters the network: +edge ports are set to `dscp-pcp` or `dscp` and remark both fields from +the result, see [Egress Remarking](#egress-remarking). Every switch +behind them keeps the default and carries the edge's decision through. + +A custom `pcp-map` is keyed by PCP and DEI. A custom `dscp-map` is keyed +by DSCP with a `trusted` flag per entry, so individual codepoints can be +believed while the rest fall through to the default priority. + +### Presets + +| Preset | Map | Mapping | +|--------|------|--------------------------------------------------| +| `ieee` | PCP | 802.1Q default decoding, PCP _n_ to priority _n_ | +| `ietf` | DSCP | RFC 4594 service classes | +| `msb` | DSCP | Top three bits of the DSCP, CS_n_ to _n_ | + +Both maps default to their first preset. The `ietf` preset follows the +RFC 4594 service classes[^5]; codepoints not listed fall through to the +default priority: + +| Service class | DSCP | Priority | +|---------------------------------|------------|---------:| +| Network control | CS6, CS7 | 6, 7 | +| Telephony, signaling | EF, CS5 | 5 | +| Real-time and conferencing | CS4, AF4x | 4 | +| Streaming and broadcast video | CS3, AF3x | 3 | +| Low-latency data, OAM | CS2, AF2x | 2 | +| High-throughput, low-priority | CS1, AF1x | 1 | +| Standard | CS0 | 0 | +/// table-caption +The `ietf` DSCP preset. +/// + +### Examples + +A trunk port trusting DSCP first, then PCP, with the standard maps: + +
admin@example:/config/> edit interface e1 qos ingress
+admin@example:/config/interface/e1/qos/ingress/> set trust dscp-pcp
+admin@example:/config/interface/e1/qos/ingress/> set default-priority 0
+admin@example:/config/interface/e1/qos/ingress/> set dscp-map preset ietf
+admin@example:/config/interface/e1/qos/ingress/> set pcp-map preset ieee
+admin@example:/config/interface/e1/qos/ingress/> leave
+
+ +An access port that believes only EF (46) and AF31 (26) and gives +everything else the port default: + +
admin@example:/config/> edit interface e2 qos ingress
+admin@example:/config/interface/e2/qos/ingress/> set trust dscp
+admin@example:/config/interface/e2/qos/ingress/> set default-priority 0
+admin@example:/config/interface/e2/qos/ingress/> set dscp-map entry 46 priority 5
+admin@example:/config/interface/e2/qos/ingress/> set dscp-map entry 26 priority 3
+admin@example:/config/interface/e2/qos/ingress/> leave
+
+ +Which trust orders a port accepts is hardware dependent and listed in +`qos capabilities supported-trust-order`. An order the port cannot +honour is rejected. On ports whose driver has no ingress classification +support the same rules are applied by the kernel to the traffic it +forwards, see [Hardware Offload](#hardware-offload). + + +## Traffic Classes and Transmission Selection + +Configured under `qos egress`: + +| Setting | Description | +|------------------------------|--------------------------------------| +| `preset` | `ieee` (default) or `ieee-sr` | +| `priority0` .. `priority7` | Custom traffic class per priority | +| `traffic-class ID algorithm` | Transmission selection algorithm | +| `traffic-class ID bandwidth` | Share of a weighted class, percent | + +The first two live under `traffic-class-table`. The number of classes +is not configuration: a port has one class per transmit queue, at most +eight, and a single-queue port has no queue structure to respect and gets +all eight. The count is reported as `max-traffic-classes` and picks the +column of the preset. The mapping is a preset, `ieee` for an ordinary +bridge or `ieee-sr` for ports carrying reserved streams, both described +below, or a custom table where an unset priority falls back to the `ieee` +value. The algorithm is `strict-priority` (default) or +`enhanced-transmission-selection`, the latter sharing what the strict +classes leave in proportion to `bandwidth`, a percentage the weighted +classes of a port sum to 100 with. The scheduler is work-conserving, so +the shares are relative and a class that has nothing to send lends its +share to the others. + +Strict-priority classes must be the highest-numbered ones, with the +weighted classes below them; other layouts are rejected. + +A port with four queues, the top two classes strict, the bottom two +sharing what is left in a 2:1 ratio. No map is set, so the `ieee` +preset supplies Table 8-5's four-class column, `0 0 1 1 2 2 3 3`: + +
admin@example:/config/> edit interface e1 qos egress
+admin@example:/config/interface/e1/qos/egress/> set traffic-class 3 algorithm strict-priority
+admin@example:/config/interface/e1/qos/egress/> set traffic-class 2 algorithm strict-priority
+admin@example:/config/interface/e1/qos/egress/> set traffic-class 1 algorithm enhanced-transmission-selection
+admin@example:/config/interface/e1/qos/egress/> set traffic-class 1 bandwidth 67
+admin@example:/config/interface/e1/qos/egress/> set traffic-class 0 algorithm enhanced-transmission-selection
+admin@example:/config/interface/e1/qos/egress/> set traffic-class 0 bandwidth 33
+admin@example:/config/interface/e1/qos/egress/> leave
+
+ + +### Rate Limit + +A port can be held below its link speed with one token bucket on the +whole port, configured under `qos egress rate-limit`. `rate` is the +sustained rate in bits per second and `burst` how many bytes may leave +at once from a full bucket; left unset, the burst covers ten +milliseconds at the configured rate, never less than one maximum-size +frame. The bucket sits above the traffic classes: it bounds what the +port sends in total, and transmission selection decides which class +gets the bounded rate. + +
admin@example:/config/> edit interface e1 qos egress
+admin@example:/config/interface/e1/qos/egress/> set rate-limit rate 10000000
+admin@example:/config/interface/e1/qos/egress/> leave
+
+ +The limit is rendered as a `tc tbf` root qdisc with the scheduler below +it, so on a port without offload it applies to the frames the CPU +sends, and on a port whose driver takes the bucket to frames the fabric +forwards as well; `offload` under `capabilities` then lists +`rate-limit`. + + +### Egress Remarking + +Configured under `qos egress remark`, both leaves default to `none`: + +| Setting | Description | +|---------|--------------------------------------------------| +| `pcp` | `from-priority` writes PCP and DEI on transmit | +| `dscp` | `from-priority` writes DSCP on transmit | + +PCP is set to the priority, DSCP to the class selector with the same +number, CS0 to CS7. Together with a trust order on the receiving port, +a downstream device then sees this device's classification rather than +the sender's marking. With `none` nothing is rewritten by configuration, +and what a tagged frame leaves with depends on the path it took: frames +the kernel forwards keep the PCP they arrived with, while a switch fabric +encodes the PCP from the frame's priority, as an IEEE 802.1Q bridge +does[^16]. With the default `pcp-map` the two are the same: + +
admin@example:/config/> edit interface e1 qos egress remark
+admin@example:/config/interface/e1/qos/egress/remark/> set pcp from-priority
+admin@example:/config/interface/e1/qos/egress/remark/> set dscp from-priority
+admin@example:/config/interface/e1/qos/egress/remark/> leave
+
+ +Remarking uses the same driver support as ingress classification, see +[Hardware Offload](#hardware-offload). Without it, DSCP is rewritten by +the kernel for the traffic it forwards. PCP is not: the kernel cannot +change a tag's priority without also setting its VLAN ID, so on a port +without driver support, PCP follows priority only where the tag is +created, on VLAN interfaces with `egress-qos pcp from-priority`, see +[VLAN Interfaces](#vlan-interfaces). + + +### Defaults + +An interface without `qos` configuration is fully specified by the +defaults, and every physical port runs them from boot: trust PCP with +the `ieee` preset, default priority 0 for untagged frames, one traffic +class per queue with the `ieee` preset, IEEE 802.1Q-2022 Table 8-5, the +standard's recommendation for ordinary bridges, and strict priority +throughout. Columns are the number of traffic classes on the port: + +| Priority | 2 | 3 | 4 | 5 | 6 | 7 | 8 | +|:---------|--:|--:|--:|--:|--:|--:|--:| +| 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | +| 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | +| 2 | 0 | 0 | 1 | 1 | 2 | 2 | 2 | +| 3 | 0 | 0 | 1 | 1 | 2 | 3 | 3 | +| 4 | 1 | 1 | 2 | 2 | 3 | 4 | 4 | +| 5 | 1 | 1 | 2 | 2 | 3 | 4 | 5 | +| 6 | 1 | 2 | 3 | 3 | 4 | 5 | 6 | +| 7 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | +/// table-caption +IEEE 802.1Q-2022 Table 8-5, recommended priority to traffic class mappings. +/// + +The factory configuration carries no `qos` settings; removing a port's +`qos` container returns it to these defaults. Virtual interfaces, +bridges, VLANs and the like, get a pipeline only when configured. + + +### Stream Reservation Layout + +The `ieee-sr` preset is IEEE 802.1Q-2022 Table 34-1, the recommended +mapping for ports carrying reserved streams. The SR classes, priority 3 +(class A) and priority 2 (class B), map to the _highest_ traffic classes +so they outrank everything else at transmission selection, with best +effort below: + +| Priority | 2 | 3 | 4 | 5 | 6 | 7 | 8 | +|:---------|--:|--:|--:|--:|--:|--:|--:| +| 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | +| 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | +| 2 (SR B) | 1 | 1 | 2 | 3 | 4 | 5 | 6 | +| 3 (SR A) | 1 | 2 | 3 | 4 | 5 | 6 | 7 | +| 4 | 0 | 0 | 1 | 1 | 1 | 1 | 2 | +| 5 | 0 | 0 | 1 | 1 | 1 | 2 | 3 | +| 6 | 0 | 0 | 1 | 2 | 2 | 3 | 4 | +| 7 | 0 | 0 | 1 | 2 | 3 | 4 | 5 | +/// table-caption +IEEE 802.1Q-2022 Table 34-1, priority to traffic class mappings with SR classes. +/// + +Apply it on ports where reserved streams are expected; the credit-based +shaper for the SR classes is a later addition: + +
admin@example:/config/> edit interface e1 qos egress traffic-class-table
+admin@example:/config/…/traffic-class-table/> set preset ieee-sr
+admin@example:/config/…/traffic-class-table/> leave
+
+ + +## Hardware Offload + +The system runs on a wide range of hardware, and offload is best effort. +The same configuration is accepted everywhere; where it ends up differs, +and each port reports it under `qos capabilities`: + +| Capability | Meaning | +|-------------------------|--------------------------------------------| +| `max-traffic-classes` | Traffic classes on the port, eight unless shown | +| `supported-trust-order` | `trust` values the driver accepts | +| `offload` | Stages the driver runs in hardware | + +`supported-trust-order` is absent when the driver has no ingress +classification support. `offload` lists `classification`, `remarking` +`transmission-selection` and `rate-limit` as the driver takes them; a +stage not listed runs in the kernel. + +Each feature maps to one Linux mechanism, and whether it reaches the +hardware depends on the driver implementing the matching hook: + +| Feature | Linux mechanism | Driver hook | Without it | +|------------------------|-----------------|---------------------|----------------------| +| Ingress classification | `dcb app` | `dcbnl` app ops | `tc flower`, software | +| Trust order | `dcb apptrust` | `dcbnl_setapptrust` | Rule order, software | +| Egress remarking | `dcb rewr` | `dcbnl_setrewr` | DSCP only, software | +| Transmission selection | `tc ets` | `TC_SETUP_QDISC_ETS`| software | +| Port rate limit | `tc tbf` | `TC_SETUP_QDISC_TBF`| software | +/// table-caption +QoS features and their Linux backends. +/// + +"Software" means the kernel does the work for every frame the CPU +handles on the port: `tc flower` rules classify what arrives, the `ets` +qdisc schedules what leaves and `pedit` rewrites its DSCP, for locally +originated, routed, and software-bridged traffic alike. On a NIC-based device that is all +traffic. On a switch it excludes frames the fabric forwards port to +port without the CPU, so there it covers routed traffic, traffic to and +from the device itself, and bridging between ports in different switch +domains. + +Driver support in the Linux kernel, as of 6.18: + +| Driver | Classification | Remarking | Traffic classes | +|-------------------------------------|----------------|---------------|-----------------| +| Microchip `sparx5`, `lan966x` | hardware | hardware | hardware | +| DSA `mv88e6xxx`, Marvell LinkStreet | hardware[^15] | hardware[^15] | hardware[^15] | +| Data-center NICs[^12] | software[^14] | DSCP, software| hardware | +| DSA `felix`, `ksz` | software[^14] | DSCP, software| hardware | +| Other NICs and SoC MACs[^13] | software | DSCP, software| software | +/// table-caption +QoS support per driver family. +/// + +On a switch whose driver lacks DCB the fabric keeps classifying +port-to-port traffic by its own defaults while the kernel classifies the +CPU path per configuration. PCP remarking has no software counterpart; +where the driver lacks it the setting is accepted and noted in the system +log. Per-board notes live in the board's `README.md` under `board/`. ### Marvell LinkStreet -This family of devices, sometimes also referred to as _SOHO_, are -managed by the `mv88e6xxx` driver in the Linux kernel. While older -chips in this family where limited to 4 output queues per port, this -documentation is _only_ valid for newer generations with 8 output -queues per port. +This family of switch chips is managed by the `mv88e6xxx` driver in the +Linux kernel. The system carries patches that expose the per-port +classification and remarking tables of the 88E6390 and 88E6393X +generations through DCB, so ingress classification, remarking and the +traffic class table are all offloaded on these chips. This section is +_only_ valid for generations with 8 output queues per port. -![Hardware offloading for Marvell Link Street](img/qos-hw-mvls.svg) +![Marvell LinkStreet offloading](img/qos-hw-mvls.svg){ width=600 } +/// figure-caption +Hardware offloading for Marvell LinkStreet. +/// The picture illustrates packets having their priority determined at -ingress, here interface _e1_ and _e3_. In this example, both packets +ingress, here interface _e1_ and _e3_. In this example, both packets are forwarded to the same outgoing interface (_e2_), subject to output -queueing. The sections below provides more information on these -topics. - -#### Default Policy - -##### Queueing - -Both layer 2 ([VLAN PCP][2]) and layer 3 ([IP DSCP][3]) priority marks -are considered when selecting the output queue of an incoming -frame. PCP to queue mapping is done 1:1. For IP packets, the 3 most -significant bits of the DSCP is used to select the queue: - -| PCP | DSCP | ⇒ | Queue | Weight | -|----:|------:|---|------:|-------:| -| 0 | 0-7 | ⇒ | 0 | 1 | -| 1 | 8-15 | ⇒ | 1 | 2 | -| 2 | 16-23 | ⇒ | 2 | 3 | -| 3 | 24-31 | ⇒ | 3 | 6 | -| 4 | 32-39 | ⇒ | 4 | 12 | -| 5 | 40-47 | ⇒ | 5 | 17 | -| 6 | 48-55 | ⇒ | 6 | 25 | -| 7 | 56-63 | ⇒ | 7 | 33 | - -For packets containing both a VLAN tag and an IP header, PCP priority -takes precedence over DSCP priority. In cases where neither are -available, packets are always assigned to queue 0. - -Each port's set of 8 egress queues operate on a Weighted Round Robin -([WRR][4]) schedule, using the weights listed in the table above. The -sum of all weights adds up to 99, meaning that the weight of any given -queue is roughly equivalent to the percentage of the available -bandwidth reserved for it. - -##### Marking - -Any priority marks available on ingress are left unmodified when the -frame egresses an output port. In the case when an IP packet ingresses -_without_ a VLAN tag, and is to egress _with_ a VLAN tag, its PCP is -set to the 3 most significant bits of it. If no priority information -is available in the frame on ingress (i.e. untagged non-IP), then -packets will egress out of tagged ports with PCP set to 0. +queueing. -## Software Forwarded Traffic +Each port has its own PCP and DSCP tables, so the `pcp-map`, `dscp-map` +and `default-priority` settings apply as configured, and all four trust +orders are accepted. Two hardware details show through: + +- A frame that is both VLAN-tagged and IP always takes its _frame_ + priority, the value written back as PCP on egress, from the tag. The + trust order `dscp-pcp` decides only which field selects the output + queue. +- The PCP of every tagged frame encodes the frame's priority, on one + chip as across a cascade of chips, which only carry the priority + between them. The `remark pcp` setting therefore changes nothing on + these switches; the DEI comes from the frame's color, never from a + table. +- Frames the CPU itself sends, routed or locally originated, are injected + past the tables, so their DSCP is remarked by the kernel instead and + their PCP comes from the VLAN interface settings described below. + +The `traffic-class-table` and the `traffic-class` list apply to hardware +forwarded frames as well: each priority is queued in its traffic class, +the strict classes are served first, and the weighted classes share the +rest by Weighted Round Robin (WRR)[^11] with the `bandwidth` shares as +weights. The `rate-limit` is the port's own egress shaper. Three +hardware details show through: + +- A frame's output queue is chosen where it enters the switch, by the + ingress port's tables, so the `traffic-class-table` is one per switch + chip, not per port. The port configured most recently defines it, + and every port whose own table differs is no longer offloaded: its + `offload` list under `capabilities` leaves out + `transmission-selection`. Give every port the same table. +- The WRR weights are one set per switch chip as well, defined the + same way by the port configured most recently for each weighted + class. Give every port with weighted classes the same shares. +- A frame crossing from one chip to another inside a device carries its + priority between them, as it would between two switches in a network, + and the far chip queues it in the class of that priority. This is the + general rule of QoS in a network: classification at the first ingress + decides the priority, and every hop after it trusts and queues by that + priority. Steer traffic with the ingress `dscp-map` and `pcp-map`; + a `traffic-class-table` other than the identity holds for frames that + enter and leave on the same chip. +- The WRR counts frames where `bandwidth` speaks of bytes, so the shares + hold for traffic of similar frame sizes. -For packets which are processed by a CPU, i.e. typically routed -traffic, and bridged traffic between interfaces that do not belong to -the same hardware switching domain, an [nftables container][1] can be -used to define a QoS policy. +Without any `qos` configuration the class table is IEEE 802.1Q-2022 +Table 8-5 with every class strict, as on any other port. -For VLAN interfaces, Infix provides support for mapping the Priority -Code Point (PCP) to internal priority on ingress, and the reverse on -egress. -![Ingress and Egress Priority mapping for VLAN interfaces](img/qos-vlan-iface.svg) +## VLAN Interfaces +For VLAN interfaces, the system supports mapping the PCP to internal +priority on ingress, and the reverse on egress. This is separate from +the `qos` settings above, which on a VLAN interface govern only its +traffic classes. + +![VLAN priority mapping](img/qos-vlan-iface.svg){ width=600 } +/// figure-caption +Ingress and egress priority mapping for VLAN interfaces. +/// These `ingress-qos` and `egress-qos` settings are done per VLAN, both -defaulting to '0'. The example below shows how to keep the PCP priority +defaulting to '0'. The example below shows how to keep the PCP priority for packets being routed between two VLAN interfaces.
admin@example:/config/> edit interface e1.10
@@ -100,15 +516,56 @@ admin@example:/config/interface/e1.20/> leave
 admin@example:/>
 
-## A complex example -The picture below shows a packet flow being subject both to software -forwarding and hardware offloading. +## Software Forwarded Traffic + +For packets which are processed by a CPU, i.e. typically routed traffic, +and bridged traffic between interfaces that do not belong to the same +hardware switching domain, the traffic class table and transmission +selection above apply in software. For classification and marking +beyond what the `qos` settings offer, an [nftables container][nft] can +be used to define a QoS policy. + +The picture below shows a packet flow subject to both: classified and +queued by the switch fabric on the way in and out, and carrying its +priority through the VLAN interfaces and a container with a traffic +control policy in between. + +![Hardware and software QoS](img/qos-complex.svg){ width=600 } +/// figure-caption +Hardware and software QoS handling. +/// -![Hardware and Software QoS Handling](img/qos-complex.svg) +[nft]: container.md#application-container-nftables -[1]: container.md#application-container-nftables -[2]: https://en.wikipedia.org/wiki/IEEE_802.1Q -[3]: https://en.wikipedia.org/wiki/Differentiated_services -[4]: https://en.wikipedia.org/wiki/Weighted_round_robin +[^1]: IEEE Std 802.1Q-2022, Bridges and Bridged Networks, + , also + +[^2]: +[^3]: RFC 2474, Definition of the Differentiated Services Field, + +[^4]: RFC 4594, Configuration Guidelines for DiffServ Service Classes, + sections 1.4.5 and 1.5, +[^5]: RFC 4594, section 2.3, Service Class Characteristics, + +[^6]: IEEE Std 802.1Q-2022, clause 8.6.6, Queuing frames +[^7]: IEEE Std 802.1Q-2022, clause 8.6.8, Transmission selection +[^8]: +[^9]: +[^10]: , and + the `dcb(8)` manual page +[^11]: +[^12]: Intel `ixgbe`, `i40e`, `ice`, Mellanox `mlx5`, Broadcom `bnxt`, + Chelsio `cxgb4`, Marvell `qede`, HiSilicon `hns3`, Netronome `nfp` +[^13]: E.g. Raspberry Pi `bcmgenet`, MediaTek `mtk_eth_soc`, Intel `igb` + and `e1000`, and `virtio_net` in QEMU +[^14]: These drivers take a DSCP map but not the PCP map or trust order; + the table is programmed as a whole, so it falls back to software +[^15]: 88E6390 and 88E6393X generations, through patches carried by the + system until they land upstream. Older generations classify by + their hardware defaults and are offloaded like `felix` and `ksz`. +[^16]: Clause 6.9.3 of IEEE Std 802.1Q-2022, the PCP encoding table. + The received PCP is only kept because the default tables decode + and encode it to itself; once classification changes the + priority, the transmitted PCP follows. diff --git a/patches/linux/6.18.52/0069-net-dsa-Skip-DCB-default-priority-init-on-unsupporte.patch b/patches/linux/6.18.52/0069-net-dsa-Skip-DCB-default-priority-init-on-unsupporte.patch new file mode 100644 index 000000000..ee419346a --- /dev/null +++ b/patches/linux/6.18.52/0069-net-dsa-Skip-DCB-default-priority-init-on-unsupporte.patch @@ -0,0 +1,45 @@ +From b0b26eb2286e08e25cb49e75f967105ba162f7d3 Mon Sep 17 00:00:00 2001 +From: Joachim Wiberg +Date: Wed, 9 Sep 2026 17:04:21 +0200 +Subject: [PATCH 69/77] net: dsa: Skip DCB default priority init on unsupported + switches +Organization: Wires + +A driver serving several chip generations has one dsa_switch_ops for +all of them, so port_get_default_prio is set even for chips that have +no per-port default priority. Returning -EOPNOTSUPP from it today +fails DCB init, and with it user port registration. + +Treat -EOPNOTSUPP as "no entry", as the DSCP init loop already does. + +Signed-off-by: Joachim Wiberg +--- + net/dsa/user.c | 11 ++++++----- + 1 file changed, 6 insertions(+), 5 deletions(-) + +diff --git a/net/dsa/user.c b/net/dsa/user.c +index c0019d0c1172d..b881c177628ce 100644 +--- a/net/dsa/user.c ++++ b/net/dsa/user.c +@@ -2511,12 +2511,13 @@ static int dsa_user_dcbnl_init(struct net_device *dev) + .priority = prio, + }; + +- if (prio < 0) ++ if (prio >= 0) { ++ err = dcb_ieee_setapp(dev, &app); ++ if (err) ++ return err; ++ } else if (prio != -EOPNOTSUPP) { + return prio; +- +- err = dcb_ieee_setapp(dev, &app); +- if (err) +- return err; ++ } + } + + if (ds->ops->port_get_dscp_prio) { +-- +2.43.0 + diff --git a/patches/linux/6.18.52/0070-net-dsa-Support-the-PCP-APP-selector.patch b/patches/linux/6.18.52/0070-net-dsa-Support-the-PCP-APP-selector.patch new file mode 100644 index 000000000..257168fe5 --- /dev/null +++ b/patches/linux/6.18.52/0070-net-dsa-Support-the-PCP-APP-selector.patch @@ -0,0 +1,172 @@ +From 797dd2550e75613fb6eade3f1244be137a8113b4 Mon Sep 17 00:00:00 2001 +From: Joachim Wiberg +Date: Wed, 9 Sep 2026 17:04:22 +0200 +Subject: [PATCH 70/77] net: dsa: Support the PCP APP selector +Organization: Wires + +The DCB APP table can map PCP and DEI to priority through the +DCB_APP_SEL_PCP selector, but DSA only dispatches the EtherType and +DSCP selectors, so 'dcb app add dev lan1 pcp-prio 5nd:5' fails with +EOPNOTSUPP on any DSA user port. + +Add port_add_pcp_prio, port_del_pcp_prio and port_get_pcp_prio switch +ops and route the selector to them, mirroring the DSCP handling: the +highest priority among the entries for a PCP/DEI pair is programmed, +and the table is pre-populated from hardware at init. The protocol +field carries DEI in bit 3 and PCP in bits 2:0, as the userspace +'dcb' tool encodes it. + +Signed-off-by: Joachim Wiberg +--- + include/net/dsa.h | 6 +++ + net/dsa/user.c | 96 +++++++++++++++++++++++++++++++++++++++++++++++ + 2 files changed, 102 insertions(+) + +diff --git a/include/net/dsa.h b/include/net/dsa.h +index c31d4e910f075..03def1d584c68 100644 +--- a/include/net/dsa.h ++++ b/include/net/dsa.h +@@ -962,6 +962,12 @@ struct dsa_switch_ops { + u8 prio); + int (*port_del_etype_prio)(struct dsa_switch *ds, int port, u16 etype, + u8 prio); ++ int (*port_get_pcp_prio)(struct dsa_switch *ds, int port, u8 pcp, ++ u8 dei); ++ int (*port_add_pcp_prio)(struct dsa_switch *ds, int port, u8 pcp, ++ u8 dei, u8 prio); ++ int (*port_del_pcp_prio)(struct dsa_switch *ds, int port, u8 pcp, ++ u8 dei, u8 prio); + + /* + * Suspend and resume +diff --git a/net/dsa/user.c b/net/dsa/user.c +index b881c177628ce..1060dcff42096 100644 +--- a/net/dsa/user.c ++++ b/net/dsa/user.c +@@ -2340,6 +2340,72 @@ dsa_user_dcbnl_add_dscp_prio(struct net_device *dev, struct dcb_app *app) + return 0; + } + ++/* The protocol of a PCP APP entry is the DEI in bit 3 and the PCP in ++ * bits 2:0, matching the order of the DCB_APP_SEL_PCP table. ++ */ ++#define DSA_DCB_PCP_MAX 16 ++#define DSA_DCB_PCP(proto) ((proto) & 0x7) ++#define DSA_DCB_DEI(proto) (((proto) >> 3) & 0x1) ++ ++static int __maybe_unused ++dsa_user_dcbnl_add_pcp_prio(struct net_device *dev, struct dcb_app *app) ++{ ++ struct dsa_port *dp = dsa_user_to_port(dev); ++ struct dsa_switch *ds = dp->ds; ++ unsigned long mask, new_prio; ++ int err, port = dp->index; ++ ++ if (!ds->ops->port_add_pcp_prio) ++ return -EOPNOTSUPP; ++ ++ if (app->protocol >= DSA_DCB_PCP_MAX) { ++ netdev_err(dev, "PCP APP entry with protocol value %u is invalid\n", ++ app->protocol); ++ return -EINVAL; ++ } ++ ++ err = dcb_ieee_setapp(dev, app); ++ if (err) ++ return err; ++ ++ mask = dcb_ieee_getapp_mask(dev, app); ++ new_prio = __fls(mask); ++ ++ err = ds->ops->port_add_pcp_prio(ds, port, DSA_DCB_PCP(app->protocol), ++ DSA_DCB_DEI(app->protocol), new_prio); ++ if (err) { ++ dcb_ieee_delapp(dev, app); ++ return err; ++ } ++ ++ return 0; ++} ++ ++static int __maybe_unused ++dsa_user_dcbnl_del_pcp_prio(struct net_device *dev, struct dcb_app *app) ++{ ++ struct dsa_port *dp = dsa_user_to_port(dev); ++ struct dsa_switch *ds = dp->ds; ++ int err, port = dp->index; ++ ++ if (!ds->ops->port_del_pcp_prio) ++ return -EOPNOTSUPP; ++ ++ err = dcb_ieee_delapp(dev, app); ++ if (err) ++ return err; ++ ++ err = ds->ops->port_del_pcp_prio(ds, port, DSA_DCB_PCP(app->protocol), ++ DSA_DCB_DEI(app->protocol), ++ app->priority); ++ if (err) { ++ dcb_ieee_setapp(dev, app); ++ return err; ++ } ++ ++ return 0; ++} ++ + static int __maybe_unused + dsa_user_dcbnl_set_etype_prio(struct net_device *dev, struct dcb_app *app) + { +@@ -2382,6 +2448,8 @@ static int __maybe_unused dsa_user_dcbnl_ieee_setapp(struct net_device *dev, + break; + case IEEE_8021QAZ_APP_SEL_DSCP: + return dsa_user_dcbnl_add_dscp_prio(dev, app); ++ case DCB_APP_SEL_PCP: ++ return dsa_user_dcbnl_add_pcp_prio(dev, app); + default: + return -EOPNOTSUPP; + } +@@ -2488,6 +2556,8 @@ static int __maybe_unused dsa_user_dcbnl_ieee_delapp(struct net_device *dev, + break; + case IEEE_8021QAZ_APP_SEL_DSCP: + return dsa_user_dcbnl_del_dscp_prio(dev, app); ++ case DCB_APP_SEL_PCP: ++ return dsa_user_dcbnl_del_pcp_prio(dev, app); + default: + return -EOPNOTSUPP; + } +@@ -2544,6 +2614,32 @@ static int dsa_user_dcbnl_init(struct net_device *dev) + } + } + ++ if (ds->ops->port_get_pcp_prio) { ++ int protocol; ++ ++ for (protocol = 0; protocol < DSA_DCB_PCP_MAX; protocol++) { ++ struct dcb_app app = { ++ .selector = DCB_APP_SEL_PCP, ++ .protocol = protocol, ++ }; ++ int prio; ++ ++ prio = ds->ops->port_get_pcp_prio(ds, port, ++ DSA_DCB_PCP(protocol), ++ DSA_DCB_DEI(protocol)); ++ if (prio == -EOPNOTSUPP) ++ continue; ++ if (prio < 0) ++ return prio; ++ ++ app.priority = prio; ++ ++ err = dcb_ieee_setapp(dev, &app); ++ if (err) ++ return err; ++ } ++ } ++ + return 0; + } + +-- +2.43.0 + diff --git a/patches/linux/6.18.52/0071-net-dsa-Support-DCB-priority-rewrite.patch b/patches/linux/6.18.52/0071-net-dsa-Support-DCB-priority-rewrite.patch new file mode 100644 index 000000000..0fee7bf3e --- /dev/null +++ b/patches/linux/6.18.52/0071-net-dsa-Support-DCB-priority-rewrite.patch @@ -0,0 +1,259 @@ +From c2319721ee7343981dcd0c57fd16a651db865381 Mon Sep 17 00:00:00 2001 +From: Joachim Wiberg +Date: Wed, 9 Sep 2026 17:10:32 +0200 +Subject: [PATCH 71/77] net: dsa: Support DCB priority rewrite +Organization: Wires + +The DCB rewrite table maps a priority back to the PCP and DEI, or the +DSCP, that frames are remarked with on egress. DSA has no +dcbnl_setrewr/delrewr, so 'dcb rewr add' on a user port falls through +to the generic table and succeeds without touching hardware. + +Add port_set_pcp_rewr/port_del_pcp_rewr and the DSCP counterparts, +plus getters to pre-populate the table from hardware at init. A +switch holds one code point per priority and selector, so a new entry +replaces any existing one for that priority, as sparx5 does. + +Signed-off-by: Joachim Wiberg +--- + include/net/dsa.h | 10 +++ + net/dsa/user.c | 188 +++++++++++++++++++++++++++++++++++++++++++++- + 2 files changed, 197 insertions(+), 1 deletion(-) + +diff --git a/include/net/dsa.h b/include/net/dsa.h +index 03def1d584c68..eec90f59b4983 100644 +--- a/include/net/dsa.h ++++ b/include/net/dsa.h +@@ -968,6 +968,16 @@ struct dsa_switch_ops { + u8 dei, u8 prio); + int (*port_del_pcp_prio)(struct dsa_switch *ds, int port, u8 pcp, + u8 dei, u8 prio); ++ int (*port_get_pcp_rewr)(struct dsa_switch *ds, int port, u8 prio, ++ u8 *pcp, u8 *dei); ++ int (*port_set_pcp_rewr)(struct dsa_switch *ds, int port, u8 prio, ++ u8 pcp, u8 dei); ++ int (*port_del_pcp_rewr)(struct dsa_switch *ds, int port, u8 prio); ++ int (*port_get_dscp_rewr)(struct dsa_switch *ds, int port, u8 prio, ++ u8 *dscp); ++ int (*port_set_dscp_rewr)(struct dsa_switch *ds, int port, u8 prio, ++ u8 dscp); ++ int (*port_del_dscp_rewr)(struct dsa_switch *ds, int port, u8 prio); + + /* + * Suspend and resume +diff --git a/net/dsa/user.c b/net/dsa/user.c +index 1060dcff42096..c22fa40ff23a0 100644 +--- a/net/dsa/user.c ++++ b/net/dsa/user.c +@@ -2563,6 +2563,190 @@ static int __maybe_unused dsa_user_dcbnl_ieee_delapp(struct net_device *dev, + } + } + ++/* Egress remarking: the DCB rewrite table maps a priority back to a PCP ++ * and DEI, or to a DSCP. Hardware holds one code point per priority and ++ * selector, so a new entry replaces any existing one for that priority. ++ */ ++static int __maybe_unused ++dsa_user_dcbnl_rewr_validate(struct net_device *dev, struct dcb_app *app) ++{ ++ struct dsa_port *dp = dsa_user_to_port(dev); ++ struct dsa_switch *ds = dp->ds; ++ u16 max; ++ ++ switch (app->selector) { ++ case DCB_APP_SEL_PCP: ++ if (!ds->ops->port_set_pcp_rewr || !ds->ops->port_del_pcp_rewr) ++ return -EOPNOTSUPP; ++ max = DSA_DCB_PCP_MAX; ++ break; ++ case IEEE_8021QAZ_APP_SEL_DSCP: ++ if (!ds->ops->port_set_dscp_rewr || !ds->ops->port_del_dscp_rewr) ++ return -EOPNOTSUPP; ++ max = 64; ++ break; ++ default: ++ return -EOPNOTSUPP; ++ } ++ ++ if (app->protocol >= max || app->priority >= IEEE_8021QAZ_MAX_TCS) { ++ netdev_err(dev, "Rewrite entry %u:%u is invalid\n", ++ app->priority, app->protocol); ++ return -EINVAL; ++ } ++ ++ return 0; ++} ++ ++static int __maybe_unused ++dsa_user_dcbnl_rewr_program(struct net_device *dev, struct dcb_app *app) ++{ ++ struct dsa_port *dp = dsa_user_to_port(dev); ++ struct dsa_switch *ds = dp->ds; ++ int port = dp->index; ++ ++ if (app->selector == DCB_APP_SEL_PCP) ++ return ds->ops->port_set_pcp_rewr(ds, port, app->priority, ++ DSA_DCB_PCP(app->protocol), ++ DSA_DCB_DEI(app->protocol)); ++ ++ return ds->ops->port_set_dscp_rewr(ds, port, app->priority, ++ app->protocol); ++} ++ ++static int __maybe_unused ++dsa_user_dcbnl_rewr_clear(struct net_device *dev, struct dcb_app *app) ++{ ++ struct dsa_port *dp = dsa_user_to_port(dev); ++ struct dsa_switch *ds = dp->ds; ++ int port = dp->index; ++ ++ if (app->selector == DCB_APP_SEL_PCP) ++ return ds->ops->port_del_pcp_rewr(ds, port, app->priority); ++ ++ return ds->ops->port_del_dscp_rewr(ds, port, app->priority); ++} ++ ++/* Protocols currently rewritten for the priority and selector of @app */ ++static u64 __maybe_unused ++dsa_user_dcbnl_rewr_mask(struct net_device *dev, struct dcb_app *app) ++{ ++ struct dcb_rewr_prio_pcp_map pcp_map; ++ struct dcb_ieee_app_prio_map dscp_map; ++ ++ if (app->selector == DCB_APP_SEL_PCP) { ++ dcb_getrewr_prio_pcp_mask_map(dev, &pcp_map); ++ return pcp_map.map[app->priority]; ++ } ++ ++ dcb_getrewr_prio_dscp_mask_map(dev, &dscp_map); ++ return dscp_map.map[app->priority]; ++} ++ ++static int __maybe_unused dsa_user_dcbnl_setrewr(struct net_device *dev, ++ struct dcb_app *app) ++{ ++ struct dcb_app old = *app; ++ bool exists; ++ u64 mask; ++ int err; ++ ++ err = dsa_user_dcbnl_rewr_validate(dev, app); ++ if (err) ++ return err; ++ ++ mask = dsa_user_dcbnl_rewr_mask(dev, app); ++ exists = mask & BIT_ULL(app->protocol); ++ ++ if (!exists) { ++ err = dcb_setrewr(dev, app); ++ if (err) ++ return err; ++ } ++ ++ err = dsa_user_dcbnl_rewr_program(dev, app); ++ if (err) { ++ if (!exists) ++ dcb_delrewr(dev, app); ++ return err; ++ } ++ ++ mask &= ~BIT_ULL(app->protocol); ++ for (old.protocol = 0; mask; old.protocol++, mask >>= 1) ++ if (mask & 1) ++ dcb_delrewr(dev, &old); ++ ++ return 0; ++} ++ ++static int __maybe_unused dsa_user_dcbnl_delrewr(struct net_device *dev, ++ struct dcb_app *app) ++{ ++ int err; ++ ++ err = dsa_user_dcbnl_rewr_validate(dev, app); ++ if (err) ++ return err; ++ ++ err = dcb_delrewr(dev, app); ++ if (err) ++ return err; ++ ++ err = dsa_user_dcbnl_rewr_clear(dev, app); ++ if (err) { ++ dcb_setrewr(dev, app); ++ return err; ++ } ++ ++ return 0; ++} ++ ++static int __maybe_unused dsa_user_dcbnl_rewr_init(struct net_device *dev) ++{ ++ struct dsa_port *dp = dsa_user_to_port(dev); ++ struct dsa_switch *ds = dp->ds; ++ int port = dp->index; ++ int err, prio; ++ ++ for (prio = 0; prio < IEEE_8021QAZ_MAX_TCS; prio++) { ++ struct dcb_app app = { .priority = prio }; ++ u8 pcp, dei, dscp; ++ ++ if (ds->ops->port_get_pcp_rewr) { ++ err = ds->ops->port_get_pcp_rewr(ds, port, prio, ++ &pcp, &dei); ++ if (err && err != -ENOENT && err != -EOPNOTSUPP) ++ return err; ++ ++ if (!err) { ++ app.selector = DCB_APP_SEL_PCP; ++ app.protocol = dei << 3 | pcp; ++ ++ err = dcb_setrewr(dev, &app); ++ if (err) ++ return err; ++ } ++ } ++ ++ if (ds->ops->port_get_dscp_rewr) { ++ err = ds->ops->port_get_dscp_rewr(ds, port, prio, &dscp); ++ if (err && err != -ENOENT && err != -EOPNOTSUPP) ++ return err; ++ ++ if (!err) { ++ app.selector = IEEE_8021QAZ_APP_SEL_DSCP; ++ app.protocol = dscp; ++ ++ err = dcb_setrewr(dev, &app); ++ if (err) ++ return err; ++ } ++ } ++ } ++ ++ return 0; ++} ++ + /* Pre-populate the DCB application priority table with the priorities + * configured during switch setup, which we read from hardware here. + */ +@@ -2640,7 +2824,7 @@ static int dsa_user_dcbnl_init(struct net_device *dev) + } + } + +- return 0; ++ return dsa_user_dcbnl_rewr_init(dev); + } + + static const struct ethtool_ops dsa_user_ethtool_ops = { +@@ -2683,6 +2867,8 @@ static const struct dcbnl_rtnl_ops __maybe_unused dsa_user_dcbnl_ops = { + .ieee_delapp = dsa_user_dcbnl_ieee_delapp, + .dcbnl_setapptrust = dsa_user_dcbnl_set_apptrust, + .dcbnl_getapptrust = dsa_user_dcbnl_get_apptrust, ++ .dcbnl_setrewr = dsa_user_dcbnl_setrewr, ++ .dcbnl_delrewr = dsa_user_dcbnl_delrewr, + }; + + static void dsa_user_get_stats64(struct net_device *dev, +-- +2.43.0 + diff --git a/patches/linux/6.18.52/0072-net-dsa-mv88e6xxx-Add-DCB-priority-classification.patch b/patches/linux/6.18.52/0072-net-dsa-mv88e6xxx-Add-DCB-priority-classification.patch new file mode 100644 index 000000000..b840707bb --- /dev/null +++ b/patches/linux/6.18.52/0072-net-dsa-mv88e6xxx-Add-DCB-priority-classification.patch @@ -0,0 +1,736 @@ +From 7221886d68f43d884fca5b6adf9a9b62b4c8142b Mon Sep 17 00:00:00 2001 +From: Joachim Wiberg +Date: Wed, 9 Sep 2026 17:10:33 +0200 +Subject: [PATCH 72/77] net: dsa: mv88e6xxx: Add DCB priority classification +Organization: Wires + +The 6390 family classifies frames per port from the tag PCP through the +IEEE Priority Mapping Table, from the DSCP through the IP Priority +Mapping Table, and falls back to the port default priority. Which +sources are trusted, and which wins when a frame is both tagged and +IP, is set by InitialPri and TagIfBoth in Port Control 0. None of it +is reachable from userspace, the driver hardcodes identity mappings +and trusts both sources at setup. + +Expose the tables and the trust order through the DSA DCB ops: + + dcb app add dev lan1 default-prio 2 pcp-prio 5nd:5 dscp-prio 46:5 + dcb apptrust set dev lan1 order dscp pcp + +A deleted entry has its Dis*Pri bits set so the port default applies, +and is skipped when the table is read back at init. The frame and +queue priorities are always set to the same value. When a frame is +both tagged and IP the frame priority comes from the tag regardless, +TagIfBoth only selects the source of the queue priority. + +Signed-off-by: Joachim Wiberg +--- + drivers/net/dsa/mv88e6xxx/chip.c | 217 +++++++++++++++++++++++++ + drivers/net/dsa/mv88e6xxx/chip.h | 15 ++ + drivers/net/dsa/mv88e6xxx/port.c | 265 +++++++++++++++++++++++++++++++ + drivers/net/dsa/mv88e6xxx/port.h | 42 +++++ + 4 files changed, 539 insertions(+) + +diff --git a/drivers/net/dsa/mv88e6xxx/chip.c b/drivers/net/dsa/mv88e6xxx/chip.c +index 1d1edf84b224e..bf5d544507c4f 100644 +--- a/drivers/net/dsa/mv88e6xxx/chip.c ++++ b/drivers/net/dsa/mv88e6xxx/chip.c +@@ -4847,6 +4847,12 @@ static const struct mv88e6xxx_ops mv88e6190_ops = { + .port_set_speed_duplex = mv88e6390_port_set_speed_duplex, + .port_max_speed_mode = mv88e6390_port_max_speed_mode, + .port_tag_remap = mv88e6390_port_tag_remap, ++ .port_get_default_prio = mv88e6390_port_get_default_prio, ++ .port_set_default_prio = mv88e6390_port_set_default_prio, ++ .port_get_dscp_prio = mv88e6390_port_get_dscp_prio, ++ .port_set_dscp_prio = mv88e6390_port_set_dscp_prio, ++ .port_get_pcp_prio = mv88e6390_port_get_pcp_prio, ++ .port_set_pcp_prio = mv88e6390_port_set_pcp_prio, + .port_set_policy = mv88e6352_port_set_policy, + .port_set_frame_mode = mv88e6351_port_set_frame_mode, + .port_set_ucast_flood = mv88e6352_port_set_ucast_flood, +@@ -4907,6 +4913,12 @@ static const struct mv88e6xxx_ops mv88e6190x_ops = { + .port_set_speed_duplex = mv88e6390x_port_set_speed_duplex, + .port_max_speed_mode = mv88e6390x_port_max_speed_mode, + .port_tag_remap = mv88e6390_port_tag_remap, ++ .port_get_default_prio = mv88e6390_port_get_default_prio, ++ .port_set_default_prio = mv88e6390_port_set_default_prio, ++ .port_get_dscp_prio = mv88e6390_port_get_dscp_prio, ++ .port_set_dscp_prio = mv88e6390_port_set_dscp_prio, ++ .port_get_pcp_prio = mv88e6390_port_get_pcp_prio, ++ .port_set_pcp_prio = mv88e6390_port_set_pcp_prio, + .port_set_policy = mv88e6352_port_set_policy, + .port_set_frame_mode = mv88e6351_port_set_frame_mode, + .port_set_ucast_flood = mv88e6352_port_set_ucast_flood, +@@ -4967,6 +4979,12 @@ static const struct mv88e6xxx_ops mv88e6191_ops = { + .port_set_speed_duplex = mv88e6390_port_set_speed_duplex, + .port_max_speed_mode = mv88e6390_port_max_speed_mode, + .port_tag_remap = mv88e6390_port_tag_remap, ++ .port_get_default_prio = mv88e6390_port_get_default_prio, ++ .port_set_default_prio = mv88e6390_port_set_default_prio, ++ .port_get_dscp_prio = mv88e6390_port_get_dscp_prio, ++ .port_set_dscp_prio = mv88e6390_port_set_dscp_prio, ++ .port_get_pcp_prio = mv88e6390_port_get_pcp_prio, ++ .port_set_pcp_prio = mv88e6390_port_set_pcp_prio, + .port_set_frame_mode = mv88e6351_port_set_frame_mode, + .port_set_ucast_flood = mv88e6352_port_set_ucast_flood, + .port_set_mcast_flood = mv88e6352_port_set_mcast_flood, +@@ -5131,6 +5149,12 @@ static const struct mv88e6xxx_ops mv88e6290_ops = { + .port_set_speed_duplex = mv88e6390_port_set_speed_duplex, + .port_max_speed_mode = mv88e6390_port_max_speed_mode, + .port_tag_remap = mv88e6390_port_tag_remap, ++ .port_get_default_prio = mv88e6390_port_get_default_prio, ++ .port_set_default_prio = mv88e6390_port_set_default_prio, ++ .port_get_dscp_prio = mv88e6390_port_get_dscp_prio, ++ .port_set_dscp_prio = mv88e6390_port_set_dscp_prio, ++ .port_get_pcp_prio = mv88e6390_port_get_pcp_prio, ++ .port_set_pcp_prio = mv88e6390_port_set_pcp_prio, + .port_set_policy = mv88e6352_port_set_policy, + .port_set_frame_mode = mv88e6351_port_set_frame_mode, + .port_set_ucast_flood = mv88e6352_port_set_ucast_flood, +@@ -5519,6 +5543,12 @@ static const struct mv88e6xxx_ops mv88e6390_ops = { + .port_set_speed_duplex = mv88e6390_port_set_speed_duplex, + .port_max_speed_mode = mv88e6390_port_max_speed_mode, + .port_tag_remap = mv88e6390_port_tag_remap, ++ .port_get_default_prio = mv88e6390_port_get_default_prio, ++ .port_set_default_prio = mv88e6390_port_set_default_prio, ++ .port_get_dscp_prio = mv88e6390_port_get_dscp_prio, ++ .port_set_dscp_prio = mv88e6390_port_set_dscp_prio, ++ .port_get_pcp_prio = mv88e6390_port_get_pcp_prio, ++ .port_set_pcp_prio = mv88e6390_port_set_pcp_prio, + .port_set_policy = mv88e6352_port_set_policy, + .port_set_frame_mode = mv88e6351_port_set_frame_mode, + .port_set_ucast_flood = mv88e6352_port_set_ucast_flood, +@@ -5583,6 +5613,12 @@ static const struct mv88e6xxx_ops mv88e6390x_ops = { + .port_set_speed_duplex = mv88e6390x_port_set_speed_duplex, + .port_max_speed_mode = mv88e6390x_port_max_speed_mode, + .port_tag_remap = mv88e6390_port_tag_remap, ++ .port_get_default_prio = mv88e6390_port_get_default_prio, ++ .port_set_default_prio = mv88e6390_port_set_default_prio, ++ .port_get_dscp_prio = mv88e6390_port_get_dscp_prio, ++ .port_set_dscp_prio = mv88e6390_port_set_dscp_prio, ++ .port_get_pcp_prio = mv88e6390_port_get_pcp_prio, ++ .port_set_pcp_prio = mv88e6390_port_set_pcp_prio, + .port_set_policy = mv88e6352_port_set_policy, + .port_set_frame_mode = mv88e6351_port_set_frame_mode, + .port_set_ucast_flood = mv88e6352_port_set_ucast_flood, +@@ -5646,6 +5682,12 @@ static const struct mv88e6xxx_ops mv88e6393x_ops = { + .port_set_speed_duplex = mv88e6393x_port_set_speed_duplex, + .port_max_speed_mode = mv88e6393x_port_max_speed_mode, + .port_tag_remap = mv88e6390_port_tag_remap, ++ .port_get_default_prio = mv88e6390_port_get_default_prio, ++ .port_set_default_prio = mv88e6390_port_set_default_prio, ++ .port_get_dscp_prio = mv88e6390_port_get_dscp_prio, ++ .port_set_dscp_prio = mv88e6390_port_set_dscp_prio, ++ .port_get_pcp_prio = mv88e6390_port_get_pcp_prio, ++ .port_set_pcp_prio = mv88e6390_port_set_pcp_prio, + .port_set_policy = mv88e6393x_port_set_policy, + .port_set_frame_mode = mv88e6351_port_set_frame_mode, + .port_set_ucast_flood = mv88e6352_port_set_ucast_flood, +@@ -7155,6 +7197,171 @@ static int mv88e6xxx_crosschip_lag_leave(struct dsa_switch *ds, int sw_index, + return err_sync ? : err_pvt; + } + ++static int mv88e6xxx_port_get_default_prio(struct dsa_switch *ds, int port) ++{ ++ struct mv88e6xxx_chip *chip = ds->priv; ++ int err; ++ ++ if (!chip->info->ops->port_get_default_prio) ++ return -EOPNOTSUPP; ++ ++ mv88e6xxx_reg_lock(chip); ++ err = chip->info->ops->port_get_default_prio(chip, port); ++ mv88e6xxx_reg_unlock(chip); ++ ++ return err; ++} ++ ++static int mv88e6xxx_port_set_default_prio(struct dsa_switch *ds, int port, ++ u8 prio) ++{ ++ struct mv88e6xxx_chip *chip = ds->priv; ++ int err; ++ ++ if (!chip->info->ops->port_set_default_prio) ++ return -EOPNOTSUPP; ++ ++ mv88e6xxx_reg_lock(chip); ++ err = chip->info->ops->port_set_default_prio(chip, port, prio); ++ mv88e6xxx_reg_unlock(chip); ++ ++ return err; ++} ++ ++static int mv88e6xxx_port_get_dscp_prio(struct dsa_switch *ds, int port, ++ u8 dscp) ++{ ++ struct mv88e6xxx_chip *chip = ds->priv; ++ int err; ++ ++ if (!chip->info->ops->port_get_dscp_prio) ++ return -EOPNOTSUPP; ++ ++ mv88e6xxx_reg_lock(chip); ++ err = chip->info->ops->port_get_dscp_prio(chip, port, dscp); ++ mv88e6xxx_reg_unlock(chip); ++ ++ return err; ++} ++ ++static int mv88e6xxx_port_add_dscp_prio(struct dsa_switch *ds, int port, ++ u8 dscp, u8 prio) ++{ ++ struct mv88e6xxx_chip *chip = ds->priv; ++ int err; ++ ++ if (!chip->info->ops->port_set_dscp_prio) ++ return -EOPNOTSUPP; ++ ++ mv88e6xxx_reg_lock(chip); ++ err = chip->info->ops->port_set_dscp_prio(chip, port, dscp, prio); ++ mv88e6xxx_reg_unlock(chip); ++ ++ return err; ++} ++ ++/* Only clear the entry if it still holds the priority being deleted, ++ * another APP entry for the same code point may have taken it over. ++ */ ++static int mv88e6xxx_port_del_dscp_prio(struct dsa_switch *ds, int port, ++ u8 dscp, u8 prio) ++{ ++ struct mv88e6xxx_chip *chip = ds->priv; ++ int err; ++ ++ if (!chip->info->ops->port_set_dscp_prio) ++ return -EOPNOTSUPP; ++ ++ mv88e6xxx_reg_lock(chip); ++ err = chip->info->ops->port_get_dscp_prio(chip, port, dscp); ++ if (err == prio) ++ err = chip->info->ops->port_set_dscp_prio(chip, port, dscp, -1); ++ else if (err >= 0 || err == -EOPNOTSUPP) ++ err = 0; ++ mv88e6xxx_reg_unlock(chip); ++ ++ return err; ++} ++ ++static int mv88e6xxx_port_get_pcp_prio(struct dsa_switch *ds, int port, ++ u8 pcp, u8 dei) ++{ ++ struct mv88e6xxx_chip *chip = ds->priv; ++ int err; ++ ++ if (!chip->info->ops->port_get_pcp_prio) ++ return -EOPNOTSUPP; ++ ++ mv88e6xxx_reg_lock(chip); ++ err = chip->info->ops->port_get_pcp_prio(chip, port, pcp, dei); ++ mv88e6xxx_reg_unlock(chip); ++ ++ return err; ++} ++ ++static int mv88e6xxx_port_add_pcp_prio(struct dsa_switch *ds, int port, ++ u8 pcp, u8 dei, u8 prio) ++{ ++ struct mv88e6xxx_chip *chip = ds->priv; ++ int err; ++ ++ if (!chip->info->ops->port_set_pcp_prio) ++ return -EOPNOTSUPP; ++ ++ mv88e6xxx_reg_lock(chip); ++ err = chip->info->ops->port_set_pcp_prio(chip, port, pcp, dei, prio); ++ mv88e6xxx_reg_unlock(chip); ++ ++ return err; ++} ++ ++static int mv88e6xxx_port_del_pcp_prio(struct dsa_switch *ds, int port, ++ u8 pcp, u8 dei, u8 prio) ++{ ++ struct mv88e6xxx_chip *chip = ds->priv; ++ int err; ++ ++ if (!chip->info->ops->port_set_pcp_prio) ++ return -EOPNOTSUPP; ++ ++ mv88e6xxx_reg_lock(chip); ++ err = chip->info->ops->port_get_pcp_prio(chip, port, pcp, dei); ++ if (err == prio) ++ err = chip->info->ops->port_set_pcp_prio(chip, port, pcp, dei, ++ -1); ++ else if (err >= 0 || err == -EOPNOTSUPP) ++ err = 0; ++ mv88e6xxx_reg_unlock(chip); ++ ++ return err; ++} ++ ++static int mv88e6xxx_dcb_set_apptrust(struct dsa_switch *ds, int port, ++ const u8 *sel, int nsel) ++{ ++ struct mv88e6xxx_chip *chip = ds->priv; ++ int err; ++ ++ mv88e6xxx_reg_lock(chip); ++ err = mv88e6xxx_port_set_apptrust(chip, port, sel, nsel); ++ mv88e6xxx_reg_unlock(chip); ++ ++ return err; ++} ++ ++static int mv88e6xxx_dcb_get_apptrust(struct dsa_switch *ds, int port, ++ u8 *sel, int *nsel) ++{ ++ struct mv88e6xxx_chip *chip = ds->priv; ++ int err; ++ ++ mv88e6xxx_reg_lock(chip); ++ err = mv88e6xxx_port_get_apptrust(chip, port, sel, nsel); ++ mv88e6xxx_reg_unlock(chip); ++ ++ return err; ++} ++ + static int mv88e6xxx_port_add_etype_prio(struct dsa_switch *ds, int port, + u16 etype, u8 prio) + { +@@ -7341,6 +7548,16 @@ static const struct dsa_switch_ops mv88e6xxx_switch_ops = { + .crosschip_lag_change = mv88e6xxx_crosschip_lag_change, + .crosschip_lag_join = mv88e6xxx_crosschip_lag_join, + .crosschip_lag_leave = mv88e6xxx_crosschip_lag_leave, ++ .port_get_default_prio = mv88e6xxx_port_get_default_prio, ++ .port_set_default_prio = mv88e6xxx_port_set_default_prio, ++ .port_get_dscp_prio = mv88e6xxx_port_get_dscp_prio, ++ .port_add_dscp_prio = mv88e6xxx_port_add_dscp_prio, ++ .port_del_dscp_prio = mv88e6xxx_port_del_dscp_prio, ++ .port_get_pcp_prio = mv88e6xxx_port_get_pcp_prio, ++ .port_add_pcp_prio = mv88e6xxx_port_add_pcp_prio, ++ .port_del_pcp_prio = mv88e6xxx_port_del_pcp_prio, ++ .port_set_apptrust = mv88e6xxx_dcb_set_apptrust, ++ .port_get_apptrust = mv88e6xxx_dcb_get_apptrust, + .port_add_etype_prio = mv88e6xxx_port_add_etype_prio, + .port_del_etype_prio = mv88e6xxx_port_del_etype_prio, + .port_setup_tc = mv88e6xxx_port_setup_tc, +diff --git a/drivers/net/dsa/mv88e6xxx/chip.h b/drivers/net/dsa/mv88e6xxx/chip.h +index 5d5e7ab63fca3..bb5739cf8cf9d 100644 +--- a/drivers/net/dsa/mv88e6xxx/chip.h ++++ b/drivers/net/dsa/mv88e6xxx/chip.h +@@ -576,6 +576,21 @@ struct mv88e6xxx_ops { + + int (*port_tag_remap)(struct mv88e6xxx_chip *chip, int port); + ++ /* Per-port priority classification, a negative prio disables ++ * the entry so the port default applies. ++ */ ++ int (*port_get_default_prio)(struct mv88e6xxx_chip *chip, int port); ++ int (*port_set_default_prio)(struct mv88e6xxx_chip *chip, int port, ++ u8 prio); ++ int (*port_get_dscp_prio)(struct mv88e6xxx_chip *chip, int port, ++ u8 dscp); ++ int (*port_set_dscp_prio)(struct mv88e6xxx_chip *chip, int port, ++ u8 dscp, int prio); ++ int (*port_get_pcp_prio)(struct mv88e6xxx_chip *chip, int port, ++ u8 pcp, u8 dei); ++ int (*port_set_pcp_prio)(struct mv88e6xxx_chip *chip, int port, ++ u8 pcp, u8 dei, int prio); ++ + int (*port_set_policy)(struct mv88e6xxx_chip *chip, int port, + enum mv88e6xxx_policy_mapping mapping, + enum mv88e6xxx_policy_action action); +diff --git a/drivers/net/dsa/mv88e6xxx/port.c b/drivers/net/dsa/mv88e6xxx/port.c +index 843972077576c..f7991eeb9d17f 100644 +--- a/drivers/net/dsa/mv88e6xxx/port.c ++++ b/drivers/net/dsa/mv88e6xxx/port.c +@@ -9,6 +9,7 @@ + */ + + #include ++#include + #include + #include + #include +@@ -1096,6 +1097,83 @@ int mv88e6xxx_port_set_fid(struct mv88e6xxx_chip *chip, int port, u16 fid) + return 0; + } + ++/* Offset 0x04: Port Control 0, InitialPri and TagIfBoth ++ * ++ * When a frame is both tagged and IP, and both sources are trusted, the ++ * frame priority (FPri) always comes from the tag; TagIfBoth only picks ++ * the source of the queue priority (QPri) and color. ++ */ ++ ++int mv88e6xxx_port_set_apptrust(struct mv88e6xxx_chip *chip, int port, ++ const u8 *sel, int nsel) ++{ ++ u16 reg, bits = 0; ++ int i, err; ++ ++ if (nsel > 2) ++ goto invalid; ++ ++ for (i = 0; i < nsel; i++) { ++ switch (sel[i]) { ++ case DCB_APP_SEL_PCP: ++ if (bits & MV88E6185_PORT_CTL0_USE_TAG) ++ goto invalid; ++ bits |= MV88E6185_PORT_CTL0_USE_TAG; ++ if (i == 0 && nsel == 2) ++ bits |= MV88E6XXX_PORT_CTL0_TAG_IF_BOTH; ++ break; ++ case IEEE_8021QAZ_APP_SEL_DSCP: ++ if (bits & MV88E6185_PORT_CTL0_USE_IP) ++ goto invalid; ++ bits |= MV88E6185_PORT_CTL0_USE_IP; ++ break; ++ default: ++ goto invalid; ++ } ++ } ++ ++ err = mv88e6xxx_port_read(chip, port, MV88E6XXX_PORT_CTL0, ®); ++ if (err) ++ return err; ++ ++ reg &= ~(MV88E6185_PORT_CTL0_USE_TAG | MV88E6185_PORT_CTL0_USE_IP | ++ MV88E6XXX_PORT_CTL0_TAG_IF_BOTH); ++ reg |= bits; ++ ++ return mv88e6xxx_port_write(chip, port, MV88E6XXX_PORT_CTL0, reg); ++ ++invalid: ++ dev_err(chip->dev, "p%d: supported trust orders: pcp, dscp, pcp dscp, dscp pcp\n", ++ port); ++ return -EINVAL; ++} ++ ++int mv88e6xxx_port_get_apptrust(struct mv88e6xxx_chip *chip, int port, ++ u8 *sel, int *nsel) ++{ ++ u16 reg; ++ int err; ++ ++ err = mv88e6xxx_port_read(chip, port, MV88E6XXX_PORT_CTL0, ®); ++ if (err) ++ return err; ++ ++ *nsel = 0; ++ ++ if (reg & MV88E6185_PORT_CTL0_USE_TAG && ++ reg & MV88E6XXX_PORT_CTL0_TAG_IF_BOTH) ++ sel[(*nsel)++] = DCB_APP_SEL_PCP; ++ ++ if (reg & MV88E6185_PORT_CTL0_USE_IP) ++ sel[(*nsel)++] = IEEE_8021QAZ_APP_SEL_DSCP; ++ ++ if (reg & MV88E6185_PORT_CTL0_USE_TAG && ++ !(reg & MV88E6XXX_PORT_CTL0_TAG_IF_BOTH)) ++ sel[(*nsel)++] = DCB_APP_SEL_PCP; ++ ++ return 0; ++} ++ + /* Offset 0x07: Default Port VLAN ID & Priority */ + + int mv88e6xxx_port_get_pvid(struct mv88e6xxx_chip *chip, int port, u16 *pvid) +@@ -1136,6 +1214,51 @@ int mv88e6xxx_port_set_pvid(struct mv88e6xxx_chip *chip, int port, u16 pvid) + return 0; + } + ++/* The default frame priority (FPri) lives here, the default queue ++ * priority (QPri) in Port Control 2. Both are set to the same value. ++ */ ++int mv88e6390_port_get_default_prio(struct mv88e6xxx_chip *chip, int port) ++{ ++ u16 reg; ++ int err; ++ ++ err = mv88e6xxx_port_read(chip, port, MV88E6XXX_PORT_DEFAULT_VLAN, ++ ®); ++ if (err) ++ return err; ++ ++ return FIELD_GET(MV88E6XXX_PORT_DEFAULT_VLAN_FPRI_MASK, reg); ++} ++ ++int mv88e6390_port_set_default_prio(struct mv88e6xxx_chip *chip, int port, ++ u8 prio) ++{ ++ u16 reg; ++ int err; ++ ++ err = mv88e6xxx_port_read(chip, port, MV88E6XXX_PORT_DEFAULT_VLAN, ++ ®); ++ if (err) ++ return err; ++ ++ reg &= ~MV88E6XXX_PORT_DEFAULT_VLAN_FPRI_MASK; ++ reg |= FIELD_PREP(MV88E6XXX_PORT_DEFAULT_VLAN_FPRI_MASK, prio); ++ ++ err = mv88e6xxx_port_write(chip, port, MV88E6XXX_PORT_DEFAULT_VLAN, ++ reg); ++ if (err) ++ return err; ++ ++ err = mv88e6xxx_port_read(chip, port, MV88E6XXX_PORT_CTL2, ®); ++ if (err) ++ return err; ++ ++ reg &= ~MV88E6390_PORT_CTL2_DEFAULT_QPRI_MASK; ++ reg |= FIELD_PREP(MV88E6390_PORT_CTL2_DEFAULT_QPRI_MASK, prio); ++ ++ return mv88e6xxx_port_write(chip, port, MV88E6XXX_PORT_CTL2, reg); ++} ++ + /* Offset 0x08: Port Control 2 Register */ + + static const char * const mv88e6xxx_port_8021q_mode_names[] = { +@@ -1647,6 +1770,80 @@ int mv88e6393x_port_led_read(struct mv88e6xxx_chip *chip, int port, + return 0; + } + ++/* Offset 0x17: IP Priority Mapping Table ++ * ++ * One entry per DSCP, holding the frame and queue priority assigned ++ * to IPv4 and IPv6 frames. A disabled entry leaves the port default. ++ */ ++ ++static int mv88e6390_port_ippmt_write(struct mv88e6xxx_chip *chip, int port, ++ u8 ptr, u16 data) ++{ ++ u16 reg; ++ ++ reg = MV88E6390_PORT_IP_PRIO_MAP_TABLE_UPDATE | ++ FIELD_PREP(MV88E6390_PORT_IP_PRIO_MAP_TABLE_PTR_MASK, ptr) | ++ (data & MV88E6390_PORT_IP_PRIO_MAP_TABLE_DATA_MASK); ++ ++ return mv88e6xxx_port_write(chip, port, ++ MV88E6390_PORT_IP_PRIO_MAP_TABLE, reg); ++} ++ ++static int mv88e6390_port_ippmt_read(struct mv88e6xxx_chip *chip, int port, ++ u8 ptr, u16 *data) ++{ ++ u16 reg; ++ int err; ++ ++ reg = FIELD_PREP(MV88E6390_PORT_IP_PRIO_MAP_TABLE_PTR_MASK, ptr); ++ ++ err = mv88e6xxx_port_write(chip, port, ++ MV88E6390_PORT_IP_PRIO_MAP_TABLE, reg); ++ if (err) ++ return err; ++ ++ err = mv88e6xxx_port_read(chip, port, ++ MV88E6390_PORT_IP_PRIO_MAP_TABLE, ®); ++ if (err) ++ return err; ++ ++ *data = reg & MV88E6390_PORT_IP_PRIO_MAP_TABLE_DATA_MASK; ++ ++ return 0; ++} ++ ++int mv88e6390_port_get_dscp_prio(struct mv88e6xxx_chip *chip, int port, ++ u8 dscp) ++{ ++ u16 data; ++ int err; ++ ++ err = mv88e6390_port_ippmt_read(chip, port, dscp, &data); ++ if (err) ++ return err; ++ ++ if (data & (MV88E6390_PORT_IP_PRIO_MAP_TABLE_DIS_QPRI | ++ MV88E6390_PORT_IP_PRIO_MAP_TABLE_DIS_FPRI)) ++ return -EOPNOTSUPP; ++ ++ return FIELD_GET(MV88E6390_PORT_IP_PRIO_MAP_TABLE_FPRI_MASK, data); ++} ++ ++int mv88e6390_port_set_dscp_prio(struct mv88e6xxx_chip *chip, int port, ++ u8 dscp, int prio) ++{ ++ u16 data; ++ ++ if (prio < 0) ++ data = MV88E6390_PORT_IP_PRIO_MAP_TABLE_DIS_QPRI | ++ MV88E6390_PORT_IP_PRIO_MAP_TABLE_DIS_FPRI; ++ else ++ data = FIELD_PREP(MV88E6390_PORT_IP_PRIO_MAP_TABLE_QPRI_MASK, prio) | ++ FIELD_PREP(MV88E6390_PORT_IP_PRIO_MAP_TABLE_FPRI_MASK, prio); ++ ++ return mv88e6390_port_ippmt_write(chip, port, dscp, data); ++} ++ + /* Offset 0x18: Port IEEE Priority Remapping Registers [0-3] + * Offset 0x19: Port IEEE Priority Remapping Registers [4-7] + */ +@@ -1711,6 +1908,74 @@ int mv88e6390_port_tag_remap(struct mv88e6xxx_chip *chip, int port) + return 0; + } + ++static int mv88e6xxx_port_ieeepmt_read(struct mv88e6xxx_chip *chip, ++ int port, u16 table, u8 ptr, u16 *data) ++{ ++ u16 reg; ++ int err; ++ ++ reg = table | ++ FIELD_PREP(MV88E6390_PORT_IEEE_PRIO_MAP_TABLE_PTR_MASK, ptr); ++ ++ err = mv88e6xxx_port_write(chip, port, ++ MV88E6390_PORT_IEEE_PRIO_MAP_TABLE, reg); ++ if (err) ++ return err; ++ ++ err = mv88e6xxx_port_read(chip, port, ++ MV88E6390_PORT_IEEE_PRIO_MAP_TABLE, ®); ++ if (err) ++ return err; ++ ++ *data = reg & MV88E6390_PORT_IEEE_PRIO_MAP_TABLE_DATA_MASK; ++ ++ return 0; ++} ++ ++static u16 mv88e6390_port_ingress_pcp_table(u8 dei) ++{ ++ if (dei) ++ return MV88E6390_PORT_IEEE_PRIO_MAP_TABLE_INGRESS_PCP_DEI; ++ ++ return MV88E6390_PORT_IEEE_PRIO_MAP_TABLE_INGRESS_PCP; ++} ++ ++int mv88e6390_port_get_pcp_prio(struct mv88e6xxx_chip *chip, int port, ++ u8 pcp, u8 dei) ++{ ++ u16 data; ++ int err; ++ ++ err = mv88e6xxx_port_ieeepmt_read(chip, port, ++ mv88e6390_port_ingress_pcp_table(dei), ++ pcp, &data); ++ if (err) ++ return err; ++ ++ if (data & (MV88E6390_PORT_IEEE_PRIO_MAP_TABLE_DIS_QPRI | ++ MV88E6390_PORT_IEEE_PRIO_MAP_TABLE_DIS_FPRI)) ++ return -EOPNOTSUPP; ++ ++ return FIELD_GET(MV88E6390_PORT_IEEE_PRIO_MAP_TABLE_FPRI_MASK, data); ++} ++ ++int mv88e6390_port_set_pcp_prio(struct mv88e6xxx_chip *chip, int port, ++ u8 pcp, u8 dei, int prio) ++{ ++ u16 data; ++ ++ if (prio < 0) ++ data = MV88E6390_PORT_IEEE_PRIO_MAP_TABLE_DIS_QPRI | ++ MV88E6390_PORT_IEEE_PRIO_MAP_TABLE_DIS_FPRI; ++ else ++ data = FIELD_PREP(MV88E6390_PORT_IEEE_PRIO_MAP_TABLE_QPRI_MASK, prio) | ++ FIELD_PREP(MV88E6390_PORT_IEEE_PRIO_MAP_TABLE_FPRI_MASK, prio); ++ ++ return mv88e6xxx_port_ieeepmt_write(chip, port, ++ mv88e6390_port_ingress_pcp_table(dei), ++ pcp, data); ++} ++ + /* Offset 0x0E: Policy Control Register */ + + static int +diff --git a/drivers/net/dsa/mv88e6xxx/port.h b/drivers/net/dsa/mv88e6xxx/port.h +index 5f733f84b3481..a9f2ed6ca79cc 100644 +--- a/drivers/net/dsa/mv88e6xxx/port.h ++++ b/drivers/net/dsa/mv88e6xxx/port.h +@@ -211,6 +211,7 @@ + + /* Offset 0x07: Default Port VLAN ID & Priority */ + #define MV88E6XXX_PORT_DEFAULT_VLAN 0x07 ++#define MV88E6XXX_PORT_DEFAULT_VLAN_FPRI_MASK 0xe000 + #define MV88E6XXX_PORT_DEFAULT_VLAN_MASK 0x0fff + + /* Offset 0x08: Port Control 2 Register */ +@@ -235,6 +236,7 @@ + #define MV88E6XXX_PORT_CTL2_EGRESS_MONITOR 0x0020 + #define MV88E6XXX_PORT_CTL2_INGRESS_MONITOR 0x0010 + #define MV88E6095_PORT_CTL2_CPU_PORT_MASK 0x000f ++#define MV88E6390_PORT_CTL2_DEFAULT_QPRI_MASK 0x0007 + + /* Offset 0x09: Egress Rate Control */ + #define MV88E6XXX_PORT_EGRESS_RATE_CTL1 0x09 +@@ -449,6 +451,17 @@ + /* Offset 0x16: LED Control (6393X family) */ + #define MV88E6393X_PORT_LED_CONTROL 0x16 + ++/* Offset 0x17: IP Priority Mapping Table */ ++#define MV88E6390_PORT_IP_PRIO_MAP_TABLE 0x17 ++#define MV88E6390_PORT_IP_PRIO_MAP_TABLE_UPDATE 0x8000 ++#define MV88E6390_PORT_IP_PRIO_MAP_TABLE_PTR_MASK 0x7e00 ++#define MV88E6390_PORT_IP_PRIO_MAP_TABLE_DATA_MASK 0x01ff ++#define MV88E6390_PORT_IP_PRIO_MAP_TABLE_YELLOW 0x0100 ++#define MV88E6390_PORT_IP_PRIO_MAP_TABLE_DIS_QPRI 0x0080 ++#define MV88E6390_PORT_IP_PRIO_MAP_TABLE_QPRI_MASK 0x0070 ++#define MV88E6390_PORT_IP_PRIO_MAP_TABLE_DIS_FPRI 0x0008 ++#define MV88E6390_PORT_IP_PRIO_MAP_TABLE_FPRI_MASK 0x0007 ++ + /* Offset 0x18: IEEE Priority Mapping Table */ + #define MV88E6390_PORT_IEEE_PRIO_MAP_TABLE 0x18 + #define MV88E6390_PORT_IEEE_PRIO_MAP_TABLE_UPDATE 0x8000 +@@ -457,11 +470,25 @@ + #define MV88E6390_PORT_IEEE_PRIO_MAP_TABLE_EGRESS_GREEN_PCP 0x1000 + #define MV88E6390_PORT_IEEE_PRIO_MAP_TABLE_EGRESS_YELLOW_PCP 0x2000 + #define MV88E6390_PORT_IEEE_PRIO_MAP_TABLE_EGRESS_AVB_PCP 0x3000 ++#define MV88E6390_PORT_IEEE_PRIO_MAP_TABLE_INGRESS_PCP_DEI 0x4000 + #define MV88E6390_PORT_IEEE_PRIO_MAP_TABLE_EGRESS_GREEN_DSCP 0x5000 + #define MV88E6390_PORT_IEEE_PRIO_MAP_TABLE_EGRESS_YELLOW_DSCP 0x6000 + #define MV88E6390_PORT_IEEE_PRIO_MAP_TABLE_EGRESS_AVB_DSCP 0x7000 + #define MV88E6390_PORT_IEEE_PRIO_MAP_TABLE_PTR_MASK 0x0e00 + #define MV88E6390_PORT_IEEE_PRIO_MAP_TABLE_DATA_MASK 0x01ff ++/* Ingress PCP tables */ ++#define MV88E6390_PORT_IEEE_PRIO_MAP_TABLE_YELLOW 0x0100 ++#define MV88E6390_PORT_IEEE_PRIO_MAP_TABLE_DIS_QPRI 0x0080 ++#define MV88E6390_PORT_IEEE_PRIO_MAP_TABLE_QPRI_MASK 0x0070 ++#define MV88E6390_PORT_IEEE_PRIO_MAP_TABLE_DIS_FPRI 0x0008 ++#define MV88E6390_PORT_IEEE_PRIO_MAP_TABLE_FPRI_MASK 0x0007 ++/* Egress PCP tables */ ++#define MV88E6390_PORT_IEEE_PRIO_MAP_TABLE_DIS_VID 0x0010 ++#define MV88E6390_PORT_IEEE_PRIO_MAP_TABLE_DIS_PCP 0x0008 ++#define MV88E6390_PORT_IEEE_PRIO_MAP_TABLE_PCP_MASK 0x0007 ++/* Egress DSCP tables */ ++#define MV88E6390_PORT_IEEE_PRIO_MAP_TABLE_EN_DSCP 0x0040 ++#define MV88E6390_PORT_IEEE_PRIO_MAP_TABLE_DSCP_MASK 0x003f + + /* Offset 0x18: Port IEEE Priority Remapping Registers (0-3) */ + #define MV88E6095_PORT_IEEE_PRIO_REMAP_0123 0x18 +@@ -543,6 +570,21 @@ int mv88e6xxx_port_set_8021q_mode(struct mv88e6xxx_chip *chip, int port, + u16 mode); + int mv88e6095_port_tag_remap(struct mv88e6xxx_chip *chip, int port); + int mv88e6390_port_tag_remap(struct mv88e6xxx_chip *chip, int port); ++int mv88e6xxx_port_set_apptrust(struct mv88e6xxx_chip *chip, int port, ++ const u8 *sel, int nsel); ++int mv88e6xxx_port_get_apptrust(struct mv88e6xxx_chip *chip, int port, ++ u8 *sel, int *nsel); ++int mv88e6390_port_get_default_prio(struct mv88e6xxx_chip *chip, int port); ++int mv88e6390_port_set_default_prio(struct mv88e6xxx_chip *chip, int port, ++ u8 prio); ++int mv88e6390_port_get_dscp_prio(struct mv88e6xxx_chip *chip, int port, ++ u8 dscp); ++int mv88e6390_port_set_dscp_prio(struct mv88e6xxx_chip *chip, int port, ++ u8 dscp, int prio); ++int mv88e6390_port_get_pcp_prio(struct mv88e6xxx_chip *chip, int port, ++ u8 pcp, u8 dei); ++int mv88e6390_port_set_pcp_prio(struct mv88e6xxx_chip *chip, int port, ++ u8 pcp, u8 dei, int prio); + int mv88e6xxx_port_set_egress_mode(struct mv88e6xxx_chip *chip, int port, + enum mv88e6xxx_egress_mode mode); + int mv88e6085_port_set_frame_mode(struct mv88e6xxx_chip *chip, int port, +-- +2.43.0 + diff --git a/patches/linux/6.18.52/0073-net-dsa-mv88e6xxx-Add-DCB-priority-rewrite.patch b/patches/linux/6.18.52/0073-net-dsa-mv88e6xxx-Add-DCB-priority-rewrite.patch new file mode 100644 index 000000000..b356f568d --- /dev/null +++ b/patches/linux/6.18.52/0073-net-dsa-mv88e6xxx-Add-DCB-priority-rewrite.patch @@ -0,0 +1,433 @@ +From 25811498ff5773e47d29b27b2786082653941dde Mon Sep 17 00:00:00 2001 +From: Joachim Wiberg +Date: Wed, 9 Sep 2026 17:12:40 +0200 +Subject: [PATCH 73/77] net: dsa: mv88e6xxx: Add DCB priority rewrite +Organization: Wires + +On egress the 6390 family looks up the frame priority assigned at +ingress in the IEEE Priority Mapping Table to rewrite the PCP of tagged +frames and, when enabled, the DSCP of IP frames. The driver programs +an identity PCP rewrite at setup and leaves DSCP rewrite off, with no +way to change either. + +Hook the tables up to the DSA DCB rewrite ops: + + dcb rewr add dev lan1 prio-pcp 5:5nd prio-dscp 5:40 + dcb rewr del dev lan1 prio-pcp 5:5nd + +Deleting a PCP entry sets DisPCP for that priority, so tagged frames +keep their PCP. DSCP marking is also gated per port by Update DSCP in +Port Control 3, which is set while any priority has an entry. The +green and yellow tables are kept equal since the color is not used, and +the AVB tables are left as they are. The DEI of a remarked frame +follows its color and cannot be set from the table, so only DEI 0 +entries are accepted. + +Signed-off-by: Joachim Wiberg +--- + drivers/net/dsa/mv88e6xxx/chip.c | 140 +++++++++++++++++++++++++++++++ + drivers/net/dsa/mv88e6xxx/chip.h | 12 +++ + drivers/net/dsa/mv88e6xxx/port.c | 125 +++++++++++++++++++++++++++ + drivers/net/dsa/mv88e6xxx/port.h | 13 +++ + 4 files changed, 290 insertions(+) + +diff --git a/drivers/net/dsa/mv88e6xxx/chip.c b/drivers/net/dsa/mv88e6xxx/chip.c +index bf5d544507c4f..1dac45b89754c 100644 +--- a/drivers/net/dsa/mv88e6xxx/chip.c ++++ b/drivers/net/dsa/mv88e6xxx/chip.c +@@ -4853,6 +4853,10 @@ static const struct mv88e6xxx_ops mv88e6190_ops = { + .port_set_dscp_prio = mv88e6390_port_set_dscp_prio, + .port_get_pcp_prio = mv88e6390_port_get_pcp_prio, + .port_set_pcp_prio = mv88e6390_port_set_pcp_prio, ++ .port_get_pcp_rewr = mv88e6390_port_get_pcp_rewr, ++ .port_set_pcp_rewr = mv88e6390_port_set_pcp_rewr, ++ .port_get_dscp_rewr = mv88e6390_port_get_dscp_rewr, ++ .port_set_dscp_rewr = mv88e6390_port_set_dscp_rewr, + .port_set_policy = mv88e6352_port_set_policy, + .port_set_frame_mode = mv88e6351_port_set_frame_mode, + .port_set_ucast_flood = mv88e6352_port_set_ucast_flood, +@@ -4919,6 +4923,10 @@ static const struct mv88e6xxx_ops mv88e6190x_ops = { + .port_set_dscp_prio = mv88e6390_port_set_dscp_prio, + .port_get_pcp_prio = mv88e6390_port_get_pcp_prio, + .port_set_pcp_prio = mv88e6390_port_set_pcp_prio, ++ .port_get_pcp_rewr = mv88e6390_port_get_pcp_rewr, ++ .port_set_pcp_rewr = mv88e6390_port_set_pcp_rewr, ++ .port_get_dscp_rewr = mv88e6390_port_get_dscp_rewr, ++ .port_set_dscp_rewr = mv88e6390_port_set_dscp_rewr, + .port_set_policy = mv88e6352_port_set_policy, + .port_set_frame_mode = mv88e6351_port_set_frame_mode, + .port_set_ucast_flood = mv88e6352_port_set_ucast_flood, +@@ -4985,6 +4993,10 @@ static const struct mv88e6xxx_ops mv88e6191_ops = { + .port_set_dscp_prio = mv88e6390_port_set_dscp_prio, + .port_get_pcp_prio = mv88e6390_port_get_pcp_prio, + .port_set_pcp_prio = mv88e6390_port_set_pcp_prio, ++ .port_get_pcp_rewr = mv88e6390_port_get_pcp_rewr, ++ .port_set_pcp_rewr = mv88e6390_port_set_pcp_rewr, ++ .port_get_dscp_rewr = mv88e6390_port_get_dscp_rewr, ++ .port_set_dscp_rewr = mv88e6390_port_set_dscp_rewr, + .port_set_frame_mode = mv88e6351_port_set_frame_mode, + .port_set_ucast_flood = mv88e6352_port_set_ucast_flood, + .port_set_mcast_flood = mv88e6352_port_set_mcast_flood, +@@ -5155,6 +5167,10 @@ static const struct mv88e6xxx_ops mv88e6290_ops = { + .port_set_dscp_prio = mv88e6390_port_set_dscp_prio, + .port_get_pcp_prio = mv88e6390_port_get_pcp_prio, + .port_set_pcp_prio = mv88e6390_port_set_pcp_prio, ++ .port_get_pcp_rewr = mv88e6390_port_get_pcp_rewr, ++ .port_set_pcp_rewr = mv88e6390_port_set_pcp_rewr, ++ .port_get_dscp_rewr = mv88e6390_port_get_dscp_rewr, ++ .port_set_dscp_rewr = mv88e6390_port_set_dscp_rewr, + .port_set_policy = mv88e6352_port_set_policy, + .port_set_frame_mode = mv88e6351_port_set_frame_mode, + .port_set_ucast_flood = mv88e6352_port_set_ucast_flood, +@@ -5549,6 +5565,10 @@ static const struct mv88e6xxx_ops mv88e6390_ops = { + .port_set_dscp_prio = mv88e6390_port_set_dscp_prio, + .port_get_pcp_prio = mv88e6390_port_get_pcp_prio, + .port_set_pcp_prio = mv88e6390_port_set_pcp_prio, ++ .port_get_pcp_rewr = mv88e6390_port_get_pcp_rewr, ++ .port_set_pcp_rewr = mv88e6390_port_set_pcp_rewr, ++ .port_get_dscp_rewr = mv88e6390_port_get_dscp_rewr, ++ .port_set_dscp_rewr = mv88e6390_port_set_dscp_rewr, + .port_set_policy = mv88e6352_port_set_policy, + .port_set_frame_mode = mv88e6351_port_set_frame_mode, + .port_set_ucast_flood = mv88e6352_port_set_ucast_flood, +@@ -5619,6 +5639,10 @@ static const struct mv88e6xxx_ops mv88e6390x_ops = { + .port_set_dscp_prio = mv88e6390_port_set_dscp_prio, + .port_get_pcp_prio = mv88e6390_port_get_pcp_prio, + .port_set_pcp_prio = mv88e6390_port_set_pcp_prio, ++ .port_get_pcp_rewr = mv88e6390_port_get_pcp_rewr, ++ .port_set_pcp_rewr = mv88e6390_port_set_pcp_rewr, ++ .port_get_dscp_rewr = mv88e6390_port_get_dscp_rewr, ++ .port_set_dscp_rewr = mv88e6390_port_set_dscp_rewr, + .port_set_policy = mv88e6352_port_set_policy, + .port_set_frame_mode = mv88e6351_port_set_frame_mode, + .port_set_ucast_flood = mv88e6352_port_set_ucast_flood, +@@ -5688,6 +5712,10 @@ static const struct mv88e6xxx_ops mv88e6393x_ops = { + .port_set_dscp_prio = mv88e6390_port_set_dscp_prio, + .port_get_pcp_prio = mv88e6390_port_get_pcp_prio, + .port_set_pcp_prio = mv88e6390_port_set_pcp_prio, ++ .port_get_pcp_rewr = mv88e6390_port_get_pcp_rewr, ++ .port_set_pcp_rewr = mv88e6390_port_set_pcp_rewr, ++ .port_get_dscp_rewr = mv88e6390_port_get_dscp_rewr, ++ .port_set_dscp_rewr = mv88e6390_port_set_dscp_rewr, + .port_set_policy = mv88e6393x_port_set_policy, + .port_set_frame_mode = mv88e6351_port_set_frame_mode, + .port_set_ucast_flood = mv88e6352_port_set_ucast_flood, +@@ -7336,6 +7364,112 @@ static int mv88e6xxx_port_del_pcp_prio(struct dsa_switch *ds, int port, + return err; + } + ++static int mv88e6xxx_port_get_pcp_rewr(struct dsa_switch *ds, int port, ++ u8 prio, u8 *pcp, u8 *dei) ++{ ++ struct mv88e6xxx_chip *chip = ds->priv; ++ int err; ++ ++ if (!chip->info->ops->port_get_pcp_rewr) ++ return -EOPNOTSUPP; ++ ++ *dei = 0; ++ ++ mv88e6xxx_reg_lock(chip); ++ err = chip->info->ops->port_get_pcp_rewr(chip, port, prio, pcp); ++ mv88e6xxx_reg_unlock(chip); ++ ++ return err; ++} ++ ++/* The DEI of a remarked frame follows its color, it cannot be set from ++ * the table, so only the DEI 0 entries can be honored. ++ */ ++static int mv88e6xxx_port_set_pcp_rewr(struct dsa_switch *ds, int port, ++ u8 prio, u8 pcp, u8 dei) ++{ ++ struct mv88e6xxx_chip *chip = ds->priv; ++ int err; ++ ++ if (!chip->info->ops->port_set_pcp_rewr) ++ return -EOPNOTSUPP; ++ ++ if (dei) { ++ dev_err(chip->dev, "p%d: PCP rewrite cannot set DEI\n", port); ++ return -EINVAL; ++ } ++ ++ mv88e6xxx_reg_lock(chip); ++ err = chip->info->ops->port_set_pcp_rewr(chip, port, prio, pcp); ++ mv88e6xxx_reg_unlock(chip); ++ ++ return err; ++} ++ ++static int mv88e6xxx_port_del_pcp_rewr(struct dsa_switch *ds, int port, ++ u8 prio) ++{ ++ struct mv88e6xxx_chip *chip = ds->priv; ++ int err; ++ ++ if (!chip->info->ops->port_set_pcp_rewr) ++ return -EOPNOTSUPP; ++ ++ mv88e6xxx_reg_lock(chip); ++ err = chip->info->ops->port_set_pcp_rewr(chip, port, prio, -1); ++ mv88e6xxx_reg_unlock(chip); ++ ++ return err; ++} ++ ++static int mv88e6xxx_port_get_dscp_rewr(struct dsa_switch *ds, int port, ++ u8 prio, u8 *dscp) ++{ ++ struct mv88e6xxx_chip *chip = ds->priv; ++ int err; ++ ++ if (!chip->info->ops->port_get_dscp_rewr) ++ return -EOPNOTSUPP; ++ ++ mv88e6xxx_reg_lock(chip); ++ err = chip->info->ops->port_get_dscp_rewr(chip, port, prio, dscp); ++ mv88e6xxx_reg_unlock(chip); ++ ++ return err; ++} ++ ++static int mv88e6xxx_port_set_dscp_rewr(struct dsa_switch *ds, int port, ++ u8 prio, u8 dscp) ++{ ++ struct mv88e6xxx_chip *chip = ds->priv; ++ int err; ++ ++ if (!chip->info->ops->port_set_dscp_rewr) ++ return -EOPNOTSUPP; ++ ++ mv88e6xxx_reg_lock(chip); ++ err = chip->info->ops->port_set_dscp_rewr(chip, port, prio, dscp); ++ mv88e6xxx_reg_unlock(chip); ++ ++ return err; ++} ++ ++static int mv88e6xxx_port_del_dscp_rewr(struct dsa_switch *ds, int port, ++ u8 prio) ++{ ++ struct mv88e6xxx_chip *chip = ds->priv; ++ int err; ++ ++ if (!chip->info->ops->port_set_dscp_rewr) ++ return -EOPNOTSUPP; ++ ++ mv88e6xxx_reg_lock(chip); ++ err = chip->info->ops->port_set_dscp_rewr(chip, port, prio, -1); ++ mv88e6xxx_reg_unlock(chip); ++ ++ return err; ++} ++ + static int mv88e6xxx_dcb_set_apptrust(struct dsa_switch *ds, int port, + const u8 *sel, int nsel) + { +@@ -7558,6 +7692,12 @@ static const struct dsa_switch_ops mv88e6xxx_switch_ops = { + .port_del_pcp_prio = mv88e6xxx_port_del_pcp_prio, + .port_set_apptrust = mv88e6xxx_dcb_set_apptrust, + .port_get_apptrust = mv88e6xxx_dcb_get_apptrust, ++ .port_get_pcp_rewr = mv88e6xxx_port_get_pcp_rewr, ++ .port_set_pcp_rewr = mv88e6xxx_port_set_pcp_rewr, ++ .port_del_pcp_rewr = mv88e6xxx_port_del_pcp_rewr, ++ .port_get_dscp_rewr = mv88e6xxx_port_get_dscp_rewr, ++ .port_set_dscp_rewr = mv88e6xxx_port_set_dscp_rewr, ++ .port_del_dscp_rewr = mv88e6xxx_port_del_dscp_rewr, + .port_add_etype_prio = mv88e6xxx_port_add_etype_prio, + .port_del_etype_prio = mv88e6xxx_port_del_etype_prio, + .port_setup_tc = mv88e6xxx_port_setup_tc, +diff --git a/drivers/net/dsa/mv88e6xxx/chip.h b/drivers/net/dsa/mv88e6xxx/chip.h +index bb5739cf8cf9d..c0af84346b897 100644 +--- a/drivers/net/dsa/mv88e6xxx/chip.h ++++ b/drivers/net/dsa/mv88e6xxx/chip.h +@@ -591,6 +591,18 @@ struct mv88e6xxx_ops { + int (*port_set_pcp_prio)(struct mv88e6xxx_chip *chip, int port, + u8 pcp, u8 dei, int prio); + ++ /* Egress remarking from the frame priority, a negative code ++ * point disables it for that priority. ++ */ ++ int (*port_get_pcp_rewr)(struct mv88e6xxx_chip *chip, int port, ++ u8 prio, u8 *pcp); ++ int (*port_set_pcp_rewr)(struct mv88e6xxx_chip *chip, int port, ++ u8 prio, int pcp); ++ int (*port_get_dscp_rewr)(struct mv88e6xxx_chip *chip, int port, ++ u8 prio, u8 *dscp); ++ int (*port_set_dscp_rewr)(struct mv88e6xxx_chip *chip, int port, ++ u8 prio, int dscp); ++ + int (*port_set_policy)(struct mv88e6xxx_chip *chip, int port, + enum mv88e6xxx_policy_mapping mapping, + enum mv88e6xxx_policy_action action); +diff --git a/drivers/net/dsa/mv88e6xxx/port.c b/drivers/net/dsa/mv88e6xxx/port.c +index f7991eeb9d17f..92618049e889f 100644 +--- a/drivers/net/dsa/mv88e6xxx/port.c ++++ b/drivers/net/dsa/mv88e6xxx/port.c +@@ -1976,6 +1976,131 @@ int mv88e6390_port_set_pcp_prio(struct mv88e6xxx_chip *chip, int port, + pcp, data); + } + ++/* Egress remarking uses the frame priority assigned at ingress to look ++ * up the PCP written to tagged frames and the DSCP written to IP frames. ++ * Green and yellow frames have separate tables, both are kept equal as ++ * the driver does not use the color. The AVB tables are left alone. ++ */ ++int mv88e6390_port_get_pcp_rewr(struct mv88e6xxx_chip *chip, int port, ++ u8 prio, u8 *pcp) ++{ ++ u16 data; ++ int err; ++ ++ err = mv88e6xxx_port_ieeepmt_read(chip, port, ++ MV88E6390_PORT_IEEE_PRIO_MAP_TABLE_EGRESS_GREEN_PCP, ++ prio, &data); ++ if (err) ++ return err; ++ ++ if (data & MV88E6390_PORT_IEEE_PRIO_MAP_TABLE_DIS_PCP) ++ return -ENOENT; ++ ++ *pcp = FIELD_GET(MV88E6390_PORT_IEEE_PRIO_MAP_TABLE_PCP_MASK, data); ++ ++ return 0; ++} ++ ++int mv88e6390_port_set_pcp_rewr(struct mv88e6xxx_chip *chip, int port, ++ u8 prio, int pcp) ++{ ++ u16 data; ++ int err; ++ ++ if (pcp < 0) ++ data = MV88E6390_PORT_IEEE_PRIO_MAP_TABLE_DIS_PCP; ++ else ++ data = FIELD_PREP(MV88E6390_PORT_IEEE_PRIO_MAP_TABLE_PCP_MASK, pcp); ++ ++ err = mv88e6xxx_port_ieeepmt_write(chip, port, ++ MV88E6390_PORT_IEEE_PRIO_MAP_TABLE_EGRESS_GREEN_PCP, ++ prio, data); ++ if (err) ++ return err; ++ ++ return mv88e6xxx_port_ieeepmt_write(chip, port, ++ MV88E6390_PORT_IEEE_PRIO_MAP_TABLE_EGRESS_YELLOW_PCP, ++ prio, data); ++} ++ ++/* Port Control 3 gates DSCP marking for the whole port, keep it set as ++ * long as any priority has an enabled entry. ++ */ ++static int mv88e6390_port_update_dscp(struct mv88e6xxx_chip *chip, int port) ++{ ++ u16 table = MV88E6390_PORT_IEEE_PRIO_MAP_TABLE_EGRESS_GREEN_DSCP; ++ bool enable = false; ++ u16 data, reg; ++ int err, prio; ++ ++ for (prio = 0; prio < 8 && !enable; prio++) { ++ err = mv88e6xxx_port_ieeepmt_read(chip, port, table, prio, ++ &data); ++ if (err) ++ return err; ++ ++ enable = data & MV88E6390_PORT_IEEE_PRIO_MAP_TABLE_EN_DSCP; ++ } ++ ++ err = mv88e6xxx_port_read(chip, port, MV88E6390_PORT_CTL3, ®); ++ if (err) ++ return err; ++ ++ if (enable) ++ reg |= MV88E6390_PORT_CTL3_UPDATE_DSCP; ++ else ++ reg &= ~MV88E6390_PORT_CTL3_UPDATE_DSCP; ++ ++ return mv88e6xxx_port_write(chip, port, MV88E6390_PORT_CTL3, reg); ++} ++ ++int mv88e6390_port_get_dscp_rewr(struct mv88e6xxx_chip *chip, int port, ++ u8 prio, u8 *dscp) ++{ ++ u16 data; ++ int err; ++ ++ err = mv88e6xxx_port_ieeepmt_read(chip, port, ++ MV88E6390_PORT_IEEE_PRIO_MAP_TABLE_EGRESS_GREEN_DSCP, ++ prio, &data); ++ if (err) ++ return err; ++ ++ if (!(data & MV88E6390_PORT_IEEE_PRIO_MAP_TABLE_EN_DSCP)) ++ return -ENOENT; ++ ++ *dscp = FIELD_GET(MV88E6390_PORT_IEEE_PRIO_MAP_TABLE_DSCP_MASK, data); ++ ++ return 0; ++} ++ ++int mv88e6390_port_set_dscp_rewr(struct mv88e6xxx_chip *chip, int port, ++ u8 prio, int dscp) ++{ ++ u16 data; ++ int err; ++ ++ if (dscp < 0) ++ data = 0; ++ else ++ data = MV88E6390_PORT_IEEE_PRIO_MAP_TABLE_EN_DSCP | ++ FIELD_PREP(MV88E6390_PORT_IEEE_PRIO_MAP_TABLE_DSCP_MASK, dscp); ++ ++ err = mv88e6xxx_port_ieeepmt_write(chip, port, ++ MV88E6390_PORT_IEEE_PRIO_MAP_TABLE_EGRESS_GREEN_DSCP, ++ prio, data); ++ if (err) ++ return err; ++ ++ err = mv88e6xxx_port_ieeepmt_write(chip, port, ++ MV88E6390_PORT_IEEE_PRIO_MAP_TABLE_EGRESS_YELLOW_DSCP, ++ prio, data); ++ if (err) ++ return err; ++ ++ return mv88e6390_port_update_dscp(chip, port); ++} ++ + /* Offset 0x0E: Policy Control Register */ + + static int +diff --git a/drivers/net/dsa/mv88e6xxx/port.h b/drivers/net/dsa/mv88e6xxx/port.h +index a9f2ed6ca79cc..ef5028707ec5a 100644 +--- a/drivers/net/dsa/mv88e6xxx/port.h ++++ b/drivers/net/dsa/mv88e6xxx/port.h +@@ -490,6 +490,11 @@ + #define MV88E6390_PORT_IEEE_PRIO_MAP_TABLE_EN_DSCP 0x0040 + #define MV88E6390_PORT_IEEE_PRIO_MAP_TABLE_DSCP_MASK 0x003f + ++/* Offset 0x19: Port Control 3 */ ++#define MV88E6390_PORT_CTL3 0x19 ++#define MV88E6390_PORT_CTL3_UPDATE_DSCP 0x0020 ++#define MV88E6390_PORT_CTL3_UPDATE_DEI 0x0008 ++ + /* Offset 0x18: Port IEEE Priority Remapping Registers (0-3) */ + #define MV88E6095_PORT_IEEE_PRIO_REMAP_0123 0x18 + +@@ -585,6 +590,14 @@ int mv88e6390_port_get_pcp_prio(struct mv88e6xxx_chip *chip, int port, + u8 pcp, u8 dei); + int mv88e6390_port_set_pcp_prio(struct mv88e6xxx_chip *chip, int port, + u8 pcp, u8 dei, int prio); ++int mv88e6390_port_get_pcp_rewr(struct mv88e6xxx_chip *chip, int port, ++ u8 prio, u8 *pcp); ++int mv88e6390_port_set_pcp_rewr(struct mv88e6xxx_chip *chip, int port, ++ u8 prio, int pcp); ++int mv88e6390_port_get_dscp_rewr(struct mv88e6xxx_chip *chip, int port, ++ u8 prio, u8 *dscp); ++int mv88e6390_port_set_dscp_rewr(struct mv88e6xxx_chip *chip, int port, ++ u8 prio, int dscp); + int mv88e6xxx_port_set_egress_mode(struct mv88e6xxx_chip *chip, int port, + enum mv88e6xxx_egress_mode mode); + int mv88e6085_port_set_frame_mode(struct mv88e6xxx_chip *chip, int port, +-- +2.43.0 + diff --git a/patches/linux/6.18.52/0074-net-dsa-mv88e6xxx-Queue-frames-by-the-mqprio-map.patch b/patches/linux/6.18.52/0074-net-dsa-mv88e6xxx-Queue-frames-by-the-mqprio-map.patch new file mode 100644 index 000000000..d6d2b59ed --- /dev/null +++ b/patches/linux/6.18.52/0074-net-dsa-mv88e6xxx-Queue-frames-by-the-mqprio-map.patch @@ -0,0 +1,398 @@ +From 48aee1f644470643a38cf236ae913fe08601010e Mon Sep 17 00:00:00 2001 +From: Joachim Wiberg +Date: Wed, 9 Sep 2026 18:31:30 +0200 +Subject: [PATCH 74/77] net: dsa: mv88e6xxx: Queue frames by the mqprio map +Organization: Wires + +The mqprio offload only sets the netdev traffic class layout. The +ingress tables assign the queue priority (QPri) equal to the frame +priority, so switched frames always land in the queue of their +priority number, whatever map 'tc qdisc add ... mqprio' installed. +Frames sent by the CPU do follow the map, since the DSA tag carries +the traffic class of the transmit queue. + +A frame's queue is chosen where it enters, by the ingress port's +tables, not where it leaves. A map installed on one port can only +take effect for switched frames if every port classifies by it, so +the chip has one map: the most recent request defines it, and it is +programmed into the PCP, DSCP and default priority entries of every +user port. Each priority lands in the first queue of its traffic +class. Ports that asked for a different map are not refused, since +ports changing map one at a time would refuse each other, but they +are no longer queued by their own map, which drivers of qdiscs that +report offload can tell. The identity map is restored when no port +asks for one. + +Signed-off-by: Joachim Wiberg +--- + drivers/net/dsa/mv88e6xxx/chip.c | 109 ++++++++++++++++++++++++++++++- + drivers/net/dsa/mv88e6xxx/chip.h | 14 ++++ + drivers/net/dsa/mv88e6xxx/port.c | 66 +++++++++++++++++-- + drivers/net/dsa/mv88e6xxx/port.h | 1 + + 4 files changed, 183 insertions(+), 7 deletions(-) + +diff --git a/drivers/net/dsa/mv88e6xxx/chip.c b/drivers/net/dsa/mv88e6xxx/chip.c +index 1dac45b89754c..673f8b7523a08 100644 +--- a/drivers/net/dsa/mv88e6xxx/chip.c ++++ b/drivers/net/dsa/mv88e6xxx/chip.c +@@ -1694,6 +1694,9 @@ static int mv88e6xxx_pot_setup(struct mv88e6xxx_chip *chip) + for (i = 0; i < ARRAY_SIZE(chip->qpri_po); i++) + refcount_set(&chip->qpri_po[i].refcnt, 0); + ++ for (i = 0; i < ARRAY_SIZE(chip->qpri); i++) ++ chip->qpri[i] = i; ++ + if (chip->info->ops->pot_clear) + return chip->info->ops->pot_clear(chip); + +@@ -3398,7 +3401,7 @@ static int mv88e6xxx_setup_port(struct mv88e6xxx_chip *chip, int port) + struct fwnode_handle *port_fwnode; + struct dsa_switch *ds = chip->ds; + struct mv88e6xxx_port *p; +- int err; ++ int err, i; + u16 reg; + u32 val; + +@@ -3600,6 +3603,10 @@ static int mv88e6xxx_setup_port(struct mv88e6xxx_chip *chip, int port) + return err; + } + ++ for (i = 0; i < ARRAY_SIZE(chip->ports[port].qpri); i++) ++ chip->ports[port].qpri[i] = i; ++ chip->ports[port].qmap = false; ++ + if (chip->info->ops->port_tag_remap) { + err = chip->info->ops->port_tag_remap(chip, port); + if (err) +@@ -4853,6 +4860,7 @@ static const struct mv88e6xxx_ops mv88e6190_ops = { + .port_set_dscp_prio = mv88e6390_port_set_dscp_prio, + .port_get_pcp_prio = mv88e6390_port_get_pcp_prio, + .port_set_pcp_prio = mv88e6390_port_set_pcp_prio, ++ .port_sync_qpri = mv88e6390_port_sync_qpri, + .port_get_pcp_rewr = mv88e6390_port_get_pcp_rewr, + .port_set_pcp_rewr = mv88e6390_port_set_pcp_rewr, + .port_get_dscp_rewr = mv88e6390_port_get_dscp_rewr, +@@ -4923,6 +4931,7 @@ static const struct mv88e6xxx_ops mv88e6190x_ops = { + .port_set_dscp_prio = mv88e6390_port_set_dscp_prio, + .port_get_pcp_prio = mv88e6390_port_get_pcp_prio, + .port_set_pcp_prio = mv88e6390_port_set_pcp_prio, ++ .port_sync_qpri = mv88e6390_port_sync_qpri, + .port_get_pcp_rewr = mv88e6390_port_get_pcp_rewr, + .port_set_pcp_rewr = mv88e6390_port_set_pcp_rewr, + .port_get_dscp_rewr = mv88e6390_port_get_dscp_rewr, +@@ -4993,6 +5002,7 @@ static const struct mv88e6xxx_ops mv88e6191_ops = { + .port_set_dscp_prio = mv88e6390_port_set_dscp_prio, + .port_get_pcp_prio = mv88e6390_port_get_pcp_prio, + .port_set_pcp_prio = mv88e6390_port_set_pcp_prio, ++ .port_sync_qpri = mv88e6390_port_sync_qpri, + .port_get_pcp_rewr = mv88e6390_port_get_pcp_rewr, + .port_set_pcp_rewr = mv88e6390_port_set_pcp_rewr, + .port_get_dscp_rewr = mv88e6390_port_get_dscp_rewr, +@@ -5167,6 +5177,7 @@ static const struct mv88e6xxx_ops mv88e6290_ops = { + .port_set_dscp_prio = mv88e6390_port_set_dscp_prio, + .port_get_pcp_prio = mv88e6390_port_get_pcp_prio, + .port_set_pcp_prio = mv88e6390_port_set_pcp_prio, ++ .port_sync_qpri = mv88e6390_port_sync_qpri, + .port_get_pcp_rewr = mv88e6390_port_get_pcp_rewr, + .port_set_pcp_rewr = mv88e6390_port_set_pcp_rewr, + .port_get_dscp_rewr = mv88e6390_port_get_dscp_rewr, +@@ -5565,6 +5576,7 @@ static const struct mv88e6xxx_ops mv88e6390_ops = { + .port_set_dscp_prio = mv88e6390_port_set_dscp_prio, + .port_get_pcp_prio = mv88e6390_port_get_pcp_prio, + .port_set_pcp_prio = mv88e6390_port_set_pcp_prio, ++ .port_sync_qpri = mv88e6390_port_sync_qpri, + .port_get_pcp_rewr = mv88e6390_port_get_pcp_rewr, + .port_set_pcp_rewr = mv88e6390_port_set_pcp_rewr, + .port_get_dscp_rewr = mv88e6390_port_get_dscp_rewr, +@@ -5639,6 +5651,7 @@ static const struct mv88e6xxx_ops mv88e6390x_ops = { + .port_set_dscp_prio = mv88e6390_port_set_dscp_prio, + .port_get_pcp_prio = mv88e6390_port_get_pcp_prio, + .port_set_pcp_prio = mv88e6390_port_set_pcp_prio, ++ .port_sync_qpri = mv88e6390_port_sync_qpri, + .port_get_pcp_rewr = mv88e6390_port_get_pcp_rewr, + .port_set_pcp_rewr = mv88e6390_port_set_pcp_rewr, + .port_get_dscp_rewr = mv88e6390_port_get_dscp_rewr, +@@ -5712,6 +5725,7 @@ static const struct mv88e6xxx_ops mv88e6393x_ops = { + .port_set_dscp_prio = mv88e6390_port_set_dscp_prio, + .port_get_pcp_prio = mv88e6390_port_get_pcp_prio, + .port_set_pcp_prio = mv88e6390_port_set_pcp_prio, ++ .port_sync_qpri = mv88e6390_port_sync_qpri, + .port_get_pcp_rewr = mv88e6390_port_get_pcp_rewr, + .port_set_pcp_rewr = mv88e6390_port_set_pcp_rewr, + .port_get_dscp_rewr = mv88e6390_port_get_dscp_rewr, +@@ -7551,12 +7565,96 @@ static int mv88e6xxx_port_del_etype_prio(struct dsa_switch *ds, int port, + return err; + } + ++/* Program @qpri into the tables of every user port, if it is news */ ++static int mv88e6xxx_sync_qpri(struct mv88e6xxx_chip *chip, const u8 *qpri) ++{ ++ int port, err; ++ ++ if (!memcmp(qpri, chip->qpri, sizeof(chip->qpri))) ++ return 0; ++ ++ memcpy(chip->qpri, qpri, sizeof(chip->qpri)); ++ ++ if (!chip->info->ops->port_sync_qpri) ++ return 0; ++ ++ for (port = 0; port < mv88e6xxx_num_ports(chip); port++) { ++ if (!dsa_is_user_port(chip->ds, port)) ++ continue; ++ ++ err = chip->info->ops->port_sync_qpri(chip, port); ++ if (err) ++ return err; ++ } ++ ++ return 0; ++} ++ ++/* Whether the chip queues by the map this port's qdisc asked for */ ++static bool mv88e6xxx_port_qpri_in_use(struct mv88e6xxx_chip *chip, int port) ++{ ++ struct mv88e6xxx_port *mp = &chip->ports[port]; ++ ++ return mp->qmap && !memcmp(mp->qpri, chip->qpri, sizeof(mp->qpri)); ++} ++ ++/* Take the queue map a port's qdisc asks for, or drop it with NULL. The ++ * queue is chosen where a frame enters, by that port's tables, so the ++ * chip has one map: the most recent request defines it, and a port ++ * whose request differs is no longer offloaded, see ++ * mv88e6xxx_port_qpri_in_use(). The identity map is restored when no ++ * port asks for one. ++ */ ++static int mv88e6xxx_port_set_qpri(struct mv88e6xxx_chip *chip, int port, ++ const u8 *qpri) ++{ ++ struct mv88e6xxx_port *mp = &chip->ports[port]; ++ int other, prio, err; ++ u8 map[8]; ++ ++ if (qpri) { ++ for (prio = 0; prio < ARRAY_SIZE(map); prio++) ++ map[prio] = min_t(u8, qpri[prio], 7); ++ ++ for (other = 0; other < mv88e6xxx_num_ports(chip); other++) { ++ if (other != port && mv88e6xxx_port_qpri_in_use(chip, other) && ++ memcmp(map, chip->qpri, sizeof(map))) { ++ dev_info(chip->dev, "p%d: queue map replaces the one p%d asked for, the chip has one\n", ++ port, other); ++ break; ++ } ++ } ++ ++ memcpy(mp->qpri, map, sizeof(mp->qpri)); ++ mp->qmap = true; ++ } else { ++ mp->qmap = false; ++ ++ for (prio = 0; prio < ARRAY_SIZE(map); prio++) ++ map[prio] = prio; ++ ++ for (other = 0; other < mv88e6xxx_num_ports(chip); other++) { ++ if (chip->ports[other].qmap) { ++ memcpy(map, chip->qpri, sizeof(map)); ++ break; ++ } ++ } ++ } ++ ++ mv88e6xxx_reg_lock(chip); ++ err = mv88e6xxx_sync_qpri(chip, map); ++ mv88e6xxx_reg_unlock(chip); ++ ++ return err; ++} ++ + static int mv88e6xxx_qos_port_mqprio(struct mv88e6xxx_chip *chip, int port, + struct tc_mqprio_qopt_offload *mqprio) + { + struct net_device *dev = dsa_to_port(chip->ds, port)->user; + struct tc_mqprio_qopt *qopt = &mqprio->qopt; +- int tc, err = 0, num_txq = 0; ++ int tc, prio, err = 0, num_txq = 0; ++ u8 qpri[8]; + + if (!qopt->num_tc) + goto out_reset; +@@ -7577,10 +7675,15 @@ static int mv88e6xxx_qos_port_mqprio(struct mv88e6xxx_chip *chip, int port, + if (err) + goto out_reset; + +- return 0; ++ /* The first queue of the priority's traffic class */ ++ for (prio = 0; prio < ARRAY_SIZE(qpri); prio++) ++ qpri[prio] = qopt->offset[qopt->prio_tc_map[prio]]; ++ ++ return mv88e6xxx_port_set_qpri(chip, port, qpri); + + out_reset: + netdev_reset_tc(dev); ++ mv88e6xxx_port_set_qpri(chip, port, NULL); + return err; + + } +diff --git a/drivers/net/dsa/mv88e6xxx/chip.h b/drivers/net/dsa/mv88e6xxx/chip.h +index c0af84346b897..f0736aeebbb8a 100644 +--- a/drivers/net/dsa/mv88e6xxx/chip.h ++++ b/drivers/net/dsa/mv88e6xxx/chip.h +@@ -306,6 +306,12 @@ struct mv88e6xxx_port { + refcount_t refcnt; + u16 proto; + } etype; ++ ++ /* Egress queue per frame priority this port's qdisc asked for, ++ * meaningful while qmap is set ++ */ ++ bool qmap; ++ u8 qpri[8]; + }; + + enum mv88e6xxx_region_id { +@@ -465,6 +471,13 @@ struct mv88e6xxx_chip { + + /* Queue priority overrides */ + struct mv88e6xxx_po qpri_po[16]; ++ ++ /* Egress queue per frame priority. A frame's queue is chosen ++ * where it enters, by that port's tables, so the map is one per ++ * chip, programmed into every user port, and follows the port ++ * that asked most recently. ++ */ ++ u8 qpri[8]; + }; + + struct mv88e6xxx_bus_ops { +@@ -590,6 +603,7 @@ struct mv88e6xxx_ops { + u8 pcp, u8 dei); + int (*port_set_pcp_prio)(struct mv88e6xxx_chip *chip, int port, + u8 pcp, u8 dei, int prio); ++ int (*port_sync_qpri)(struct mv88e6xxx_chip *chip, int port); + + /* Egress remarking from the frame priority, a negative code + * point disables it for that priority. +diff --git a/drivers/net/dsa/mv88e6xxx/port.c b/drivers/net/dsa/mv88e6xxx/port.c +index 92618049e889f..255f6229a13af 100644 +--- a/drivers/net/dsa/mv88e6xxx/port.c ++++ b/drivers/net/dsa/mv88e6xxx/port.c +@@ -1214,8 +1214,18 @@ int mv88e6xxx_port_set_pvid(struct mv88e6xxx_chip *chip, int port, u16 pvid) + return 0; + } + ++/* Frames are queued by their queue priority (QPri), which the ingress ++ * tables assign alongside the frame priority (FPri). The chip's queue ++ * map decides which queue a priority lands in; identity until one is ++ * set. ++ */ ++static u8 mv88e6xxx_qpri(struct mv88e6xxx_chip *chip, u8 prio) ++{ ++ return chip->qpri[prio & 0x7]; ++} ++ + /* The default frame priority (FPri) lives here, the default queue +- * priority (QPri) in Port Control 2. Both are set to the same value. ++ * priority (QPri) in Port Control 2. + */ + int mv88e6390_port_get_default_prio(struct mv88e6xxx_chip *chip, int port) + { +@@ -1254,7 +1264,8 @@ int mv88e6390_port_set_default_prio(struct mv88e6xxx_chip *chip, int port, + return err; + + reg &= ~MV88E6390_PORT_CTL2_DEFAULT_QPRI_MASK; +- reg |= FIELD_PREP(MV88E6390_PORT_CTL2_DEFAULT_QPRI_MASK, prio); ++ reg |= FIELD_PREP(MV88E6390_PORT_CTL2_DEFAULT_QPRI_MASK, ++ mv88e6xxx_qpri(chip, prio)); + + return mv88e6xxx_port_write(chip, port, MV88E6XXX_PORT_CTL2, reg); + } +@@ -1838,7 +1849,8 @@ int mv88e6390_port_set_dscp_prio(struct mv88e6xxx_chip *chip, int port, + data = MV88E6390_PORT_IP_PRIO_MAP_TABLE_DIS_QPRI | + MV88E6390_PORT_IP_PRIO_MAP_TABLE_DIS_FPRI; + else +- data = FIELD_PREP(MV88E6390_PORT_IP_PRIO_MAP_TABLE_QPRI_MASK, prio) | ++ data = FIELD_PREP(MV88E6390_PORT_IP_PRIO_MAP_TABLE_QPRI_MASK, ++ mv88e6xxx_qpri(chip, prio)) | + FIELD_PREP(MV88E6390_PORT_IP_PRIO_MAP_TABLE_FPRI_MASK, prio); + + return mv88e6390_port_ippmt_write(chip, port, dscp, data); +@@ -1968,7 +1980,8 @@ int mv88e6390_port_set_pcp_prio(struct mv88e6xxx_chip *chip, int port, + data = MV88E6390_PORT_IEEE_PRIO_MAP_TABLE_DIS_QPRI | + MV88E6390_PORT_IEEE_PRIO_MAP_TABLE_DIS_FPRI; + else +- data = FIELD_PREP(MV88E6390_PORT_IEEE_PRIO_MAP_TABLE_QPRI_MASK, prio) | ++ data = FIELD_PREP(MV88E6390_PORT_IEEE_PRIO_MAP_TABLE_QPRI_MASK, ++ mv88e6xxx_qpri(chip, prio)) | + FIELD_PREP(MV88E6390_PORT_IEEE_PRIO_MAP_TABLE_FPRI_MASK, prio); + + return mv88e6xxx_port_ieeepmt_write(chip, port, +@@ -1976,6 +1989,51 @@ int mv88e6390_port_set_pcp_prio(struct mv88e6xxx_chip *chip, int port, + pcp, data); + } + ++/* Rewrite the QPri of every enabled entry after the mqprio map changed */ ++int mv88e6390_port_sync_qpri(struct mv88e6xxx_chip *chip, int port) ++{ ++ int err, i, prio; ++ u16 table, data; ++ ++ for (i = 0; i < 64; i++) { ++ err = mv88e6390_port_ippmt_read(chip, port, i, &data); ++ if (err) ++ return err; ++ ++ if (data & MV88E6390_PORT_IP_PRIO_MAP_TABLE_DIS_FPRI) ++ continue; ++ ++ prio = FIELD_GET(MV88E6390_PORT_IP_PRIO_MAP_TABLE_FPRI_MASK, data); ++ err = mv88e6390_port_set_dscp_prio(chip, port, i, prio); ++ if (err) ++ return err; ++ } ++ ++ for (i = 0; i < 16; i++) { ++ table = mv88e6390_port_ingress_pcp_table(i >> 3); ++ ++ err = mv88e6xxx_port_ieeepmt_read(chip, port, table, i & 0x7, ++ &data); ++ if (err) ++ return err; ++ ++ if (data & MV88E6390_PORT_IEEE_PRIO_MAP_TABLE_DIS_FPRI) ++ continue; ++ ++ prio = FIELD_GET(MV88E6390_PORT_IEEE_PRIO_MAP_TABLE_FPRI_MASK, data); ++ err = mv88e6390_port_set_pcp_prio(chip, port, i & 0x7, i >> 3, ++ prio); ++ if (err) ++ return err; ++ } ++ ++ err = mv88e6390_port_get_default_prio(chip, port); ++ if (err < 0) ++ return err; ++ ++ return mv88e6390_port_set_default_prio(chip, port, err); ++} ++ + /* Egress remarking uses the frame priority assigned at ingress to look + * up the PCP written to tagged frames and the DSCP written to IP frames. + * Green and yellow frames have separate tables, both are kept equal as +diff --git a/drivers/net/dsa/mv88e6xxx/port.h b/drivers/net/dsa/mv88e6xxx/port.h +index ef5028707ec5a..273dacfe24396 100644 +--- a/drivers/net/dsa/mv88e6xxx/port.h ++++ b/drivers/net/dsa/mv88e6xxx/port.h +@@ -590,6 +590,7 @@ int mv88e6390_port_get_pcp_prio(struct mv88e6xxx_chip *chip, int port, + u8 pcp, u8 dei); + int mv88e6390_port_set_pcp_prio(struct mv88e6xxx_chip *chip, int port, + u8 pcp, u8 dei, int prio); ++int mv88e6390_port_sync_qpri(struct mv88e6xxx_chip *chip, int port); + int mv88e6390_port_get_pcp_rewr(struct mv88e6xxx_chip *chip, int port, + u8 prio, u8 *pcp); + int mv88e6390_port_set_pcp_rewr(struct mv88e6xxx_chip *chip, int port, +-- +2.43.0 + diff --git a/patches/linux/6.18.52/0075-net-dsa-tag_dsa-Read-the-tag-priority-before-the-hea.patch b/patches/linux/6.18.52/0075-net-dsa-tag_dsa-Read-the-tag-priority-before-the-hea.patch new file mode 100644 index 000000000..58184271f --- /dev/null +++ b/patches/linux/6.18.52/0075-net-dsa-tag_dsa-Read-the-tag-priority-before-the-hea.patch @@ -0,0 +1,59 @@ +From b8d9f22c758ac3d7a8ff56ffabe0d886f84161a1 Mon Sep 17 00:00:00 2001 +From: Joachim Wiberg +Date: Fri, 11 Sep 2026 15:54:16 +0200 +Subject: [PATCH 75/77] net: dsa: tag_dsa: Read the tag priority before the + header is stripped +Organization: Wires + +Routed frames left the switch with a priority unrelated to their +DSCP, constant for a given sender. The tag priority is copied to +skb->priority after the DSA header has been removed, and for untagged +frames dsa_strip_etype_header() has by then moved the MAC addresses +over the old header bytes, so the value read is a byte of the source +MAC address. Tagged frames were unaffected since the 802.1Q tag +written in place keeps the PCP bits where the DSA PRI field was. + +Read the priority right after the header is parsed. + +Signed-off-by: Joachim Wiberg +--- + net/dsa/tag_dsa.c | 16 +++++++++------- + 1 file changed, 9 insertions(+), 7 deletions(-) + +diff --git a/net/dsa/tag_dsa.c b/net/dsa/tag_dsa.c +index 55c296e0b5b0e..35595aa21f524 100644 +--- a/net/dsa/tag_dsa.c ++++ b/net/dsa/tag_dsa.c +@@ -276,6 +276,15 @@ static struct sk_buff *dsa_rcv_ll(struct sk_buff *skb, struct net_device *dev, + if (!skb->dev) + return NULL; + ++ /* Ideally we would have a way of configuring the mapping of PCP ++ * bits to skb->priority on DSA ports (like ingress-qos-map for ++ * vlan interfaces). For now, assume that a 1:1 mapping is more ++ * useful than ignoring the priority altogether. Read it before ++ * the header is stripped below, which moves the MAC addresses ++ * over these bytes. ++ */ ++ skb->priority = dsa_header[2] >> 5; ++ + /* When using LAG offload, skb->dev is not a DSA user interface, + * so we cannot call dsa_default_offload_fwd_mark and we need to + * special-case it. +@@ -325,13 +334,6 @@ static struct sk_buff *dsa_rcv_ll(struct sk_buff *skb, struct net_device *dev, + dsa_strip_etype_header(skb, DSA_HLEN + extra); + } + +- /* Ideally we would have a way of configuring the mapping of PCP +- * bits to skb->priority on DSA ports (like ingress-qos-map for +- * vlan interfaces). For now, assume that a 1:1 mapping is more +- * useful than ignoring the priority altogether. +- */ +- skb->priority = dsa_header[2] >> 5; +- + return skb; + } + +-- +2.43.0 + diff --git a/patches/linux/6.18.52/0076-net-dsa-mv88e6xxx-Offload-the-tbf-qdisc.patch b/patches/linux/6.18.52/0076-net-dsa-mv88e6xxx-Offload-the-tbf-qdisc.patch new file mode 100644 index 000000000..287e10887 --- /dev/null +++ b/patches/linux/6.18.52/0076-net-dsa-mv88e6xxx-Offload-the-tbf-qdisc.patch @@ -0,0 +1,275 @@ +From 21d713883139d436c6e58e2ae326bb303bd0dfe3 Mon Sep 17 00:00:00 2001 +From: Joachim Wiberg +Date: Thu, 17 Sep 2026 17:09:02 +0200 +Subject: [PATCH 76/77] net: dsa: mv88e6xxx: Offload the tbf qdisc +Organization: Wires + +A tbf qdisc on a switch port shapes only what the CPU sends; frames +switched between ports never pass the qdisc. The chip has an egress +rate limiter per port that does the same job for both. + +Take a root tbf and program its rate into the port's Egress Rate +Control, counting Layer 2 bytes as the qdisc does. The limiter steps +in fixed increments per decade of rates, 64 kbit/s below 1 Mbit/s up +to 100 Mbit/s below 10 Gbit/s, so the rate is rounded to the nearest +step. A burst size has no hardware equivalent and is ignored. A +child qdisc is accepted and left to bring its own offload. + +Signed-off-by: Joachim Wiberg +--- + drivers/net/dsa/mv88e6xxx/chip.c | 52 ++++++++++++++++++++++++++++++ + drivers/net/dsa/mv88e6xxx/chip.h | 7 +++++ + drivers/net/dsa/mv88e6xxx/port.c | 54 ++++++++++++++++++++++++++++++++ + drivers/net/dsa/mv88e6xxx/port.h | 9 ++++++ + 4 files changed, 122 insertions(+) + +diff --git a/drivers/net/dsa/mv88e6xxx/chip.c b/drivers/net/dsa/mv88e6xxx/chip.c +index 673f8b7523a08..aab76fadf7530 100644 +--- a/drivers/net/dsa/mv88e6xxx/chip.c ++++ b/drivers/net/dsa/mv88e6xxx/chip.c +@@ -32,6 +32,7 @@ + #include + #include + #include ++#include + #include + + #include "chip.h" +@@ -4861,6 +4862,7 @@ static const struct mv88e6xxx_ops mv88e6190_ops = { + .port_get_pcp_prio = mv88e6390_port_get_pcp_prio, + .port_set_pcp_prio = mv88e6390_port_set_pcp_prio, + .port_sync_qpri = mv88e6390_port_sync_qpri, ++ .port_set_egress_rate = mv88e6390_port_set_egress_rate, + .port_get_pcp_rewr = mv88e6390_port_get_pcp_rewr, + .port_set_pcp_rewr = mv88e6390_port_set_pcp_rewr, + .port_get_dscp_rewr = mv88e6390_port_get_dscp_rewr, +@@ -4932,6 +4934,7 @@ static const struct mv88e6xxx_ops mv88e6190x_ops = { + .port_get_pcp_prio = mv88e6390_port_get_pcp_prio, + .port_set_pcp_prio = mv88e6390_port_set_pcp_prio, + .port_sync_qpri = mv88e6390_port_sync_qpri, ++ .port_set_egress_rate = mv88e6390_port_set_egress_rate, + .port_get_pcp_rewr = mv88e6390_port_get_pcp_rewr, + .port_set_pcp_rewr = mv88e6390_port_set_pcp_rewr, + .port_get_dscp_rewr = mv88e6390_port_get_dscp_rewr, +@@ -5003,6 +5006,7 @@ static const struct mv88e6xxx_ops mv88e6191_ops = { + .port_get_pcp_prio = mv88e6390_port_get_pcp_prio, + .port_set_pcp_prio = mv88e6390_port_set_pcp_prio, + .port_sync_qpri = mv88e6390_port_sync_qpri, ++ .port_set_egress_rate = mv88e6390_port_set_egress_rate, + .port_get_pcp_rewr = mv88e6390_port_get_pcp_rewr, + .port_set_pcp_rewr = mv88e6390_port_set_pcp_rewr, + .port_get_dscp_rewr = mv88e6390_port_get_dscp_rewr, +@@ -5178,6 +5182,7 @@ static const struct mv88e6xxx_ops mv88e6290_ops = { + .port_get_pcp_prio = mv88e6390_port_get_pcp_prio, + .port_set_pcp_prio = mv88e6390_port_set_pcp_prio, + .port_sync_qpri = mv88e6390_port_sync_qpri, ++ .port_set_egress_rate = mv88e6390_port_set_egress_rate, + .port_get_pcp_rewr = mv88e6390_port_get_pcp_rewr, + .port_set_pcp_rewr = mv88e6390_port_set_pcp_rewr, + .port_get_dscp_rewr = mv88e6390_port_get_dscp_rewr, +@@ -5577,6 +5582,7 @@ static const struct mv88e6xxx_ops mv88e6390_ops = { + .port_get_pcp_prio = mv88e6390_port_get_pcp_prio, + .port_set_pcp_prio = mv88e6390_port_set_pcp_prio, + .port_sync_qpri = mv88e6390_port_sync_qpri, ++ .port_set_egress_rate = mv88e6390_port_set_egress_rate, + .port_get_pcp_rewr = mv88e6390_port_get_pcp_rewr, + .port_set_pcp_rewr = mv88e6390_port_set_pcp_rewr, + .port_get_dscp_rewr = mv88e6390_port_get_dscp_rewr, +@@ -5652,6 +5658,7 @@ static const struct mv88e6xxx_ops mv88e6390x_ops = { + .port_get_pcp_prio = mv88e6390_port_get_pcp_prio, + .port_set_pcp_prio = mv88e6390_port_set_pcp_prio, + .port_sync_qpri = mv88e6390_port_sync_qpri, ++ .port_set_egress_rate = mv88e6390_port_set_egress_rate, + .port_get_pcp_rewr = mv88e6390_port_get_pcp_rewr, + .port_set_pcp_rewr = mv88e6390_port_set_pcp_rewr, + .port_get_dscp_rewr = mv88e6390_port_get_dscp_rewr, +@@ -5726,6 +5733,7 @@ static const struct mv88e6xxx_ops mv88e6393x_ops = { + .port_get_pcp_prio = mv88e6390_port_get_pcp_prio, + .port_set_pcp_prio = mv88e6390_port_set_pcp_prio, + .port_sync_qpri = mv88e6390_port_sync_qpri, ++ .port_set_egress_rate = mv88e6390_port_set_egress_rate, + .port_get_pcp_rewr = mv88e6390_port_get_pcp_rewr, + .port_set_pcp_rewr = mv88e6390_port_set_pcp_rewr, + .port_get_dscp_rewr = mv88e6390_port_get_dscp_rewr, +@@ -7685,7 +7693,49 @@ static int mv88e6xxx_qos_port_mqprio(struct mv88e6xxx_chip *chip, int port, + netdev_reset_tc(dev); + mv88e6xxx_port_set_qpri(chip, port, NULL); + return err; ++} ++ ++static int mv88e6xxx_qos_port_tbf(struct mv88e6xxx_chip *chip, int port, ++ struct tc_tbf_qopt_offload *qopt) ++{ ++ struct mv88e6xxx_port *mp = &chip->ports[port]; ++ u64 bps; ++ int err; ++ ++ if (!chip->info->ops->port_set_egress_rate) ++ return -EOPNOTSUPP; ++ ++ if (qopt->parent != TC_H_ROOT) ++ return -EOPNOTSUPP; ++ ++ switch (qopt->command) { ++ case TC_TBF_REPLACE: ++ bps = qopt->replace_params.rate.rate_bytes_ps * 8; ++ ++ mv88e6xxx_reg_lock(chip); ++ err = chip->info->ops->port_set_egress_rate(chip, port, bps); ++ mv88e6xxx_reg_unlock(chip); ++ if (err) ++ return err; + ++ mp->tbf_handle = qopt->handle; ++ return 0; ++ case TC_TBF_DESTROY: ++ mp->tbf_handle = 0; ++ ++ mv88e6xxx_reg_lock(chip); ++ err = chip->info->ops->port_set_egress_rate(chip, port, 0); ++ mv88e6xxx_reg_unlock(chip); ++ return err; ++ case TC_TBF_STATS: ++ case TC_TBF_GRAFT: ++ /* The child brings its own offload, and stats stay in ++ * software; either is what marks the tbf offloaded ++ */ ++ return mp->tbf_handle ? 0 : -EOPNOTSUPP; ++ default: ++ return -EOPNOTSUPP; ++ } + } + + static int mv88e6xxx_qos_query_caps(struct tc_query_caps_base *base) +@@ -7711,6 +7761,8 @@ static int mv88e6xxx_port_setup_tc(struct dsa_switch *ds, int port, + return mv88e6xxx_qos_query_caps(type_data); + case TC_SETUP_QDISC_MQPRIO: + return mv88e6xxx_qos_port_mqprio(chip, port, type_data); ++ case TC_SETUP_QDISC_TBF: ++ return mv88e6xxx_qos_port_tbf(chip, port, type_data); + default: + return -EOPNOTSUPP; + } +diff --git a/drivers/net/dsa/mv88e6xxx/chip.h b/drivers/net/dsa/mv88e6xxx/chip.h +index f0736aeebbb8a..b443317008875 100644 +--- a/drivers/net/dsa/mv88e6xxx/chip.h ++++ b/drivers/net/dsa/mv88e6xxx/chip.h +@@ -312,6 +312,9 @@ struct mv88e6xxx_port { + */ + bool qmap; + u8 qpri[8]; ++ ++ /* Offloaded tbf qdisc at the root, 0 when none */ ++ u32 tbf_handle; + }; + + enum mv88e6xxx_region_id { +@@ -605,6 +608,10 @@ struct mv88e6xxx_ops { + u8 pcp, u8 dei, int prio); + int (*port_sync_qpri)(struct mv88e6xxx_chip *chip, int port); + ++ /* Egress shaper in bits per second, 0 turns it off */ ++ int (*port_set_egress_rate)(struct mv88e6xxx_chip *chip, int port, ++ u64 bps); ++ + /* Egress remarking from the frame priority, a negative code + * point disables it for that priority. + */ +diff --git a/drivers/net/dsa/mv88e6xxx/port.c b/drivers/net/dsa/mv88e6xxx/port.c +index 255f6229a13af..47834060cabf7 100644 +--- a/drivers/net/dsa/mv88e6xxx/port.c ++++ b/drivers/net/dsa/mv88e6xxx/port.c +@@ -1480,6 +1480,60 @@ int mv88e6097_port_egress_rate_limiting(struct mv88e6xxx_chip *chip, int port) + 0x0001); + } + ++/* Shape the port to a Layer 2 bit rate, or turn the shaper off with 0. ++ * The rate is a decrement value stepping a counter at one of four fixed ++ * intervals, each covering a decade of rates, so it is rounded to the ++ * nearest step of that decade. ++ */ ++int mv88e6390_port_set_egress_rate(struct mv88e6xxx_chip *chip, int port, ++ u64 bps) ++{ ++ u32 step, rate; ++ u16 dec; ++ int err; ++ ++ if (!bps) { ++ err = mv88e6xxx_port_write(chip, port, ++ MV88E6XXX_PORT_EGRESS_RATE_CTL2, 0); ++ if (err) ++ return err; ++ ++ return mv88e6097_port_egress_rate_limiting(chip, port); ++ } ++ ++ if (bps < 64000) { ++ return -ERANGE; ++ } else if (bps < 1000000) { ++ step = 64000; ++ rate = 0x1e84; ++ } else if (bps < 100000000) { ++ step = 1000000; ++ rate = 0x01f4; ++ } else if (bps < 1000000000) { ++ step = 10000000; ++ rate = 0x0032; ++ } else if (bps <= 10000000000ULL) { ++ step = 100000000; ++ rate = 0x0005; ++ } else { ++ return -ERANGE; ++ } ++ ++ dec = DIV_ROUND_CLOSEST_ULL(bps, step); ++ if (!dec || dec > MV88E6390_PORT_EGRESS_RATE_CTL1_DEC_MASK) ++ return -ERANGE; ++ ++ err = mv88e6xxx_port_write(chip, port, MV88E6XXX_PORT_EGRESS_RATE_CTL1, ++ dec); ++ if (err) ++ return err; ++ ++ return mv88e6xxx_port_write(chip, port, MV88E6XXX_PORT_EGRESS_RATE_CTL2, ++ FIELD_PREP(MV88E6390_PORT_EGRESS_RATE_CTL2_COUNT_MASK, ++ MV88E6390_PORT_EGRESS_RATE_CTL2_COUNT_LAYER2) | ++ rate); ++} ++ + /* Offset 0x0B: Port Association Vector */ + + int mv88e6xxx_port_set_assoc_vector(struct mv88e6xxx_chip *chip, int port, +diff --git a/drivers/net/dsa/mv88e6xxx/port.h b/drivers/net/dsa/mv88e6xxx/port.h +index 273dacfe24396..49674351b5329 100644 +--- a/drivers/net/dsa/mv88e6xxx/port.h ++++ b/drivers/net/dsa/mv88e6xxx/port.h +@@ -240,9 +240,16 @@ + + /* Offset 0x09: Egress Rate Control */ + #define MV88E6XXX_PORT_EGRESS_RATE_CTL1 0x09 ++#define MV88E6390_PORT_EGRESS_RATE_CTL1_DEC_MASK 0x007f + + /* Offset 0x0A: Egress Rate Control 2 */ + #define MV88E6XXX_PORT_EGRESS_RATE_CTL2 0x0a ++#define MV88E6390_PORT_EGRESS_RATE_CTL2_COUNT_MASK 0xc000 ++#define MV88E6390_PORT_EGRESS_RATE_CTL2_COUNT_FRAMES 0x0 ++#define MV88E6390_PORT_EGRESS_RATE_CTL2_COUNT_LAYER1 0x1 ++#define MV88E6390_PORT_EGRESS_RATE_CTL2_COUNT_LAYER2 0x2 ++#define MV88E6390_PORT_EGRESS_RATE_CTL2_COUNT_LAYER3 0x3 ++#define MV88E6390_PORT_EGRESS_RATE_CTL2_RATE_MASK 0x3fff + + /* Offset 0x0B: Port Association Vector */ + #define MV88E6XXX_PORT_ASSOC_VECTOR 0x0b +@@ -591,6 +598,8 @@ int mv88e6390_port_get_pcp_prio(struct mv88e6xxx_chip *chip, int port, + int mv88e6390_port_set_pcp_prio(struct mv88e6xxx_chip *chip, int port, + u8 pcp, u8 dei, int prio); + int mv88e6390_port_sync_qpri(struct mv88e6xxx_chip *chip, int port); ++int mv88e6390_port_set_egress_rate(struct mv88e6xxx_chip *chip, int port, ++ u64 bps); + int mv88e6390_port_get_pcp_rewr(struct mv88e6xxx_chip *chip, int port, + u8 prio, u8 *pcp); + int mv88e6390_port_set_pcp_rewr(struct mv88e6xxx_chip *chip, int port, +-- +2.43.0 + diff --git a/patches/linux/6.18.52/0077-net-dsa-mv88e6xxx-Offload-the-ets-qdisc.patch b/patches/linux/6.18.52/0077-net-dsa-mv88e6xxx-Offload-the-ets-qdisc.patch new file mode 100644 index 000000000..22d3c5c9c --- /dev/null +++ b/patches/linux/6.18.52/0077-net-dsa-mv88e6xxx-Offload-the-ets-qdisc.patch @@ -0,0 +1,554 @@ +From 08883b16de388b3e9780a54dbe3b9c828f356375 Mon Sep 17 00:00:00 2001 +From: Joachim Wiberg +Date: Thu, 17 Sep 2026 17:09:45 +0200 +Subject: [PATCH 77/77] net: dsa: mv88e6xxx: Offload the ets qdisc +Organization: Wires + +An ets qdisc on a switch port schedules only what the CPU sends. The +mqprio offload puts switched frames in the queue of their traffic +class, but says nothing about how the queues are served, so the chip +keeps its power-on weighted round robin whatever algorithm the user +configured. + +Take an ets qdisc at the root, or under an offloaded tbf, and program +the port from it. The strict bands become the strict queues in the +port's schedule mode, the priority map becomes the chip's queue map, +as for mqprio, and the weighted bands become the round robin weights. +The chip walks one queue service sequence for all its ports, so the +weights are shared like the queue map: the most recent request sets +the weight of the queues it serves by weight, and a port that asked +for another weight on one of them is no longer reported offloaded. +The sequence is rebuilt from the chip's weights, interleaved evenly +with each queue present at least once, and reverts to the power-on +weights when no port is left. The sequence counts frames where ets +quanta count bytes, so the shares hold for frames of similar size. + +Frames from the CPU are queued by the traffic class of their transmit +queue, so lay the queues out one per class and map the priorities as +the ingress tables do. Removing the qdisc drops the port's requests, +the round robin schedule and the queue layout. + +Signed-off-by: Joachim Wiberg +--- + drivers/net/dsa/mv88e6xxx/chip.c | 256 ++++++++++++++++++++++++++++ + drivers/net/dsa/mv88e6xxx/chip.h | 20 +++ + drivers/net/dsa/mv88e6xxx/global2.c | 42 +++++ + drivers/net/dsa/mv88e6xxx/global2.h | 5 + + drivers/net/dsa/mv88e6xxx/port.c | 28 +++ + drivers/net/dsa/mv88e6xxx/port.h | 10 ++ + 6 files changed, 361 insertions(+) + +diff --git a/drivers/net/dsa/mv88e6xxx/chip.c b/drivers/net/dsa/mv88e6xxx/chip.c +index aab76fadf7530..842a6a85ca294 100644 +--- a/drivers/net/dsa/mv88e6xxx/chip.c ++++ b/drivers/net/dsa/mv88e6xxx/chip.c +@@ -4862,6 +4862,8 @@ static const struct mv88e6xxx_ops mv88e6190_ops = { + .port_get_pcp_prio = mv88e6390_port_get_pcp_prio, + .port_set_pcp_prio = mv88e6390_port_set_pcp_prio, + .port_sync_qpri = mv88e6390_port_sync_qpri, ++ .port_set_sched = mv88e6390_port_set_sched, ++ .set_qos_weights = mv88e6390_g2_set_qos_weights, + .port_set_egress_rate = mv88e6390_port_set_egress_rate, + .port_get_pcp_rewr = mv88e6390_port_get_pcp_rewr, + .port_set_pcp_rewr = mv88e6390_port_set_pcp_rewr, +@@ -4934,6 +4936,8 @@ static const struct mv88e6xxx_ops mv88e6190x_ops = { + .port_get_pcp_prio = mv88e6390_port_get_pcp_prio, + .port_set_pcp_prio = mv88e6390_port_set_pcp_prio, + .port_sync_qpri = mv88e6390_port_sync_qpri, ++ .port_set_sched = mv88e6390_port_set_sched, ++ .set_qos_weights = mv88e6390_g2_set_qos_weights, + .port_set_egress_rate = mv88e6390_port_set_egress_rate, + .port_get_pcp_rewr = mv88e6390_port_get_pcp_rewr, + .port_set_pcp_rewr = mv88e6390_port_set_pcp_rewr, +@@ -5006,6 +5010,8 @@ static const struct mv88e6xxx_ops mv88e6191_ops = { + .port_get_pcp_prio = mv88e6390_port_get_pcp_prio, + .port_set_pcp_prio = mv88e6390_port_set_pcp_prio, + .port_sync_qpri = mv88e6390_port_sync_qpri, ++ .port_set_sched = mv88e6390_port_set_sched, ++ .set_qos_weights = mv88e6390_g2_set_qos_weights, + .port_set_egress_rate = mv88e6390_port_set_egress_rate, + .port_get_pcp_rewr = mv88e6390_port_get_pcp_rewr, + .port_set_pcp_rewr = mv88e6390_port_set_pcp_rewr, +@@ -5182,6 +5188,8 @@ static const struct mv88e6xxx_ops mv88e6290_ops = { + .port_get_pcp_prio = mv88e6390_port_get_pcp_prio, + .port_set_pcp_prio = mv88e6390_port_set_pcp_prio, + .port_sync_qpri = mv88e6390_port_sync_qpri, ++ .port_set_sched = mv88e6390_port_set_sched, ++ .set_qos_weights = mv88e6390_g2_set_qos_weights, + .port_set_egress_rate = mv88e6390_port_set_egress_rate, + .port_get_pcp_rewr = mv88e6390_port_get_pcp_rewr, + .port_set_pcp_rewr = mv88e6390_port_set_pcp_rewr, +@@ -5582,6 +5590,8 @@ static const struct mv88e6xxx_ops mv88e6390_ops = { + .port_get_pcp_prio = mv88e6390_port_get_pcp_prio, + .port_set_pcp_prio = mv88e6390_port_set_pcp_prio, + .port_sync_qpri = mv88e6390_port_sync_qpri, ++ .port_set_sched = mv88e6390_port_set_sched, ++ .set_qos_weights = mv88e6390_g2_set_qos_weights, + .port_set_egress_rate = mv88e6390_port_set_egress_rate, + .port_get_pcp_rewr = mv88e6390_port_get_pcp_rewr, + .port_set_pcp_rewr = mv88e6390_port_set_pcp_rewr, +@@ -5658,6 +5668,8 @@ static const struct mv88e6xxx_ops mv88e6390x_ops = { + .port_get_pcp_prio = mv88e6390_port_get_pcp_prio, + .port_set_pcp_prio = mv88e6390_port_set_pcp_prio, + .port_sync_qpri = mv88e6390_port_sync_qpri, ++ .port_set_sched = mv88e6390_port_set_sched, ++ .set_qos_weights = mv88e6390_g2_set_qos_weights, + .port_set_egress_rate = mv88e6390_port_set_egress_rate, + .port_get_pcp_rewr = mv88e6390_port_get_pcp_rewr, + .port_set_pcp_rewr = mv88e6390_port_set_pcp_rewr, +@@ -5733,6 +5745,8 @@ static const struct mv88e6xxx_ops mv88e6393x_ops = { + .port_get_pcp_prio = mv88e6390_port_get_pcp_prio, + .port_set_pcp_prio = mv88e6390_port_set_pcp_prio, + .port_sync_qpri = mv88e6390_port_sync_qpri, ++ .port_set_sched = mv88e6390_port_set_sched, ++ .set_qos_weights = mv88e6390_g2_set_qos_weights, + .port_set_egress_rate = mv88e6390_port_set_egress_rate, + .port_get_pcp_rewr = mv88e6390_port_get_pcp_rewr, + .port_set_pcp_rewr = mv88e6390_port_set_pcp_rewr, +@@ -7695,6 +7709,246 @@ static int mv88e6xxx_qos_port_mqprio(struct mv88e6xxx_chip *chip, int port, + return err; + } + ++/* The DSA tag carries the traffic class of the transmit queue, so lay the ++ * queues out one per class and map the priorities like the ingress tables ++ * do, for frames from the CPU to land in the same queue as switched ones. ++ */ ++static int mv88e6xxx_port_set_tx_map(struct net_device *dev, const u8 *qpri) ++{ ++ int tc, prio, err; ++ ++ err = netdev_set_num_tc(dev, 8); ++ if (err) ++ return err; ++ ++ for (tc = 0; tc < 8; tc++) ++ netdev_set_tc_queue(dev, tc, 1, tc); ++ ++ for (prio = 0; prio < 8; prio++) ++ netdev_set_prio_tc_map(dev, prio, qpri[prio]); ++ ++ return 0; ++} ++ ++/* Band 0 is dequeued first, queue 7 is served first */ ++static u8 mv88e6xxx_ets_queue(const struct tc_ets_qopt_offload_replace_params *p, ++ u8 band) ++{ ++ return p->bands - 1 - min_t(u8, band, p->bands - 1); ++} ++ ++/* Whether the chip's sequence serves this port's weighted queues by the ++ * weights its ets asked for; the chip walks one sequence for all ports ++ */ ++static bool mv88e6xxx_port_wrr_in_use(struct mv88e6xxx_chip *chip, int port) ++{ ++ struct mv88e6xxx_port *mp = &chip->ports[port]; ++ int q; ++ ++ if (!mp->ets) ++ return false; ++ ++ for (q = 0; q < 8; q++) ++ if (mp->wrr[q] && mp->wrr[q] != chip->wrr[q]) ++ return false; ++ ++ return true; ++} ++ ++/* Interleave the queues evenly, each as many times as its weight, in a ++ * sequence that fits the table. Every queue appears at least once, or ++ * frames in it would never leave the port. ++ */ ++static unsigned int mv88e6xxx_wrr_sequence(const u8 *weight, u8 *seq, ++ unsigned int max) ++{ ++ unsigned int total, scale = 1, len, q, best; ++ int credit[8]; ++ u8 w[8]; ++ ++ do { ++ total = 0; ++ for (q = 0; q < 8; q++) { ++ w[q] = max_t(u8, DIV_ROUND_CLOSEST(weight[q], scale), 1); ++ total += w[q]; ++ } ++ scale++; ++ } while (total > max); ++ ++ memset(credit, 0, sizeof(credit)); ++ for (len = 0; len < total; len++) { ++ best = 0; ++ for (q = 0; q < 8; q++) { ++ credit[q] += w[q]; ++ if (credit[q] > credit[best]) ++ best = q; ++ } ++ credit[best] -= total; ++ seq[len] = best; ++ } ++ ++ return len; ++} ++ ++/* Load the chip's weights, or its power-on weights when no port asks ++ * for any ++ */ ++static int mv88e6xxx_ets_load_weights(struct mv88e6xxx_chip *chip) ++{ ++ static const u8 defaults[8] = { 1, 2, 3, 6, 12, 17, 25, 33 }; ++ u8 seq[MV88E6390_G2_QOS_WEIGHTS_MAX_LEN]; ++ const u8 *weight = chip->wrr; ++ int q; ++ ++ for (q = 0; q < 8 && !chip->wrr[q]; q++) ++ ; ++ if (q == 8) ++ weight = defaults; ++ ++ return chip->info->ops->set_qos_weights(chip, seq, ++ mv88e6xxx_wrr_sequence(weight, seq, ++ ARRAY_SIZE(seq))); ++} ++ ++/* Forget the port's requests; the chip keeps its map and weights as ++ * long as any port asks for them, else the identity map and the ++ * power-on weights come back ++ */ ++static int mv88e6xxx_ets_destroy(struct mv88e6xxx_chip *chip, int port) ++{ ++ struct net_device *dev = dsa_to_port(chip->ds, port)->user; ++ struct mv88e6xxx_port *mp = &chip->ports[port]; ++ int other, q, err; ++ ++ mp->ets = false; ++ memset(mp->wrr, 0, sizeof(mp->wrr)); ++ ++ memset(chip->wrr, 0, sizeof(chip->wrr)); ++ for (other = 0; other < mv88e6xxx_num_ports(chip); other++) ++ for (q = 0; q < 8; q++) ++ if (chip->ports[other].wrr[q]) ++ chip->wrr[q] = chip->ports[other].wrr[q]; ++ ++ mv88e6xxx_reg_lock(chip); ++ err = chip->info->ops->port_set_sched(chip, port, 0); ++ if (!err) ++ err = mv88e6xxx_ets_load_weights(chip); ++ mv88e6xxx_reg_unlock(chip); ++ if (err) ++ return err; ++ ++ netdev_reset_tc(dev); ++ ++ return mv88e6xxx_port_set_qpri(chip, port, NULL); ++} ++ ++static int mv88e6xxx_ets_replace(struct mv88e6xxx_chip *chip, int port, ++ struct tc_ets_qopt_offload *qopt) ++{ ++ struct tc_ets_qopt_offload_replace_params *p = &qopt->replace_params; ++ struct net_device *dev = dsa_to_port(chip->ds, port)->user; ++ struct mv88e6xxx_port *mp = &chip->ports[port]; ++ unsigned int nstrict = 0, band; ++ u8 qpri[8], wrr[8] = {}; ++ int prio, other, q, err; ++ ++ if (qopt->parent != TC_H_ROOT && ++ !(mp->tbf_handle && TC_H_MAJ(qopt->parent) == mp->tbf_handle)) { ++ dev_err(chip->dev, "p%d: ets must be the root or under an offloaded tbf\n", ++ port); ++ return -EOPNOTSUPP; ++ } ++ ++ if (!p->bands || p->bands > 8) ++ return -EOPNOTSUPP; ++ ++ /* Strict bands come first, with no quantum */ ++ while (nstrict < p->bands && !p->quanta[nstrict]) ++ nstrict++; ++ ++ for (band = nstrict; band < p->bands; band++) { ++ if (!p->quanta[band]) ++ return -EOPNOTSUPP; ++ ++ wrr[mv88e6xxx_ets_queue(p, band)] = max_t(u8, p->weights[band], 1); ++ } ++ ++ /* One sequence per chip: the most recent request defines the ++ * weight of the queues it serves by weight, a port that asked ++ * for another weight on one of them is no longer offloaded ++ */ ++ for (other = 0; other < mv88e6xxx_num_ports(chip); other++) { ++ if (other == port || !mv88e6xxx_port_wrr_in_use(chip, other)) ++ continue; ++ ++ for (q = 0; q < 8; q++) { ++ if (!wrr[q] || !chip->ports[other].wrr[q] || ++ wrr[q] == chip->ports[other].wrr[q]) ++ continue; ++ ++ dev_info(chip->dev, "p%d: weight of queue %d replaces the one p%d asked for, the chip has one set\n", ++ port, q, other); ++ break; ++ } ++ } ++ ++ for (prio = 0; prio < ARRAY_SIZE(qpri); prio++) ++ qpri[prio] = mv88e6xxx_ets_queue(p, p->priomap[prio]); ++ ++ mv88e6xxx_reg_lock(chip); ++ err = chip->info->ops->port_set_sched(chip, port, ++ 8 - (p->bands - nstrict)); ++ if (err) ++ goto out_unlock; ++ ++ memcpy(mp->wrr, wrr, sizeof(mp->wrr)); ++ mp->ets = true; ++ for (q = 0; q < 8; q++) ++ if (wrr[q]) ++ chip->wrr[q] = wrr[q]; ++ err = mv88e6xxx_ets_load_weights(chip); ++out_unlock: ++ mv88e6xxx_reg_unlock(chip); ++ if (err) ++ goto out_reset; ++ ++ err = mv88e6xxx_port_set_qpri(chip, port, qpri); ++ if (err) ++ goto out_reset; ++ ++ err = mv88e6xxx_port_set_tx_map(dev, qpri); ++ if (err) ++ goto out_reset; ++ ++ return 0; ++ ++out_reset: ++ mv88e6xxx_ets_destroy(chip, port); ++ return err; ++} ++ ++static int mv88e6xxx_qos_port_ets(struct mv88e6xxx_chip *chip, int port, ++ struct tc_ets_qopt_offload *qopt) ++{ ++ if (!chip->info->ops->port_set_sched || ++ !chip->info->ops->set_qos_weights) ++ return -EOPNOTSUPP; ++ ++ switch (qopt->command) { ++ case TC_ETS_REPLACE: ++ return mv88e6xxx_ets_replace(chip, port, qopt); ++ case TC_ETS_DESTROY: ++ return mv88e6xxx_ets_destroy(chip, port); ++ case TC_ETS_STATS: ++ /* Nothing to report, but this is what marks it offloaded */ ++ return mv88e6xxx_port_qpri_in_use(chip, port) && ++ mv88e6xxx_port_wrr_in_use(chip, port) ? 0 : -EOPNOTSUPP; ++ case TC_ETS_GRAFT: ++ default: ++ return -EOPNOTSUPP; ++ } ++} ++ + static int mv88e6xxx_qos_port_tbf(struct mv88e6xxx_chip *chip, int port, + struct tc_tbf_qopt_offload *qopt) + { +@@ -7761,6 +8015,8 @@ static int mv88e6xxx_port_setup_tc(struct dsa_switch *ds, int port, + return mv88e6xxx_qos_query_caps(type_data); + case TC_SETUP_QDISC_MQPRIO: + return mv88e6xxx_qos_port_mqprio(chip, port, type_data); ++ case TC_SETUP_QDISC_ETS: ++ return mv88e6xxx_qos_port_ets(chip, port, type_data); + case TC_SETUP_QDISC_TBF: + return mv88e6xxx_qos_port_tbf(chip, port, type_data); + default: +diff --git a/drivers/net/dsa/mv88e6xxx/chip.h b/drivers/net/dsa/mv88e6xxx/chip.h +index b443317008875..7bf7cdea0bb12 100644 +--- a/drivers/net/dsa/mv88e6xxx/chip.h ++++ b/drivers/net/dsa/mv88e6xxx/chip.h +@@ -313,6 +313,12 @@ struct mv88e6xxx_port { + bool qmap; + u8 qpri[8]; + ++ /* Offloaded ets qdisc, with the round robin weight per queue it ++ * asked for, 0 for queues it serves strictly or not at all ++ */ ++ bool ets; ++ u8 wrr[8]; ++ + /* Offloaded tbf qdisc at the root, 0 when none */ + u32 tbf_handle; + }; +@@ -481,6 +487,11 @@ struct mv88e6xxx_chip { + * that asked most recently. + */ + u8 qpri[8]; ++ ++ /* Round robin weight per queue in the loaded service sequence, ++ * one sequence per chip like the queue map; 0 where no port asked ++ */ ++ u8 wrr[8]; + }; + + struct mv88e6xxx_bus_ops { +@@ -608,6 +619,15 @@ struct mv88e6xxx_ops { + u8 pcp, u8 dei, int prio); + int (*port_sync_qpri)(struct mv88e6xxx_chip *chip, int port); + ++ /* Transmission selection: the number of highest queues served ++ * strictly, the rest by weighted round robin; the weights are a ++ * queue service sequence shared by all ports of the chip. ++ */ ++ int (*port_set_sched)(struct mv88e6xxx_chip *chip, int port, ++ unsigned int strict); ++ int (*set_qos_weights)(struct mv88e6xxx_chip *chip, const u8 *seq, ++ unsigned int len); ++ + /* Egress shaper in bits per second, 0 turns it off */ + int (*port_set_egress_rate)(struct mv88e6xxx_chip *chip, int port, + u64 bps); +diff --git a/drivers/net/dsa/mv88e6xxx/global2.c b/drivers/net/dsa/mv88e6xxx/global2.c +index 3f16a7ef3fa28..a23541e3cb734 100644 +--- a/drivers/net/dsa/mv88e6xxx/global2.c ++++ b/drivers/net/dsa/mv88e6xxx/global2.c +@@ -297,6 +297,48 @@ int mv88e6xxx_g2_set_switch_mac(struct mv88e6xxx_chip *chip, u8 *addr) + return err; + } + ++/* Offset 0x1C: QoS Weights Register */ ++ ++static int mv88e6390_g2_qos_weights_write(struct mv88e6xxx_chip *chip, ++ u8 ptr, u8 data) ++{ ++ u16 val = MV88E6XXX_G2_QOS_WEIGHTS_UPDATE | ++ FIELD_PREP(MV88E6390_G2_QOS_WEIGHTS_PTR_MASK, ptr) | ++ FIELD_PREP(MV88E6XXX_G2_QOS_WEIGHTS_DATA_MASK, data); ++ ++ return mv88e6xxx_g2_write(chip, MV88E6XXX_G2_QOS_WEIGHTS, val); ++} ++ ++/* Load the weighted round robin sequence the queue controller walks on ++ * every port: @len queue numbers, served one frame each in turn. Two ++ * entries per octet, the earlier one in the low bits, and the length ++ * written last since that is what puts the new table in use. ++ */ ++int mv88e6390_g2_set_qos_weights(struct mv88e6xxx_chip *chip, const u8 *seq, ++ unsigned int len) ++{ ++ unsigned int i; ++ u8 data; ++ int err; ++ ++ if (!len || len > MV88E6390_G2_QOS_WEIGHTS_MAX_LEN) ++ return -EINVAL; ++ ++ for (i = 0; i < len; i += 2) { ++ data = seq[i] & 0x7; ++ if (i + 1 < len) ++ data |= (seq[i + 1] & 0x7) << 4; ++ ++ err = mv88e6390_g2_qos_weights_write(chip, i / 2, data); ++ if (err) ++ return err; ++ } ++ ++ return mv88e6390_g2_qos_weights_write(chip, ++ MV88E6390_G2_QOS_WEIGHTS_PTR_LEN, ++ len); ++} ++ + /* Offset 0x0E: ATU Statistics */ + + int mv88e6xxx_g2_atu_stats_set(struct mv88e6xxx_chip *chip, u16 kind, u16 bin) +diff --git a/drivers/net/dsa/mv88e6xxx/global2.h b/drivers/net/dsa/mv88e6xxx/global2.h +index 496269c26c508..7d2dda5e18666 100644 +--- a/drivers/net/dsa/mv88e6xxx/global2.h ++++ b/drivers/net/dsa/mv88e6xxx/global2.h +@@ -266,6 +266,8 @@ + #define MV88E6XXX_G2_QOS_WEIGHTS_UPDATE 0x8000 + #define MV88E6352_G2_QOS_WEIGHTS_PTR_MASK 0x3f00 + #define MV88E6390_G2_QOS_WEIGHTS_PTR_MASK 0x7f00 ++#define MV88E6390_G2_QOS_WEIGHTS_PTR_LEN 0x40 ++#define MV88E6390_G2_QOS_WEIGHTS_MAX_LEN 128 + #define MV88E6XXX_G2_QOS_WEIGHTS_DATA_MASK 0x00ff + + /* Offset 0x1D: Misc Register */ +@@ -332,6 +334,9 @@ int mv88e6xxx_g2_smi_phy_write_c45(struct mv88e6xxx_chip *chip, + int addr, int devad, int reg, u16 val); + int mv88e6xxx_g2_set_switch_mac(struct mv88e6xxx_chip *chip, u8 *addr); + ++int mv88e6390_g2_set_qos_weights(struct mv88e6xxx_chip *chip, const u8 *seq, ++ unsigned int len); ++ + int mv88e6xxx_g2_get_eeprom8(struct mv88e6xxx_chip *chip, + struct ethtool_eeprom *eeprom, u8 *data); + int mv88e6xxx_g2_set_eeprom8(struct mv88e6xxx_chip *chip, +diff --git a/drivers/net/dsa/mv88e6xxx/port.c b/drivers/net/dsa/mv88e6xxx/port.c +index 47834060cabf7..be5aed565fafd 100644 +--- a/drivers/net/dsa/mv88e6xxx/port.c ++++ b/drivers/net/dsa/mv88e6xxx/port.c +@@ -2088,6 +2088,34 @@ int mv88e6390_port_sync_qpri(struct mv88e6xxx_chip *chip, int port) + return mv88e6390_port_set_default_prio(chip, port, err); + } + ++/* Offset 0x1C: Port Queue Control */ ++ ++static int mv88e6390_port_queue_ctl_write(struct mv88e6xxx_chip *chip, ++ int port, u8 ptr, u8 data) ++{ ++ u16 reg; ++ ++ reg = MV88E6390_PORT_QUEUE_CTL_UPDATE | ++ FIELD_PREP(MV88E6390_PORT_QUEUE_CTL_PTR_MASK, ptr) | ++ FIELD_PREP(MV88E6390_PORT_QUEUE_CTL_DATA_MASK, data); ++ ++ return mv88e6xxx_port_write(chip, port, MV88E6390_PORT_QUEUE_CTL, reg); ++} ++ ++/* Serve the @strict highest queues in strict priority order and the ++ * rest by weighted round robin, with the weights from Global 2. ++ */ ++int mv88e6390_port_set_sched(struct mv88e6xxx_chip *chip, int port, ++ unsigned int strict) ++{ ++ u8 mode = min_t(unsigned int, strict, ++ MV88E6390_PORT_QUEUE_CTL_SCHED_STRICT_MASK); ++ ++ return mv88e6390_port_queue_ctl_write(chip, port, ++ MV88E6390_PORT_QUEUE_CTL_PTR_SCHED, ++ mode); ++} ++ + /* Egress remarking uses the frame priority assigned at ingress to look + * up the PCP written to tagged frames and the DSCP written to IP frames. + * Green and yellow frames have separate tables, both are kept equal as +diff --git a/drivers/net/dsa/mv88e6xxx/port.h b/drivers/net/dsa/mv88e6xxx/port.h +index 49674351b5329..1434174a09370 100644 +--- a/drivers/net/dsa/mv88e6xxx/port.h ++++ b/drivers/net/dsa/mv88e6xxx/port.h +@@ -502,6 +502,14 @@ + #define MV88E6390_PORT_CTL3_UPDATE_DSCP 0x0020 + #define MV88E6390_PORT_CTL3_UPDATE_DEI 0x0008 + ++/* Offset 0x1C: Port Queue Control */ ++#define MV88E6390_PORT_QUEUE_CTL 0x1c ++#define MV88E6390_PORT_QUEUE_CTL_UPDATE 0x8000 ++#define MV88E6390_PORT_QUEUE_CTL_PTR_MASK 0x7f00 ++#define MV88E6390_PORT_QUEUE_CTL_PTR_SCHED 0x00 ++#define MV88E6390_PORT_QUEUE_CTL_DATA_MASK 0x00ff ++#define MV88E6390_PORT_QUEUE_CTL_SCHED_STRICT_MASK 0x07 ++ + /* Offset 0x18: Port IEEE Priority Remapping Registers (0-3) */ + #define MV88E6095_PORT_IEEE_PRIO_REMAP_0123 0x18 + +@@ -598,6 +606,8 @@ int mv88e6390_port_get_pcp_prio(struct mv88e6xxx_chip *chip, int port, + int mv88e6390_port_set_pcp_prio(struct mv88e6xxx_chip *chip, int port, + u8 pcp, u8 dei, int prio); + int mv88e6390_port_sync_qpri(struct mv88e6xxx_chip *chip, int port); ++int mv88e6390_port_set_sched(struct mv88e6xxx_chip *chip, int port, ++ unsigned int strict); + int mv88e6390_port_set_egress_rate(struct mv88e6xxx_chip *chip, int port, + u64 bps); + int mv88e6390_port_get_pcp_rewr(struct mv88e6xxx_chip *chip, int port, +-- +2.43.0 + diff --git a/src/confd/src/Makefile.am b/src/confd/src/Makefile.am index 7e9a8b74f..62c568b71 100644 --- a/src/confd/src/Makefile.am +++ b/src/confd/src/Makefile.am @@ -46,6 +46,7 @@ confd_plugin_la_SOURCES = \ if-vlan.c \ if-gre.c \ if-vxlan.c \ + if-qos.c \ if-wifi.c \ if-wireguard.c \ keystore.c \ diff --git a/src/confd/src/if-qos.c b/src/confd/src/if-qos.c new file mode 100644 index 000000000..a333d89ac --- /dev/null +++ b/src/confd/src/if-qos.c @@ -0,0 +1,843 @@ +/* SPDX-License-Identifier: BSD-3-Clause */ + +#include +#include +#include + +#include +#include + +#include "interfaces.h" + +#define NUM_PRIO 8 +#define ETS_QUANTUM_UNIT 1514 /* one frame of bytes per percent of share */ +#define MAX_TC 8 + +/* + * IEEE 802.1Q-2022 Table 8-5, recommended priority to traffic class + * mappings for an ordinary bridge. Indexed by [num_tc - 1][priority]. + */ +static const uint8_t tc_ieee[MAX_TC][NUM_PRIO] = { + { 0, 0, 0, 0, 0, 0, 0, 0 }, + { 0, 0, 0, 0, 1, 1, 1, 1 }, + { 0, 0, 0, 0, 1, 1, 2, 2 }, + { 0, 0, 1, 1, 2, 2, 3, 3 }, + { 0, 0, 1, 1, 2, 2, 3, 4 }, + { 1, 0, 2, 2, 3, 3, 4, 5 }, + { 1, 0, 2, 3, 4, 4, 5, 6 }, + { 1, 0, 2, 3, 4, 5, 6, 7 }, +}; + +/* + * IEEE 802.1Q-2022 Table 34-1, priority to traffic class with SR + * classes A (priority 3) and B (priority 2) on the two highest classes. + */ +static const uint8_t tc_ieee_sr[MAX_TC][NUM_PRIO] = { + { 0, 0, 0, 0, 0, 0, 0, 0 }, + { 0, 0, 1, 1, 0, 0, 0, 0 }, + { 0, 0, 1, 2, 0, 0, 0, 0 }, + { 0, 0, 2, 3, 1, 1, 1, 1 }, + { 0, 0, 3, 4, 1, 1, 2, 2 }, + { 0, 0, 4, 5, 1, 1, 2, 3 }, + { 0, 0, 5, 6, 1, 2, 3, 4 }, + { 1, 0, 6, 7, 2, 3, 4, 5 }, +}; + +/* RFC 4594 per-hop-behaviour groups to priority; other codepoints untrusted. */ +static const uint8_t dscp_ietf[][2] = { + { 0, 0 }, + { 8, 1 }, { 10, 1 }, { 12, 1 }, { 14, 1 }, + { 16, 2 }, { 18, 2 }, { 20, 2 }, { 22, 2 }, + { 24, 3 }, { 26, 3 }, { 28, 3 }, { 30, 3 }, + { 32, 4 }, { 34, 4 }, { 36, 4 }, { 38, 4 }, + { 40, 5 }, { 46, 5 }, + { 48, 6 }, + { 56, 7 }, +}; + +enum tsa { + TSA_STRICT, + TSA_ETS, + TSA_UNSUPPORTED, +}; + +struct qos_egress { + int num_tc; + uint8_t map[NUM_PRIO]; + enum tsa algo[MAX_TC]; + uint8_t bandwidth[MAX_TC]; /* percent, ETS classes only */ + uint64_t rate; /* bits/s, 0 = no rate limit */ + uint32_t burst; /* bytes */ +}; + +/* + * A non-presence container with defaults always exists in the tree; only + * a leaf, list entry or presence container that is not a default makes + * it configuration. + */ +static bool qos_is_explicit(struct lyd_node *node) +{ + struct lyd_node *child; + + if (!node || !node->schema) + return false; + + switch (node->schema->nodetype) { + case LYS_LEAF: + case LYS_LEAFLIST: + return !(node->flags & LYD_DEFAULT); + case LYS_LIST: + return true; + case LYS_CONTAINER: + if (node->schema->flags & LYS_PRESENCE) + return true; + break; + default: + break; + } + + LY_LIST_FOR(lyd_child(node), child) + if (qos_is_explicit(child)) + return true; + + return false; +} + +/* + * Drivers with DCB rewrite support, and the trust orders their dcb + * apptrust accepts; the kernel has no query for either. Same table as + * yanger's capabilities. The rewrite table cannot be probed by trying: + * a driver with dcbnl operations but no dcbnl_setrewr gets the kernel's + * generic table, which accepts the entries and programs nothing. + */ +static const struct { + const char *driver; + const char *orders[5]; + bool pcp_encoded; /* fabric always encodes PCP from the priority */ +} dcb_drivers[] = { + { "sparx5-switch", { "pcp", "dscp", "dscp-pcp", NULL }, false }, + { "lan966x-switch", { "pcp", "dscp", "dscp-pcp", NULL }, false }, + { "mv88e6085", { "pcp", "dscp", "pcp-dscp", "dscp-pcp", NULL }, true }, +}; + +/* + * One class per transmit queue, at most eight. A single queue has no + * queue structure to respect, so the kernel's eight classes apply. + * Interfaces that do not exist yet get eight as well. + */ +static int qos_num_tc(const char *ifname) +{ + char path[PATH_MAX]; + struct dirent *d; + int n = 0; + DIR *dir; + + snprintf(path, sizeof(path), "/sys/class/net/%s/queues", ifname); + dir = opendir(path); + if (!dir) + return MAX_TC; + + while ((d = readdir(dir))) + if (!strncmp(d->d_name, "tx-", 3)) + n++; + closedir(dir); + + return n > 1 ? MIN(n, MAX_TC) : MAX_TC; +} + +/* Physical ports get the defaults rendered; virtual interfaces only on request. */ +static bool qos_is_port(const char *ifname) +{ + char path[PATH_MAX]; + + snprintf(path, sizeof(path), "/sys/class/net/%s/device", ifname); + return access(path, F_OK) == 0; +} + +/* Switch port behind a DSA conduit: the fabric never sees frames the CPU sends. */ +static bool qos_is_dsa(const char *ifname) +{ + char path[PATH_MAX], line[128]; + bool dsa = false; + FILE *fp; + + snprintf(path, sizeof(path), "/sys/class/net/%s/uevent", ifname); + fp = fopen(path, "r"); + if (!fp) + return false; + + while (fgets(line, sizeof(line), fp)) + if (!strcmp(chomp(line), "DEVTYPE=dsa")) + dsa = true; + fclose(fp); + + return dsa; +} + +static const char *qos_driver(const char *ifname, char *buf, size_t len) +{ + char path[PATH_MAX], line[128]; + const char *driver = NULL; + FILE *fp; + + snprintf(path, sizeof(path), "/sys/class/net/%s/device/uevent", ifname); + fp = fopen(path, "r"); + if (!fp) + return NULL; + + while (fgets(line, sizeof(line), fp)) { + if (strncmp(line, "DRIVER=", 7)) + continue; + strlcpy(buf, chomp(line + 7), len); + driver = buf; + break; + } + fclose(fp); + + return driver; +} + +static int qos_dcb_driver(const char *ifname) +{ + char buf[64]; + const char *driver = qos_driver(ifname, buf, sizeof(buf)); + size_t i; + + if (driver) + for (i = 0; i < NELEMS(dcb_drivers); i++) + if (!strcmp(dcb_drivers[i].driver, driver)) + return i; + + return -1; +} + +/* + * A switch fabric that encodes the PCP from the frame priority on every + * port, as an 802.1Q bridge does, is left to it: the tables it boots + * with are the identity encoding and are never disabled, so a cascade + * of chips, which carries only the priority between them, behaves like + * a single chip. remark pcp then changes nothing. + */ +static bool qos_pcp_encoded(const char *ifname) +{ + int i = qos_dcb_driver(ifname); + + return i >= 0 && dcb_drivers[i].pcp_encoded; +} + +/* Unknown drivers are not limited: without DCB the order is honoured in software. */ +static bool qos_trust_supported(const char *ifname, const char *order) +{ + int i = qos_dcb_driver(ifname); + size_t j; + + if (i < 0) + return true; + + for (j = 0; dcb_drivers[i].orders[j]; j++) + if (!strcmp(dcb_drivers[i].orders[j], order)) + return true; + + return false; +} + +static enum tsa tsa_from_str(const char *val) +{ + const char *id = strrchr(val, ':'); + + id = id ? id + 1 : val; + + if (!strcmp(id, "strict-priority")) + return TSA_STRICT; + if (!strcmp(id, "enhanced-transmission-selection")) + return TSA_ETS; + + return TSA_UNSUPPORTED; +} + +/* The trust leaf as dcb apptrust order words, none as an empty order. */ +static const char *trust_order(struct lyd_node *ingress) +{ + const char *val = ingress ? lydx_get_cattr(ingress, "trust") : NULL; + + if (!val) + return "pcp"; + if (!strcmp(val, "dscp-pcp")) + return "dscp pcp"; + if (!strcmp(val, "pcp-dscp")) + return "pcp dscp"; + if (!strcmp(val, "none")) + return ""; + + return val; +} + +static int qos_parse_egress(struct lyd_node *egress, const char *ifname, struct qos_egress *eg) +{ + struct lyd_node *table, *tc; + const char *val; + int i; + + memset(eg, 0, sizeof(*eg)); + eg->num_tc = qos_num_tc(ifname); + + table = egress ? lydx_get_child(egress, "traffic-class-table") : NULL; + + /* Preset, or custom leaves with the ieee preset behind unset ones. */ + val = table ? lydx_get_cattr(table, "preset") : NULL; + memcpy(eg->map, val && !strcmp(val, "ieee-sr") ? tc_ieee_sr[eg->num_tc - 1] + : tc_ieee[eg->num_tc - 1], sizeof(eg->map)); + for (i = 0; i < NUM_PRIO && table; i++) { + char name[16]; + + snprintf(name, sizeof(name), "priority%d", i); + val = lydx_get_cattr(table, name); + if (val) + eg->map[i] = atoi(val); + } + + for (i = 0; i < MAX_TC; i++) + eg->algo[i] = TSA_STRICT; + + if (!egress) + return 0; + + LYX_LIST_FOR_EACH(lyd_child(egress), tc, "traffic-class") { + int id = atoi(lydx_get_cattr(tc, "id")); + + if (id < 0 || id >= MAX_TC) + return -EINVAL; + + val = lydx_get_cattr(tc, "algorithm"); + eg->algo[id] = val ? tsa_from_str(val) : TSA_STRICT; + + val = lydx_get_cattr(tc, "bandwidth"); + if (val) + eg->bandwidth[id] = strtoul(val, NULL, 10); + } + + table = lydx_get_child(egress, "rate-limit"); + if (table) { + val = lydx_get_cattr(table, "rate"); + eg->rate = val ? strtoull(val, NULL, 10) : 0; + + /* Ten milliseconds at rate, never below one frame */ + val = lydx_get_cattr(table, "burst"); + eg->burst = val ? strtoul(val, NULL, 10) : eg->rate / 8 / 100; + if (eg->burst < 1518) + eg->burst = 1518; + } + + return 0; +} + +/* + * Checks the YANG model cannot express: map values and class ids + * against the port's class count, the algorithms rendered today, and + * the layout tc ets can render (strict bands first, i.e. the highest + * classes). + */ +static int qos_validate(sr_session_ctx_t *session, struct lyd_node *cif, struct lyd_node *qos) +{ + const char *ifname = lydx_get_cattr(cif, "name"); + struct lyd_node *egress, *tc; + struct qos_egress eg; + struct lyd_node *ingress = lydx_get_child(qos, "ingress"); + const char *trust; + bool ets = false; + int i; + + trust = ingress ? lydx_get_cattr(ingress, "trust") : NULL; + if (trust && strcmp(trust, "none") && !qos_trust_supported(ifname, trust)) { + sr_session_set_error_message(session, "%s: trust order %s not supported " + "by this port, see qos capabilities", ifname, trust); + return -EINVAL; + } + + egress = lydx_get_child(qos, "egress"); + if (qos_parse_egress(egress, ifname, &eg)) { + sr_session_set_error_message(session, "%s: invalid traffic class table", ifname); + return -EINVAL; + } + + for (i = 0; i < NUM_PRIO; i++) { + if (eg.map[i] >= eg.num_tc) { + sr_session_set_error_message(session, "%s: priority%d maps to traffic " + "class %d, port has %d classes", ifname, i, + eg.map[i], eg.num_tc); + return -EINVAL; + } + } + + if (!egress) + return 0; + + LYX_LIST_FOR_EACH(lyd_child(egress), tc, "traffic-class") { + int id = atoi(lydx_get_cattr(tc, "id")); + + if (id >= eg.num_tc) { + sr_session_set_error_message(session, "%s: traffic class %d, port has " + "%d classes", ifname, id, eg.num_tc); + return -EINVAL; + } + + if (eg.algo[id] == TSA_UNSUPPORTED) { + sr_session_set_error_message(session, "%s: traffic class %d: algorithm %s " + "not supported, use strict-priority or " + "enhanced-transmission-selection", ifname, id, + lydx_get_cattr(tc, "algorithm")); + return -EINVAL; + } + } + + /* Walk from the top: strict classes, then weighted, never back. */ + for (i = eg.num_tc - 1; i >= 0; i--) { + if (eg.algo[i] == TSA_ETS) + ets = true; + else if (ets) { + sr_session_set_error_message(session, "%s: traffic class %d: strict-priority " + "classes must be the highest-numbered ones, " + "above all weighted classes", ifname, i); + return -EINVAL; + } + } + + return 0; +} + +enum trust { + TRUST_PCP, + TRUST_DSCP, +}; + +struct qos_ingress { + const char *order; + int nfields; + enum trust field[2]; + int dflt; + int8_t pcp[NUM_PRIO][2]; /* [pcp][dei] to priority, -1 unset */ + int8_t dscp[64]; /* to priority, -1 untrusted */ +}; + +static void qos_parse_ingress(struct lyd_node *ingress, struct qos_ingress *in) +{ + struct lyd_node *map, *entry; + const char *val, *preset, *word; + int i; + + memset(in, 0, sizeof(*in)); + memset(in->pcp, -1, sizeof(in->pcp)); + memset(in->dscp, -1, sizeof(in->dscp)); + + in->order = trust_order(ingress); + for (word = in->order; *word; word = *word == ' ' ? word + 1 : word) { + in->field[in->nfields++] = strncmp(word, "pcp", 3) ? TRUST_DSCP : TRUST_PCP; + word += strcspn(word, " "); + } + + if (ingress) { + val = lydx_get_cattr(ingress, "default-priority"); + in->dflt = val ? atoi(val) : 0; + } + + /* The one preset is the 802.1Q default decoding, PCP n to priority n. */ + map = ingress ? lydx_get_child(ingress, "pcp-map") : NULL; + if (!map || !lydx_get_child(map, "entry")) { + for (i = 0; i < NUM_PRIO; i++) + in->pcp[i][0] = in->pcp[i][1] = i; + } else { + LYX_LIST_FOR_EACH(lyd_child(map), entry, "entry") { + int pcp = atoi(lydx_get_cattr(entry, "pcp")); + int dei = lydx_get_bool(entry, "dei") ? 1 : 0; + + in->pcp[pcp][dei] = atoi(lydx_get_cattr(entry, "priority")); + } + } + + map = ingress ? lydx_get_child(ingress, "dscp-map") : NULL; + if (!map || !lydx_get_child(map, "entry")) { + preset = map ? lydx_get_cattr(map, "preset") : NULL; + if (preset && !strcmp(preset, "msb")) { + for (i = 0; i < 64; i++) + in->dscp[i] = i >> 3; + } else { + for (i = 0; i < (int)NELEMS(dscp_ietf); i++) + in->dscp[dscp_ietf[i][0]] = dscp_ietf[i][1]; + } + } else { + LYX_LIST_FOR_EACH(lyd_child(map), entry, "entry") { + if (!lydx_get_bool(entry, "trusted")) + continue; + + in->dscp[atoi(lydx_get_cattr(entry, "dscp"))] = + atoi(lydx_get_cattr(entry, "priority")); + } + } +} + +/* + * One DCB APP table per port, the trust order as dcb apptrust. Drivers + * without the operations fail the calls; app_err then selects the + * software rendering below. + */ +static void gen_ingress_dcb(FILE *fp, const char *ifname, const struct qos_ingress *in) +{ + int i, dei, n; + + fputs("trust_err=0 app_err=0 rewr_err=0\n", fp); + fprintf(fp, "dcb apptrust set dev %s order %s 2>/dev/null || trust_err=1\n", ifname, in->order); + fprintf(fp, "dcb app flush dev %s default-prio pcp-prio dscp-prio 2>/dev/null || app_err=1\n", + ifname); + fprintf(fp, "dcb app add dev %s", ifname); + + for (i = 0, n = 0; i < NUM_PRIO; i++) + for (dei = 0; dei < 2; dei++) + if (in->pcp[i][dei] >= 0) + fprintf(fp, "%s %d%s:%d", n++ ? "" : " pcp-prio", i, + dei ? "de" : "nd", in->pcp[i][dei]); + + for (i = 0, n = 0; i < 64; i++) + if (in->dscp[i] >= 0) + fprintf(fp, "%s %d:%d", n++ ? "" : " dscp-prio", i, in->dscp[i]); + + /* default-prio takes every following word as a priority, so it goes last */ + fprintf(fp, " default-prio %d 2>/dev/null || app_err=1\n", in->dflt); +} + +/* + * Software classification: tc flower on a clsact ingress, one block of + * rules per trusted field in trust order, then a catch-all for the + * default priority. First match wins. flower cannot match DEI, so the + * DEI 0 entry is used for both. Tagged IP needs its own rules since the + * DSCP then sits behind the VLAN header, and a pref holds one protocol, + * so each variant gets its own. A hundred rules per port is normal, so + * they go through one tc batch rather than one process each. + */ +static void gen_ingress_flower(FILE *fp, const char *ifname, const struct qos_ingress *in) +{ + static const char *ipproto[] = { + "protocol ip flower", + "protocol ipv6 flower", + "protocol 802.1Q flower vlan_ethtype ip", + "protocol 802.1Q flower vlan_ethtype ipv6", + }; + int f, i, p, pref = 100; + + fputs("tc -force -batch - <nfields; f++, pref += 100) { + if (in->field[f] == TRUST_PCP) { + for (i = 0; i < NUM_PRIO; i++) { + int prio = in->pcp[i][0] >= 0 ? in->pcp[i][0] : in->pcp[i][1]; + + if (prio < 0) + continue; + fprintf(fp, "filter add dev %s ingress pref %d protocol 802.1Q " + "flower vlan_prio %d action skbedit priority %d\n", + ifname, pref, i, prio); + } + continue; + } + + for (i = 0; i < 64; i++) { + if (in->dscp[i] < 0) + continue; + for (p = 0; p < (int)NELEMS(ipproto); p++) + fprintf(fp, "filter add dev %s ingress pref %d %s ip_tos 0x%02x/0xfc " + "action skbedit priority %d\n", ifname, pref + p, ipproto[p], + i << 2, in->dscp[i]); + } + } + + fprintf(fp, "filter add dev %s ingress pref 900 matchall action skbedit priority %d\n", + ifname, in->dflt); + fputs("EOF\n", fp); +} + +static void gen_ingress(FILE *fp, const char *ifname, struct lyd_node *ingress) +{ + struct qos_ingress in; + + qos_parse_ingress(ingress, &in); + gen_ingress_dcb(fp, ifname, &in); + + fputs("if [ $app_err -ne 0 ]; then\n", fp); + gen_ingress_flower(fp, ifname, &in); + fputs("fi\n", fp); +} + +/* + * Software DSCP remarking: tc basic filters on the egress side matching + * the skb priority and the frame's ethertype, rewriting the DS field + * with pedit and fixing the IPv4 header checksum. Tagged frames carry + * the ethertype behind the tag when the NIC has no VLAN offload, hence + * the second pair of rules. PCP has no software counterpart: act_vlan + * cannot change the priority without also setting the VLAN ID. + */ +static void gen_remark_pedit(FILE *fp, const char *ifname) +{ + static const struct { + const char *match; + const char *munge; + const char *csum; + } variants[] = { + { "cmp(u16 at 12 layer link eq 0x0800)", "ip dsfield", " pipe action csum ip" }, + { "cmp(u16 at 12 layer link eq 0x86dd)", "ip6 traffic_class", "" }, + { "cmp(u16 at 12 layer link eq 0x8100) and cmp(u16 at 16 layer link eq 0x0800)", + "ip dsfield", " pipe action csum ip" }, + { "cmp(u16 at 12 layer link eq 0x8100) and cmp(u16 at 16 layer link eq 0x86dd)", + "ip6 traffic_class", "" }, + }; + int i, v; + + fputs("tc -force -batch - </dev/null || rewr_err=1\n", ifname, + encoded ? "" : " prio-pcp"); + /* One code point per priority, a DEI 1 entry would replace the DEI 0 one */ + if (!encoded && pcp && !strcmp(pcp, "from-priority")) { + fprintf(fp, "dcb rewr add dev %s prio-pcp", ifname); + for (i = 0; i < NUM_PRIO; i++) + fprintf(fp, " %d:%dnd", i, i); + fputs(" 2>/dev/null || rewr_err=1\n", fp); + } + if (dscp && !strcmp(dscp, "from-priority")) { + fprintf(fp, "dcb rewr add dev %s prio-dscp", ifname); + for (i = 0; i < NUM_PRIO; i++) + fprintf(fp, " %d:%d", i, i << 3); + fputs(" 2>/dev/null || rewr_err=1\n", fp); + } + } + + /* + * On a DSA switch the hardware tables only see forwarded frames; + * frames the CPU sends are injected past them, so those are + * remarked by the kernel on the port's egress as well. + */ + if (dscp && !strcmp(dscp, "from-priority")) { + bool dsa = qos_is_dsa(ifname); + + if (!dsa) + fputs("if [ $rewr_err -ne 0 ]; then\n", fp); + gen_remark_pedit(fp, ifname); + if (!dsa) + fputs("fi\n", fp); + } +} + +/* + * Only settings the user configured are reported; the defaults are + * rendered on every port with a qos container and would flood the log + * on hardware without DCB. Software classification honours the trust + * order itself, so trust_err only matters when the DCB table was taken. + */ +static void gen_dcb_log(FILE *fp, const char *ifname, struct lyd_node *ingress, + struct lyd_node *remark) +{ + if (qos_is_explicit(ingress)) { + fprintf(fp, "[ $app_err -eq 0 ] || logger -t confd -p user.notice " + "\"%s: no DCB support in driver, classifying in software\"\n", ifname); + fprintf(fp, "[ $trust_err -eq 0 ] || [ $app_err -ne 0 ] || logger -t confd -p user.notice " + "\"%s: dcb apptrust unsupported by driver, trust order not applied\"\n", + ifname); + } + + if (qos_is_explicit(remark)) { + const char *pcp = lydx_get_cattr(remark, "pcp"); + + fprintf(fp, "[ $rewr_err -eq 0 ] || logger -t confd -p user.notice " + "\"%s: no DCB rewrite support in driver, remarking DSCP in software\"\n", + ifname); + if (pcp && !strcmp(pcp, "from-priority")) + fprintf(fp, "[ $rewr_err -eq 0 ] || logger -t confd -p user.notice " + "\"%s: PCP remarking needs driver support, not applied\"\n", ifname); + } +} + +/* + * tc ets carries the whole layout, and a driver that offloads it says + * so in the qdisc: band 0 is dequeued first, so class N-1 is band 0, + * strict bands come first, and quanta are listed for the weighted + * bands in band order, one frame per percent of share so no band gets + * less than a frame of credit per round. + */ +static void gen_egress(FILE *fp, const char *ifname, struct qos_egress *eg) +{ + const char *attach = "root"; + int i, nstrict = 0; + + fprintf(fp, "tc qdisc del dev %s root 2>/dev/null\n", ifname); + + /* + * The rate limit is one bucket on the whole port, so it takes + * the root and the scheduler hangs below it. + */ + if (eg->rate) { + fprintf(fp, "tc qdisc add dev %s root handle 1: tbf rate %" PRIu64 "bit burst %u" + " latency 100ms\n", ifname, eg->rate, eg->burst); + attach = "parent 1:1 handle 2:"; + } + + if (eg->num_tc < 2) + return; + + for (i = eg->num_tc - 1; i >= 0 && eg->algo[i] == TSA_STRICT; i--) + nstrict++; + + fprintf(fp, "tc qdisc add dev %s %s ets bands %d strict %d", ifname, attach, eg->num_tc, nstrict); + if (nstrict < eg->num_tc) { + fputs(" quanta", fp); + for (i = eg->num_tc - 1 - nstrict; i >= 0; i--) + fprintf(fp, " %u", eg->bandwidth[i] * ETS_QUANTUM_UNIT); + } + fputs(" priomap", fp); + for (i = 0; i < NUM_PRIO; i++) + fprintf(fp, " %d", eg->num_tc - 1 - eg->map[i]); + fputc('\n', fp); +} + +static int gen_reset(struct dagger *net, const char *ifname) +{ + FILE *fp; + + fp = dagger_fopen_net_init(net, ifname, NETDAG_INIT_POST, "qos.sh"); + if (!fp) + return -EIO; + + fprintf(fp, "tc qdisc del dev %s root 2>/dev/null\n", ifname); + fprintf(fp, "tc qdisc del dev %s clsact 2>/dev/null\n", ifname); + fprintf(fp, "dcb app flush dev %s default-prio pcp-prio dscp-prio 2>/dev/null\n", ifname); + fprintf(fp, "dcb rewr flush dev %s%s prio-dscp 2>/dev/null\n", ifname, + qos_pcp_encoded(ifname) ? "" : " prio-pcp"); + fprintf(fp, "dcb apptrust set dev %s order 2>/dev/null\n", ifname); + fputs("exit 0\n", fp); + fclose(fp); + + return 0; +} + +/* + * Whether a diff subtree carries a change to configuration. A leaf + * going back to its default shows up flagged default with the old value + * in metadata, which lydx_get_diff() reads; a deleted list entry or + * presence container always counts. + */ +static bool qos_has_change(struct lyd_node *node) +{ + struct lyd_node *child; + struct lydx_diff nd; + + if (!node || !node->schema) + return false; + + switch (node->schema->nodetype) { + case LYS_LEAF: + case LYS_LEAFLIST: + return lydx_get_diff(node, &nd); + case LYS_LIST: + return true; + case LYS_CONTAINER: + if (node->schema->flags & LYS_PRESENCE) + return true; + break; + default: + break; + } + + LY_LIST_FOR(lyd_child(node), child) + if (qos_has_change(child)) + return true; + + return false; +} + +int netdag_gen_qos(sr_session_ctx_t *session, struct dagger *net, struct lyd_node *cif, + struct lyd_node *dif) +{ + const char *ifname = lydx_get_cattr(cif, "name"); + struct lyd_node *qos, *dqos, *ingress, *remark; + struct qos_egress eg; + FILE *fp; + int err; + + qos = lydx_get_child(cif, "qos"); + dqos = lydx_get_child(dif, "qos"); + + /* + * The defaults are a complete pipeline and render on every + * physical port, at creation and whenever qos changes. Virtual + * interfaces get one only when configured; removing it there + * resets the interface. + */ + if (!qos_has_change(dqos) && lydx_get_op(dif) != LYDX_OP_CREATE) + return 0; + + if (!qos_is_explicit(qos) && !qos_is_port(ifname)) { + if (qos_has_change(dqos)) + return gen_reset(net, ifname); + return 0; + } + + err = qos_validate(session, cif, qos); + if (err) + return err; + + err = qos_parse_egress(lydx_get_child(qos, "egress"), ifname, &eg); + if (err) + return err; + + fp = dagger_fopen_net_init(net, ifname, NETDAG_INIT_POST, "qos.sh"); + if (!fp) + return -EIO; + + ingress = lydx_get_child(qos, "ingress"); + remark = lydx_get_descendant(lyd_child(qos), "egress", "remark", NULL); + + fprintf(fp, "tc qdisc del dev %s clsact 2>/dev/null\n", ifname); + gen_ingress(fp, ifname, ingress); + gen_remark(fp, ifname, remark); + gen_dcb_log(fp, ifname, ingress, remark); + + /* + * Replacing the root qdisc reprograms the port's queues, which on + * offloading hardware drops traffic for a moment. Only do it when + * the traffic classes changed, not for an ingress or remark edit. + */ + if (lydx_get_op(dif) == LYDX_OP_CREATE || + qos_has_change(lydx_get_descendant(lyd_child(dqos), "egress", "traffic-class-table", NULL)) || + qos_has_change(lydx_get_descendant(lyd_child(dqos), "egress", "traffic-class", NULL)) || + qos_has_change(lydx_get_descendant(lyd_child(dqos), "egress", "rate-limit", NULL))) + gen_egress(fp, ifname, &eg); + fclose(fp); + + return 0; +} diff --git a/src/confd/src/interfaces.c b/src/confd/src/interfaces.c index be1bc222b..f880c20c0 100644 --- a/src/confd/src/interfaces.c +++ b/src/confd/src/interfaces.c @@ -737,6 +737,10 @@ static sr_error_t netdag_gen_iface(sr_session_ctx_t *session, struct dagger *net goto err_close_ip; } + err = netdag_gen_qos(session, net, cif, dif); + if (err) + goto err_close_ip; + /* Set Addresses */ err = err ? : netdag_gen_link_mtu(ip, dif); err = err ? : netdag_gen_link_addr(ip, cif, dif); diff --git a/src/confd/src/interfaces.h b/src/confd/src/interfaces.h index a7bb77c07..430d781ea 100644 --- a/src/confd/src/interfaces.h +++ b/src/confd/src/interfaces.h @@ -167,6 +167,10 @@ int ifchange_cand_infer_dhcp(sr_session_ctx_t *session, const char *path); /* if-vxlan.c */ int vxlan_gen(struct lyd_node *dif, struct lyd_node *cif, FILE *ip); +/* if-qos.c */ +int netdag_gen_qos(sr_session_ctx_t *session, struct dagger *net, struct lyd_node *cif, + struct lyd_node *dif); + /* infix-if-wireguard */ int wireguard_validate_peers(sr_session_ctx_t *session, struct lyd_node *cif); int wireguard_gen(struct lyd_node *dif, struct lyd_node *cif, FILE *ip, struct dagger *net); diff --git a/src/confd/yang/confd.inc b/src/confd/yang/confd.inc index 62ba37b9d..aeba520b0 100644 --- a/src/confd/yang/confd.inc +++ b/src/confd/yang/confd.inc @@ -28,7 +28,7 @@ MODULES=( "iana-hardware@2018-03-13.yang" "ietf-hardware@2018-03-13.yang -e hardware-state -e hardware-sensor" "infix-hardware@2026-07-02.yang" - "ieee802-dot1q-types@2022-10-29.yang" + "ieee802-dot1q-types@2023-10-26.yang" "infix-ip@2026-04-28.yang" "infix-if-type@2026-01-07.yang" "infix-routing@2026-03-11.yang" @@ -48,7 +48,7 @@ MODULES=( "ieee802-ethernet-phy-type@2025-09-10.yang" "infix-ethernet-interface@2026-05-21.yang" "infix-factory-default@2023-06-28.yang" - "infix-interfaces@2026-06-11.yang -e vlan-filtering" + "infix-interfaces@2026-09-07.yang -e vlan-filtering -e rate-limit" "ietf-crypto-types -e cleartext-symmetric-keys" "infix-crypto-types@2026-02-14.yang" "ietf-keystore -e symmetric-keys" diff --git a/src/confd/yang/confd/ieee802-dot1q-types@2022-10-29.yang b/src/confd/yang/confd/ieee802-dot1q-types@2023-10-26.yang similarity index 60% rename from src/confd/yang/confd/ieee802-dot1q-types@2022-10-29.yang rename to src/confd/yang/confd/ieee802-dot1q-types@2023-10-26.yang index b2f709971..9f436ca59 100644 --- a/src/confd/yang/confd/ieee802-dot1q-types@2022-10-29.yang +++ b/src/confd/yang/confd/ieee802-dot1q-types@2023-10-26.yang @@ -1,4 +1,5 @@ module ieee802-dot1q-types { + yang-version "1.1"; namespace urn:ieee:std:802.1Q:yang:ieee802-dot1q-types; prefix dot1q-types; import ietf-yang-types { @@ -12,19 +13,51 @@ module ieee802-dot1q-types { Contact: IEEE 802.1 Working Group Chair Postal: C/O IEEE 802.1 Working Group - IEEE Standards Association - 445 Hoes Lane - Piscataway, NJ 08854 - USA + IEEE Standards Association + 445 Hoes Lane + Piscataway, NJ 08854 + USA E-mail: stds-802-1-chairs@ieee.org"; description "Common types used within dot1Q-bridge modules. - Copyright (C) IEEE (2022). + Copyright (C) IEEE (2023). This version of this YANG module is part of IEEE Std 802.1Q; see the - standard itself for full legal notices."; + standard itself for full legal notices."; + revision 2023-10-26 { + description + "Published as part of IEEE Std 802.1Qcj-2023. This revision adds + the isid-type. + + The following reference statement identifies each referenced IEEE + Standard as updated by applicable amendments."; + reference + "IEEE Std 802.1Q Bridges and Bridged Networks: + IEEE Std 802.1Q-2022, IEEE Std 802.1Qcz-2023, IEEE Std 802.1Qcw-2023, + IEEE Std 802.1Qcj-2023. + IEEE Std 802 Overview and Architecture: IEEE Std 802-2014."; + } + revision 2023-10-22 { + description + "Published as part of IEEE Std 802.1Qcw-2023. + + The following reference statement identifies each referenced IEEE + Standard as updated by applicable amendments."; + reference + "IEEE Std 802.1Q Bridges and Bridged Networks: + IEEE Std 802.1Q-2022, IEEE Std 802.1Qcz-2023, IEEE Std 802.1Qcw-2023. + IEEE Std 802 Overview and Architecture: + IEEE Std 802-2014."; + } + revision 2023-07-03 { + description + "Published as part of IEEE Std 802.1Qcz-2023."; + reference + "IEEE Std 802.1Qcz-2023, Bridges and Bridged Networks - Congestion + Isolation."; + } revision 2022-10-29 { description "Published as part of IEEE Std 802.1Q-2022."; @@ -33,28 +66,27 @@ module ieee802-dot1q-types { } revision 2020-06-04 { description - "Published as part of IEEE Std 802.1Qcx-2020. Second version."; + "Published as part of IEEE Std 802.1Qcx-2020."; reference "IEEE Std 802.1Qcx-2020, Bridges and Bridged Networks - YANG Data Model for Connectivity Fault Management."; } revision 2018-03-07 { description - "Published as part of IEEE Std 802.1Q-2018. Initial version."; + "Published as part of IEEE Std 802.1Q-2018."; reference "IEEE Std 802.1Q-2018, Bridges and Bridged Networks."; } identity dot1q-vlan-type { description - "Base identity from which all 802.1Q VLAN tag types are derived - from."; + "Base identity from which all 802.1Q VLAN tag types are derived."; } identity c-vlan { base dot1q-vlan-type; description - "An 802.1Q Customer VLAN, using the 81-00 EtherType"; + "An 802.1Q Customer VLAN, using the 81-00 EtherType."; reference - "5.5 of IEEE Std 802.1Q-2022"; + "5.5 of IEEE Std 802.1Q"; } identity s-vlan { base dot1q-vlan-type; @@ -62,48 +94,51 @@ module ieee802-dot1q-types { "An 802.1Q Service VLAN, using the 88-A8 EtherType originally introduced in 802.1ad, and incorporated into 802.1Q (2011)"; reference - "5.6 of IEEE Std 802.1Q-2022"; + "5.6 of IEEE Std 802.1Q"; } identity transmission-selection-algorithm { description - "Specify the transmission selection algorithms of IEEE Std - 802.1Q-2022 Table 8-6"; + "Specify the transmission selection algorithms of IEEE Std 802.1Q + Table 8-6"; } identity strict-priority { base transmission-selection-algorithm; description "Indicates the strict priority transmission selection algorithm."; reference - "Table 8-6 of IEEE Std 802.1Q-2022"; + "Table 8-6 of IEEE Std 802.1Q"; } identity credit-based-shaper { base transmission-selection-algorithm; description - "Indicates the credit based shaper transmission selection - algorithm."; + "Indicates the credit-based shaper transmission selection algorithm."; reference - "Table 8-6 of IEEE Std 802.1Q-2022"; + "Table 8-6 of IEEE Std 802.1Q"; } identity enhanced-transmission-selection { base transmission-selection-algorithm; description "Indicates the enhanced transmission selection algorithm."; reference - "Table 8-6 of IEEE Std 802.1Q-2022"; + "Table 8-6 of IEEE Std 802.1Q"; } identity asynchronous-traffic-shaping { base transmission-selection-algorithm; description "Indicates the asynchronous transmission selection algorithm."; reference - "Table 8-6 of IEEE Std 802.1Q-2022"; + "Table 8-6 of IEEE Std 802.1Q"; } identity vendor-specific { base transmission-selection-algorithm; description "Indicates a vendor specific transmission selection algorithm."; reference - "Table 8-6 of IEEE Std 802.1Q-2022"; + "Table 8-6 of IEEE Std 802.1Q"; + } + identity type-of-operation { + description + "Represents the operation type (name)."; } typedef name-type { type string { @@ -129,9 +164,17 @@ module ieee802-dot1q-types { "A range of priorities from 0 to 7 (inclusive). The Priority Code Point (PCP) is a 3-bit field that refers to the class of service associated with an 802.1Q VLAN tagged frame. The field specifies a - priority value between 0 and 7, these values can be used by - quality of service (QoS) to prioritize different classes of - traffic."; + priority value between 0 and 7, these values can be used by quality of + service (QoS) to prioritize different classes of traffic."; + } + typedef num-traffic-class-type { + type uint8 { + range "1..8"; + } + description + "The number of traffic classes supported or participating in a + particular feature. There are between 1 and 8 supported traffic + classes defined by IEEE Std 802.1Q."; } typedef vid-range-type { type string { @@ -142,13 +185,12 @@ module ieee802-dot1q-types { "(,[1-9][0-9]{0,3}(-[1-9][0-9]{0,3})?)*)"; } description - "A list of VLAN Ids, or non overlapping VLAN ranges, in ascending + "A list of VLAN IDs, or non overlapping VLAN ranges, in ascending order, between 1 and 4094. - This type is used to match an ordered list of VLAN Ids, or - contiguous ranges of VLAN Ids. Valid VLAN Ids must be in the range - 1 to 4094, and included in the list in non overlapping ascending - order. + This type is used to match an ordered list of VLAN IDs, or contiguous + ranges of VLAN IDs. Valid VLAN IDs must be in the range 1 to 4094, and + included in the list in non overlapping ascending order. For example: 1,10-100,250,500-1000"; } @@ -160,23 +202,21 @@ module ieee802-dot1q-types { "The vlanid type uniquely identifies a VLAN. This is the 12-bit VLAN-ID used in the VLAN Tag header. The range is defined by the referenced specification. This type is in the value set and its - semantics equivalent to the VlanId textual convention of the - SMIv2."; + semantics equivalent to the VlanId textual convention of the SMIv2."; } typedef vlan-index-type { type uint32 { range "1..4094 | 4096..4294967295"; } description - "A value used to index per-VLAN tables. Values of 0 and 4095 are - not permitted. The range of valid VLAN indices. If the value is - greater than 4095, then it represents a VLAN with scope local to - the particular agent, i.e., one without a global VLAN-ID assigned - to it. Such VLANs are outside the scope of IEEE 802.1Q, but it is - convenient to be able to manage them in the same way using this - YANG module."; + "A value used to index per-VLAN tables. Values of 0 and 4095 are not + permitted. The range of valid VLAN indices. If the value is greater + than 4095, then it represents a VLAN with scope local to the + particular agent, i.e., one without a global VLAN-ID assigned to it. + Such VLANs are outside the scope of IEEE 802.1Q, but it is convenient + to be able to manage them in the same way using this YANG module."; reference - "9.6 of IEEE Std 802.1Q-2022"; + "9.6 of IEEE Std 802.1Q"; } typedef mstid-type { type uint32 { @@ -186,7 +226,7 @@ module ieee802-dot1q-types { "In an MSTP Bridge, an MSTID, i.e., a value used to identify a spanning tree (or MST) instance"; reference - "13.8 of IEEE Std 802.1Q-2022"; + "13.8 of IEEE Std 802.1Q"; } typedef pcp-selection-type { type enumeration { @@ -210,7 +250,7 @@ module ieee802-dot1q-types { description "Priority Code Point selection types."; reference - "12.6.2.5.3, 6.9.3 of IEEE Std 802.1Q-2022"; + "12.6.2.5.3, 6.9.3 of IEEE Std 802.1Q"; } typedef protocol-frame-format-type { type enumeration { @@ -238,7 +278,7 @@ module ieee802-dot1q-types { description "A value representing the frame format to be matched."; reference - "12.10.1.7.1 of IEEE Std 802.1Q-2022"; + "12.10.1.7.1 of IEEE Std 802.1Q"; } typedef ethertype-type { type string { @@ -248,7 +288,7 @@ module ieee802-dot1q-types { "The EtherType value represented in the canonical order defined by IEEE 802. The canonical representation uses uppercase characters."; reference - "9.2 of IEEE Std 802-2014"; + "9.2 of IEEE Std 802"; } typedef dot1q-tag-type { type identityref { @@ -257,7 +297,7 @@ module ieee802-dot1q-types { description "Identifies a specific 802.1Q tag type"; reference - "9.5 IEEE Std 802.1Q-2022"; + "9.5 IEEE Std 802.1Q"; } typedef traffic-class-type { type uint8 { @@ -268,12 +308,25 @@ module ieee802-dot1q-types { Bridge. Larger values are associated with higher priority traffic classes."; reference - "3.273 of IEEE Std 802.1Q-2022"; + "3.273 of IEEE Std 802.1Q"; + } + typedef isid-type { + type uint32 { + range "1|255..16777214"; + } + description + "The i-sid type represents a Backbone Service Instance Identifier + (I-SID). This is the 24-bit I-SID field used in the I-TAG TCI of a + Provider Backbone Bridging frame. The values 0, 2-254, and 16777215 + are reserved for future standardization. The value 255 is dedicated + to use by the SPB protocol."; + reference + "3.108, 9.7 of IEEE Std 802.1Q-2022"; } grouping dot1q-tag-classifier-grouping { description "A grouping which represents an 802.1Q VLAN, matching both the - EtherType and a single VLAN Id."; + EtherType and a single VLAN ID."; leaf tag-type { type dot1q-tag-type; mandatory true; @@ -284,13 +337,13 @@ module ieee802-dot1q-types { type vlanid; mandatory true; description - "VLAN Id"; + "VLAN ID"; } } grouping dot1q-tag-or-any-classifier-grouping { description "A grouping which represents an 802.1Q VLAN, matching both the - EtherType and a single VLAN Id or 'any' to match on any VLAN Id."; + EtherType and a single VLAN ID or 'any' to match on any VLAN ID."; leaf tag-type { type dot1q-tag-type; mandatory true; @@ -304,20 +357,20 @@ module ieee802-dot1q-types { enum any { value 4095; description - "Matches 'any' VLAN in the range 1 to 4094 that is not - matched by a more specific VLAN Id match"; + "Matches 'any' VLAN in the range 1 to 4094 that is not matched + by a more specific VLAN ID match"; } } } mandatory true; description - "VLAN Id or any"; + "VLAN ID or any"; } } grouping dot1q-tag-ranges-classifier-grouping { description - "A grouping which represents an 802.1Q VLAN that matches a range - of VLAN Ids."; + "A grouping which represents an 802.1Q VLAN that matches a range of + VLAN IDs."; leaf tag-type { type dot1q-tag-type; mandatory true; @@ -328,14 +381,14 @@ module ieee802-dot1q-types { type vid-range-type; mandatory true; description - "VLAN Ids"; + "VLAN IDs"; } } grouping dot1q-tag-ranges-or-any-classifier-grouping { description "A grouping which represents an 802.1Q VLAN, matching both the - EtherType and a single VLAN Id, ordered list of ranges, or 'any' - to match on any VLAN Id."; + EtherType and a single VLAN ID, ordered list of ranges, or 'any' to + match on any VLAN ID."; leaf tag-type { type dot1q-tag-type; mandatory true; @@ -355,23 +408,22 @@ module ieee802-dot1q-types { } mandatory true; description - "VLAN Ids or any"; + "VLAN IDs or any"; } } grouping priority-regeneration-table-grouping { description - "The priority regeneration table provides the ability to map - incoming priority values on a per-Port basis, under management - control."; + "The priority regeneration table provides the ability to map incoming + priority values on a per-Port basis, under management control."; reference - "6.9.4 of IEEE Std 802.1Q-2022"; + "6.9.4 of IEEE Std 802.1Q"; leaf priority0 { type priority-type; default "0"; description "Priority 0"; reference - "12.6.2.3, 6.9.4 of IEEE Std 802.1Q-2022"; + "12.6.2.3, 6.9.4 of IEEE Std 802.1Q"; } leaf priority1 { type priority-type; @@ -379,7 +431,7 @@ module ieee802-dot1q-types { description "Priority 1"; reference - "12.6.2.3, 6.9.4 of IEEE Std 802.1Q-2022"; + "12.6.2.3, 6.9.4 of IEEE Std 802.1Q"; } leaf priority2 { type priority-type; @@ -387,7 +439,7 @@ module ieee802-dot1q-types { description "Priority 2"; reference - "12.6.2.3, 6.9.4 of IEEE Std 802.1Q-2022"; + "12.6.2.3, 6.9.4 of IEEE Std 802.1Q"; } leaf priority3 { type priority-type; @@ -395,7 +447,7 @@ module ieee802-dot1q-types { description "Priority 3"; reference - "12.6.2.3, 6.9.4 of IEEE Std 802.1Q-2022"; + "12.6.2.3, 6.9.4 of IEEE Std 802.1Q"; } leaf priority4 { type priority-type; @@ -403,7 +455,7 @@ module ieee802-dot1q-types { description "Priority 4"; reference - "12.6.2.3, 6.9.4 of IEEE Std 802.1Q-2022"; + "12.6.2.3, 6.9.4 of IEEE Std 802.1Q"; } leaf priority5 { type priority-type; @@ -411,7 +463,7 @@ module ieee802-dot1q-types { description "Priority 5"; reference - "12.6.2.3, 6.9.4 of IEEE Std 802.1Q-2022"; + "12.6.2.3, 6.9.4 of IEEE Std 802.1Q"; } leaf priority6 { type priority-type; @@ -419,7 +471,7 @@ module ieee802-dot1q-types { description "Priority 6"; reference - "12.6.2.3, 6.9.4 of IEEE Std 802.1Q-2022"; + "12.6.2.3, 6.9.4 of IEEE Std 802.1Q"; } leaf priority7 { type priority-type; @@ -427,53 +479,53 @@ module ieee802-dot1q-types { description "Priority 7"; reference - "12.6.2.3, 6.9.4 of IEEE Std 802.1Q-2022"; + "12.6.2.3, 6.9.4 of IEEE Std 802.1Q"; } } grouping pcp-decoding-table-grouping { description - "The Priority Code Point decoding table enables the decoding of - the priority and drop-eligible parameters from the PCP."; + "The Priority Code Point decoding table enables the decoding of the + priority and drop-eligible parameters from the PCP."; reference - "6.9.3 of IEEE Std 802.1Q-2022"; + "6.9.3 of IEEE Std 802.1Q"; list pcp-decoding-map { key "pcp"; description - "This map associates the priority code point field found in the - VLAN to a priority and drop eligible value based upon the - priority code point selection type."; + "This map associates the priority code point field found in the VLAN + with a priority and drop eligible value based upon the priority code + point selection type."; leaf pcp { type pcp-selection-type; description "The priority code point selection type."; reference - "12.6.2.7, 6.9.3 of IEEE Std 802.1Q-2022"; + "12.6.2.7, 6.9.3 of IEEE Std 802.1Q"; } list priority-map { key "priority-code-point"; description - "This map associated a priority code point value to priority + "This map associates a priority code point value with priority and drop eligible parameters."; leaf priority-code-point { type priority-type; description "Priority associated with the pcp."; reference - "12.6.2.7, 6.9.3 of IEEE Std 802.1Q-2022"; + "12.6.2.7, 6.9.3 of IEEE Std 802.1Q"; } leaf priority { type priority-type; description "Priority associated with the pcp."; reference - "12.6.2.7, 6.9.3 of IEEE Std 802.1Q-2022"; + "12.6.2.7, 6.9.3 of IEEE Std 802.1Q"; } leaf drop-eligible { type boolean; description "Drop eligible value for pcp"; reference - "12.6.2.7, 6.9.3 of IEEE Std 802.1Q-2022"; + "12.6.2.7, 6.9.3 of IEEE Std 802.1Q"; } } } @@ -483,62 +535,62 @@ module ieee802-dot1q-types { "The Priority Code Point encoding table encodes the priority and drop-eligible parameters in the PCP field of the VLAN tag."; reference - "12.6.2.9, 6.9.3 of IEEE Std 802.1Q-2022"; + "12.6.2.9, 6.9.3 of IEEE Std 802.1Q"; list pcp-encoding-map { key "pcp"; description - "This map associated the priority and drop-eligible parameters - to the priority used to encode the PCP of the VLAN based upon + "This map associates the priority and drop-eligible parameters + with the priority used to encode the PCP of the VLAN based upon the priority code point selection type."; leaf pcp { type pcp-selection-type; description "The priority code point selection type."; reference - "12.6.2.7, 6.9.3 of IEEE Std 802.1Q-2022"; + "12.6.2.7, 6.9.3 of IEEE Std 802.1Q"; } list priority-map { key "priority dei"; description - "This map associated the priority and drop-eligible parameters - to the priority code point field of the VLAN tag."; + "This map associates the priority and drop-eligible parameters + with the priority code point field of the VLAN tag."; leaf priority { type priority-type; description "Priority associated with the pcp."; reference - "12.6.2.7, 6.9.3 of IEEE Std 802.1Q-2022"; + "12.6.2.7, 6.9.3 of IEEE Std 802.1Q"; } leaf dei { type boolean; description "The drop eligible value."; reference - "12.6.2, 8.6.6 of IEEE Std 802.1Q-2022"; + "12.6.2, 8.6.6 of IEEE Std 802.1Q"; } leaf priority-code-point { type priority-type; description "PCP value for priority when DEI value"; reference - "12.6.2.9, 6.9.3 of IEEE Std 802.1Q-2022"; + "12.6.2.9, 6.9.3 of IEEE Std 802.1Q"; } } } } grouping service-access-priority-table-grouping { description - "The Service Access Priority Table associates a received priority - with a service access priority."; + "The Service Access Priority Table associates a received priority with + a service access priority."; reference - "12.6.2.17, 6.13.1 of IEEE Std 802.1Q-2022"; + "12.6.2.17, 6.13.1 of IEEE Std 802.1Q"; leaf priority0 { type priority-type; default "0"; description "Service access priority value for priority 0"; reference - "12.6.2.17, 6.13.1 of IEEE Std 802.1Q-2022"; + "12.6.2.17, 6.13.1 of IEEE Std 802.1Q"; } leaf priority1 { type priority-type; @@ -546,7 +598,7 @@ module ieee802-dot1q-types { description "Service access priority value for priority 1"; reference - "12.6.2.17, 6.13.1 of IEEE Std 802.1Q-2022"; + "12.6.2.17, 6.13.1 of IEEE Std 802.1Q"; } leaf priority2 { type priority-type; @@ -554,7 +606,7 @@ module ieee802-dot1q-types { description "Service access priority value for priority 2"; reference - "12.6.2.17, 6.13.1 of IEEE Std 802.1Q-2022"; + "12.6.2.17, 6.13.1 of IEEE Std 802.1Q"; } leaf priority3 { type priority-type; @@ -562,7 +614,7 @@ module ieee802-dot1q-types { description "Service access priority value for priority 3"; reference - "12.6.2.17, 6.13.1 of IEEE Std 802.1Q-2022"; + "12.6.2.17, 6.13.1 of IEEE Std 802.1Q"; } leaf priority4 { type priority-type; @@ -570,7 +622,7 @@ module ieee802-dot1q-types { description "Service access priority value for priority 4"; reference - "12.6.2.17, 6.13.1 of IEEE Std 802.1Q-2022"; + "12.6.2.17, 6.13.1 of IEEE Std 802.1Q"; } leaf priority5 { type priority-type; @@ -578,7 +630,7 @@ module ieee802-dot1q-types { description "Service access priority value for priority 5"; reference - "12.6.2.17, 6.13.1 of IEEE Std 802.1Q-2022"; + "12.6.2.17, 6.13.1 of IEEE Std 802.1Q"; } leaf priority6 { type priority-type; @@ -586,7 +638,7 @@ module ieee802-dot1q-types { description "Service access priority value for priority 6"; reference - "12.6.2.17, 6.13.1 of IEEE Std 802.1Q-2022"; + "12.6.2.17, 6.13.1 of IEEE Std 802.1Q"; } leaf priority7 { type priority-type; @@ -594,34 +646,40 @@ module ieee802-dot1q-types { description "Service access priority value for priority 7"; reference - "12.6.2.17, 6.13.1 of IEEE Std 802.1Q-2022"; + "12.6.2.17, 6.13.1 of IEEE Std 802.1Q"; } } grouping traffic-class-table-grouping { description "The Traffic Class Table models the operations that can be - performed on, or inquire about, the current contents of the + performed on, or can inquire about, the current contents of the Traffic Class Table (8.6.6) for a given Port."; reference - "12.6.3, 8.6.6 of IEEE Std 802.1Q-2022"; + "12.6.3, 8.6.6 of IEEE Std 802.1Q"; list traffic-class-map { key "priority"; + status "deprecated"; description - "The priority index into the traffic class table."; + "The priority index into the traffic class table. This list modeled + the Traffic Class Table incorrectly. available-traffic-class should + be a single value per port and not a list of all possible + available-traffic-class. For more information see maintenance issue + 0230. The status of this object is deprecated. It is replaced by + sibling container traffic-class-table."; leaf priority { type priority-type; description "The priority of the traffic class entry."; reference - "8.6.6 of IEEE Std 802.1Q-2022"; + "8.6.6 of IEEE Std 802.1Q"; } list available-traffic-class { key "num-traffic-class"; description - "The traffic class index associated with a given priority - within the traffic class table."; + "The traffic class index associated with a given priority within + the traffic class table."; reference - "8.6.6 of IEEE Std 802.1Q-2022"; + "8.6.6 of IEEE Std 802.1Q"; leaf num-traffic-class { type uint8 { range "1..8"; @@ -629,38 +687,124 @@ module ieee802-dot1q-types { description "The available number of traffic classes."; reference - "8.6.6 of IEEE Std 802.1Q-2022"; + "8.6.6 of IEEE Std 802.1Q"; } leaf traffic-class { type traffic-class-type; description - "The traffic class index associated with a given traffic - class entry."; + "The traffic class index associated with a given traffic class + entry."; reference - "8.6.6 of IEEE Std 802.1Q-2022"; + "8.6.6 of IEEE Std 802.1Q"; } } } + container traffic-class-table { + description + "The priority to traffic class mapping."; + reference + "8.6.6 of IEEE Std 802.1Q"; + leaf number-of-traffic-classes { + type uint8 { + range "1..8"; + } + description + "The number of egress traffic classes supported on this port. This + object may be optionally read-only."; + reference + "12.6.3.1 of IEEE Std 802.1Q"; + } + leaf priority0 { + type traffic-class-type; + must + "current() < ../number-of-traffic-classes"; + description + "The traffic class index associated with priority 0"; + reference + "8.6.6 of IEEE Std 802.1Q"; + } + leaf priority1 { + type traffic-class-type; + must + "current() < ../number-of-traffic-classes"; + description + "The traffic class index associated with priority 1"; + reference + "8.6.6 of IEEE Std 802.1Q"; + } + leaf priority2 { + type traffic-class-type; + must + "current() < ../number-of-traffic-classes"; + description + "The traffic class index associated with priority 2"; + reference + "8.6.6 of IEEE Std 802.1Q"; + } + leaf priority3 { + type traffic-class-type; + must + "current() < ../number-of-traffic-classes"; + description + "The traffic class index associated with priority 3"; + reference + "8.6.6 of IEEE Std 802.1Q"; + } + leaf priority4 { + type traffic-class-type; + must + "current() < ../number-of-traffic-classes"; + description + "The traffic class index associated with priority 4"; + reference + "8.6.6 of IEEE Std 802.1Q"; + } + leaf priority5 { + type traffic-class-type; + must + "current() < ../number-of-traffic-classes"; + description + "The traffic class index associated with priority 5"; + reference + "8.6.6 of IEEE Std 802.1Q"; + } + leaf priority6 { + type traffic-class-type; + must + "current() < ../number-of-traffic-classes"; + description + "The traffic class index associated with priority 6"; + reference + "8.6.6 of IEEE Std 802.1Q"; + } + leaf priority7 { + type traffic-class-type; + must + "current() < ../number-of-traffic-classes"; + description + "The traffic class index associated with priority 7"; + reference + "8.6.6 of IEEE Std 802.1Q"; + } + } } grouping transmission-selection-table-grouping { description - "The Transmission Selection Algorithm Table models the operations - that can be performed on, or inquire about, the current contents - of the Transmission Selection Algorithm Table (12.20.2) for a - given Port."; + "The Transmission Selection Algorithm Table models the operations that + can be performed on, or can inquire about, the current contents of the + Transmission Selection Algorithm Table (12.20.2) for a given Port."; reference - "12.20.2, 8.6.8 of IEEE Std 802.1Q-2022"; + "12.20.2, 8.6.8 of IEEE Std 802.1Q"; list transmission-selection-algorithm-map { key "traffic-class"; description - "The traffic class to index into the transmission selection - table."; + "The traffic class to index into the transmission selection table."; leaf traffic-class { type traffic-class-type; description "The traffic class of the entry."; reference - "8.6.6 of IEEE Std 802.1Q-2022"; + "8.6.6 of IEEE Std 802.1Q"; } leaf transmission-selection-algorithm { type identityref { @@ -669,7 +813,7 @@ module ieee802-dot1q-types { description "Transmission selection algorithm"; reference - "8.6.8, Table 8-6 of IEEE Std 802.1Q-2022"; + "8.6.8, Table 8-6 of IEEE Std 802.1Q"; } } } @@ -678,7 +822,7 @@ module ieee802-dot1q-types { "A set of control indicators, one for each Port. A Port Map, containing a control element for each outbound Port"; reference - "8.8.1, 8.8.2 of IEEE Std 802.1Q-2022"; + "8.8.1, 8.8.2 of IEEE Std 802.1Q"; list port-map { key "port-ref"; description @@ -688,7 +832,7 @@ module ieee802-dot1q-types { description "The interface port reference associated with this map."; reference - "8.8.1 of IEEE Std 802.1Q-2022"; + "8.8.1 of IEEE Std 802.1Q"; } choice map-type { description @@ -710,30 +854,29 @@ module ieee802-dot1q-types { } enum forward-filter { description - "Forwarded or filtered on the basis of dynamic - filtering information, or on the basis of the default - Group filtering behavior for the outbound Port (8.8.6) - if no dynamic filtering information is present - specifically for the MAC address."; + "Forwarded or filtered on the basis of dynamic filtering + information, or on the basis of the default Group + filtering behavior for the outbound Port (8.8.6) if no + dynamic filtering information is present specifically for + the MAC address."; } } description - "containing a control element for each outbound Port, - specifying that a frame with a destination MAC address, - and in the case of VLAN Bridge components, VID that meets - this specification."; + "A control element for each outbound Port, specifying that + a frame with a destination MAC address, and in the case of + VLAN Bridge components, VID that meets this specification."; reference - "8.8.1 of IEEE Std 802.1Q-2022"; + "8.8.1 of IEEE Std 802.1Q"; } leaf connection-identifier { type port-number-type; description - "A Port MAP may contain a connection identifier (8.8.12) - for each outbound port. The connection identifier may be - associated with the Bridge Port value maintained in a - Dynamic Filtering Entry of the FDB for Bridge Ports."; + "A Port MAP may contain a connection identifier (8.8.12) for + each outbound port. The connection identifier may be + associated with the Bridge Port value maintained in a Dynamic + Filtering Entry of the FDB for Bridge Ports."; reference - "8.8.1, 8.8.12 of IEEE Std 802.1Q-2022"; + "8.8.1, 8.8.12 of IEEE Std 802.1Q"; } } container static-vlan-registration-entries { @@ -759,10 +902,10 @@ module ieee802-dot1q-types { } } description - "The Registrar Administrative Control values for MVRP and - MIRP for the VID."; + "The Registrar Administrative Control values for MVRP and MIRP + for the VID."; reference - "8.8.2 of IEEE Std 802.1Q-2022"; + "8.8.2 of IEEE Std 802.1Q"; } leaf vlan-transmitted { type enumeration { @@ -779,7 +922,7 @@ module ieee802-dot1q-types { "Whether frames are to be VLAN-tagged or untagged when transmitted."; reference - "8.8.2 of IEEE Std 802.1Q-2022"; + "8.8.2 of IEEE Std 802.1Q"; } } container mac-address-registration-entries { @@ -799,12 +942,11 @@ module ieee802-dot1q-types { } } description - "containing a control element for each outbound Port, - specifying that a frame with a destination MAC address, - and in the case of VLAN Bridge components, VID that meets - this specification."; + "A control element for each outbound Port, specifying that + a frame with a destination MAC address, and in the case of + VLAN Bridge components, VID that meets this specification."; reference - "8.8.4 of IEEE Std 802.1Q-2022"; + "8.8.4 of IEEE Std 802.1Q"; } } container dynamic-vlan-registration-entries { @@ -819,12 +961,11 @@ module ieee802-dot1q-types { } } description - "containing a control element for each outbound Port, - specifying that a frame with a destination MAC address, - and in the case of VLAN Bridge components, VID that meets - this specification."; + "A control element for each outbound Port, specifying that + a frame with a destination MAC address, and in the case of + VLAN Bridge components, VID that meets this specification."; reference - "8.8.5 of IEEE Std 802.1Q-2022"; + "8.8.5 of IEEE Std 802.1Q"; } } container dynamic-reservation-entries { @@ -844,12 +985,11 @@ module ieee802-dot1q-types { } } description - "Containing a control element for each outbound Port, - specifying that a frame with a destination MAC address, - and in the case of VLAN Bridge components, VID that meets - this specification."; + "A control element for each outbound Port, specifying that + a frame with a destination MAC address, and in the case of + VLAN Bridge components, VID that meets this specification."; reference - "8.8.7 of IEEE Std 802.1Q-2022"; + "8.8.7 of IEEE Std 802.1Q"; } } container dynamic-filtering-entries { @@ -864,12 +1004,11 @@ module ieee802-dot1q-types { } } description - "Containing a control element for each outbound Port, - specifying that a frame with a destination MAC address, - and in the case of VLAN Bridge components, VID that meets - this specification."; + "A control element for each outbound Port, specifying that + a frame with a destination MAC address, and in the case of + VLAN Bridge components, VID that meets this specification."; reference - "8.8.3 of IEEE Std 802.1Q-2022"; + "8.8.3 of IEEE Std 802.1Q"; } } } @@ -879,7 +1018,7 @@ module ieee802-dot1q-types { description "Grouping of bridge port statistics."; reference - "12.6.1.1.3 of IEEE Std 802.1Q-2022"; + "12.6.1.1.3 of IEEE Std 802.1Q"; leaf delay-exceeded-discards { type yang:counter64; description @@ -887,7 +1026,7 @@ module ieee802-dot1q-types { transit delay through the Bridge. It is incremented by both transparent and source route Bridges."; reference - "12.6.1.1.3, 8.6.6 of IEEE Std 802.1Q-2022"; + "12.6.1.1.3, 8.6.6 of IEEE Std 802.1Q"; } leaf mtu-exceeded-discards { type yang:counter64; @@ -896,59 +1035,57 @@ module ieee802-dot1q-types { size. It is incremented by both transparent and source route Bridges."; reference - "Item g) in 12.6.1.1.3 of IEEE Std 802.1Q-2022"; + "Item g) in 12.6.1.1.3 of IEEE Std 802.1Q"; } leaf frame-rx { type yang:counter64; description - "The number of frames that have been received by this port from - its segment. Note that a frame received on the interface - corresponding to this port is only counted by this object if and - only if it is for a protocol being processed by the local - bridging function, including Bridge management frames."; + "The number of frames that have been received by this port from its + segment. Note that a frame received on the interface corresponding + to this port is only counted by this object if and only if it is for + a protocol being processed by the local bridging function, including + Bridge management frames."; reference - "12.6.1.1.3 of IEEE Std 802.1Q-2022"; + "12.6.1.1.3 of IEEE Std 802.1Q"; } leaf octets-rx { type yang:counter64; description - "The total number of octets in all valid frames received - (including BPDUs, frames addressed to the Bridge as an end - station, and frames that were submitted to the Forwarding - Process)."; + "The total number of octets in all valid frames received (including + BPDUs, frames addressed to the Bridge as an end station, and frames + that were submitted to the Forwarding Process)."; reference - "12.6.1.1.3 of IEEE Std 802.1Q-2022"; + "12.6.1.1.3 of IEEE Std 802.1Q"; } leaf frame-tx { type yang:counter64; description - "The number of frames that have been transmitted by this port to - its segment. Note that a frame transmitted on the interface + "The number of frames that have been transmitted by this port to its + segment. Note that a frame transmitted on the interface corresponding to this port is only counted by this object if and - only if it is for a protocol being processed by the local - bridging function, including Bridge management frames."; + only if it is for a protocol being processed by the local bridging + function, including Bridge management frames."; } leaf octets-tx { type yang:counter64; description - "The total number of octets that have been transmitted by this - port to its segment."; + "The total number of octets that have been transmitted by this port + to its segment."; } leaf discard-inbound { type yang:counter64; description - "Count of received valid frames that were discarded (i.e., - filtered) by the Forwarding Process."; + "Count of received valid frames that were discarded (i.e., filtered) + by the Forwarding Process."; reference - "12.6.1.1.3 of IEEE Std 802.1Q-2022"; + "12.6.1.1.3 of IEEE Std 802.1Q"; } leaf forward-outbound { type yang:counter64; description - "The number of frames forwarded to the associated MAC Entity - (8.5)."; + "The number of frames forwarded to the associated MAC Entity (8.5)."; reference - "12.6.1.1.3 of IEEE Std 802.1Q-2022"; + "12.6.1.1.3 of IEEE Std 802.1Q"; } leaf discard-lack-of-buffers { type yang:counter64; @@ -956,7 +1093,7 @@ module ieee802-dot1q-types { "The count of frames that were to be transmitted through the associated Port but were discarded due to lack of buffers."; reference - "12.6.1.1.3 of IEEE Std 802.1Q-2022"; + "12.6.1.1.3 of IEEE Std 802.1Q"; } leaf discard-transit-delay-exceeded { type yang:counter64; @@ -965,16 +1102,48 @@ module ieee802-dot1q-types { transit delay through the Bridge. It is incremented by both transparent and source route Bridges."; reference - "12.6.1.1.3 of IEEE Std 802.1Q-2022"; + "12.6.1.1.3 of IEEE Std 802.1Q"; } leaf discard-on-error { type yang:counter64; description - "The number of frames that were to be forwarded on the - associated MAC but could not be transmitted (e.g., frame would - be too large, 6.5.8)."; + "The number of frames that were to be forwarded on the associated + MAC but could not be transmitted (e.g., frame would be too large, + 6.5.8)."; reference - "12.6.1.1.3 of IEEE Std 802.1Q-2022"; + "12.6.1.1.3 of IEEE Std 802.1Q"; + } + } + grouping base-gate-control-entries { + description + "Grouping of gate control entries."; + list gate-control-entry { + key "index"; + description + "List of gate control entries."; + leaf index { + type uint32; + description + "The index for this entry."; + } + leaf operation-name { + type identityref { + base type-of-operation; + } + mandatory true; + description + "The name (type) of the operation for this entry."; + } + leaf time-interval-value { + type uint32; + description + "timeIntervalValue is a 32-bit unsigned integer, representing a + number of nanoseconds. After timeIntervalValue nanoseconds have + elapsed since the completion of the previous entry in the gate + control list, control passes to the next entry."; + reference + "12.29.1.2.3, 8.6.8.4 of IEEE Std 802.1Q"; + } } } } diff --git a/src/confd/yang/confd/infix-if-qos.yang b/src/confd/yang/confd/infix-if-qos.yang new file mode 100644 index 000000000..de37a5f4d --- /dev/null +++ b/src/confd/yang/confd/infix-if-qos.yang @@ -0,0 +1,532 @@ +submodule infix-if-qos { + yang-version 1.1; + belongs-to infix-interfaces { + prefix infix-if; + } + + import ietf-interfaces { + prefix if; + } + import ietf-inet-types { + prefix inet; + } + import ieee802-dot1q-types { + prefix dot1q-types; + } + + organization "KernelKit"; + contact "kernelkit@googlegroups.com"; + description "Per-interface Quality of Service. + + Ingress classification to a (priority, drop precedence) pair, + priority-to-traffic-class binding, per-traffic-class transmission + selection, and egress remarking. + + Applies to every interface type, with no dependency on a bridge + model. IEEE 802.1Q names and types are used where 802.1Q defines + the concept. + + CBS, TAS, frame preemption, and PSFP come from the IEEE 802.1DC + modules. VLAN sub-interface PCP mapping stays in + infix-if-vlan.yang."; + + revision 2026-09-07 { + description "Initial revision: ingress classification, traffic-class + table, transmission selection, and egress remarking."; + reference "internal"; + } + + /* + * Features + */ + + /* + * Reachable from Linux on the PCP paths only. The DCB APP PCP + * selector indexes 16 values, PCP plus DEI, so dcb app pcp-prio keys + * on both; on egress dcb rewr prio-pcp takes the nd/de suffix. sparx5 + * offloads both directions in mainline. The DSCP paths carry no + * second dimension. + */ + feature drop-eligible { + description "Platform can carry a single drop-eligible bit alongside + the priority. + + This is the drop_eligible parameter of IEEE 802.1Q-2022 6.9.3."; + reference "6.9.3 of IEEE Std 802.1Q-2022"; + } + + /* + * No Linux interface assigns more than the one bit. Platforms with + * more levels program them through a platform-specific path. + */ + feature drop-precedence { + description "Platform can carry more than two levels of drop + precedence through classification, policing and queue admission. + + Supersedes drop-eligible. A platform advertising this need not + advertise drop-eligible as well; one advertising only drop-eligible + accepts values 0 and 1."; + } + + /* + * Typedefs and groupings + */ + + /* + * Enabled where tc tbf offloads to the port shaper, or where the CPU is + * the forwarding path and software tbf is the real thing. + */ + feature rate-limit { + description "Platform supports a port-level egress rate cap."; + } + + typedef trust-order-type { + type enumeration { + enum none { + description "Trust nothing, every frame gets the defaults."; + } + enum pcp { + description "Believe the PCP of VLAN-tagged frames."; + } + enum dscp { + description "Believe the DSCP of IP frames."; + } + enum dscp-pcp { + description "DSCP of IP frames first, then PCP of tagged frames."; + } + enum pcp-dscp { + description "PCP of tagged frames first, then DSCP of IP frames."; + } + } + description "Trusted classification fields in order of precedence."; + } + + typedef drop-precedence-type { + type uint8 { + range "0..3"; + } + description "Drop precedence assigned during ingress classification. + + 0 is least likely to be discarded under congestion. IEEE + 802.1Q-2022 models a single drop_eligible bit (6.9.3); this is its + generalisation. Platforms with two levels use 0 and 1. Read + max-drop-precedence-levels for the depth implemented; values at or + above it are rejected."; + reference "6.9.3 of IEEE Std 802.1Q-2022"; + } + + grouping classified-result { + description "What ingress classification yields for one frame."; + + leaf priority { + type dot1q-types:priority-type; + mandatory true; + description "Internal priority, 0 to 7."; + reference "6.9.3 of IEEE Std 802.1Q-2022"; + } + + leaf drop-precedence { + if-feature "drop-eligible or drop-precedence"; + type drop-precedence-type; + description "Drop precedence. + + Platforms with fewer levels than the value given reject it; see + max-drop-precedence-levels."; + } + } + + /* + * Data Nodes + */ + + augment "/if:interfaces/if:interface" { + description "Per-interface QoS."; + + container qos { + description "Ingress classification and egress transmission selection."; + + container capabilities { + config false; + description "What the platform implements. + + Read these before writing configuration that depends on table + depth."; + + leaf max-drop-precedence-levels { + if-feature "drop-eligible or drop-precedence"; + type uint8 { + range "1..4"; + } + description "Number of drop-precedence levels. + + Two means only 0 and 1 may be used, which is what a platform + advertising drop-eligible alone reports."; + } + + leaf max-traffic-classes { + type dot1q-types:num-traffic-class-type; + default 8; + description "Number of egress traffic classes. + + The port's transmit queue count when that is between two + and seven. A port with a single queue has no queue + structure to respect and gets the kernel's eight classes."; + reference "12.6.3.1 of IEEE Std 802.1Q-2022"; + } + + leaf-list offload { + type enumeration { + enum classification { + description "Ingress classification and trust order run + in the switch fabric."; + } + enum remarking { + description "Egress remarking runs in the switch fabric."; + } + enum transmission-selection { + description "Traffic classes and transmission selection + run in the switch fabric."; + } + enum rate-limit { + description "The port rate limit runs in the switch fabric."; + } + } + description "Pipeline stages the driver has taken into + hardware. A stage not listed runs in the kernel, which on + a switch covers only CPU-forwarded frames."; + } + + /* Rendered from the driver's dcb apptrust policy set. */ + leaf-list supported-trust-order { + type trust-order-type; + description "Trust orders the platform accepts; none is always + accepted. + + An order that is not listed is rejected rather than + reordered."; + } + } + + container ingress { + description "Classification."; + + /* Rendered as dcb apptrust, which has the same ordering semantics. */ + leaf trust { + type trust-order-type; + default "pcp"; + description "Which header fields are believed, and in what order. + + The first that yields a result for a frame wins. The + default is pcp, the 802.1Q bridge behaviour: the tag is + trusted and untagged frames get the port defaults. Edge + ports that classify on behalf of the network set dscp or + dscp-pcp. none turns classification off, assigning + default-priority and default-drop-precedence to every + frame. Platforms constrain the available orders; see + supported-trust-order."; + } + + /* Rendered as dcb app default-prio. */ + leaf default-priority { + type dot1q-types:priority-type; + default 0; + description "Priority assigned to frames not matched by any + trusted classifier."; + reference "12.6.2 of IEEE Std 802.1Q-2022"; + } + + leaf default-drop-precedence { + if-feature "drop-eligible or drop-precedence"; + type drop-precedence-type; + default 0; + description "Drop precedence assigned to frames not matched by + any trusted classifier."; + } + + /* + * Rendered as dcb app pcp-prio, which carries the priority + * only; drop precedence needs a platform-specific path. + */ + container pcp-map { + description "PCP and DEI of the outermost VLAN tag to a + (priority, drop precedence) pair."; + reference "6.9.3 of IEEE Std 802.1Q-2022"; + + choice mode { + default "preset"; + description "A named preset, or an explicit table."; + + case preset { + leaf preset { + type enumeration { + enum ieee { + description "The 8P0D default of IEEE 802.1Q-2022 + Table 6-3: PCP n maps to priority n."; + } + } + default "ieee"; + description "Named PCP mapping."; + reference "6.9.3, Table 6-3 of IEEE Std 802.1Q-2022"; + } + } + + case custom { + list entry { + key "pcp dei"; + description "Explicit per-(PCP, DEI) mapping. + + Omitted combinations fall back to default-priority and + default-drop-precedence."; + + leaf pcp { + type dot1q-types:priority-type; + description "PCP value in the tag."; + } + + leaf dei { + type boolean; + description "DEI bit in the tag."; + } + + uses classified-result; + } + } + } + } + + /* Rendered as dcb app dscp-prio. */ + container dscp-map { + description "DSCP to a (priority, drop precedence) pair."; + + choice mode { + default "preset"; + description "A named preset, or an explicit table."; + + case preset { + leaf preset { + type enumeration { + enum msb { + description "Top three bits of the DSCP field select + priority 0 to 7."; + } + enum ietf { + description "RFC 4594 per-hop-behaviour groups. + + Unmapped codepoints fall back to + default-priority."; + } + } + default "ietf"; + description "Named DSCP mapping."; + reference "RFC 4594"; + } + } + + case custom { + list entry { + key "dscp"; + description "Explicit per-codepoint mapping."; + + leaf dscp { + type inet:dscp; + description "Codepoint in the IP header."; + } + + leaf trusted { + type boolean; + default true; + description "Whether this codepoint is trusted. + + When false the codepoint is ignored and the frame + falls through to the next trusted field, or to + default-priority."; + } + + uses classified-result; + } + } + } + } + } + + container egress { + description "Traffic-class binding, selection, and remarking."; + + /* + * Values at or above the active class count are rejected by + * the daemon. + */ + container traffic-class-table { + description "Priority to traffic-class binding. + + Node names follow the IEEE 802.1Q-2022 traffic class + table."; + reference "12.6.3, 8.6.6 of IEEE Std 802.1Q-2022"; + + choice mode { + default "preset"; + description "A named preset, or an explicit table."; + + case preset { + leaf preset { + type enumeration { + enum ieee { + description "The ordinary bridge mapping of IEEE + 802.1Q-2022 Table 8-5. + + The table column for the port's traffic class + count, capabilities/max-traffic-classes."; + } + enum ieee-sr { + description "The TSN mapping of IEEE 802.1Q-2022 + Table 34-1: SR classes A and B on the two + highest traffic classes. + + The map only; transmission selection algorithms + are set per traffic class."; + } + } + default "ieee"; + description "Named priority to traffic-class mapping."; + reference "Table 8-5, Table 34-1 of IEEE Std + 802.1Q-2022"; + } + } + + case custom { + /* + * A priority left unset falls back to the ieee preset's + * value for it. + */ + leaf priority0 { type dot1q-types:traffic-class-type; } + leaf priority1 { type dot1q-types:traffic-class-type; } + leaf priority2 { type dot1q-types:traffic-class-type; } + leaf priority3 { type dot1q-types:traffic-class-type; } + leaf priority4 { type dot1q-types:traffic-class-type; } + leaf priority5 { type dot1q-types:traffic-class-type; } + leaf priority6 { type dot1q-types:traffic-class-type; } + leaf priority7 { type dot1q-types:traffic-class-type; } + } + } + } + + list traffic-class { + key "id"; + max-elements 8; + description "Per-traffic-class transmission selection. + + A class with no entry runs strict-priority."; + + /* Ids at or above capabilities/max-traffic-classes are + * rejected by the daemon. + */ + leaf id { + type dot1q-types:traffic-class-type; + description "Traffic class index."; + } + + leaf algorithm { + type identityref { + base dot1q-types:transmission-selection-algorithm; + } + default "dot1q-types:strict-priority"; + description "Transmission selection algorithm. + + Selecting credit-based-shaper turns the shaper on; its + parameters live in ieee802-dot1dc-cbsa-if."; + reference "12.20.2, 8.6.8 and Table 8-6 of IEEE Std + 802.1Q-2022"; + } + + /* Rendered as a tc ets quantum: the share times the MTU. */ + leaf bandwidth { + when "derived-from-or-self(../algorithm, 'dot1q-types:" + + "enhanced-transmission-selection')"; + type uint8 { + range "1..100"; + } + units "percent"; + must "sum(../../traffic-class[derived-from-or-self(" + + "algorithm, 'dot1q-types:enhanced-transmission-" + + "selection')]/bandwidth) = 100" { + error-message "The ETS shares on a port must sum to 100."; + } + mandatory true; + description "TCBandwidth: the percentage of the bandwidth + left by the strict and CBS classes that this class is + assigned. The ETS classes on a port sum to 100."; + reference "37.2, 37.3 of IEEE Std 802.1Q-2022"; + } + } + + /* Rendered as dcb rewr, whose prio-pcp values carry the DEI bit. */ + container remark { + description "Rewrite PCP, DEI, and DSCP on transmit from the + internal (priority, drop precedence) pair."; + reference "12.6.2.9, 6.9.3 of IEEE Std 802.1Q-2022"; + + leaf pcp { + type enumeration { + enum none { + description "No rewrite is configured. The PCP a tagged + frame leaves with is what the forwarding path gives + it: the received PCP where frames pass the kernel, + the frame's priority on a switch fabric that encodes + it as 6.9.3 of IEEE Std 802.1Q-2022 specifies."; + } + enum from-priority { + description "Derive PCP and DEI from the classification + result."; + } + } + default "none"; + description "PCP and DEI rewrite on transmit."; + } + + leaf dscp { + type enumeration { + enum none { + description "Leave DSCP untouched."; + } + enum from-priority { + description "Derive DSCP from the classification + result."; + } + } + default "none"; + description "DSCP rewrite on transmit."; + } + } + + /* Rendered as a tc tbf root above the scheduler. */ + container rate-limit { + if-feature "rate-limit"; + presence "Bounds the transmit rate of the port."; + description "One token bucket on the whole port, above the + traffic classes. It bounds what the port sends in total + and knows nothing about classes: transmission selection + decides which class gets the bounded rate."; + + leaf rate { + type uint64 { + range "8..max"; + } + units "bits/second"; + mandatory true; + description "Sustained transmit rate."; + } + + leaf burst { + type uint32 { + range "1518..max"; + } + units "bytes"; + description "Bytes that may leave at once when the bucket is + full. Unset, the daemon sizes it for ten milliseconds + at rate, never below one maximum-size frame."; + } + } + } + } + } +} diff --git a/src/confd/yang/confd/infix-if-qos@2026-09-07.yang b/src/confd/yang/confd/infix-if-qos@2026-09-07.yang new file mode 120000 index 000000000..591c3dc64 --- /dev/null +++ b/src/confd/yang/confd/infix-if-qos@2026-09-07.yang @@ -0,0 +1 @@ +infix-if-qos.yang \ No newline at end of file diff --git a/src/confd/yang/confd/infix-interfaces.yang b/src/confd/yang/confd/infix-interfaces.yang index d86a9789d..83ae593d4 100644 --- a/src/confd/yang/confd/infix-interfaces.yang +++ b/src/confd/yang/confd/infix-interfaces.yang @@ -36,11 +36,19 @@ module infix-interfaces { include infix-if-wifi; include infix-if-wireguard; include infix-if-ptp; + include infix-if-qos; organization "KernelKit"; contact "kernelkit@googlegroups.com"; description "Linux bridge and lag extensions for ietf-interfaces."; + revision 2026-09-07 { + description "Add per-interface QoS: ingress classification, traffic + class table and transmission selection, see + infix-if-qos@2026-09-07."; + reference "internal"; + } + revision 2026-06-11 { description "Fix WireGuard key-format must expressions, see infix-if-wireguard@2026-06-11."; diff --git a/src/confd/yang/confd/infix-interfaces@2026-06-11.yang b/src/confd/yang/confd/infix-interfaces@2026-09-07.yang similarity index 100% rename from src/confd/yang/confd/infix-interfaces@2026-06-11.yang rename to src/confd/yang/confd/infix-interfaces@2026-09-07.yang diff --git a/src/statd/python/yanger/ietf_interfaces/link.py b/src/statd/python/yanger/ietf_interfaces/link.py index f4ae79a2b..19143beaa 100644 --- a/src/statd/python/yanger/ietf_interfaces/link.py +++ b/src/statd/python/yanger/ietf_interfaces/link.py @@ -140,15 +140,74 @@ def ptp_capabilities(ifname, systemjson): return result or None -def interface(iplink, ipaddr, systemjson=None): +# Trust orders a driver's dcb apptrust accepts, keyed by driver name; the +# kernel has no query for it. Same table as confd's qos validation. +QOS_TRUST_ORDERS = { + "sparx5-switch": ["pcp", "dscp", "dscp-pcp"], + "lan966x-switch": ["pcp", "dscp", "dscp-pcp"], + "mv88e6085": ["pcp", "dscp", "pcp-dscp", "dscp-pcp"], +} + + +def qos_capabilities(iplink, qdiscs): + """Return infix-interfaces:qos/capabilities dict for a link, or None""" + ifname = iplink["ifname"] + result = {} + + # One class per transmit queue, at most eight. A single queue has + # no queue structure to respect, so the kernel's eight classes apply, + # which is the model's default and left implicit. + txq = iplink.get("num_tx_queues", 1) + if 1 < txq < 8: + result["max-traffic-classes"] = txq + + driver = None + if uevent := HOST.read(f"/sys/class/net/{ifname}/device/uevent"): + for line in uevent.splitlines(): + if line.startswith("DRIVER="): + driver = line[7:].strip() + if trust := QOS_TRUST_ORDERS.get(driver): + result["supported-trust-order"] = trust + + # Stages the driver runs. DCB tables exist only on drivers with the + # operations, so only those ports are asked. ets and tbf report + # offloaded when the driver took them. + offload = [] + if trust: + app = HOST.run_json(["dcb", "-j", "app", "show", "dev", ifname], {}) + if app.get("dscp_prio") or app.get("pcp_prio"): + offload.append("classification") + rewr = HOST.run_json(["dcb", "-j", "rewr", "show", "dev", ifname], {}) + if rewr.get("prio_pcp") or rewr.get("prio_dscp"): + offload.append("remarking") + + for qdisc in qdiscs.get(ifname, []): + kind, offloaded = qdisc.get("kind"), qdisc.get("offloaded", False) + if kind == "ets" and offloaded: + offload.append("transmission-selection") + if kind == "tbf" and qdisc.get("root") and offloaded: + offload.append("rate-limit") + + if offload: + result["offload"] = offload + + return result or None + + +def interface(iplink, ipaddr, systemjson=None, qdiscs=None): interface = interface_common(iplink, ipaddr) if systemjson is None: systemjson = {} + if qdiscs is None: + qdiscs = {} if ptpcap := ptp_capabilities(iplink["ifname"], systemjson): interface["infix-interfaces:ptp-capabilities"] = ptpcap + if qoscap := qos_capabilities(iplink, qdiscs): + interface["infix-interfaces:qos"] = {"capabilities": qoscap} + match interface["type"]: case "infix-if-type:bridge": if br := bridge.bridge(iplink): @@ -201,6 +260,11 @@ def interfaces(ifname=None): addrs = common.ipaddrs(ifname) systemjson = HOST.read_json("/run/system.json", {}) + qdiscs = {} + for qdisc in HOST.run_json(["tc", "-j", "qdisc", "show"], []): + if dev := qdisc.get("dev"): + qdiscs.setdefault(dev, []).append(qdisc) + interfaces = [] for ifname, iplink in links.items(): if iplink.get("group") == "internal": @@ -212,6 +276,6 @@ def interfaces(ifname=None): ipaddr = addrs.get(ifname, {}) - interfaces.append(interface(iplink, ipaddr, systemjson)) + interfaces.append(interface(iplink, ipaddr, systemjson, qdiscs)) return interfaces diff --git a/test/.env b/test/.env index 16ef67f13..98176d9f9 100644 --- a/test/.env +++ b/test/.env @@ -2,7 +2,7 @@ # shellcheck disable=SC2034,SC2154 # Current container image -INFIX_TEST=ghcr.io/kernelkit/infix-test:2.11 +INFIX_TEST=ghcr.io/kernelkit/infix-test:2.12 ixdir=$(readlink -f "$testdir/..") logdir=$(readlink -f "$testdir/.log") diff --git a/test/case/interfaces/Readme.adoc b/test/case/interfaces/Readme.adoc index 4e1c8b372..c0ec25b67 100644 --- a/test/case/interfaces/Readme.adoc +++ b/test/case/interfaces/Readme.adoc @@ -11,7 +11,7 @@ Tests verifying interface configuration and management: - Linux bridge creation, STP, and VLAN handling - Link aggregation (LAG) setup and failover behavior - IGMP multicast group management and forwarding - - QoS traffic classification and priority handling + - QoS traffic classification, traffic classes, and transmission selection - Interface enable/disable state management - Virtual Ethernet (VETH) pair lifecycle management - GRE tunneling configuration and data plane operation @@ -105,6 +105,26 @@ include::vlan_qos/Readme.adoc[] <<< +include::qos_traffic_class/Readme.adoc[] + +<<< + +include::qos_validation/Readme.adoc[] + +<<< + +include::qos_ingress_classify/Readme.adoc[] + +<<< + +include::qos_classify_dscp/Readme.adoc[] + +<<< + +include::qos_factory_default/Readme.adoc[] + +<<< + include::verify_all_interface_types/Readme.adoc[] <<< diff --git a/test/case/interfaces/all.yaml b/test/case/interfaces/all.yaml index d6584c710..d2a290d06 100644 --- a/test/case/interfaces/all.yaml +++ b/test/case/interfaces/all.yaml @@ -50,6 +50,9 @@ - name: Layer-2 Multicast Filtering Tests suite: igmp.yaml +- name: Quality of Service Tests + suite: qos.yaml + - name: Layer-2/3 Tunnel Tests suite: tunnels.yaml diff --git a/test/case/interfaces/qos.yaml b/test/case/interfaces/qos.yaml new file mode 100644 index 000000000..e048105b8 --- /dev/null +++ b/test/case/interfaces/qos.yaml @@ -0,0 +1,33 @@ +--- +- name: QoS Traffic Classes and Transmission Selection + case: qos_traffic_class/test.py + +- name: QoS Configuration Validation + case: qos_validation/test.py + +- name: QoS Ingress Classification and Egress Remarking + case: qos_ingress_classify/test.py + +- name: QoS DSCP Classification and Remarking End to End + case: qos_classify_dscp/test.py + +- name: QoS Defaults Out of the Box + case: qos_factory_default/test.py + +- name: QoS Bridge Classification and Remarking + case: qos_bridge_classify/test.py + +- name: QoS Queue Assignment + case: qos_queue_assignment/test.py + +- name: QoS Rate Limit + case: qos_rate_limit/test.py + +- name: QoS Strict Priority + case: qos_strict_priority/test.py + +- name: QoS Weighted Sharing + case: qos_weighted_sharing/test.py + +- name: QoS Mixed Selection + case: qos_mixed_selection/test.py diff --git a/test/case/interfaces/qos_bridge_classify/Readme.adoc b/test/case/interfaces/qos_bridge_classify/Readme.adoc new file mode 120000 index 000000000..ae32c8412 --- /dev/null +++ b/test/case/interfaces/qos_bridge_classify/Readme.adoc @@ -0,0 +1 @@ +test.adoc \ No newline at end of file diff --git a/test/case/interfaces/qos_bridge_classify/test.adoc b/test/case/interfaces/qos_bridge_classify/test.adoc new file mode 100644 index 000000000..e4842de69 --- /dev/null +++ b/test/case/interfaces/qos_bridge_classify/test.adoc @@ -0,0 +1,44 @@ +=== QoS Bridge Classification and Remarking + +ifdef::topdoc[:imagesdir: {topdoc}../../test/case/interfaces/qos_bridge_classify] + +==== Description + +Bridge two ports and send frames straight through the switch, so that on +a switch chip the traffic never passes the CPU and only the hardware +tables can classify and remark it. The DSCP and PCP on the wire at the +egress port reveal the priority the ingress port assigned: + + - trust dscp: untagged IP frames on VLAN 20, DSCP 46 (EF) to priority 5, + DSCP 4 unknown to the preset to the default priority 1 + - trust pcp with a custom map: tagged frames on VLAN 10, PCP 3 to + priority 6, PCP 0 to priority 4, PCP 7 unmapped to the default + priority 2 + - remark off: DSCP leaves as it came in, and so does the PCP unless the + switch fabric encodes it from the priority, which its rewrite table + then still shows + +With remarking on, the egress port writes the class selector of the +priority as DSCP and, where the driver offloads remarking, the priority +as PCP; PCP remarking has no software path, so on other ports only the +DSCP is checked. The test is skipped on a switch whose driver cannot +offload classification, since frames the fabric forwards never see the +kernel's rules. + +==== Topology + +image::topology.svg[QoS Bridge Classification and Remarking topology, align=center, scaledwidth=75%] + +==== Sequence + +. Set up topology and attach to target DUT +. Configure a VLAN bridge, VLAN 10 tagged and VLAN 20 untagged on ingress +. Set up host namespaces, VLAN 10 tagged on both, VLAN 20 untagged on the sender +. Verify classification and remarking are offloaded +. Send untagged IP frames with trust dscp, verify PCP and DSCP from priority +. Switch to trust pcp with a custom map and default priority 2 +. Send tagged frames, verify PCP and DSCP from the mapped priority +. Turn remarking off +. Send tagged frames with DSCP 46, verify PCP and DSCP are untouched + + diff --git a/test/case/interfaces/qos_bridge_classify/test.py b/test/case/interfaces/qos_bridge_classify/test.py new file mode 100755 index 000000000..aee454126 --- /dev/null +++ b/test/case/interfaces/qos_bridge_classify/test.py @@ -0,0 +1,229 @@ +#!/usr/bin/env python3 +""" +QoS Bridge Classification and Remarking + +Bridge two ports and send frames straight through the switch, so that on +a switch chip the traffic never passes the CPU and only the hardware +tables can classify and remark it. The DSCP and PCP on the wire at the +egress port reveal the priority the ingress port assigned: + + - trust dscp: untagged IP frames on VLAN 20, DSCP 46 (EF) to priority 5, + DSCP 4 unknown to the preset to the default priority 1 + - trust pcp with a custom map: tagged frames on VLAN 10, PCP 3 to + priority 6, PCP 0 to priority 4, PCP 7 unmapped to the default + priority 2 + - remark off: DSCP leaves as it came in, and so does the PCP unless the + switch fabric encodes it from the priority, which its rewrite table + then still shows + +With remarking on, the egress port writes the class selector of the +priority as DSCP and, where the driver offloads remarking, the priority +as PCP; PCP remarking has no software path, so on other ports only the +DSCP is checked. The test is skipped on a switch whose driver cannot +offload classification, since frames the fabric forwards never see the +kernel's rules. +""" +import re +import infamy +from infamy.util import until + +# DSCP cases with trust dscp: (dscp, expected priority) +DSCP_CASES = ((46, 5), (0, 0), (26, 3), (4, 1)) +# PCP cases with trust pcp and a custom map: (pcp, expected priority) +PCP_CASES = ((3, 6), (0, 4), (7, 2)) + + +def capabilities(target, port): + data = target.get_data(f"/ietf-interfaces:interfaces/interface[name='{port}']" + "/infix-interfaces:qos/capabilities") + for iface in data["interfaces"]["interface"]: + qos = iface.get("qos") or iface.get("infix-interfaces:qos") or {} + return qos.get("capabilities", {}) + return {} + + +def qos_config(target, td0, td1, ingress, remark): + target.put_config_dicts({"ietf-interfaces": { + "interfaces": { + "interface": [ + {"name": td0, "infix-interfaces:qos": {"ingress": ingress}}, + {"name": td1, "infix-interfaces:qos": {"egress": {"remark": remark}}}, + ] + } + }}) + + +def parse(packets): + """Return {icmp id: (vid, pcp, dscp)} from tcpdump -e -v output""" + seen = {} + for m in re.finditer(r"vlan (\d+), p (\d+), .*?\(tos 0x([0-9a-f]+).*?" + r"ICMP echo request, id (\d+)", packets, re.S): + seen[int(m.group(4))] = (int(m.group(1)), int(m.group(2)), int(m.group(3), 16) >> 2) + return seen + + +with infamy.Test() as test: + with test.step("Set up topology and attach to target DUT"): + env = infamy.Env() + target = env.attach("target", "mgmt") + tgtssh = env.attach("target", "mgmt", "ssh") + _, td0 = env.ltop.xlate("target", "data1") + _, td1 = env.ltop.xlate("target", "data2") + _, hd0 = env.ltop.xlate("host", "data1") + _, hd1 = env.ltop.xlate("host", "data2") + + dcb = bool(capabilities(target, td0).get("supported-trust-order")) + uevent = tgtssh.runsh(f"cat /sys/class/net/{td0}/uevent").stdout + dsa = "DEVTYPE=dsa" in uevent.split() + print(f"{td0}: DCB {'supported' if dcb else 'not supported'}, DSA port: {dsa}") + if dsa and not dcb: + print("switch forwards in hardware without DCB support, skipping") + test.skip() + + with test.step("Configure a VLAN bridge, VLAN 10 tagged and VLAN 20 untagged on ingress"): + target.put_config_dicts({"ietf-interfaces": { + "interfaces": { + "interface": [ + { + "name": "br0", + "type": "infix-if-type:bridge", + "enabled": True, + "bridge": { + "vlans": { + "vlan": [ + {"vid": 10, "tagged": [td0, td1]}, + {"vid": 20, "untagged": [td0], "tagged": [td1]}, + ] + } + } + }, + { + "name": td0, + "enabled": True, + "infix-interfaces:bridge-port": {"pvid": 20, "bridge": "br0"}, + "infix-interfaces:qos": { + "ingress": { + "trust": "dscp", + "default-priority": 1, + "dscp-map": {"preset": "ietf"}, + } + } + }, + { + "name": td1, + "enabled": True, + "infix-interfaces:bridge-port": {"bridge": "br0"}, + "infix-interfaces:qos": { + "egress": {"remark": {"pcp": "from-priority", "dscp": "from-priority"}} + } + }, + ] + } + }}) + + with infamy.IsolatedMacVlan(hd0) as ns0, \ + infamy.IsolatedMacVlan(hd1) as ns1: + + with test.step("Set up host namespaces, VLAN 10 tagged on both, VLAN 20 untagged on the sender"): + ns0.runsh(""" + set -ex + ip link set iface up + ip addr add 192.168.20.1/24 dev iface + ip link add dev vlan10 link iface up type vlan id 10 + ip addr add 192.168.10.1/24 dev vlan10 + """) + ns1.runsh(""" + set -ex + ip link set iface up + ip link add dev vlan10 link iface up type vlan id 10 + ip addr add 192.168.10.2/24 dev vlan10 + ip link add dev vlan20 link iface up type vlan id 20 + ip addr add 192.168.20.2/24 dev vlan20 + """) + ns0.must_reach("192.168.20.2") + ns0.must_reach("192.168.10.2") + + if dcb: + with test.step("Verify classification and remarking are offloaded"): + until(lambda: "classification" in capabilities(target, td0).get("offload", [])) + until(lambda: "remarking" in capabilities(target, td1).get("offload", [])) + remark_hw = "remarking" in capabilities(target, td1).get("offload", []) + + def expect(seen, ident, vid, pcp, dscp, what): + """Check one captured echo request, PCP only where the driver remarks it""" + assert ident in seen, f"{what}: no echo request captured" + got = seen[ident] + want = (vid, pcp if remark_hw else got[1], dscp) + assert got == want, f"{what}: got (vid, pcp, dscp) {got}, expected {want}" + + def capture(send): + pcap = ns1.pcap("icmp[icmptype] == icmp-echo") + with pcap: + send() + packets = pcap.tcpdump("-e -v") + print(packets) + return parse(packets) + + def set_pcp(pcp): + """Tag everything the sender puts on VLAN 10 with this PCP, whatever its TOS""" + qmap = " ".join(f"{prio}:{pcp}" for prio in range(8)) + ns0.runsh(f"ip link set dev vlan10 type vlan egress-qos-map {qmap}") + + def send_dscp(): + for n, (dscp, _) in enumerate(DSCP_CASES): + ns0.runsh(f"ping -c1 -w2 -Q {dscp << 2} -e {100 + n} 192.168.20.2") + + def send_pcp(): + for n, (pcp, _) in enumerate(PCP_CASES): + set_pcp(pcp) + ns0.runsh(f"ping -c1 -w2 -e {200 + n} 192.168.10.2") + + with test.step("Send untagged IP frames with trust dscp, verify PCP and DSCP from priority"): + seen = capture(send_dscp) + for n, (dscp, prio) in enumerate(DSCP_CASES): + expect(seen, 100 + n, 20, prio, prio << 3, f"DSCP {dscp}") + + with test.step("Switch to trust pcp with a custom map and default priority 2"): + qos_config(target, td0, td1, { + "trust": "pcp", + "default-priority": 2, + "pcp-map": { + "entry": [ + {"pcp": 3, "dei": "false", "priority": 6}, + {"pcp": 3, "dei": "true", "priority": 6}, + {"pcp": 0, "dei": "false", "priority": 4}, + {"pcp": 0, "dei": "true", "priority": 4}, + ] + }, + }, {"pcp": "from-priority", "dscp": "from-priority"}) + ns0.must_reach("192.168.10.2") + + with test.step("Send tagged frames, verify PCP and DSCP from the mapped priority"): + seen = capture(send_pcp) + for n, (pcp, prio) in enumerate(PCP_CASES): + expect(seen, 200 + n, 10, prio, prio << 3, f"PCP {pcp}") + + with test.step("Turn remarking off"): + qos_config(target, td0, td1, {"trust": "pcp", "default-priority": 2}, + {"pcp": "none", "dscp": "none"}) + ns0.must_reach("192.168.10.2") + + with test.step("Send tagged frames with DSCP 46, verify PCP and DSCP are untouched"): + def send_plain(): + for n, (pcp, _) in enumerate(PCP_CASES): + set_pcp(pcp) + ns0.runsh(f"ping -c1 -w2 -Q {46 << 2} -e {300 + n} 192.168.10.2") + + # A fabric that encodes the PCP from the priority keeps its + # table with remarking off, and says so in the rewrite table + rewr = tgtssh.runsh(f"dcb rewr show dev {td1}").stdout + encoded = "prio-pcp" in rewr + print(f"{td1} encodes PCP from priority: {encoded}") + + seen = capture(send_plain) + for n, (pcp, prio) in enumerate(PCP_CASES): + assert 300 + n in seen, f"no echo request with PCP {pcp} captured" + want = (10, prio if encoded else pcp, 46) + assert seen[300 + n] == want, f"PCP {pcp}: got {seen[300 + n]}, expected {want}" + + test.succeed() diff --git a/test/case/interfaces/qos_bridge_classify/topology.dot b/test/case/interfaces/qos_bridge_classify/topology.dot new file mode 100644 index 000000000..f288f2f95 --- /dev/null +++ b/test/case/interfaces/qos_bridge_classify/topology.dot @@ -0,0 +1,24 @@ +graph "1x3" { + layout="neato"; + overlap="false"; + esep="+80"; + + node [shape=record, fontname="DejaVu Sans Mono, Book"]; + edge [color="cornflowerblue", penwidth="2", fontname="DejaVu Serif, Book"]; + + host [ + label="host | { mgmt | data1 | data2 }", + pos="0,12!", + requires="controller", + ]; + + target [ + label="{ mgmt | data1 | data2 } | target", + pos="10,12!", + requires="infix", + ]; + + host:mgmt -- target:mgmt [requires="mgmt", color="lightgray"] + host:data1 -- target:data1 [color="black"] + host:data2 -- target:data2 [color="black"] +} diff --git a/test/case/interfaces/qos_bridge_classify/topology.svg b/test/case/interfaces/qos_bridge_classify/topology.svg new file mode 100644 index 000000000..9517e6397 --- /dev/null +++ b/test/case/interfaces/qos_bridge_classify/topology.svg @@ -0,0 +1,51 @@ + + + + + + +1x3 + + + +host + +host + +mgmt + +data1 + +data2 + + + +target + +mgmt + +data1 + +data2 + +target + + + +host:mgmt--target:mgmt + + + + +host:data1--target:data1 + + + + +host:data2--target:data2 + + + + diff --git a/test/case/interfaces/qos_classify_dscp/Readme.adoc b/test/case/interfaces/qos_classify_dscp/Readme.adoc new file mode 120000 index 000000000..ae32c8412 --- /dev/null +++ b/test/case/interfaces/qos_classify_dscp/Readme.adoc @@ -0,0 +1 @@ +test.adoc \ No newline at end of file diff --git a/test/case/interfaces/qos_classify_dscp/test.adoc b/test/case/interfaces/qos_classify_dscp/test.adoc new file mode 100644 index 000000000..148b9d538 --- /dev/null +++ b/test/case/interfaces/qos_classify_dscp/test.adoc @@ -0,0 +1,44 @@ +=== QoS DSCP Classification and Remarking End to End + +ifdef::topdoc[:imagesdir: {topdoc}../../test/case/interfaces/qos_classify_dscp] + +==== Description + +Send IP packets with different DSCP values into a routed port that trusts +DSCP with the ietf preset, and route them out over a VLAN interface whose +egress PCP is derived from the internal priority. The PCP on the wire +then reveals the priority the classifier assigned: + + DSCP 0 (CS0) -> priority 0 + DSCP 8 (CS1) -> priority 1 + DSCP 18 (AF21) -> priority 2 + DSCP 26 (AF31) -> priority 3 + DSCP 34 (AF41) -> priority 4 + DSCP 46 (EF) -> priority 5 + DSCP 48 (CS6) -> priority 6 + DSCP 56 (CS7) -> priority 7 + DSCP 4 -> priority 0, not in the preset, port default + +Then enable DSCP remarking on the egress port and repeat: every packet +must leave with the class selector of its priority, CS0 to CS7, e.g. EF +in, CS5 out. + +Works on any port: classification and remarking run in the switch fabric +where the driver supports them and in the kernel otherwise, and routed +traffic passes the kernel in both cases. + +==== Topology + +image::topology.svg[QoS DSCP Classification and Remarking End to End topology, align=center, scaledwidth=75%] + +==== Sequence + +. Set up topology and attach to target DUT +. Configure routed ingress port trusting DSCP and VLAN egress from priority +. Set up host namespaces on both sides +. Send ICMP echo with DSCP {dscp}, {what} {prio} +. Verify the PCP of each echo request matches its DSCP class +. Enable DSCP remarking from priority on the egress port +. Verify each echo request leaves with the class selector of its priority + + diff --git a/test/case/interfaces/qos_classify_dscp/test.py b/test/case/interfaces/qos_classify_dscp/test.py new file mode 100755 index 000000000..fadc3a815 --- /dev/null +++ b/test/case/interfaces/qos_classify_dscp/test.py @@ -0,0 +1,147 @@ +#!/usr/bin/env python3 +""" +QoS DSCP Classification and Remarking End to End + +Send IP packets with different DSCP values into a routed port that trusts +DSCP with the ietf preset, and route them out over a VLAN interface whose +egress PCP is derived from the internal priority. The PCP on the wire +then reveals the priority the classifier assigned: + + DSCP 0 (CS0) -> priority 0 + DSCP 8 (CS1) -> priority 1 + DSCP 18 (AF21) -> priority 2 + DSCP 26 (AF31) -> priority 3 + DSCP 34 (AF41) -> priority 4 + DSCP 46 (EF) -> priority 5 + DSCP 48 (CS6) -> priority 6 + DSCP 56 (CS7) -> priority 7 + DSCP 4 -> priority 0, not in the preset, port default + +Then enable DSCP remarking on the egress port and repeat: every packet +must leave with the class selector of its priority, CS0 to CS7, e.g. EF +in, CS5 out. + +Works on any port: classification and remarking run in the switch fabric +where the driver supports them and in the kernel otherwise, and routed +traffic passes the kernel in both cases. +""" +import re +import infamy + +# DSCP to expected priority per the ietf preset, with a default fallback +CASES = ((0, 0), (8, 1), (18, 2), (26, 3), (34, 4), (46, 5), (48, 6), (56, 7), (4, 0)) + +with infamy.Test() as test: + with test.step("Set up topology and attach to target DUT"): + env = infamy.Env() + target = env.attach("target", "mgmt") + _, td0 = env.ltop.xlate("target", "data1") + _, td1 = env.ltop.xlate("target", "data2") + _, hd0 = env.ltop.xlate("host", "data1") + _, hd1 = env.ltop.xlate("host", "data2") + + with test.step("Configure routed ingress port trusting DSCP and VLAN egress from priority"): + target.put_config_dicts({"ietf-interfaces": { + "interfaces": { + "interface": [ + { + "name": td0, + "enabled": True, + "ipv4": { + "forwarding": True, + "address": [{"ip": "192.168.10.1", "prefix-length": 24}] + }, + "infix-interfaces:qos": { + "ingress": { + "trust": "dscp", + "default-priority": 0, + "dscp-map": {"preset": "ietf"}, + } + } + }, + { + "name": td1, + "enabled": True, + }, + { + "name": "vlan11", + "type": "infix-if-type:vlan", + "vlan": { + "id": 11, + "lower-layer-if": td1, + "egress-qos": {"pcp": "from-priority"}, + }, + "ipv4": { + "forwarding": True, + "address": [{"ip": "192.168.11.1", "prefix-length": 24}] + } + } + ] + } + }}) + + with infamy.IsolatedMacVlan(hd0) as ns0, \ + infamy.IsolatedMacVlan(hd1) as ns1: + + with test.step("Set up host namespaces on both sides"): + ns0.runsh(""" + set -ex + ip link set iface up + ip addr add 192.168.10.2/24 dev iface + """) + ns0.addroute("default", "192.168.10.1") + + ns1.runsh(""" + set -ex + ip link set iface up + ip link add dev vlan11 link iface up type vlan id 11 + ip addr add 192.168.11.2/24 dev vlan11 + """) + ns1.addroute("default", "192.168.11.1") + + ns0.must_reach("192.168.11.2") + + def capture(what): + """Ping once per case with the case index as ICMP id, return {id: (pcp, dscp)}""" + pcap = ns1.pcap("vlan 11 and icmp[icmptype] == icmp-echo") + with pcap: + for n, (dscp, prio) in enumerate(CASES): + with test.step(f"Send ICMP echo with DSCP {dscp}, {what} {prio}"): + ns0.runsh(f"ping -c1 -w2 -Q {dscp << 2} -e {100 + n} 192.168.11.2") + packets = pcap.tcpdump("-e -v") + print(packets) + + seen = {} + for m in re.finditer(r"vlan 11, p (\d+), .*?\(tos 0x([0-9a-f]+).*?" + r"ICMP echo request, id (\d+)", packets, re.S): + seen[int(m.group(3))] = (int(m.group(1)), int(m.group(2), 16) >> 2) + return seen + + seen = capture("expect PCP") + with test.step("Verify the PCP of each echo request matches its DSCP class"): + for n, (dscp, prio) in enumerate(CASES): + assert 100 + n in seen, f"no echo request with DSCP {dscp} captured" + assert seen[100 + n] == (prio, dscp), f"DSCP {dscp}: got {seen[100 + n]}" + + with test.step("Enable DSCP remarking from priority on the egress port"): + target.put_config_dicts({"ietf-interfaces": { + "interfaces": { + "interface": [{ + "name": td1, + "infix-interfaces:qos": { + "egress": {"remark": {"dscp": "from-priority"}} + } + }] + } + }}) + + # Applying the change may reprogram the port; wait for the path + ns0.must_reach("192.168.11.2") + + seen = capture("expect CS") + with test.step("Verify each echo request leaves with the class selector of its priority"): + for n, (dscp, prio) in enumerate(CASES): + assert 100 + n in seen, f"no echo request with DSCP {dscp} captured" + assert seen[100 + n] == (prio, prio << 3), f"DSCP {dscp}: got {seen[100 + n]}" + + test.succeed() diff --git a/test/case/interfaces/qos_classify_dscp/topology.dot b/test/case/interfaces/qos_classify_dscp/topology.dot new file mode 120000 index 000000000..7689a6778 --- /dev/null +++ b/test/case/interfaces/qos_classify_dscp/topology.dot @@ -0,0 +1 @@ +../../../infamy/topologies/1x3.dot \ No newline at end of file diff --git a/test/case/interfaces/qos_classify_dscp/topology.svg b/test/case/interfaces/qos_classify_dscp/topology.svg new file mode 100644 index 000000000..9517e6397 --- /dev/null +++ b/test/case/interfaces/qos_classify_dscp/topology.svg @@ -0,0 +1,51 @@ + + + + + + +1x3 + + + +host + +host + +mgmt + +data1 + +data2 + + + +target + +mgmt + +data1 + +data2 + +target + + + +host:mgmt--target:mgmt + + + + +host:data1--target:data1 + + + + +host:data2--target:data2 + + + + diff --git a/test/case/interfaces/qos_factory_default/Readme.adoc b/test/case/interfaces/qos_factory_default/Readme.adoc new file mode 120000 index 000000000..ae32c8412 --- /dev/null +++ b/test/case/interfaces/qos_factory_default/Readme.adoc @@ -0,0 +1 @@ +test.adoc \ No newline at end of file diff --git a/test/case/interfaces/qos_factory_default/test.adoc b/test/case/interfaces/qos_factory_default/test.adoc new file mode 100644 index 000000000..f20cb09f2 --- /dev/null +++ b/test/case/interfaces/qos_factory_default/test.adoc @@ -0,0 +1,32 @@ +=== QoS Defaults Out of the Box + +ifdef::topdoc[:imagesdir: {topdoc}../../test/case/interfaces/qos_factory_default] + +==== Description + +An interface without any qos configuration is fully specified by the +model's defaults, and every physical port runs them from boot: + + - trust PCP, the tag of VLAN-tagged frames, with default priority 0 + for everything else + - one traffic class per transmit queue, eight on a single-queue port, + mapped per IEEE 802.1Q-2022 Table 8-5 with strict priority throughout + +Verify that the running configuration carries no qos container, that the +class count follows the queue count rule, that the root qdisc carries the +Table 8-5 map for it, and that classification is in place: in the DCB +tables on a port whose driver has them, as tc flower rules otherwise. + +==== Topology + +image::topology.svg[QoS Defaults Out of the Box topology, align=center, scaledwidth=75%] + +==== Sequence + +. Set up topology and attach to target DUT +. Verify the running configuration has no qos container +. Verify the class count is the queue count, or eight for one queue +. Verify the root qdisc carries the Table 8-5 map, all strict +. Verify classification trusts PCP by default + + diff --git a/test/case/interfaces/qos_factory_default/test.py b/test/case/interfaces/qos_factory_default/test.py new file mode 100755 index 000000000..2cb9c9787 --- /dev/null +++ b/test/case/interfaces/qos_factory_default/test.py @@ -0,0 +1,104 @@ +#!/usr/bin/env python3 +""" +QoS Defaults Out of the Box + +An interface without any qos configuration is fully specified by the +model's defaults, and every physical port runs them from boot: + + - trust PCP, the tag of VLAN-tagged frames, with default priority 0 + for everything else + - one traffic class per transmit queue, eight on a single-queue port, + mapped per IEEE 802.1Q-2022 Table 8-5 with strict priority throughout + +Verify that the running configuration carries no qos container, that the +class count follows the queue count rule, that the root qdisc carries the +Table 8-5 map for it, and that classification is in place: in the DCB +tables on a port whose driver has them, as tc flower rules otherwise. +""" +import json +import infamy +from infamy.util import until + +TABLE_8_5 = { + 2: [0, 0, 0, 0, 1, 1, 1, 1], + 3: [0, 0, 0, 0, 1, 1, 2, 2], + 4: [0, 0, 1, 1, 2, 2, 3, 3], + 5: [0, 0, 1, 1, 2, 2, 3, 4], + 6: [1, 0, 2, 2, 3, 3, 4, 5], + 7: [1, 0, 2, 3, 4, 4, 5, 6], + 8: [1, 0, 2, 3, 4, 5, 6, 7], +} + + +def capabilities(target, port): + data = target.get_data(f"/ietf-interfaces:interfaces/interface[name='{port}']" + "/infix-interfaces:qos/capabilities") + for iface in data["interfaces"]["interface"]: + qos = iface.get("qos") or iface.get("infix-interfaces:qos") or {} + return qos.get("capabilities", {}) + return {} + + +def running_qos(target, port): + running = target.get_config_dict("/ietf-interfaces:interfaces") + for iface in running["interfaces"]["interface"]: + if iface["name"] == port: + return iface.get("qos") or iface.get("infix-interfaces:qos") + return None + + +def root_qdisc(ssh, port): + out = ssh.runsh(f"tc -j qdisc show dev {port}").stdout + for qdisc in json.loads(out or "[]"): + if qdisc.get("root"): + return qdisc + return None + + +def tx_queues(ssh, port): + out = ssh.runsh(f"ls /sys/class/net/{port}/queues").stdout + return len([q for q in out.split() if q.startswith("tx-")]) + + +with infamy.Test() as test: + with test.step("Set up topology and attach to target DUT"): + env = infamy.Env() + target = env.attach("target", "mgmt") + tgtssh = env.attach("target", "mgmt", "ssh") + _, port = env.ltop.xlate("target", "data") + + with test.step("Verify the running configuration has no qos container"): + assert running_qos(target, port) is None, f"{port} has qos configuration" + + with test.step("Verify the class count is the queue count, or eight for one queue"): + caps = capabilities(target, port) + queues = tx_queues(tgtssh, port) + expected = min(queues, 8) if queues > 1 else 8 + print(f"{port}: {queues} tx queues, capabilities {caps}") + assert caps.get("max-traffic-classes", 8) == expected, caps + num_tc = expected + + with test.step("Verify the root qdisc carries the Table 8-5 map, all strict"): + def default_table(): + qdisc = root_qdisc(tgtssh, port) + if not qdisc: + return False + opts = qdisc.get("options", {}) + if qdisc["kind"] == "ets": + return (opts.get("bands") == num_tc and opts.get("strict") == num_tc and + opts.get("priomap", [])[:8] == [num_tc - 1 - tc for tc in TABLE_8_5[num_tc]]) + return False + until(default_table) + print(json.dumps(root_qdisc(tgtssh, port))) + + with test.step("Verify classification trusts PCP by default"): + if caps.get("supported-trust-order"): + out = tgtssh.runsh(f"dcb apptrust show dev {port}").stdout + assert out.replace(":", "").split() == ["order", "pcp"], out + else: + out = tgtssh.runsh(f"tc -j filter show dev {port} ingress").stdout + keys = [f["options"].get("keys", {}) for f in json.loads(out or "[]") if f.get("options")] + assert sum(1 for k in keys if "vlan_prio" in k) == 8, keys + assert not any("ip_tos" in k for k in keys), keys + + test.succeed() diff --git a/test/case/interfaces/qos_factory_default/topology.dot b/test/case/interfaces/qos_factory_default/topology.dot new file mode 120000 index 000000000..4f53d15af --- /dev/null +++ b/test/case/interfaces/qos_factory_default/topology.dot @@ -0,0 +1 @@ +../../../infamy/topologies/1x2.dot \ No newline at end of file diff --git a/test/case/interfaces/qos_factory_default/topology.svg b/test/case/interfaces/qos_factory_default/topology.svg new file mode 100644 index 000000000..ff3d246be --- /dev/null +++ b/test/case/interfaces/qos_factory_default/topology.svg @@ -0,0 +1,42 @@ + + + + + + +1x2 + + + +host + +host + +mgmt + +data + + + +target + +mgmt + +data + +target + + + +host:mgmt--target:mgmt + + + + +host:data--target:data + + + + diff --git a/test/case/interfaces/qos_ingress_classify/Readme.adoc b/test/case/interfaces/qos_ingress_classify/Readme.adoc new file mode 120000 index 000000000..ae32c8412 --- /dev/null +++ b/test/case/interfaces/qos_ingress_classify/Readme.adoc @@ -0,0 +1 @@ +test.adoc \ No newline at end of file diff --git a/test/case/interfaces/qos_ingress_classify/test.adoc b/test/case/interfaces/qos_ingress_classify/test.adoc new file mode 100644 index 000000000..eb358d325 --- /dev/null +++ b/test/case/interfaces/qos_ingress_classify/test.adoc @@ -0,0 +1,42 @@ +=== QoS Ingress Classification and Egress Remarking + +ifdef::topdoc[:imagesdir: {topdoc}../../test/case/interfaces/qos_ingress_classify] + +==== Description + +Configure the trust order, default priority, the standard PCP and DSCP +presets, and remarking on transmit, then verify the rendering: + + - trust dscp-pcp, DSCP first then PCP + - default priority 2 + - PCP preset ieee: PCP n to priority n + - DSCP preset ietf: RFC 4594 groups, e.g. EF (46) to priority 5 + - remark pcp and dscp from priority: priority 5 to PCP 5 and DSCP 40 + +On a port whose driver has DCB operations the kernel's DCB tables must +match, and the operational datastore reports classification and +remarking as offloaded. On any other port the same classification must +be rendered as tc flower rules on the port's ingress, in trust order, +with a catch-all for the default priority, and DSCP remarking as pedit +rules on its egress; PCP remarking needs driver support. +Removing the configuration returns the port to the defaults: trust pcp +with default priority 0, and no remarking. + +==== Topology + +image::topology.svg[QoS Ingress Classification and Egress Remarking topology, align=center, scaledwidth=75%] + +==== Sequence + +. Set up topology and attach to target DUT +. Configure trust dscp-pcp, default priority 2, presets, and remarking +. Verify dcb apptrust order is dscp pcp +. Verify dcb app default priority and PCP and DSCP maps +. Verify dcb rewr priority to PCP and DSCP maps +. Verify classification and remarking are reported as offloaded +. Verify tc flower rules: DSCP block before PCP block, then default +. Verify DSCP remarking as pedit rules on egress +. Verify classification is not reported as offloaded +. Remove qos configuration and verify the defaults are back + + diff --git a/test/case/interfaces/qos_ingress_classify/test.py b/test/case/interfaces/qos_ingress_classify/test.py new file mode 100755 index 000000000..c6c05b4b2 --- /dev/null +++ b/test/case/interfaces/qos_ingress_classify/test.py @@ -0,0 +1,194 @@ +#!/usr/bin/env python3 +""" +QoS Ingress Classification and Egress Remarking + +Configure the trust order, default priority, the standard PCP and DSCP +presets, and remarking on transmit, then verify the rendering: + + - trust dscp-pcp, DSCP first then PCP + - default priority 2 + - PCP preset ieee: PCP n to priority n + - DSCP preset ietf: RFC 4594 groups, e.g. EF (46) to priority 5 + - remark pcp and dscp from priority: priority 5 to PCP 5 and DSCP 40 + +On a port whose driver has DCB operations the kernel's DCB tables must +match, and the operational datastore reports classification and +remarking as offloaded. On any other port the same classification must +be rendered as tc flower rules on the port's ingress, in trust order, +with a catch-all for the default priority, and DSCP remarking as pedit +rules on its egress; PCP remarking needs driver support. +Removing the configuration returns the port to the defaults: trust pcp +with default priority 0, and no remarking. +""" +import json +import re +import infamy +from infamy.util import until + + +def capabilities(target, port): + data = target.get_data(f"/ietf-interfaces:interfaces/interface[name='{port}']" + "/infix-interfaces:qos/capabilities") + for iface in data["interfaces"]["interface"]: + qos = iface.get("qos") or iface.get("infix-interfaces:qos") or {} + return qos.get("capabilities", {}) + return {} + + +def dscp_num(name): + """dcb prints DSCP by name when it knows one: CS1, AF21, EF ...""" + if name.startswith("CS"): + return int(name[2:]) * 8 + if name.startswith("AF"): + return int(name[2]) * 8 + int(name[3]) * 2 + if name == "EF": + return 46 + return int(name) + + +def dcb_tokens(ssh, cmd, port): + """Return {table: [tokens]} from dcb ... show dev PORT text output, DSCP as numbers""" + out = ssh.runsh(f"dcb {cmd} show dev {port}").stdout + tables = {} + for line in out.splitlines(): + name, _, rest = line.partition(" ") + name = name.rstrip(":") + if not name: + continue + tokens = rest.split() + if name == "dscp-prio": + tokens = [f"{dscp_num(k)}:{v}" for k, v in (t.split(":") for t in tokens)] + elif name == "prio-dscp": + tokens = [f"{k}:{dscp_num(v)}" for k, v in (t.split(":") for t in tokens)] + tables[name] = tokens + return tables + + +def skbedit_priority(act): + """tc prints the priority as a classid: 'none' for 0, else e.g. ':5' in hex""" + prio = str(act["priority"]) + if prio == "none": + return 0 + return int(prio.rsplit(":", 1)[-1] or "0", 16) + + +def flower_rules(ssh, port): + """Return [(pref, keys, priority)] for skbedit filters on the port's ingress""" + out = ssh.runsh(f"tc -j filter show dev {port} ingress").stdout + rules = [] + for flt in json.loads(out or "[]"): + opts = flt.get("options") + if not opts: + continue + for act in opts.get("actions", []): + if act.get("kind") == "skbedit" and "priority" in act: + rules.append((flt["pref"], opts.get("keys", {}), skbedit_priority(act))) + return rules + + +with infamy.Test() as test: + with test.step("Set up topology and attach to target DUT"): + env = infamy.Env() + target = env.attach("target", "mgmt") + tgtssh = env.attach("target", "mgmt", "ssh") + _, port = env.ltop.xlate("target", "data") + dcb = bool(capabilities(target, port).get("supported-trust-order")) + print(f"{port}: DCB {'supported' if dcb else 'not supported'}") + + with test.step("Configure trust dscp-pcp, default priority 2, presets, and remarking"): + target.put_config_dicts({"ietf-interfaces": { + "interfaces": { + "interface": [{ + "name": port, + "enabled": True, + "infix-interfaces:qos": { + "ingress": { + "trust": "dscp-pcp", + "default-priority": 2, + "pcp-map": {"preset": "ieee"}, + "dscp-map": {"preset": "ietf"}, + }, + "egress": { + "remark": {"pcp": "from-priority", "dscp": "from-priority"} + } + } + }] + } + }}) + + if dcb: + with test.step("Verify dcb apptrust order is dscp pcp"): + until(lambda: dcb_tokens(tgtssh, "apptrust", port).get("order") == ["dscp", "pcp"]) + + with test.step("Verify dcb app default priority and PCP and DSCP maps"): + app = dcb_tokens(tgtssh, "app", port) + print(app) + assert app.get("default-prio") == ["2"], f"default-prio {app.get('default-prio')}" + pcp = set(app.get("pcp-prio", [])) + for want in ("0nd:0", "1nd:1", "0de:0", "1de:1", "7nd:7", "7de:7"): + assert want in pcp, f"missing {want} in pcp-prio {sorted(pcp)}" + dscp = set(app.get("dscp-prio", [])) + for want in ("0:0", "8:1", "26:3", "46:5", "48:6", "56:7"): + assert want in dscp, f"missing {want} in dscp-prio {sorted(dscp)}" + + with test.step("Verify dcb rewr priority to PCP and DSCP maps"): + rewr = dcb_tokens(tgtssh, "rewr", port) + print(rewr) + pcp = set(rewr.get("prio-pcp", [])) + for want in ("0:0nd", "5:5nd", "7:7nd"): + assert want in pcp, f"missing {want} in prio-pcp {sorted(pcp)}" + dscp = set(rewr.get("prio-dscp", [])) + for want in ("0:0", "1:8", "5:40", "7:56"): + assert want in dscp, f"missing {want} in prio-dscp {sorted(dscp)}" + + with test.step("Verify classification and remarking are reported as offloaded"): + until(lambda: {"classification", "remarking"} <= + set(capabilities(target, port).get("offload", []))) + else: + with test.step("Verify tc flower rules: DSCP block before PCP block, then default"): + until(lambda: len(flower_rules(tgtssh, port)) > 0) + rules = flower_rules(tgtssh, port) + print(f"{len(rules)} skbedit rules") + + # ip_tos is printed as value/mask, e.g. 0xb8/0xfc; key on the DSCP + dscp = {} + for r in rules: + if "ip_tos" in r[1]: + tos = int(str(r[1]["ip_tos"]).split("/")[0], 0) + dscp.setdefault(tos >> 2, set()).add(r[2]) + pcp = {r[1]["vlan_prio"]: r for r in rules if "vlan_prio" in r[1]} + dflt = [r for r in rules if not r[1]] + print(f"dscp {dscp}\npcp {pcp}\ndefault {dflt}") + + assert dscp.get(46) == {5}, f"EF: {dscp.get(46)}" + assert dscp.get(8) == {1}, f"CS1: {dscp.get(8)}" + assert all(pcp[p][2] == p for p in range(8)), pcp + assert dflt and dflt[0][2] == 2, dflt + # four variants per codepoint: IPv4, IPv6, tagged IPv4, tagged IPv6 + assert sum(1 for r in rules if "ip_tos" in r[1]) == 4 * len(dscp), len(rules) + + dscp_pref = {r[0] for r in rules if "ip_tos" in r[1]} + pcp_pref = {r[0] for r in pcp.values()} + assert max(dscp_pref) < min(pcp_pref) < dflt[0][0], (dscp_pref, pcp_pref, dflt) + + with test.step("Verify DSCP remarking as pedit rules on egress"): + # tc -j is not valid JSON for basic filters with ematches, so count in text + out = tgtssh.runsh(f"tc filter show dev {port} egress").stdout + pedits = [l for l in out.splitlines() if re.match(r"\s*action order \d+:\s+pedit", l)] + assert len(pedits) == 32, f"{len(pedits)} pedit rules" + + with test.step("Verify classification is not reported as offloaded"): + assert "classification" not in capabilities(target, port).get("offload", []) + + with test.step("Remove qos configuration and verify the defaults are back"): + target.delete_xpath(f"/ietf-interfaces:interfaces/interface[name='{port}']" + "/infix-interfaces:qos") + if dcb: + until(lambda: dcb_tokens(tgtssh, "app", port).get("default-prio") == ["0"]) + assert dcb_tokens(tgtssh, "apptrust", port).get("order") == ["pcp"] + assert not dcb_tokens(tgtssh, "rewr", port).get("prio-dscp") + else: + until(lambda: [r for r in flower_rules(tgtssh, port) if not r[1]] == [(900, {}, 0)]) + assert not tgtssh.runsh(f"tc filter show dev {port} egress").stdout.strip() + + test.succeed() diff --git a/test/case/interfaces/qos_ingress_classify/topology.dot b/test/case/interfaces/qos_ingress_classify/topology.dot new file mode 120000 index 000000000..4f53d15af --- /dev/null +++ b/test/case/interfaces/qos_ingress_classify/topology.dot @@ -0,0 +1 @@ +../../../infamy/topologies/1x2.dot \ No newline at end of file diff --git a/test/case/interfaces/qos_ingress_classify/topology.svg b/test/case/interfaces/qos_ingress_classify/topology.svg new file mode 100644 index 000000000..ff3d246be --- /dev/null +++ b/test/case/interfaces/qos_ingress_classify/topology.svg @@ -0,0 +1,42 @@ + + + + + + +1x2 + + + +host + +host + +mgmt + +data + + + +target + +mgmt + +data + +target + + + +host:mgmt--target:mgmt + + + + +host:data--target:data + + + + diff --git a/test/case/interfaces/qos_mixed_selection/Readme.adoc b/test/case/interfaces/qos_mixed_selection/Readme.adoc new file mode 120000 index 000000000..ae32c8412 --- /dev/null +++ b/test/case/interfaces/qos_mixed_selection/Readme.adoc @@ -0,0 +1 @@ +test.adoc \ No newline at end of file diff --git a/test/case/interfaces/qos_mixed_selection/test.adoc b/test/case/interfaces/qos_mixed_selection/test.adoc new file mode 100644 index 000000000..4d1324fcb --- /dev/null +++ b/test/case/interfaces/qos_mixed_selection/test.adoc @@ -0,0 +1,38 @@ +=== QoS Mixed Selection + +ifdef::topdoc[:imagesdir: {topdoc}../../test/case/interfaces/qos_mixed_selection] + +==== Description + +Three talkers, marked EF, AF21 and CS1, bridged through the DUT and out +one egress port that cannot carry them all. The port is negotiated down +to 100 Mbit/s where its PHY allows, or rate limited to 10 Mbit/s where +it has no PHY. The ingress port classifies EF to priority 4, AF21 to 3 +and CS1 to 2, classes 4, 3 and 2 in the default table: EF in a strict +class above the two weighted ones, which share 60 and 30 percent, with +the two classes below holding five percent each and carrying nothing. + +EF offers 40 percent of what the port drains, the two weighted talkers +60 percent each, so the port is oversubscribed and both weighted classes +always have a backlog. EF must arrive intact, and the two weighted +flows must divide what EF leaves 2:1, within five points, both of them +losing more than a tenth of their frames. The test's own iperf3 control +traffic, DSCP 0, is classified to priority 7. Skipped on a switch +fabric whose scheduler is not offloaded, where forwarded frames never +meet the configured algorithm. + +==== Topology + +image::topology.svg[QoS Mixed Selection topology, align=center, scaledwidth=75%] + +==== Sequence + +. Set up topology and attach to target DUT +. Bridge the two ports, classify by DSCP on ingress, EF strict above AF21 and CS1 at 2:1 +. Slow the egress port so its queues can fill +. Set up the talker and listener namespaces +. EF at 40 percent, AF21 and CS1 at 60 percent each +. Verify EF arrived intact +. Verify AF21 and CS1 both lost frames and split the rest 2:1 + + diff --git a/test/case/interfaces/qos_mixed_selection/test.py b/test/case/interfaces/qos_mixed_selection/test.py new file mode 100755 index 000000000..3fe99c650 --- /dev/null +++ b/test/case/interfaces/qos_mixed_selection/test.py @@ -0,0 +1,126 @@ +#!/usr/bin/env python3 +""" +QoS Mixed Selection + +Three talkers, marked EF, AF21 and CS1, bridged through the DUT and out +one egress port that cannot carry them all. The port is negotiated down +to 100 Mbit/s where its PHY allows, or rate limited to 10 Mbit/s where +it has no PHY. The ingress port classifies EF to priority 4, AF21 to 3 +and CS1 to 2, classes 4, 3 and 2 in the default table: EF in a strict +class above the two weighted ones, which share 60 and 30 percent, with +the two classes below holding five percent each and carrying nothing. + +EF offers 40 percent of what the port drains, the two weighted talkers +60 percent each, so the port is oversubscribed and both weighted classes +always have a backlog. EF must arrive intact, and the two weighted +flows must divide what EF leaves 2:1, within five points, both of them +losing more than a tenth of their frames. The test's own iperf3 control +traffic, DSCP 0, is classified to priority 7. Skipped on a switch +fabric whose scheduler is not offloaded, where forwarded frames never +meet the configured algorithm. +""" +import infamy +import infamy.qos as qos +from infamy.util import until + +EF, AF21, CS1 = 46 << 2, 18 << 2, 8 << 2 # TOS bytes of the three markings +STRICT, HIGH, LOW = 4, 3, 2 # priorities, and classes, of the markings +FILLER = 5 # share of each class below, carrying nothing + +SHARES = {HIGH: 60, LOW: 30} +SHARES.update({tc: FILLER for tc in range(LOW)}) + + +with infamy.Test() as test: + with test.step("Set up topology and attach to target DUT"): + env = infamy.Env() + target = env.attach("target", "mgmt") + tgtssh = env.attach("target", "mgmt", "ssh") + _, td0 = env.ltop.xlate("target", "data1") + _, td1 = env.ltop.xlate("target", "data2") + _, hd0 = env.ltop.xlate("host", "data1") + _, hd1 = env.ltop.xlate("host", "data2") + + num_tc = qos.num_classes(target, td1) + print(f"{td1}: {num_tc} traffic classes") + if num_tc < 4: + print("no class left for the control traffic above a strict and two weighted, skipping") + test.skip() + + uevent = tgtssh.runsh(f"cat /sys/class/net/{td1}/uevent").stdout + dsa = "DEVTYPE=dsa" in uevent.split() + if dsa and "transmission-selection" not in qos.offload(target, td1): + print("switch fabric forwards past a scheduler its driver does not offload, skipping") + test.skip() + + with test.step("Bridge the two ports, classify by DSCP on ingress, EF strict above AF21 and CS1 at 2:1"): + target.put_config_dicts({"ietf-interfaces": { + "interfaces": { + "interface": [ + {"name": "br0", "type": "infix-if-type:bridge", "enabled": True}, + { + "name": td0, + "enabled": True, + "infix-interfaces:bridge-port": {"bridge": "br0"}, + "infix-interfaces:qos": { + "ingress": {"trust": "dscp", + "dscp-map": qos.dscp_map(**{"0": 7, "46": STRICT, + "18": HIGH, "8": LOW})} + } + }, + { + "name": td1, + "enabled": True, + "infix-interfaces:bridge-port": {"bridge": "br0"}, + "infix-interfaces:qos": { + "egress": {"traffic-class": qos.traffic_classes(SHARES)} + } + }, + ] + } + }}) + quanta = [SHARES[tc] * qos.ETS_QUANTUM_UNIT for tc in sorted(SHARES, reverse=True)] + until(lambda: qos.scheduler_matches(qos.scheduler(tgtssh, td1), num_tc, + qos.TABLE_8_5[num_tc], + strict=num_tc - len(SHARES), quanta=quanta)) + qos.show_offload(target, tgtssh, td1, dsa) + + with test.step("Slow the egress port so its queues can fill"): + drain = qos.slow_port(target, tgtssh, td1, until) + if not drain: + print(f"{td1} can neither negotiate down nor be rate limited, skipping") + test.skip() + print(f"{td1} drains {drain // 1_000_000} Mbit/s") + + with infamy.IsolatedMacVlan(hd0) as ns0, \ + infamy.IsolatedMacVlan(hd1) as ns1: + + with test.step("Set up the talker and listener namespaces"): + ns0.runsh("set -ex; ip link set iface up; ip addr add 192.168.20.1/24 dev iface") + ns1.runsh("set -ex; ip link set iface up; ip addr add 192.168.20.2/24 dev iface") + ns0.must_reach("192.168.20.2") + + with test.step("EF at 40 percent, AF21 and CS1 at 60 percent each"): + flows = [qos.Flow("EF", 5201, EF, int(drain * 0.4)), + qos.Flow("AF21", 5202, AF21, int(drain * 0.6)), + qos.Flow("CS1", 5203, CS1, int(drain * 0.6))] + total = qos.run_flows(ns0, ns1, "192.168.20.2", flows) + ef, af21, cs1 = flows + print(f"shares: EF {ef.share(total):.1f}%, AF21 {af21.share(total):.1f}%, " + f"CS1 {cs1.share(total):.1f}%") + + with test.step("Verify EF arrived intact"): + assert ef.result["lost_percent"] < 1, \ + f"EF lost {ef.result['lost']} of {ef.result['offered']} datagrams" + + with test.step("Verify AF21 and CS1 both lost frames and split the rest 2:1"): + for flow in (af21, cs1): + assert flow.result["lost_percent"] > 10, \ + f"{flow.name} lost {flow.result['lost_percent']:.1f}%, the port was never full" + rest = af21.result["packets"] + cs1.result["packets"] + af21_share = af21.share(rest) + print(f"of what EF left: AF21 {af21_share:.1f}%, CS1 {100 - af21_share:.1f}%") + assert abs(af21_share - 100 * 60 / 90) < 5, \ + f"AF21 got {af21_share:.1f}% of what EF left, configured 2:1" + + test.succeed() diff --git a/test/case/interfaces/qos_mixed_selection/topology.dot b/test/case/interfaces/qos_mixed_selection/topology.dot new file mode 100644 index 000000000..f288f2f95 --- /dev/null +++ b/test/case/interfaces/qos_mixed_selection/topology.dot @@ -0,0 +1,24 @@ +graph "1x3" { + layout="neato"; + overlap="false"; + esep="+80"; + + node [shape=record, fontname="DejaVu Sans Mono, Book"]; + edge [color="cornflowerblue", penwidth="2", fontname="DejaVu Serif, Book"]; + + host [ + label="host | { mgmt | data1 | data2 }", + pos="0,12!", + requires="controller", + ]; + + target [ + label="{ mgmt | data1 | data2 } | target", + pos="10,12!", + requires="infix", + ]; + + host:mgmt -- target:mgmt [requires="mgmt", color="lightgray"] + host:data1 -- target:data1 [color="black"] + host:data2 -- target:data2 [color="black"] +} diff --git a/test/case/interfaces/qos_mixed_selection/topology.svg b/test/case/interfaces/qos_mixed_selection/topology.svg new file mode 100644 index 000000000..9517e6397 --- /dev/null +++ b/test/case/interfaces/qos_mixed_selection/topology.svg @@ -0,0 +1,51 @@ + + + + + + +1x3 + + + +host + +host + +mgmt + +data1 + +data2 + + + +target + +mgmt + +data1 + +data2 + +target + + + +host:mgmt--target:mgmt + + + + +host:data1--target:data1 + + + + +host:data2--target:data2 + + + + diff --git a/test/case/interfaces/qos_queue_assignment/Readme.adoc b/test/case/interfaces/qos_queue_assignment/Readme.adoc new file mode 120000 index 000000000..ae32c8412 --- /dev/null +++ b/test/case/interfaces/qos_queue_assignment/Readme.adoc @@ -0,0 +1 @@ +test.adoc \ No newline at end of file diff --git a/test/case/interfaces/qos_queue_assignment/test.adoc b/test/case/interfaces/qos_queue_assignment/test.adoc new file mode 100644 index 000000000..d7a2c651f --- /dev/null +++ b/test/case/interfaces/qos_queue_assignment/test.adoc @@ -0,0 +1,35 @@ +=== QoS Queue Assignment + +ifdef::topdoc[:imagesdir: {topdoc}../../test/case/interfaces/qos_queue_assignment] + +==== Description + +Verify that a frame is queued in the traffic class its priority maps to, +for the ieee preset, the ieee-sr preset and a custom table. The +existing tests check the tables as rendered tc arguments; this one +checks the queue a frame actually reached. + +Frames enter tagged on a VLAN interface whose ingress-qos takes the +priority straight from the PCP, so classification contributes nothing +and the traffic class table on the egress port is all that is under +test. A hundred datagrams are sent at each priority, routed through the +DUT to the listener, and the per-class counters of the egress scheduler +are read before and after: the class the table names must grow by at +least that many, and no other class by anywhere near it. The DUT's own +chatter, mDNS and neighbour discovery at priority 0 and control frames +at priority 7, is a handful of frames and stays well below the burst. +No load, no congestion. + +==== Topology + +image::topology.svg[QoS Queue Assignment topology, align=center, scaledwidth=75%] + +==== Sequence + +. Set up topology and attach to target DUT +. Configure a routed path, tagged ingress with priority from PCP, egress under test +. Set up the talker on VLAN 10 and the listener, resolve neighbours +. Apply the {name} traffic class table on the egress port +. Send {COUNT} frames at each priority, verify the class each reached + + diff --git a/test/case/interfaces/qos_queue_assignment/test.py b/test/case/interfaces/qos_queue_assignment/test.py new file mode 100755 index 000000000..5c688ef52 --- /dev/null +++ b/test/case/interfaces/qos_queue_assignment/test.py @@ -0,0 +1,142 @@ +#!/usr/bin/env python3 +""" +QoS Queue Assignment + +Verify that a frame is queued in the traffic class its priority maps to, +for the ieee preset, the ieee-sr preset and a custom table. The +existing tests check the tables as rendered tc arguments; this one +checks the queue a frame actually reached. + +Frames enter tagged on a VLAN interface whose ingress-qos takes the +priority straight from the PCP, so classification contributes nothing +and the traffic class table on the egress port is all that is under +test. A hundred datagrams are sent at each priority, routed through the +DUT to the listener, and the per-class counters of the egress scheduler +are read before and after: the class the table names must grow by at +least that many, and no other class by anywhere near it. The DUT's own +chatter, mDNS and neighbour discovery at priority 0 and control frames +at priority 7, is a handful of frames and stays well below the burst. +No load, no congestion. +""" +import infamy +import infamy.qos as qos +from infamy.util import until + +PRIORITIES = range(8) +COUNT = 100 # per priority; the DUT's own chatter is a few frames + +with infamy.Test() as test: + with test.step("Set up topology and attach to target DUT"): + env = infamy.Env() + target = env.attach("target", "mgmt") + tgtssh = env.attach("target", "mgmt", "ssh") + _, td0 = env.ltop.xlate("target", "data1") + _, td1 = env.ltop.xlate("target", "data2") + _, hd0 = env.ltop.xlate("host", "data1") + _, hd1 = env.ltop.xlate("host", "data2") + + num_tc = qos.num_classes(target, td0) + print(f"{td1}: {num_tc} traffic classes") + tables = { + "ieee": ({"preset": "ieee"}, qos.TABLE_8_5[num_tc]), + "ieee-sr": ({"preset": "ieee-sr"}, qos.TABLE_34_1[num_tc]), + } + # Table 8-5 with the two lowest classes swapped, custom on any class count + custom = [1 if tc == 0 else 0 if tc == 1 else tc for tc in qos.TABLE_8_5[num_tc]] + tables["custom"] = ({f"priority{p}": tc for p, tc in enumerate(custom)}, custom) + + with test.step("Configure a routed path, tagged ingress with priority from PCP, egress under test"): + target.put_config_dicts({"ietf-interfaces": { + "interfaces": { + "interface": [ + {"name": td0, "enabled": True}, + { + "name": f"{td0}.10", + "type": "infix-if-type:vlan", + "vlan": { + "id": 10, + "lower-layer-if": td0, + "ingress-qos": {"priority": "from-pcp"}, + }, + "ipv4": { + "forwarding": True, + "address": [{"ip": "192.168.10.1", "prefix-length": 24}] + } + }, + { + "name": td1, + "enabled": True, + "ipv4": { + "forwarding": True, + "address": [{"ip": "192.168.11.1", "prefix-length": 24}] + } + }, + ] + } + }}) + + with infamy.IsolatedMacVlan(hd0) as ns0, \ + infamy.IsolatedMacVlan(hd1) as ns1: + + with test.step("Set up the talker on VLAN 10 and the listener, resolve neighbours"): + ns0.runsh(""" + set -ex + ip link set iface up + ip link add dev vlan10 link iface up type vlan id 10 + ip addr add 192.168.10.2/24 dev vlan10 + """) + ns0.addroute("default", "192.168.10.1") + ns1.runsh(""" + set -ex + ip link set iface up + ip addr add 192.168.11.2/24 dev iface + """) + ns1.addroute("default", "192.168.11.1") + ns0.must_reach("192.168.11.2") + dut_mac = qos.neighbour_mac(ns0, "192.168.10.1") + assert dut_mac, "DUT MAC not resolved" + print(f"DUT {td0}.10 is {dut_mac}") + + for name, (table, prio_map) in tables.items(): + with test.step(f"Apply the {name} traffic class table on the egress port"): + # Preset and custom leaves are a choice, so clear the previous + # table; the first round has none to clear + try: + target.delete_xpath(qos.xpath(td1, "/egress/traffic-class-table")) + except ValueError: + pass + target.put_config_dicts({"ietf-interfaces": { + "interfaces": { + "interface": [{ + "name": td1, + "infix-interfaces:qos": {"egress": {"traffic-class-table": table}} + }] + } + }}) + until(lambda: qos.scheduler_matches(qos.scheduler(tgtssh, td1), num_tc, prio_map)) + ns0.must_reach("192.168.11.2") + + with test.step(f"Send {COUNT} frames at each priority, verify the class each reached"): + for prio in PRIORITIES: + before = qos.class_stats(tgtssh, td1, num_tc) + qos.mausezahn(ns0, "iface", "192.168.10.2", "192.168.11.2", dut_mac, + count=COUNT, vid=10, pcp=prio) + + def landed(): + delta = qos.stats_delta(before, qos.class_stats(tgtssh, td1, num_tc)) + return delta.get(prio_map[prio], {}).get("packets", 0) >= COUNT + + until(landed, attempts=20) + delta = qos.stats_delta(before, qos.class_stats(tgtssh, td1, num_tc)) + got = {tc: d["packets"] for tc, d in delta.items() if d["packets"]} + print(f"{name}: priority {prio} -> class {prio_map[prio]}: {got}") + + for tc, d in delta.items(): + if tc == prio_map[prio]: + assert d["packets"] >= COUNT, \ + f"priority {prio}: class {tc} got {d['packets']}, expected at least {COUNT}" + else: + assert d["packets"] < COUNT // 2, \ + f"priority {prio}: class {tc} got {d['packets']} frames it should not have" + + test.succeed() diff --git a/test/case/interfaces/qos_queue_assignment/topology.dot b/test/case/interfaces/qos_queue_assignment/topology.dot new file mode 100644 index 000000000..f288f2f95 --- /dev/null +++ b/test/case/interfaces/qos_queue_assignment/topology.dot @@ -0,0 +1,24 @@ +graph "1x3" { + layout="neato"; + overlap="false"; + esep="+80"; + + node [shape=record, fontname="DejaVu Sans Mono, Book"]; + edge [color="cornflowerblue", penwidth="2", fontname="DejaVu Serif, Book"]; + + host [ + label="host | { mgmt | data1 | data2 }", + pos="0,12!", + requires="controller", + ]; + + target [ + label="{ mgmt | data1 | data2 } | target", + pos="10,12!", + requires="infix", + ]; + + host:mgmt -- target:mgmt [requires="mgmt", color="lightgray"] + host:data1 -- target:data1 [color="black"] + host:data2 -- target:data2 [color="black"] +} diff --git a/test/case/interfaces/qos_queue_assignment/topology.svg b/test/case/interfaces/qos_queue_assignment/topology.svg new file mode 100644 index 000000000..9517e6397 --- /dev/null +++ b/test/case/interfaces/qos_queue_assignment/topology.svg @@ -0,0 +1,51 @@ + + + + + + +1x3 + + + +host + +host + +mgmt + +data1 + +data2 + + + +target + +mgmt + +data1 + +data2 + +target + + + +host:mgmt--target:mgmt + + + + +host:data1--target:data1 + + + + +host:data2--target:data2 + + + + diff --git a/test/case/interfaces/qos_rate_limit/Readme.adoc b/test/case/interfaces/qos_rate_limit/Readme.adoc new file mode 120000 index 000000000..ae32c8412 --- /dev/null +++ b/test/case/interfaces/qos_rate_limit/Readme.adoc @@ -0,0 +1 @@ +test.adoc \ No newline at end of file diff --git a/test/case/interfaces/qos_rate_limit/test.adoc b/test/case/interfaces/qos_rate_limit/test.adoc new file mode 100644 index 000000000..2d9b4ebec --- /dev/null +++ b/test/case/interfaces/qos_rate_limit/test.adoc @@ -0,0 +1,33 @@ +=== QoS Rate Limit + +ifdef::topdoc[:imagesdir: {topdoc}../../test/case/interfaces/qos_rate_limit] + +==== Description + +One talker, bridged through the DUT and out an egress port with a +10 Mbit/s rate limit, offering three times that. What arrives at the +listener must be the limit, within a fifth, whatever the priority of the +flow: once marked EF, in a strict class near the top, once marked CS1, +in the lowest class. Then both at once, each offering one and a half +times the limit, must still add up to the limit. + +A limit that lets more through is not a limit, and a limit that only +bites on some queues is a scheduler fault dressed up as one. The test's +own iperf3 control traffic, DSCP 0, is classified to priority 7 at the +ingress port. Skipped on a switch fabric whose driver does not offload +the rate limit, where forwarded frames never meet the bucket. + +==== Topology + +image::topology.svg[QoS Rate Limit topology, align=center, scaledwidth=75%] + +==== Sequence + +. Set up topology and attach to target DUT +. Bridge the two ports, trust DSCP on ingress, rate limit the egress port +. Set up the talker and listener namespaces +. One EF flow at three times the limit, expect the limit through +. One CS1 flow at three times the limit, expect the limit through +. Both at one and a half times the limit, expect the limit through in total + + diff --git a/test/case/interfaces/qos_rate_limit/test.py b/test/case/interfaces/qos_rate_limit/test.py new file mode 100755 index 000000000..58fdf75f8 --- /dev/null +++ b/test/case/interfaces/qos_rate_limit/test.py @@ -0,0 +1,104 @@ +#!/usr/bin/env python3 +""" +QoS Rate Limit + +One talker, bridged through the DUT and out an egress port with a +10 Mbit/s rate limit, offering three times that. What arrives at the +listener must be the limit, within a fifth, whatever the priority of the +flow: once marked EF, in a strict class near the top, once marked CS1, +in the lowest class. Then both at once, each offering one and a half +times the limit, must still add up to the limit. + +A limit that lets more through is not a limit, and a limit that only +bites on some queues is a scheduler fault dressed up as one. The test's +own iperf3 control traffic, DSCP 0, is classified to priority 7 at the +ingress port. Skipped on a switch fabric whose driver does not offload +the rate limit, where forwarded frames never meet the bucket. +""" +import infamy +import infamy.qos as qos +from infamy.util import until + +LIMIT = 10_000_000 # bit/s, Layer 2 +EF, CS1 = 46 << 2, 8 << 2 # TOS bytes of the two markings +SIZE = 1000 # UDP payload, 1042 bytes on the wire +TOLERANCE = 0.2 + + +def check(what, throughput): + """throughput is payload bit/s; the limit counts the 42 header bytes too""" + expect = LIMIT * SIZE / (SIZE + 42) + print(f"{what}: {throughput / 1e6:.2f} Mbit/s through, limit lets {expect / 1e6:.2f}") + assert abs(throughput - expect) <= TOLERANCE * expect, \ + f"{what}: {throughput / 1e6:.2f} Mbit/s through a {LIMIT / 1e6:.0f} Mbit/s limit" + + +with infamy.Test() as test: + with test.step("Set up topology and attach to target DUT"): + env = infamy.Env() + target = env.attach("target", "mgmt") + tgtssh = env.attach("target", "mgmt", "ssh") + _, td0 = env.ltop.xlate("target", "data1") + _, td1 = env.ltop.xlate("target", "data2") + _, hd0 = env.ltop.xlate("host", "data1") + _, hd1 = env.ltop.xlate("host", "data2") + + with test.step("Bridge the two ports, trust DSCP on ingress, rate limit the egress port"): + target.put_config_dicts({"ietf-interfaces": { + "interfaces": { + "interface": [ + {"name": "br0", "type": "infix-if-type:bridge", "enabled": True}, + { + "name": td0, + "enabled": True, + "infix-interfaces:bridge-port": {"bridge": "br0"}, + "infix-interfaces:qos": { + "ingress": {"trust": "dscp", + "dscp-map": qos.dscp_map(**{"0": 7, "46": 5, "8": 1})} + } + }, + { + "name": td1, + "enabled": True, + "infix-interfaces:bridge-port": {"bridge": "br0"}, + "infix-interfaces:qos": { + "egress": {"rate-limit": {"rate": LIMIT}} + } + }, + ] + } + }}) + until(lambda: (qos.root_qdisc(tgtssh, td1) or {}).get("kind") == "tbf") + qos.show_shaper(tgtssh, td1) + + uevent = tgtssh.runsh(f"cat /sys/class/net/{td1}/uevent").stdout + if "DEVTYPE=dsa" in uevent.split() and "rate-limit" not in qos.offload(target, td1): + print("switch fabric forwards past a rate limit its driver does not offload, skipping") + test.skip() + + with infamy.IsolatedMacVlan(hd0) as ns0, \ + infamy.IsolatedMacVlan(hd1) as ns1: + + with test.step("Set up the talker and listener namespaces"): + ns0.runsh("set -ex; ip link set iface up; ip addr add 192.168.20.1/24 dev iface") + ns1.runsh("set -ex; ip link set iface up; ip addr add 192.168.20.2/24 dev iface") + ns0.must_reach("192.168.20.2") + + with test.step("One EF flow at three times the limit, expect the limit through"): + flow = qos.Flow("EF", 5201, EF, 3 * LIMIT, size=SIZE) + qos.run_flows(ns0, ns1, "192.168.20.2", [flow]) + check("EF alone", flow.throughput()) + + with test.step("One CS1 flow at three times the limit, expect the limit through"): + flow = qos.Flow("CS1", 5202, CS1, 3 * LIMIT, size=SIZE) + qos.run_flows(ns0, ns1, "192.168.20.2", [flow]) + check("CS1 alone", flow.throughput()) + + with test.step("Both at one and a half times the limit, expect the limit through in total"): + flows = [qos.Flow("EF", 5201, EF, int(1.5 * LIMIT), size=SIZE), + qos.Flow("CS1", 5202, CS1, int(1.5 * LIMIT), size=SIZE)] + total = qos.run_flows(ns0, ns1, "192.168.20.2", flows) + print(f"shares: EF {flows[0].share(total):.1f}%, CS1 {flows[1].share(total):.1f}%") + check("EF and CS1 together", sum(f.throughput() for f in flows)) + + test.succeed() diff --git a/test/case/interfaces/qos_rate_limit/topology.dot b/test/case/interfaces/qos_rate_limit/topology.dot new file mode 100644 index 000000000..f288f2f95 --- /dev/null +++ b/test/case/interfaces/qos_rate_limit/topology.dot @@ -0,0 +1,24 @@ +graph "1x3" { + layout="neato"; + overlap="false"; + esep="+80"; + + node [shape=record, fontname="DejaVu Sans Mono, Book"]; + edge [color="cornflowerblue", penwidth="2", fontname="DejaVu Serif, Book"]; + + host [ + label="host | { mgmt | data1 | data2 }", + pos="0,12!", + requires="controller", + ]; + + target [ + label="{ mgmt | data1 | data2 } | target", + pos="10,12!", + requires="infix", + ]; + + host:mgmt -- target:mgmt [requires="mgmt", color="lightgray"] + host:data1 -- target:data1 [color="black"] + host:data2 -- target:data2 [color="black"] +} diff --git a/test/case/interfaces/qos_rate_limit/topology.svg b/test/case/interfaces/qos_rate_limit/topology.svg new file mode 100644 index 000000000..9517e6397 --- /dev/null +++ b/test/case/interfaces/qos_rate_limit/topology.svg @@ -0,0 +1,51 @@ + + + + + + +1x3 + + + +host + +host + +mgmt + +data1 + +data2 + + + +target + +mgmt + +data1 + +data2 + +target + + + +host:mgmt--target:mgmt + + + + +host:data1--target:data1 + + + + +host:data2--target:data2 + + + + diff --git a/test/case/interfaces/qos_strict_priority/Readme.adoc b/test/case/interfaces/qos_strict_priority/Readme.adoc new file mode 120000 index 000000000..ae32c8412 --- /dev/null +++ b/test/case/interfaces/qos_strict_priority/Readme.adoc @@ -0,0 +1 @@ +test.adoc \ No newline at end of file diff --git a/test/case/interfaces/qos_strict_priority/test.adoc b/test/case/interfaces/qos_strict_priority/test.adoc new file mode 100644 index 000000000..bd2cfff3f --- /dev/null +++ b/test/case/interfaces/qos_strict_priority/test.adoc @@ -0,0 +1,48 @@ +=== QoS Strict Priority + +ifdef::topdoc[:imagesdir: {topdoc}../../test/case/interfaces/qos_strict_priority] + +==== Description + +Two talkers, one marked EF and one CS1, bridged through the DUT and out +one egress port that cannot carry both. The port is negotiated down to +100 Mbit/s where its PHY allows, or rate limited to 10 Mbit/s where it +has no PHY, and each talker offers 80 percent of what the port drains, +so the queues fill and the scheduler decides who gets through. + +Classification at the ingress port decides the priority, and with it +the queue, all the way to the listener: on one chip, across a cascade, +or through a network of switches. So the flows are steered with the +DSCP map on the ingress port and the class table is left at its +default. Three measurements, in this order: + + - baseline: both markings classified to the same priority share one + queue, and the low flow keeps at least a tenth of it; one FIFO under + tail drop is not fair, but it favours nobody by marking + - split: EF classified to priority 5, strict above CS1 at priority 2, + arrives without loss and CS1 takes what is left + - starve: EF raised to 120 percent of the port on its own leaves CS1 + below 5 percent of the served datagrams + +The low flow must lose more than a tenth of its frames in every +measurement; a port that was never full proves nothing. The test's own +iperf3 control traffic, DSCP 0, is classified to priority 7 so the +contest never cuts it off. Skipped on a switch fabric whose scheduler +is not offloaded, where forwarded frames never meet the configured +algorithm. + +==== Topology + +image::topology.svg[QoS Strict Priority topology, align=center, scaledwidth=75%] + +==== Sequence + +. Set up topology and attach to target DUT +. Bridge the two ports, classify by DSCP on ingress, control traffic on top +. Slow the egress port so its queues can fill +. Set up the talker and listener namespaces +. Baseline: both flows at one priority, 80 percent each, expect neither starved +. Split: EF strict above CS1, 80 percent each, expect EF without loss +. Starve: EF at 120 percent of the port, expect CS1 below 5 percent + + diff --git a/test/case/interfaces/qos_strict_priority/test.py b/test/case/interfaces/qos_strict_priority/test.py new file mode 100755 index 000000000..9aaebc3d9 --- /dev/null +++ b/test/case/interfaces/qos_strict_priority/test.py @@ -0,0 +1,142 @@ +#!/usr/bin/env python3 +""" +QoS Strict Priority + +Two talkers, one marked EF and one CS1, bridged through the DUT and out +one egress port that cannot carry both. The port is negotiated down to +100 Mbit/s where its PHY allows, or rate limited to 10 Mbit/s where it +has no PHY, and each talker offers 80 percent of what the port drains, +so the queues fill and the scheduler decides who gets through. + +Classification at the ingress port decides the priority, and with it +the queue, all the way to the listener: on one chip, across a cascade, +or through a network of switches. So the flows are steered with the +DSCP map on the ingress port and the class table is left at its +default. Three measurements, in this order: + + - baseline: both markings classified to the same priority share one + queue, and the low flow keeps at least a tenth of it; one FIFO under + tail drop is not fair, but it favours nobody by marking + - split: EF classified to priority 5, strict above CS1 at priority 2, + arrives without loss and CS1 takes what is left + - starve: EF raised to 120 percent of the port on its own leaves CS1 + below 5 percent of the served datagrams + +The low flow must lose more than a tenth of its frames in every +measurement; a port that was never full proves nothing. The test's own +iperf3 control traffic, DSCP 0, is classified to priority 7 so the +contest never cuts it off. Skipped on a switch fabric whose scheduler +is not offloaded, where forwarded frames never meet the configured +algorithm. +""" +import infamy +import infamy.qos as qos +from infamy.util import until + +EF, CS1 = 46 << 2, 8 << 2 # TOS bytes of the two markings +HIGH, LOW = 5, 2 # priorities the two markings are classified to + + +with infamy.Test() as test: + with test.step("Set up topology and attach to target DUT"): + env = infamy.Env() + target = env.attach("target", "mgmt") + tgtssh = env.attach("target", "mgmt", "ssh") + _, td0 = env.ltop.xlate("target", "data1") + _, td1 = env.ltop.xlate("target", "data2") + _, hd0 = env.ltop.xlate("host", "data1") + _, hd1 = env.ltop.xlate("host", "data2") + + num_tc = qos.num_classes(target, td1) + print(f"{td1}: {num_tc} traffic classes, EF at priority {HIGH}, CS1 at priority {LOW}") + + uevent = tgtssh.runsh(f"cat /sys/class/net/{td1}/uevent").stdout + dsa = "DEVTYPE=dsa" in uevent.split() + if dsa and "transmission-selection" not in qos.offload(target, td1): + print("switch fabric forwards past a scheduler its driver does not offload, skipping") + test.skip() + + with test.step("Bridge the two ports, classify by DSCP on ingress, control traffic on top"): + target.put_config_dicts({"ietf-interfaces": { + "interfaces": { + "interface": [ + {"name": "br0", "type": "infix-if-type:bridge", "enabled": True}, + { + "name": td0, + "enabled": True, + "infix-interfaces:bridge-port": {"bridge": "br0"}, + "infix-interfaces:qos": { + "ingress": {"trust": "dscp", + "dscp-map": qos.dscp_map(**{"0": 7, "46": HIGH, "8": LOW})} + } + }, + { + "name": td1, + "enabled": True, + "infix-interfaces:bridge-port": {"bridge": "br0"}, + }, + ] + } + }}) + + with test.step("Slow the egress port so its queues can fill"): + drain = qos.slow_port(target, tgtssh, td1, until) + if not drain: + print(f"{td1} can neither negotiate down nor be rate limited, skipping") + test.skip() + print(f"{td1} drains {drain // 1_000_000} Mbit/s") + + with infamy.IsolatedMacVlan(hd0) as ns0, \ + infamy.IsolatedMacVlan(hd1) as ns1: + + with test.step("Set up the talker and listener namespaces"): + ns0.runsh("set -ex; ip link set iface up; ip addr add 192.168.20.1/24 dev iface") + ns1.runsh("set -ex; ip link set iface up; ip addr add 192.168.20.2/24 dev iface") + ns0.must_reach("192.168.20.2") + + def apply(ef_prio, cs1_prio): + """Classify the two markings, then let the rendering settle""" + target.put_config_dicts({"ietf-interfaces": { + "interfaces": { + "interface": [{ + "name": td0, + "infix-interfaces:qos": { + "ingress": {"dscp-map": qos.dscp_map(**{"0": 7, "46": ef_prio, + "8": cs1_prio})} + } + }] + } + }}) + until(lambda: qos.dscp_prio(tgtssh, td0, 46) == ef_prio and + qos.dscp_prio(tgtssh, td0, 8) == cs1_prio) + qos.show_offload(target, tgtssh, td1, dsa) + ns0.must_reach("192.168.20.2") + + def measure(high_rate, low_rate=0.8): + flows = [qos.Flow("EF", 5201, EF, int(drain * high_rate)), + qos.Flow("CS1", 5202, CS1, int(drain * low_rate))] + total = qos.run_flows(ns0, ns1, "192.168.20.2", flows) + high, low = flows + print(f"shares: EF {high.share(total):.1f}%, CS1 {low.share(total):.1f}%") + assert low.result["lost_percent"] > 10, \ + f"the low priority flow lost {low.result['lost_percent']:.1f}%, the port was never full" + return high, low, total + + with test.step("Baseline: both flows at one priority, 80 percent each, expect neither starved"): + apply(LOW, LOW) + high, low, total = measure(0.8) + assert low.share(total) >= 10, f"CS1 got {low.share(total):.1f}% with no priority in play" + + with test.step("Split: EF strict above CS1, 80 percent each, expect EF without loss"): + apply(HIGH, LOW) + high, low, total = measure(0.8) + # EF offers less than the port drains, so it passes intact; a + # stray datagram at the iperf3 startup edge is not congestion + assert high.result["lost_percent"] < 1, \ + f"EF lost {high.result['lost']} of {high.result['offered']} datagrams" + + with test.step("Starve: EF at 120 percent of the port, expect CS1 below 5 percent"): + high, low, total = measure(1.2) + assert low.share(total) < 5, f"CS1 got {low.share(total):.1f}% of the port" + + test.succeed() diff --git a/test/case/interfaces/qos_strict_priority/topology.dot b/test/case/interfaces/qos_strict_priority/topology.dot new file mode 100644 index 000000000..f288f2f95 --- /dev/null +++ b/test/case/interfaces/qos_strict_priority/topology.dot @@ -0,0 +1,24 @@ +graph "1x3" { + layout="neato"; + overlap="false"; + esep="+80"; + + node [shape=record, fontname="DejaVu Sans Mono, Book"]; + edge [color="cornflowerblue", penwidth="2", fontname="DejaVu Serif, Book"]; + + host [ + label="host | { mgmt | data1 | data2 }", + pos="0,12!", + requires="controller", + ]; + + target [ + label="{ mgmt | data1 | data2 } | target", + pos="10,12!", + requires="infix", + ]; + + host:mgmt -- target:mgmt [requires="mgmt", color="lightgray"] + host:data1 -- target:data1 [color="black"] + host:data2 -- target:data2 [color="black"] +} diff --git a/test/case/interfaces/qos_strict_priority/topology.svg b/test/case/interfaces/qos_strict_priority/topology.svg new file mode 100644 index 000000000..9517e6397 --- /dev/null +++ b/test/case/interfaces/qos_strict_priority/topology.svg @@ -0,0 +1,51 @@ + + + + + + +1x3 + + + +host + +host + +mgmt + +data1 + +data2 + + + +target + +mgmt + +data1 + +data2 + +target + + + +host:mgmt--target:mgmt + + + + +host:data1--target:data1 + + + + +host:data2--target:data2 + + + + diff --git a/test/case/interfaces/qos_traffic_class/Readme.adoc b/test/case/interfaces/qos_traffic_class/Readme.adoc new file mode 120000 index 000000000..ae32c8412 --- /dev/null +++ b/test/case/interfaces/qos_traffic_class/Readme.adoc @@ -0,0 +1 @@ +test.adoc \ No newline at end of file diff --git a/test/case/interfaces/qos_traffic_class/test.adoc b/test/case/interfaces/qos_traffic_class/test.adoc new file mode 100644 index 000000000..0befc650b --- /dev/null +++ b/test/case/interfaces/qos_traffic_class/test.adoc @@ -0,0 +1,38 @@ +=== QoS Traffic Classes and Transmission Selection + +ifdef::topdoc[:imagesdir: {topdoc}../../test/case/interfaces/qos_traffic_class] + +==== Description + +Configure a custom priority to traffic class map on a port with the two +lowest classes sharing 67:33 and the rest strict priority, and verify the +root qdisc reflects it: an ets qdisc with the strict bands first, the +shares as quanta of one frame per percent, and the priomap inverted as +802.1Q numbering requires. A driver that offloads it marks the qdisc +offloaded. + +The port's class count comes from its transmit queues, or eight for a +single-queue port. Switching the table to the ieee-sr preset must put +the SR classes, priorities 2 and 3, on the two highest classes. A rate +limit must put a tbf root qdisc above the scheduler, with the same ets +table beneath it, and go away with the setting. Removing the qos +container must restore the default table, IEEE 802.1Q-2022 Table 8-5, +and the operational datastore must report whether transmission +selection is offloaded throughout. + +==== Topology + +image::topology.svg[QoS Traffic Classes and Transmission Selection topology, align=center, scaledwidth=75%] + +==== Sequence + +. Set up topology and attach to target DUT +. Configure a custom map, the two lowest classes sharing 67:33 +. Verify the root qdisc carries the custom map and shares +. Verify the offload capability matches the qdisc +. Switch to the ieee-sr preset and verify SR classes on top +. Add a 10 Mbit/s rate limit and verify the tbf root above the ets table +. Remove the rate limit and verify the scheduler is the root again +. Remove qos configuration and verify the default table is back + + diff --git a/test/case/interfaces/qos_traffic_class/test.py b/test/case/interfaces/qos_traffic_class/test.py new file mode 100755 index 000000000..8622297f0 --- /dev/null +++ b/test/case/interfaces/qos_traffic_class/test.py @@ -0,0 +1,172 @@ +#!/usr/bin/env python3 +""" +QoS Traffic Classes and Transmission Selection + +Configure a custom priority to traffic class map on a port with the two +lowest classes sharing 67:33 and the rest strict priority, and verify the +root qdisc reflects it: an ets qdisc with the strict bands first, the +shares as quanta of one frame per percent, and the priomap inverted as +802.1Q numbering requires. A driver that offloads it marks the qdisc +offloaded. + +The port's class count comes from its transmit queues, or eight for a +single-queue port. Switching the table to the ieee-sr preset must put +the SR classes, priorities 2 and 3, on the two highest classes. A rate +limit must put a tbf root qdisc above the scheduler, with the same ets +table beneath it, and go away with the setting. Removing the qos +container must restore the default table, IEEE 802.1Q-2022 Table 8-5, +and the operational datastore must report whether transmission +selection is offloaded throughout. +""" +import json +import infamy +from infamy.util import until + +# IEEE 802.1Q-2022 Table 8-5 and Table 34-1, indexed by class count +TABLE_8_5 = { + 2: [0, 0, 0, 0, 1, 1, 1, 1], + 3: [0, 0, 0, 0, 1, 1, 2, 2], + 4: [0, 0, 1, 1, 2, 2, 3, 3], + 5: [0, 0, 1, 1, 2, 2, 3, 4], + 6: [1, 0, 2, 2, 3, 3, 4, 5], + 7: [1, 0, 2, 3, 4, 4, 5, 6], + 8: [1, 0, 2, 3, 4, 5, 6, 7], +} +TABLE_34_1 = { + 2: [0, 0, 1, 1, 0, 0, 0, 0], + 3: [0, 0, 1, 2, 0, 0, 0, 0], + 4: [0, 0, 2, 3, 1, 1, 1, 1], + 5: [0, 0, 3, 4, 1, 1, 2, 2], + 6: [0, 0, 4, 5, 1, 1, 2, 3], + 7: [0, 0, 5, 6, 1, 2, 3, 4], + 8: [1, 0, 6, 7, 2, 3, 4, 5], +} + + +def qos_xpath(port, path=""): + return f"/ietf-interfaces:interfaces/interface[name='{port}']/infix-interfaces:qos{path}" + + +def capabilities(target, port): + data = target.get_data(qos_xpath(port, "/capabilities")) + for iface in data["interfaces"]["interface"]: + qos = iface.get("qos") or iface.get("infix-interfaces:qos") or {} + return qos.get("capabilities", {}) + return {} + + +def root_qdisc(ssh, port): + """Return the root qdisc of port as a dict, or None""" + out = ssh.runsh(f"tc -j qdisc show dev {port}").stdout + for qdisc in json.loads(out or "[]"): + if qdisc.get("root"): + return qdisc + return None + + +def qdisc_matches(qdisc, num_tc, prio_map, strict, quanta): + """Check a root qdisc against the expected 802.1Q table""" + if not qdisc: + return False + opts = qdisc.get("options", {}) + if qdisc["kind"] == "ets": + return (opts.get("bands") == num_tc and opts.get("strict") == strict + and opts.get("quanta", []) == quanta + and opts.get("priomap", [])[:8] == [num_tc - 1 - tc for tc in prio_map]) + return False + + +with infamy.Test() as test: + with test.step("Set up topology and attach to target DUT"): + env = infamy.Env() + target = env.attach("target", "mgmt") + tgtssh = env.attach("target", "mgmt", "ssh") + _, port = env.ltop.xlate("target", "data") + + num_tc = capabilities(target, port).get("max-traffic-classes", 8) + print(f"{port}: {num_tc} traffic classes") + assert num_tc and 2 <= num_tc <= 8, f"max-traffic-classes {num_tc}" + offloaded = "transmission-selection" in capabilities(target, port).get("offload", []) + + with test.step("Configure a custom map, the two lowest classes sharing 67:33"): + # Table 8-5 with the two lowest classes swapped, so the map is + # visibly custom on any class count + custom = [1 if tc == 0 else 0 if tc == 1 else tc for tc in TABLE_8_5[num_tc]] + table = {f"priority{prio}": tc for prio, tc in enumerate(custom)} + + target.put_config_dicts({"ietf-interfaces": { + "interfaces": { + "interface": [{ + "name": port, + "enabled": True, + "infix-interfaces:qos": { + "egress": { + "traffic-class-table": table, + "traffic-class": [ + {"id": 1, "algorithm": "ieee802-dot1q-types:enhanced-transmission-selection", + "bandwidth": 67}, + {"id": 0, "algorithm": "ieee802-dot1q-types:enhanced-transmission-selection", + "bandwidth": 33}, + ] + } + } + }] + } + }}) + + with test.step("Verify the root qdisc carries the custom map and shares"): + until(lambda: qdisc_matches(root_qdisc(tgtssh, port), num_tc, custom, num_tc - 2, + [67 * 1514, 33 * 1514])) + print(json.dumps(root_qdisc(tgtssh, port))) + + with test.step("Verify the offload capability matches the qdisc"): + taken = root_qdisc(tgtssh, port).get("offloaded", False) + until(lambda: ("transmission-selection" in capabilities(target, port).get("offload", [])) + == taken) + + with test.step("Switch to the ieee-sr preset and verify SR classes on top"): + target.delete_xpath(qos_xpath(port, "/egress")) + target.put_config_dicts({"ietf-interfaces": { + "interfaces": { + "interface": [{ + "name": port, + "infix-interfaces:qos": { + "egress": {"traffic-class-table": {"preset": "ieee-sr"}} + } + }] + } + }}) + until(lambda: qdisc_matches(root_qdisc(tgtssh, port), num_tc, TABLE_34_1[num_tc], num_tc, [])) + + with test.step("Add a 10 Mbit/s rate limit and verify the tbf root above the ets table"): + target.put_config_dicts({"ietf-interfaces": { + "interfaces": { + "interface": [{ + "name": port, + "infix-interfaces:qos": { + "egress": {"rate-limit": {"rate": 10000000}} + } + }] + } + }}) + + def rate_limited(): + out = tgtssh.runsh(f"tc -j qdisc show dev {port}").stdout + qdiscs = json.loads(out or "[]") + root = [q for q in qdiscs if q.get("root")] + child = [q for q in qdiscs if q.get("parent") == "1:1"] + return (root and root[0]["kind"] == "tbf" and root[0]["options"]["rate"] == 1250000 + and child and qdisc_matches(child[0], num_tc, TABLE_34_1[num_tc], num_tc, [])) + until(rate_limited) + print(tgtssh.runsh(f"tc -j qdisc show dev {port}").stdout) + + with test.step("Remove the rate limit and verify the scheduler is the root again"): + target.delete_xpath(qos_xpath(port, "/egress/rate-limit")) + until(lambda: qdisc_matches(root_qdisc(tgtssh, port), num_tc, TABLE_34_1[num_tc], num_tc, [])) + + with test.step("Remove qos configuration and verify the default table is back"): + target.delete_xpath(qos_xpath(port)) + until(lambda: qdisc_matches(root_qdisc(tgtssh, port), num_tc, TABLE_8_5[num_tc], num_tc, [])) + assert ("transmission-selection" in capabilities(target, port).get("offload", [])) == offloaded + + test.succeed() diff --git a/test/case/interfaces/qos_traffic_class/topology.dot b/test/case/interfaces/qos_traffic_class/topology.dot new file mode 120000 index 000000000..4f53d15af --- /dev/null +++ b/test/case/interfaces/qos_traffic_class/topology.dot @@ -0,0 +1 @@ +../../../infamy/topologies/1x2.dot \ No newline at end of file diff --git a/test/case/interfaces/qos_traffic_class/topology.svg b/test/case/interfaces/qos_traffic_class/topology.svg new file mode 100644 index 000000000..ff3d246be --- /dev/null +++ b/test/case/interfaces/qos_traffic_class/topology.svg @@ -0,0 +1,42 @@ + + + + + + +1x2 + + + +host + +host + +mgmt + +data + + + +target + +mgmt + +data + +target + + + +host:mgmt--target:mgmt + + + + +host:data--target:data + + + + diff --git a/test/case/interfaces/qos_validation/Readme.adoc b/test/case/interfaces/qos_validation/Readme.adoc new file mode 120000 index 000000000..ae32c8412 --- /dev/null +++ b/test/case/interfaces/qos_validation/Readme.adoc @@ -0,0 +1 @@ +test.adoc \ No newline at end of file diff --git a/test/case/interfaces/qos_validation/test.adoc b/test/case/interfaces/qos_validation/test.adoc new file mode 100644 index 000000000..0c44e9f1c --- /dev/null +++ b/test/case/interfaces/qos_validation/test.adoc @@ -0,0 +1,34 @@ +=== QoS Configuration Validation + +ifdef::topdoc[:imagesdir: {topdoc}../../test/case/interfaces/qos_validation] + +==== Description + +Verify that traffic class layouts the device cannot render are rejected +at commit time, with the running configuration left untouched: + + - a strict-priority class below a weighted class + - weighted shares that do not sum to 100 percent + - a transmission selection algorithm outside strict-priority and + enhanced-transmission-selection + - a traffic class beyond the port's class count + - a priority mapped to a class beyond the port's class count + +The last two apply on ports with fewer than eight classes. A valid +layout must still be accepted afterwards. + +==== Topology + +image::topology.svg[QoS Configuration Validation topology, align=center, scaledwidth=75%] + +==== Sequence + +. Set up topology and attach to target DUT +. Reject strict-priority class below a weighted class +. Reject weighted shares that do not sum to 100 +. Reject credit-based-shaper algorithm +. Reject traffic class beyond the port's class count +. Reject priority mapped beyond the port's class count +. Accept a valid layout with strict classes above weighted + + diff --git a/test/case/interfaces/qos_validation/test.py b/test/case/interfaces/qos_validation/test.py new file mode 100755 index 000000000..92a06ac19 --- /dev/null +++ b/test/case/interfaces/qos_validation/test.py @@ -0,0 +1,115 @@ +#!/usr/bin/env python3 +""" +QoS Configuration Validation + +Verify that traffic class layouts the device cannot render are rejected +at commit time, with the running configuration left untouched: + + - a strict-priority class below a weighted class + - weighted shares that do not sum to 100 percent + - a transmission selection algorithm outside strict-priority and + enhanced-transmission-selection + - a traffic class beyond the port's class count + - a priority mapped to a class beyond the port's class count + +The last two apply on ports with fewer than eight classes. A valid +layout must still be accepted afterwards. +""" +import infamy + +STRICT = "ieee802-dot1q-types:strict-priority" +ETS = "ieee802-dot1q-types:enhanced-transmission-selection" +CBS = "ieee802-dot1q-types:credit-based-shaper" + + +def num_classes(target, port): + data = target.get_data(f"/ietf-interfaces:interfaces/interface[name='{port}']" + "/infix-interfaces:qos/capabilities") + for iface in data["interfaces"]["interface"]: + qos = iface.get("qos") or iface.get("infix-interfaces:qos") or {} + return qos.get("capabilities", {}).get("max-traffic-classes", 8) + return None + + +def egress_config(port, egress): + return {"ietf-interfaces": { + "interfaces": { + "interface": [{ + "name": port, + "enabled": True, + "infix-interfaces:qos": {"egress": egress} + }] + } + }} + + +def must_reject(target, port, egress, what, *reasons): + """The commit must fail, and the error must name one of the reasons""" + try: + target.put_config_dicts(egress_config(port, egress)) + except Exception as err: + text = getattr(getattr(err, "response", None), "text", None) or str(err) + matched = [reason for reason in reasons if reason in text] + assert matched, f"{what} rejected for another reason:\n{text}" + print(f"Rejected as expected: {matched[0]}") + return + raise AssertionError(f"{what} was accepted") + + +with infamy.Test() as test: + with test.step("Set up topology and attach to target DUT"): + env = infamy.Env() + target = env.attach("target", "mgmt") + _, port = env.ltop.xlate("target", "data") + num_tc = num_classes(target, port) + print(f"{port}: {num_tc} traffic classes") + assert num_tc and num_tc >= 2, f"max-traffic-classes {num_tc}" + top = num_tc - 1 + + with test.step("Reject strict-priority class below a weighted class"): + must_reject(target, port, { + "traffic-class": [ + {"id": top, "algorithm": ETS, "bandwidth": 100}, + {"id": top - 1, "algorithm": STRICT}, + ] + }, "strict class below weighted class", + "strict-priority classes must be the highest-numbered") + + with test.step("Reject weighted shares that do not sum to 100"): + # A must in the model, not the daemon: NETCONF relays the model's + # message, the RESTCONF error body only says validation failed + must_reject(target, port, { + "traffic-class": [ + {"id": 1, "algorithm": ETS, "bandwidth": 60}, + {"id": 0, "algorithm": ETS, "bandwidth": 30}, + ] + }, "shares summing to 90", "must sum to 100", "Validation failed") + + with test.step("Reject credit-based-shaper algorithm"): + must_reject(target, port, { + "traffic-class": [{"id": top, "algorithm": CBS}] + }, "credit-based-shaper", "not supported, use strict-priority") + + if num_tc < 8: + with test.step("Reject traffic class beyond the port's class count"): + must_reject(target, port, { + "traffic-class": [{"id": num_tc}] + }, f"traffic class {num_tc} on a {num_tc} class port", + f"traffic class {num_tc}, port has {num_tc} classes") + + with test.step("Reject priority mapped beyond the port's class count"): + must_reject(target, port, { + "traffic-class-table": {"priority7": num_tc}, + }, f"priority7 mapped to class {num_tc} on a {num_tc} class port", + f"priority7 maps to traffic class {num_tc}, port has {num_tc} classes") + + with test.step("Accept a valid layout with strict classes above weighted"): + target.put_config_dicts(egress_config(port, { + "traffic-class": [ + {"id": top, "algorithm": STRICT}, + {"id": 1, "algorithm": ETS, "bandwidth": 67}, + {"id": 0, "algorithm": ETS, "bandwidth": 33}, + ] + })) + + test.succeed() diff --git a/test/case/interfaces/qos_validation/topology.dot b/test/case/interfaces/qos_validation/topology.dot new file mode 120000 index 000000000..4f53d15af --- /dev/null +++ b/test/case/interfaces/qos_validation/topology.dot @@ -0,0 +1 @@ +../../../infamy/topologies/1x2.dot \ No newline at end of file diff --git a/test/case/interfaces/qos_validation/topology.svg b/test/case/interfaces/qos_validation/topology.svg new file mode 100644 index 000000000..ff3d246be --- /dev/null +++ b/test/case/interfaces/qos_validation/topology.svg @@ -0,0 +1,42 @@ + + + + + + +1x2 + + + +host + +host + +mgmt + +data + + + +target + +mgmt + +data + +target + + + +host:mgmt--target:mgmt + + + + +host:data--target:data + + + + diff --git a/test/case/interfaces/qos_weighted_sharing/Readme.adoc b/test/case/interfaces/qos_weighted_sharing/Readme.adoc new file mode 120000 index 000000000..ae32c8412 --- /dev/null +++ b/test/case/interfaces/qos_weighted_sharing/Readme.adoc @@ -0,0 +1 @@ +test.adoc \ No newline at end of file diff --git a/test/case/interfaces/qos_weighted_sharing/test.adoc b/test/case/interfaces/qos_weighted_sharing/test.adoc new file mode 100644 index 000000000..1b0868609 --- /dev/null +++ b/test/case/interfaces/qos_weighted_sharing/test.adoc @@ -0,0 +1,46 @@ +=== QoS Weighted Sharing + +ifdef::topdoc[:imagesdir: {topdoc}../../test/case/interfaces/qos_weighted_sharing] + +==== Description + +Two talkers, one marked EF and one marked CS1, bridged through the DUT +and out one egress port that cannot carry both. The port is negotiated +down to 100 Mbit/s where its PHY allows, or rate limited to 10 Mbit/s +where it has no PHY. The ingress port classifies EF to priority 3 and +CS1 to priority 2, classes 3 and 2 in the default table, both running +enhanced transmission selection; the two classes below hold five +percent each and carry nothing, since strict classes must sit above the +weighted ones. Each talker offers 80 percent of what the port drains, +so both classes always have a backlog and the scheduler decides the +split. + +Two measurements: + + - equal weights, 45 and 45 percent, must give an even split. This is + the baseline, and fails on its own if the shares are applied to the + wrong bands + - 2:1, configured as 60 and 30 percent, must move the split to match + +Each flow's share of the delivered datagrams must land within five +points of its configured share of the two, and both flows must lose more +than a tenth of their frames in both measurements; a port that was never +full proves nothing. The test's own iperf3 control traffic, DSCP 0, is +classified to priority 7. Skipped on a switch fabric whose scheduler is +not offloaded, where forwarded frames never meet the configured +algorithm. + +==== Topology + +image::topology.svg[QoS Weighted Sharing topology, align=center, scaledwidth=75%] + +==== Sequence + +. Set up topology and attach to target DUT +. Bridge the two ports, classify by DSCP on ingress, EF and CS1 in two weighted classes +. Slow the egress port so its queues can fill +. Set up the talker and listener namespaces +. Equal weights, both flows at 80 percent, expect an even split +. Weights 2:1, both flows at 80 percent, expect a 67:33 split + + diff --git a/test/case/interfaces/qos_weighted_sharing/test.py b/test/case/interfaces/qos_weighted_sharing/test.py new file mode 100755 index 000000000..d3e7e3aa9 --- /dev/null +++ b/test/case/interfaces/qos_weighted_sharing/test.py @@ -0,0 +1,148 @@ +#!/usr/bin/env python3 +""" +QoS Weighted Sharing + +Two talkers, one marked EF and one marked CS1, bridged through the DUT +and out one egress port that cannot carry both. The port is negotiated +down to 100 Mbit/s where its PHY allows, or rate limited to 10 Mbit/s +where it has no PHY. The ingress port classifies EF to priority 3 and +CS1 to priority 2, classes 3 and 2 in the default table, both running +enhanced transmission selection; the two classes below hold five +percent each and carry nothing, since strict classes must sit above the +weighted ones. Each talker offers 80 percent of what the port drains, +so both classes always have a backlog and the scheduler decides the +split. + +Two measurements: + + - equal weights, 45 and 45 percent, must give an even split. This is + the baseline, and fails on its own if the shares are applied to the + wrong bands + - 2:1, configured as 60 and 30 percent, must move the split to match + +Each flow's share of the delivered datagrams must land within five +points of its configured share of the two, and both flows must lose more +than a tenth of their frames in both measurements; a port that was never +full proves nothing. The test's own iperf3 control traffic, DSCP 0, is +classified to priority 7. Skipped on a switch fabric whose scheduler is +not offloaded, where forwarded frames never meet the configured +algorithm. +""" +import infamy +import infamy.qos as qos +from infamy.util import until + +EF, CS1 = 46 << 2, 8 << 2 # TOS bytes of the two markings +HIGH, LOW = 3, 2 # priorities, and classes, of the two markings +FILLER = 5 # share of each class below the two, carrying nothing + + +def layout(high, low): + """The weighted classes: the two under test and the fillers below""" + shares = {HIGH: high, LOW: low} + shares.update({tc: FILLER for tc in range(LOW)}) + return qos.traffic_classes(shares) + + +with infamy.Test() as test: + with test.step("Set up topology and attach to target DUT"): + env = infamy.Env() + target = env.attach("target", "mgmt") + tgtssh = env.attach("target", "mgmt", "ssh") + _, td0 = env.ltop.xlate("target", "data1") + _, td1 = env.ltop.xlate("target", "data2") + _, hd0 = env.ltop.xlate("host", "data1") + _, hd1 = env.ltop.xlate("host", "data2") + + num_tc = qos.num_classes(target, td1) + print(f"{td1}: {num_tc} traffic classes") + if num_tc < 3: + print("no class left for the control traffic above two weighted ones, skipping") + test.skip() + + uevent = tgtssh.runsh(f"cat /sys/class/net/{td1}/uevent").stdout + dsa = "DEVTYPE=dsa" in uevent.split() + if dsa and "transmission-selection" not in qos.offload(target, td1): + print("switch fabric forwards past a scheduler its driver does not offload, skipping") + test.skip() + + with test.step("Bridge the two ports, classify by DSCP on ingress, EF and CS1 in two weighted classes"): + target.put_config_dicts({"ietf-interfaces": { + "interfaces": { + "interface": [ + {"name": "br0", "type": "infix-if-type:bridge", "enabled": True}, + { + "name": td0, + "enabled": True, + "infix-interfaces:bridge-port": {"bridge": "br0"}, + "infix-interfaces:qos": { + "ingress": {"trust": "dscp", + "dscp-map": qos.dscp_map(**{"0": 7, "46": HIGH, "8": LOW})} + } + }, + { + "name": td1, + "enabled": True, + "infix-interfaces:bridge-port": {"bridge": "br0"}, + "infix-interfaces:qos": { + "egress": {"traffic-class": layout(45, 45)} + } + }, + ] + } + }}) + + with test.step("Slow the egress port so its queues can fill"): + drain = qos.slow_port(target, tgtssh, td1, until) + if not drain: + print(f"{td1} can neither negotiate down nor be rate limited, skipping") + test.skip() + print(f"{td1} drains {drain // 1_000_000} Mbit/s") + + with infamy.IsolatedMacVlan(hd0) as ns0, \ + infamy.IsolatedMacVlan(hd1) as ns1: + + with test.step("Set up the talker and listener namespaces"): + ns0.runsh("set -ex; ip link set iface up; ip addr add 192.168.20.1/24 dev iface") + ns1.runsh("set -ex; ip link set iface up; ip addr add 192.168.20.2/24 dev iface") + ns0.must_reach("192.168.20.2") + + def apply(high, low): + target.put_config_dicts({"ietf-interfaces": { + "interfaces": { + "interface": [{ + "name": td1, + "infix-interfaces:qos": {"egress": {"traffic-class": layout(high, low)}} + }] + } + }}) + quanta = [share * qos.ETS_QUANTUM_UNIT + for share in [high, low] + [FILLER] * LOW] + until(lambda: qos.scheduler_matches(qos.scheduler(tgtssh, td1), num_tc, + qos.TABLE_8_5[num_tc], + strict=num_tc - 2 - LOW, quanta=quanta)) + qos.show_offload(target, tgtssh, td1, dsa) + ns0.must_reach("192.168.20.2") + + def measure(high, low): + apply(high, low) + flows = [qos.Flow("EF", 5201, EF, int(drain * 0.8)), + qos.Flow("CS1", 5202, CS1, int(drain * 0.8))] + total = qos.run_flows(ns0, ns1, "192.168.20.2", flows) + ef, cs1 = flows + expect = 100.0 * high / (high + low) + print(f"shares: EF {ef.share(total):.1f}%, CS1 {cs1.share(total):.1f}%, " + f"configured {high}:{low}") + for flow in flows: + assert flow.result["lost_percent"] > 10, \ + f"{flow.name} lost {flow.result['lost_percent']:.1f}%, the port was never full" + assert abs(ef.share(total) - expect) < 5, \ + f"EF got {ef.share(total):.1f}% of the port, configured {expect:.0f}%" + + with test.step("Equal weights, both flows at 80 percent, expect an even split"): + measure(45, 45) + + with test.step("Weights 2:1, both flows at 80 percent, expect a 67:33 split"): + measure(60, 30) + + test.succeed() diff --git a/test/case/interfaces/qos_weighted_sharing/topology.dot b/test/case/interfaces/qos_weighted_sharing/topology.dot new file mode 100644 index 000000000..f288f2f95 --- /dev/null +++ b/test/case/interfaces/qos_weighted_sharing/topology.dot @@ -0,0 +1,24 @@ +graph "1x3" { + layout="neato"; + overlap="false"; + esep="+80"; + + node [shape=record, fontname="DejaVu Sans Mono, Book"]; + edge [color="cornflowerblue", penwidth="2", fontname="DejaVu Serif, Book"]; + + host [ + label="host | { mgmt | data1 | data2 }", + pos="0,12!", + requires="controller", + ]; + + target [ + label="{ mgmt | data1 | data2 } | target", + pos="10,12!", + requires="infix", + ]; + + host:mgmt -- target:mgmt [requires="mgmt", color="lightgray"] + host:data1 -- target:data1 [color="black"] + host:data2 -- target:data2 [color="black"] +} diff --git a/test/case/interfaces/qos_weighted_sharing/topology.svg b/test/case/interfaces/qos_weighted_sharing/topology.svg new file mode 100644 index 000000000..9517e6397 --- /dev/null +++ b/test/case/interfaces/qos_weighted_sharing/topology.svg @@ -0,0 +1,51 @@ + + + + + + +1x3 + + + +host + +host + +mgmt + +data1 + +data2 + + + +target + +mgmt + +data1 + +data2 + +target + + + +host:mgmt--target:mgmt + + + + +host:data1--target:data1 + + + + +host:data2--target:data2 + + + + diff --git a/test/docker/Dockerfile b/test/docker/Dockerfile index 1267d7c05..1de3f6459 100644 --- a/test/docker/Dockerfile +++ b/test/docker/Dockerfile @@ -2,8 +2,11 @@ FROM alpine:3.18.0 # NOTE: please add packages alphabetically! RUN apk add --no-cache \ + bash \ + bison \ busybox-extras \ chrony \ + coreutils \ curl \ dhcp-server-vanilla \ dnsmasq \ @@ -12,21 +15,29 @@ RUN apk add --no-cache \ ethtool \ fakeroot \ file \ + flex \ font-dejavu \ gcc \ git \ graphviz \ + iperf3 \ iproute2 \ iputils \ jq \ libc-dev \ + libnet-dev \ + libnetfilter_conntrack-dev \ + libnl3-dev \ + libpcap-dev \ libyang-dev \ linux-headers \ make \ + ncurses-dev \ nmap \ openssh-client \ openssl \ pandoc-cli \ + pkgconf \ python3-dev \ qemu-img \ qemu-system-x86_64 \ @@ -37,13 +48,29 @@ RUN apk add --no-cache \ sshpass \ tcpdump \ tshark \ - weasyprint + userspace-rcu-dev \ + weasyprint \ + zlib-dev ARG MTOOL_VERSION="3.0" RUN wget https://github.com/troglobit/mtools/releases/download/v3.0/mtools-$MTOOL_VERSION.tar.gz -O /tmp/mtools-$MTOOL_VERSION.tar.gz RUN cd /tmp/ && tar zxvf mtools-$MTOOL_VERSION.tar.gz RUN cd /tmp/mtools-$MTOOL_VERSION && make && make install +# trafgen and mausezahn, traffic generators for the QoS tests. Alpine +# has no netsniff-ng package, and mausezahn needs libcli, which is only +# in edge/testing, so both are built from their release tarballs. +ARG LIBCLI_VERSION="1.10.7" +RUN wget https://github.com/dparrish/libcli/archive/refs/tags/V$LIBCLI_VERSION.tar.gz -O /tmp/libcli-$LIBCLI_VERSION.tar.gz +RUN cd /tmp/ && tar zxf libcli-$LIBCLI_VERSION.tar.gz +RUN cd /tmp/libcli-$LIBCLI_VERSION && make && make install PREFIX=/usr + +ARG NETSNIFF_NG_VERSION="0.6.9" +RUN wget https://github.com/netsniff-ng/netsniff-ng/archive/refs/tags/v$NETSNIFF_NG_VERSION.tar.gz -O /tmp/netsniff-ng-$NETSNIFF_NG_VERSION.tar.gz +RUN cd /tmp/ && tar zxf netsniff-ng-$NETSNIFF_NG_VERSION.tar.gz +RUN cd /tmp/netsniff-ng-$NETSNIFF_NG_VERSION && ./configure --disable-geoip && \ + make trafgen mausezahn && make trafgen_install mausezahn_install + # Alpine's QEMU package does not bundle this for some reason, copied # from Ubuntu COPY docker/qemu-ifup /etc diff --git a/test/docker/pip-requirements.txt b/test/docker/pip-requirements.txt index 06bb9ebaa..634d921ae 100644 --- a/test/docker/pip-requirements.txt +++ b/test/docker/pip-requirements.txt @@ -7,6 +7,10 @@ pydot==1.4.2 pyyaml==6.0.1 passlib==1.7.4 requests~=2.32.4 +# urllib3 2.8.0 normalises the zone id of an IPv6 link-local URL host as a +# percent-escape, fe80::1%d2a becomes fe80::1%D2a, and the request fails +# with a name resolution error. Stay on 2.7 until it is fixed upstream. +urllib3~=2.7.0 pynmea2==1.19.0 # GHSA-cq46-m9x9-j8w2: scapy <=2.6.1 has pickle deserialization vuln in session # loading (-s flag). Low risk: test framework only uses packet crafting (Ether, diff --git a/test/infamy/qos.py b/test/infamy/qos.py new file mode 100644 index 000000000..aedce2f1c --- /dev/null +++ b/test/infamy/qos.py @@ -0,0 +1,387 @@ +""" +QoS helpers: capabilities, the rendered scheduler, per-class counters, +and traffic marked the way the tests need it. + +Traffic class numbering follows IEEE 802.1Q: class 0 is the lowest. The +tc ets qdisc numbers its bands the other way, so everything here talks +in traffic classes and hides that. +""" +import json +import re +import subprocess +import time + +from infamy.util import until + +# IEEE 802.1Q-2022 Table 8-5 and Table 34-1, priority to traffic class, +# indexed by the number of classes +TABLE_8_5 = { + 2: [0, 0, 0, 0, 1, 1, 1, 1], + 3: [0, 0, 0, 0, 1, 1, 2, 2], + 4: [0, 0, 1, 1, 2, 2, 3, 3], + 5: [0, 0, 1, 1, 2, 2, 3, 4], + 6: [1, 0, 2, 2, 3, 3, 4, 5], + 7: [1, 0, 2, 3, 4, 4, 5, 6], + 8: [1, 0, 2, 3, 4, 5, 6, 7], +} +TABLE_34_1 = { + 2: [0, 0, 1, 1, 0, 0, 0, 0], + 3: [0, 0, 1, 2, 0, 0, 0, 0], + 4: [0, 0, 2, 3, 1, 1, 1, 1], + 5: [0, 0, 3, 4, 1, 1, 2, 2], + 6: [0, 0, 4, 5, 1, 1, 2, 3], + 7: [0, 0, 5, 6, 1, 2, 3, 4], + 8: [1, 0, 6, 7, 2, 3, 4, 5], +} + +STRICT = "ieee802-dot1q-types:strict-priority" +ETS = "ieee802-dot1q-types:enhanced-transmission-selection" + +ETS_QUANTUM_UNIT = 1514 # one frame per percent of bandwidth, as rendered + + +def xpath(port, path=""): + return f"/ietf-interfaces:interfaces/interface[name='{port}']/infix-interfaces:qos{path}" + + +def capabilities(target, port): + """The port's qos/capabilities container from the operational datastore""" + data = target.get_data(xpath(port, "/capabilities")) + for iface in data["interfaces"]["interface"]: + qos = iface.get("qos") or iface.get("infix-interfaces:qos") or {} + return qos.get("capabilities", {}) + return {} + + +def num_classes(target, port): + return capabilities(target, port).get("max-traffic-classes", 8) + + +def offload(target, port): + return capabilities(target, port).get("offload", []) + + +def dscp_map(**prio): + """A custom DSCP map: dscp_map(**{"0": 7, "46": 5}) marks DSCP 0 as + priority 7 and DSCP 46 as priority 5. Unlisted codepoints fall to + the port's default priority. + + Classification at the ingress port is what decides the queue all the + way to the listener, on one chip as across a cascade or a network of + switches, so the tests steer their flows here rather than with a + class table on the egress port. + """ + return {"entry": [{"dscp": int(dscp), "priority": p} for dscp, p in prio.items()]} + + +def traffic_classes(weighted, strict=()): + """The traffic-class list for a layout: weighted is {class: percent} + + The shares are TCBandwidth percentages and must sum to 100, so 2:1 + is {1: 67, 0: 33}. + """ + classes = [{"id": tc, "algorithm": STRICT} for tc in strict] + for tc, share in weighted.items(): + classes.append({"id": tc, "algorithm": ETS, "bandwidth": share}) + return classes + + +def qdiscs(ssh, port): + out = ssh.runsh(f"tc -j qdisc show dev {port}").stdout + return json.loads(out or "[]") + + +def root_qdisc(ssh, port): + for qdisc in qdiscs(ssh, port): + if qdisc.get("root"): + return qdisc + return None + + +def scheduler(ssh, port): + """The ets qdisc: the root, or the child of a tbf root""" + root = root_qdisc(ssh, port) + if root and root["kind"] == "tbf": + for qdisc in qdiscs(ssh, port): + if qdisc.get("parent") == "1:1": + return qdisc + return None + return root + + +def scheduler_matches(qdisc, num_tc, prio_map, strict=None, quanta=None): + """Check a scheduler qdisc against a priority to class map and layout + + strict is the number of strict classes and quanta the shares of the + others from the top down; either left as None is not checked. + """ + if not qdisc: + return False + opts = qdisc.get("options", {}) + if qdisc["kind"] == "ets": + if opts.get("bands") != num_tc: + return False + if opts.get("priomap", [])[:8] != [num_tc - 1 - tc for tc in prio_map]: + return False + if strict is not None and opts.get("strict") != strict: + return False + if quanta is not None and opts.get("quanta", []) != list(quanta): + return False + return True + return False + + +def class_stats(ssh, port, num_tc): + """Per traffic class counters of the scheduler: {tc: {packets, bytes, drops}} + + Read from tc -s class show. iproute2 6.14 renders classes as JSON, + older releases ignore -j for classes and print text, so both are + parsed. + """ + out = ssh.runsh(f"tc -s -j class show dev {port}").stdout.strip() + stats = {} + + def tc_of(kind, handle): + minor = int(handle.split(":")[1], 16) + if kind == "ets": + return num_tc - minor # band 0, minor 1, is the top class + return None + + if out.startswith("["): + for cls in json.loads(out): + tc = tc_of(cls.get("class"), cls.get("handle", "0:0")) + if tc is not None: + st = cls.get("stats", cls) # tc 6.14 nests the counters + stats[tc] = {"packets": st.get("packets", 0), + "bytes": st.get("bytes", 0), + "drops": st.get("drops", 0)} + return stats + + current = None + for line in out.splitlines(): + head = re.match(r"class (\S+) (\S+)", line) + if head: + current = tc_of(head.group(1), head.group(2)) + continue + sent = re.match(r"\s*Sent (\d+) bytes (\d+) pkt \(dropped (\d+)", line) + if sent and current is not None: + stats[current] = {"bytes": int(sent.group(1)), "packets": int(sent.group(2)), + "drops": int(sent.group(3))} + return stats + + +def stats_delta(before, after): + return {tc: {k: after[tc][k] - before.get(tc, {}).get(k, 0) for k in after[tc]} + for tc in after} + + +def neighbour_mac(ns, ip): + """MAC of ip from the namespace's neighbour table, None if unresolved""" + out = ns.runsh(f"ip -j neigh show {ip}").stdout + for entry in json.loads(out or "[]"): + if entry.get("lladdr"): + return entry["lladdr"] + return None + + +def mausezahn(ns, iface, src_ip, dst_ip, dst_mac, count=10, delay="5msec", + vid=None, pcp=None, dscp=None, dport=7777): + """Send count UDP datagrams from the namespace with an exact marking + + vid and pcp add a VLAN tag with that PCP; dscp sets the IP DSCP. + mausezahn writes the frame itself, so the source address is given + rather than taken from a VLAN device it knows nothing about. + """ + cmd = ["mausezahn", iface, "-c", str(count), "-d", delay, + "-A", src_ip, "-B", dst_ip, "-b", dst_mac] + if vid is not None: + cmd += ["-Q", f"{pcp or 0}:{vid}"] + params = f"dp={dport}" + if dscp is not None: + params += f",dscp={dscp}" + cmd += ["-t", "udp", params] + return ns.run(cmd, check=True, text=True, capture_output=True) + + +def supported_pmd_types(target, port): + """PMD types the port can negotiate, empty when it has no PHY""" + data = target.get_data(f"/ietf-interfaces:interfaces/interface[name='{port}']") + for iface in data["interfaces"]["interface"]: + eth = iface.get("ieee802-ethernet-interface:ethernet") or iface.get("ethernet") or {} + return eth.get("infix-ethernet-interface:supported-pmd-types", []) + return [] + + +def slow_port(target, ssh, port, until): + """Make the port the bottleneck, return the rate it drains in bit/s + + A port with a PHY that can do 100BASE-TX is negotiated down to it, + so the queues fill against a real link. A port without a PHY, as on + a virtual rig, gets a 10 Mbit/s rate limit instead, which is what + puts its queues under load. Returns 0 when neither is possible. + """ + pmd = "ieee802-ethernet-phy-type:pmd-type-100BASE-TX" + if pmd in supported_pmd_types(target, port): + target.put_config_dicts({"ietf-interfaces": { + "interfaces": { + "interface": [{ + "name": port, + "ethernet": {"auto-negotiation": { + "infix-ethernet-interface:advertised-pmd-types": [pmd]}} + }] + } + }}) + + def linked(): + out = ssh.runsh(f"ip -j link show {port}").stdout + link = json.loads(out or "[]") + return link and "LOWER_UP" in link[0].get("flags", []) and scheduler(ssh, port) + + until(linked, attempts=60) + print(ssh.runsh(f"ethtool {port} | grep -i speed").stdout.strip()) + return 100_000_000 + + if not supported_pmd_types(target, port): + target.put_config_dicts({"ietf-interfaces": { + "interfaces": { + "interface": [{ + "name": port, + "infix-interfaces:qos": {"egress": {"rate-limit": {"rate": 10_000_000}}} + }] + } + }}) + until(lambda: (root_qdisc(ssh, port) or {}).get("kind") == "tbf") + show_shaper(ssh, port) + return 10_000_000 + + return 0 + + +def dscp_name_to_num(name): + """dcb prints DSCP by name when it knows one: CS1, AF21, EF ...""" + if name.startswith("CS"): + return int(name[2:]) * 8 + if name.startswith("AF"): + return int(name[2]) * 8 + int(name[3]) * 2 + if name == "EF": + return 46 + return int(name) + + +def dscp_prio(ssh, port, dscp): + """The priority the port classifies a DSCP to, or None when unmapped + + Read from the DCB table on a port whose driver has one, otherwise + from the flower rules of the software classifier. + """ + out = ssh.runsh(f"dcb app show dev {port} 2>/dev/null").stdout + for line in out.splitlines(): + name, _, rest = line.partition(" ") + if name.rstrip(":") != "dscp-prio": + continue + for token in rest.split(): + key, _, prio = token.partition(":") + if dscp_name_to_num(key) == dscp: + return int(prio) + return None + + out = ssh.runsh(f"tc -j filter show dev {port} ingress").stdout + for flt in json.loads(out or "[]"): + opts = flt.get("options") or {} + tos = str(opts.get("keys", {}).get("ip_tos", "")) + if not tos or int(tos.split("/")[0], 0) >> 2 != dscp: + continue + for act in opts.get("actions", []): + if act.get("kind") == "skbedit" and "priority" in act: + prio = str(act["priority"]) + return 0 if prio == "none" else int(prio.rsplit(":", 1)[-1] or "0", 16) + return None + + +def show_offload(target, ssh, port, dsa): + """Log what the fabric took after a scheduler change: the offload list + and the switch driver's recent messages. On a switch port the + scheduler must be offloaded, or the measurement is meaningless""" + if dsa: + until(lambda: "transmission-selection" in offload(target, port)) + print(f"{port} offload: {offload(target, port)}") + log = ssh.runsh("sudo dmesg | grep -i 'mv88e6xxx\\|dsa' | tail -5").stdout.strip() + if log: + print(log) + + +def show_shaper(ssh, port): + """Log what the rate limit became: the root qdisc with its offloaded + flag, and on a switch port the port registers, where a shaper the + driver took shows up as the egress rate control words""" + print(json.dumps(root_qdisc(ssh, port))) + print(ssh.runsh(f"ethtool {port} | grep -i speed").stdout.strip()) + if "DEVTYPE=dsa" in ssh.runsh(f"cat /sys/class/net/{port}/uevent").stdout.split(): + regs = ssh.runsh(f"sudo ethtool -d {port}").stdout.strip() + print("\n".join(regs.splitlines()[:16])) + + +class Flow: + """One iperf3 UDP flow, talker to listener, told apart by its port""" + def __init__(self, name, port, tos, rate_bps, seconds=4, size=1000): + self.name, self.port, self.tos = name, port, tos + self.rate, self.seconds, self.size = rate_bps, seconds, size + self.result = None + + def throughput(self): + """Payload bit rate that arrived, over the flow's duration""" + return 8.0 * self.result["packets"] * self.size / self.seconds + + def share(self, total): + return 100.0 * self.result["packets"] / total if total else 0.0 + + +def run_flows(talker, listener, dst, flows): + """Run the flows at once, one server and one client process each + + Fills in flow.result from the listener side: bytes and packets + received, lost packets and the loss in percent. Returns the total + datagrams received across the flows. + """ + servers = {} + for f in flows: + servers[f.name] = listener.popen(["iperf3", "-s", "-1", "--json", "-p", str(f.port)], + stdout=subprocess.PIPE, stderr=subprocess.PIPE, + text=True) + time.sleep(1) + + clients = {} + for f in flows: + clients[f.name] = talker.popen(["iperf3", "-c", dst, "-p", str(f.port), "-u", + "-b", str(f.rate), "-t", str(f.seconds), + "-l", str(f.size), "--tos", str(f.tos), "--json"], + stdout=subprocess.PIPE, stderr=subprocess.PIPE, + text=True) + + offered = {} + for f in flows: + out, err = clients[f.name].communicate(timeout=f.seconds + 30) + if clients[f.name].returncode: + print(f"{f.name}: iperf3 client failed: {err.strip() or out.strip()[:200]}") + offered[f.name] = json.loads(out)["end"]["sum"]["packets"] + + total = 0 + for f in flows: + out, err = servers[f.name].communicate(timeout=30) + end = json.loads(out)["end"] + got = end.get("sum_received") or end["sum"] + # iperf3's receiver keeps its packet count as the highest sequence + # number seen, so it hides every loss but a tail drop. The byte + # count is what arrived, and datagrams are one size, so count + # those; offered minus received is the honest loss. + sent = offered[f.name] + received = got["bytes"] // f.size + lost = max(sent - received, 0) + f.result = {"bytes": got["bytes"], "packets": received, + "offered": sent, "lost": lost, + "lost_percent": 100.0 * lost / sent if sent else 0.0} + total += f.result["packets"] + print(f"{f.name}: {f.result['packets']}/{sent} datagrams through, " + f"{lost} lost ({f.result['lost_percent']:.1f}%)") + + return total diff --git a/test/infamy/restconf.py b/test/infamy/restconf.py index ad004a8dc..53fab3179 100644 --- a/test/infamy/restconf.py +++ b/test/infamy/restconf.py @@ -66,6 +66,9 @@ def requests_workaround(method, url, json, headers, auth, verify=False, retry=0) response = requests_workaround(method, url, json, headers, auth, verify, retry) else: + # The RESTCONF error body names the offending node and why + if response.text: + print(f"{method} {url}: HTTP {response.status_code}: {response.text.strip()}") raise e return response