Skip to content

class-of-service: add show class-of-service rewrite-rule (#6848) - #6858

Open
psaab wants to merge 6 commits into
masterfrom
fix/6848-cos-show-rewrite-rule
Open

class-of-service: add show class-of-service rewrite-rule (#6848)#6858
psaab wants to merge 6 commits into
masterfrom
fix/6848-cos-show-rewrite-rule

Conversation

@psaab

@psaab psaab commented Aug 5, 2026

Copy link
Copy Markdown
Owner

Closes #6848. Advances #4228 (Gap 7 residual).

The gap

show class-of-service had interface, classifier, scheduler-map and
forwarding-class. rewrite-rule — the fifth Junos CoS show command — was
never wired.

It matters more now than when #4228 was written. At that point rewrite-rules
held only dscp, so the missing command hid one table. Since then the config
models three more families — ieee-802.1 (#4228 Gap 4), inet-precedence and
exp (#4316) — and all three are accepted-but-inert: they commit clean and
have no runtime effect, because the dataplane rewrites DSCP on egress only.

So an operator could configure four kinds of rewrite rule, three of which do
nothing, with no command to display any of them. The only signal was a
commit-time advisory that scrolls past once.

Enforcement is a column, not a footnote

Rewrite rule: rw-dscp, Code point type: dscp, Enforced: yes
  Forwarding class  Loss priority  Code point
  best-effort       low            000000
  premium           low            101110

Rewrite rule: rw-pcp, Code point type: ieee-802.1, Enforced: no (accepted for
Junos compatibility; the dataplane rewrites dscp only)
  Forwarding class  Loss priority  Code point
  premium           high           101

Rendering an inert rule identically to an enforced one would reproduce, inside
the command built to expose the problem, exactly the silence that made it worth
fixing. The marker names what the operator loses rather than only that a flag is
off.

The four families do not carry equal data

dscp and ieee-802.1 compile to full entry lists. inet-precedence and exp
record only rule names — the compiler builds no runtime structure because
nothing consumes one. Those render Code points not modeled rather than an
empty table, which would imply a fidelity the config does not have.

TestShowTextCoSRewriteRuleNameOnlyFamiliesAreProducible6848 authors such a
rule with a code point in real set syntax and asserts the value does not
surface — so if the compiler ever starts modeling those entries, the test fails
and says to render them, rather than leaving a view that quietly under-reports
real config.

Both surfaces

The local CLI and the gRPC server are separate dispatchers. A command registered
in only one works interactively and silently falls through to help text on the
remote cli binary — the surface most operators use. Both are wired, sharing
one topic encoder rather than two that can drift.

Drive-by fix: completion promised a filter the parser did not implement

Both commands' cmdtree nodes offer rule names directly under the command, so
tab-completing a name and pressing enter submits a bare positional — which
the keyword-only parser ignored, dumping every rule. The shared parser now
accepts it, so classifier and rewrite-rule both do what their completion
advertises.

This deliberately touches the pre-existing classifier command. Leaving it
would mean two sibling commands with identical advertised grammar behaving
differently, which is worse than the gap. No test pinned the old
dump-everything behavior.

Drift guard

format.CoSRewriteRuleTypes is the SSOT for the type filter; the cmdtree
completion node is pinned against it. Without that, completion could offer a
value the renderer ignores (operator filters, gets everything) or a family could
render but be uncompletable (operator never finds it) — both quiet.

Validation

Full go test ./... green, scored from a real exit code (REAL_EXIT=0),
not a pipe.

The #6532 fabric secret-render audit caught the new topic as reachable from
the peer chassis over the cluster-fabric allowlist and required it be added to
showTextAuditTopics. A genuine cross-cutting catch, not a formality — ShowText
is peer-reachable, so every topic it renders is in scope for that audit.

Fail-on-revert by mutation, build+vet clean at each step so every RED is an
assertion, not a build break:

# Mutation RED
N1 every family reported enforced inert pin (format + gRPC)
N2 negative control — dscp reported inert enforced pin, both surfaces
N3 name-only families dropped from the renderer 4 format tests + 3 gRPC tests
N4 gRPC dispatch arm removed (remote unwired) all 4 gRPC tests, unknown topic
N5 bare-positional name revert local grammar pin
N6 exp dropped from cmdtree type SSOT coherence pin

N2 is the control that makes N1 mean anything: without it, "some marker is
present" would satisfy the inert assertions.

Docs

docs/cos-validation-notes.md gains a section on the inert-rule problem, the
unequal-data split, and the rule that a family which starts being enforced must
flip cosRewriteRuleEnforced in the same change that drops its commit
advisory — otherwise this command reports a working rewrite as inert.

Not included

clear class-of-service statistics (#4228 Gap 7b) remains deferred: there is no
CoS-queue stat-reset RPC — counters reset only on config change — so it needs a
helper-side reset path.

🤖 Generated with Claude Code

https://claude.ai/code/session_015oARShYtiJJ2H4UB4nXGqi

Paul Saab and others added 6 commits August 5, 2026 10:40
This is the one Junos CoS show command the #4228 Gap 7 pass did not land.
The `show class-of-service` subtree had `interface`, `classifier`,
`scheduler-map` and `forwarding-class`; `rewrite-rule` was absent.

It matters more now than when Gap 7 was written. At that point
`rewrite-rules` held only `dscp`, so the missing command hid one table.
Since then the config models three more families -- `ieee-802.1` (#4228
Gap 4), `inet-precedence` and `exp` (#4316) -- and all three are
ACCEPTED-BUT-INERT: they commit clean and have no runtime effect,
because the userspace dataplane rewrites DSCP on egress only. So an
operator could configure four kinds of rewrite rule, three of which do
nothing, with no operational command to display any of them. The only
signal was a commit-time advisory that scrolls past once.

Enforcement is therefore a COLUMN of the output, not a footnote:

  Rewrite rule: rw-dscp, Code point type: dscp, Enforced: yes
  Rewrite rule: rw-pcp, Code point type: ieee-802.1, Enforced: no
  (accepted for Junos compatibility; the dataplane rewrites dscp only)

Rendering an inert rule identically to an enforced one would reproduce,
inside the command built to expose the problem, exactly the silence that
made it worth fixing. The inert marker says what the operator loses
rather than only that a flag is off.

The four families do not carry equal data, and the renderer does not
paper over that. `dscp` and `ieee-802.1` compile to full entry lists
(forwarding-class, loss-priority, code-point). `inet-precedence` and
`exp` record only rule NAMES -- the compiler builds no runtime structure
because nothing consumes one -- so they render a "Code points not
modeled" line instead of an empty table, which would imply a fidelity
the config does not have.

Wired on BOTH surfaces. The local CLI and the gRPC server are separate
dispatchers; a command registered in only one works interactively and
silently falls through to help text on the remote `cli` binary, which is
the surface most operators use. The gRPC topic encoding is shared with
the classifier command rather than duplicated.

Also fixes a latent completion/behavior mismatch in the pre-existing
sibling. Both commands' cmdtree nodes offer rule names directly under
the command, so tab-completing a name and pressing enter submits a bare
positional -- which the keyword-only parser ignored, dumping every rule.
The shared parser now accepts it, so `classifier` and `rewrite-rule`
both do what their completion advertises. No test pinned the old
dump-everything behavior.

format.CoSRewriteRuleTypes is the SSOT for the `type` filter and the
cmdtree completion node is pinned against it, so the two cannot drift
into offering a value the renderer ignores (filter silently returns
everything) or hiding a family that renders.

Validation: full `go test ./...` green, scored from a real exit code
rather than a pipe. The #6532 fabric secret-render audit caught the new
topic as reachable from the peer chassis over the cluster-fabric
allowlist and was extended to cover it. Fail-on-revert established by
mutation with build and vet clean at each step, so every RED is an
assertion failure rather than a build break.

Advances #4228 (Gap 7 residual).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015oARShYtiJJ2H4UB4nXGqi
Gate MAJOR on #6858, and the failure this command was written to
prevent, reproduced one level in.

`Enforced` was computed from the code-point TYPE alone. But runtime DSCP
rewriting happens only for the rule an egress interface REFERENCES: the
rewrite table is built from
`tables.dscp_rewrite_rules.get(&iface.cos_dscp_rewrite_rule)`
(forwarding_build/cos.rs), so a dscp rule that no unit binds rewrites
nothing. Configure a rule and never bind it, and the command reported
`Enforced: yes`.

That is strictly worse than shipping no command: it converts an
unanswered question into a confidently wrong answer, inside the very view
added because an operator could not tell an acting rule from an inert
one.

There are THREE states, not two, and the middle one was collapsed into
the first:

  1. `dscp` and bound by some unit  -> Enforced: yes
  2. `dscp` and bound by nothing    -> Enforced: no (not bound -- no
                                       interface unit references this rule)
  3. any other code-point type      -> Enforced: no (accepted for Junos
                                       compatibility; dataplane rewrites
                                       dscp only)

State 2 deliberately does NOT borrow state 3's wording. The type IS
supported and the binding is what is missing; conflating them would send
the operator to fix the wrong thing. For an unsupported type the type
reason is reported even when the rule is also unbound, because the type
is the dominant fact -- such a rule would not act however it were bound.

Scanning `CoSInterface.Units` is sufficient for the bound set and is not
an oversight: the compiler folds an interface-level binding into every
configured unit (applyCoSInterfaceLevelBindings), which `CoSInterface.Level`
documents and which is why the snapshot builder iterates Units too.

BOTH fixtures pinned the defect, not only the one the gate named. The
gRPC fixture bound no rewrite rule -- and neither did the format-level
one, whose base `testCoSConfig` binds classifiers but no rewrite rule --
and both asserted `Enforced: yes`. I surveyed every `Enforced:` assertion
in the tree to establish that those two are the complete set rather than
assuming the named one was alone. Both now BIND the rule, so
`Enforced: yes` is earned rather than assumed.

New coverage: an unbound-is-not-enforced pin at the format level and a
producible one over gRPC, the latter authored in real `set` syntax with
no bind line so the state is demonstrably reachable from an operator
config and not only from a hand-built struct; plus a bound-IS-enforced
positive control, without which "never reports yes" would satisfy the
unbound assertion.

Validation: full `go test ./...` green, scored from a real exit code.
Fail-on-revert established by mutation with build and vet clean at each
step.

Advances #4228 (Gap 7 residual).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015oARShYtiJJ2H4UB4nXGqi
Resolve the sole conflict, _Log.md, by union: every entry from both
sides is retained and none is rewritten. The file is no longer
append-ordered, so line counts and prefix checks say nothing useful
about the result; the resolution was verified structurally instead, by
confirming that each pre-merge side diffs into the merged file with
add-hunks only and no changed-or-deleted hunk on either side.

Every other path merged without conflict. Because a clean textual
auto-merge can still break compilation when a signature moves on one
side, that was confirmed by building rather than by inspection: go build
./... clean on the merged tree.

Advances #6858.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant