class-of-service: add show class-of-service rewrite-rule (#6848) - #6858
Open
psaab wants to merge 6 commits into
Open
class-of-service: add show class-of-service rewrite-rule (#6848)#6858psaab wants to merge 6 commits into
show class-of-service rewrite-rule (#6848)#6858psaab wants to merge 6 commits into
Conversation
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
# Conflicts: # _Log.md
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.
# Conflicts: # _Log.md
# Conflicts: # _Log.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #6848. Advances #4228 (Gap 7 residual).
The gap
show class-of-servicehadinterface,classifier,scheduler-mapandforwarding-class.rewrite-rule— the fifth Junos CoS show command — wasnever wired.
It matters more now than when #4228 was written. At that point
rewrite-rulesheld only
dscp, so the missing command hid one table. Since then the configmodels three more families —
ieee-802.1(#4228 Gap 4),inet-precedenceandexp(#4316) — and all three are accepted-but-inert: they commit clean andhave 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
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
dscpandieee-802.1compile to full entry lists.inet-precedenceandexprecord only rule names — the compiler builds no runtime structure because
nothing consumes one. Those render
Code points not modeledrather than anempty table, which would imply a fidelity the config does not have.
TestShowTextCoSRewriteRuleNameOnlyFamiliesAreProducible6848authors such arule with a code point in real
setsyntax and asserts the value does notsurface — 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
clibinary — the surface most operators use. Both are wired, sharingone 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
classifierandrewrite-ruleboth do what their completionadvertises.
This deliberately touches the pre-existing
classifiercommand. Leaving itwould 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.CoSRewriteRuleTypesis the SSOT for thetypefilter; the cmdtreecompletion 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 — ShowTextis 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:
unknown topicexpdropped from cmdtreetypeN2 is the control that makes N1 mean anything: without it, "some marker is
present" would satisfy the inert assertions.
Docs
docs/cos-validation-notes.mdgains a section on the inert-rule problem, theunequal-data split, and the rule that a family which starts being enforced must
flip
cosRewriteRuleEnforcedin the same change that drops its commitadvisory — otherwise this command reports a working rewrite as inert.
Not included
clear class-of-service statistics(#4228 Gap 7b) remains deferred: there is noCoS-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