Validating an inventory
netviz validate answers one question: is this inventory usable? This page
is about working with the answer — how the three passes fit together, what a
finding's parts mean, how a severity becomes an exit code, and the four ways to
tell netviz that your network is the exception.
docs/validation-rules.md is the other half: one section
per rule, why the rule is worth having, and how to switch that one off. Every
report netviz emits deep-links into it, so you rarely have to find a rule
there by hand. netviz validate is the flag-by-flag
reference for the command itself.
Contents
- The three passes
- How to read a finding
- Severities, and the exit code that follows
--strict, and when you want it- Saying "not here": the four suppressions
- Repairing what can be repaired
- Output for a machine
- Every rule
The three passes
Validation is not one check but three, run in a fixed order, each one depending on the previous having succeeded.
Pass 1 — discovery walks the folder tree and decides what is part of the inventory at all: which files are read, which paths are skipped, which documents a multi-document file holds, and whether two of them answer to the same name. Most of it is not configurable, because it is not a judgement — it is how the loader works. It is documented as a pass anyway, because "my file is not being loaded" is a validation question in practice. Pass 1 — discovery has the five loading rules and the two problems this stage reports.
Pass 2 — schema parses each surviving document into the model for its kind.
Everything here is an error and none of it can be suppressed: a document that
does not parse is not in the graph, and no severity setting can make a missing
element benign. This is also where interfaces[].range is expanded and
spec.from is merged, which is why a mistake in a template is reported against
the template rather than fifty times against the devices that inherit it.
Pass 2 — schema lists the constraints by
area, and docs/schema-reference.md is the field-by-field
lookup table behind them.
Pass 3 — semantics asks the interesting question: the documents all parse, but do they agree with each other? Cables must land on interfaces that exist, addresses must be unique where uniqueness is physical, the two ends of a link must be configured compatibly. These are the only rules that can be disabled, re-graded or suppressed, because they are the only ones that are judgements — a network can be built that way, badly, and an inventory that means to describe it must be able to say so. Pass 3 — semantics writes each one up individually.
Why an earlier error hides later ones
A pass can only judge what reached it. A file discovery skipped is not in the inventory, so nothing is ever said about its contents. A document that lost a name collision, or that failed the schema, is not in the graph — so the pass-3 rules that would have had an opinion about it never see it, and the rules that do fire are often about the hole it left rather than about the mistake itself.
The practical consequence is worth internalising: fix errors from the top and
re-run. A report can get longer after a fix, not shorter, and that is the
system working. The repository's own broken-fixture directory shows it at scale
— one file per rule, each reusing the names pc-a and pc-b, so loading the
whole directory as a single inventory makes almost every document lose a name
collision in pass 1:
$ netviz -i tests/fixtures/invalid validate
...
e002-double-termination.yaml#0:4 NV-N002 metadata.name: duplicate element name 'pc-a' (first declared at e001-unknown-endpoint.yaml#0:7); this document is ignored
...
e005-vlan-mismatch.yaml#1:18 I002 interface 'sw-b:GigabitEthernet0/1' is enabled but terminates no cable; mark it 'enabled: false' if the port is spare
...
Not one of the pass-3 findings in that report is the finding the file it names
was written to demonstrate. e005-vlan-mismatch.yaml is about a VLAN mismatch;
what it reports here is a spare port, because the cable that would have created
the mismatch was in a document pass 1 dropped. Load the file on its own and the
rule it is about is the only thing it says — which is exactly why
tests/fixtures/invalid/README.md insists
they be loaded one at a time.
Passes 1 and 2 are how an inventory is loaded, so every command that reads the
tree runs them and reports what they found. Pass 3 runs wherever a wrong answer
would matter. render, path,
watch and export validate before they
produce anything and refuse an inventory with errors unless --force is given;
each of them also takes --strict. import validates the
tree it has just written, and says which of the findings an incomplete capture is
expected to trip. validate is simply the command whose only job is to report.
How to read a finding
A text finding is three columns — location, rule, message:
$ netviz -i tests/fixtures/invalid/e001-unknown-endpoint.yaml validate
errors (1):
e001-unknown-endpoint.yaml#1:19 E001 cable 'cbl-dangling' endpoint pc-ghost:eth0: no element named 'pc-ghost' is declared in this inventory
1 error
The location. e001-unknown-endpoint.yaml#1:19 is the file relative to the
inventory root, then the index of the document within that file — 0-based,
counting --- separators — then a line number. In text output the line is where
the document begins, which is the anchor a person needs to find it. The
structured formats carry the line and column of the offending value instead,
because a code-scanning UI puts a squiggle under a character rather than
scrolling you to a document.
The rule id. E001 is the id the validator uses everywhere a rule can be
named: --disable, [validate] in netviz.toml, the netviz/ignore
annotation. The letter is the severity the rule was first assigned — E error,
W warning, I info — and it does not change when an inventory re-grades the
rule, so E004 = "warning" is a perfectly ordinary line to write. Ids are
permanent: once assigned, an id is never reused for a different rule, so a
suppression written today keeps meaning what it meant.
The NV-* alias. Every rule also answers to the identifier
§10 of the specification gives it — E001 is
NV-C002 and NV-C003. Both spellings work in every suppression mechanism. The
aliases exist so the published specification and the implementation cannot drift
apart, and the structured formats report both.
The message names every element involved, not just the one the finding is anchored at, because any of them can suppress it. A finding about a cable names the cable and both endpoints, and an annotation on any of the three silences it.
The help URI. In json, sarif and github output each finding carries a
permanent link to its section of docs/validation-rules.md, built from the rule
id and title in the code — so a stranger reading a CI annotation can open the
write-up without knowing anything about netviz's docs layout:
$ netviz -q -i tests/fixtures/invalid/e001-unknown-endpoint.yaml validate -F github
::error file=tests/fixtures/invalid/e001-unknown-endpoint.yaml,line=26,col=7,title=E001 unknown cable endpoint::cable 'cbl-dangling' endpoint pc-ghost:eth0: no element named 'pc-ghost' is declared in this inventory
Line 26, column 7 is - pc-ghost:eth0 — the value that is wrong, not the
document that holds it. The title is the rule id plus the heading of its
section, and the same string is the SARIF name; the SARIF helpUri is the full
link. -q above is what dropped the human summary and left only the workflow
command; see Output for a machine.
Severities, and the exit code that follows
| Severity | Meaning | Effect |
|---|---|---|
error |
The inventory does not describe a network that could exist. | validate exits 1; render refuses to draw unless --force. |
warning |
Legal, but usually a mistake. | Reported; the run succeeds. |
info |
Worth knowing. | Reported; the run succeeds. |
Findings are grouped by severity, most severe first, and within a group ordered by file, then position, then rule id. The order is deterministic, so two runs over an unchanged inventory produce byte-identical output and a report can be committed and diffed.
The exit code answers the same question the command does:
| Code | Meaning |
|---|---|
0 |
No errors. Warnings and infos may still have been reported. |
1 |
At least one error, or a document that could not be loaded at all. |
2 |
Usage error — an unknown option, or an unknown rule id in --disable — or an unusable netviz.toml. |
3 |
The inventory could not be discovered or read at all. |
2 and 3 are failures to run the check rather than results of it, which is
why no structured document is written for them.
A severity is not fixed by the code. [validate.severity] in
netviz.toml re-grades
any pass-3 rule, and a handful of rules are already graded more harshly than the
specification proposes for reasons written up in
Where this differs from the specification.
--strict, and when you want it
--strict promotes every surviving warning to an error, so any finding fails the
run. Nothing else changes: the same findings are reported, in the same order,
with the same ids.
$ netviz -i tests/fixtures/invalid/w103-orphan-device.yaml validate
warnings (1):
w103-orphan-device.yaml#0:6 W103 device 'pc-a' terminates no cable and hosts no adapter; it is drawn as an isolated node
1 warning
$ netviz -i tests/fixtures/invalid/w103-orphan-device.yaml validate --strict
errors (1):
w103-orphan-device.yaml#0:6 W103 device 'pc-a' terminates no cable and hosts no adapter; it is drawn as an isolated node
1 error
Want it in CI, and want it off at your desk. A warning is netviz's way of
saying "this is legal, and it is usually a mistake" — which is a useful thing to
be told while you are editing and a bad thing to have merged. The pattern that
works is --strict in the pipeline plus explicit suppressions for the warnings
your network really does trip, so the exceptions are written down in the
inventory instead of tolerated by everybody's habit.
--strict can only turn strictness on. strict = true in netviz.toml
makes it the default for a tree, and there is deliberately no --no-strict to
undo that from a command line: the file is where a decision about the inventory
belongs. --strict is also taken by render,
path, watch and
export, where it decides whether a warning is enough to refuse the
artefact.
Saying "not here": the four suppressions
Four mechanisms, all additive — a finding is silenced if any of them applies:
--disable RULEon the command line. Repeatable, accepts either spelling of an id and the wildcards*,allandany. It adds to whatevernetviz.tomlalready ignores and cannot re-enable something the file disabled. Best for a one-off: narrowing a noisy report while you work through the rest of it.ignoreand[validate.severity]innetviz.toml. The per-inventory decision, versioned with the tree, and the only place that can re-grade a rule rather than silence it. Seedocs/configuration.md.- The
netviz/ignoreannotation on an element. The per-element exception —netviz/ignore: "W103, E004"in an element'smetadata.annotations. Because a finding names every element it involves, annotating either end of a cable silences a finding about that cable, and so does annotating the cable. Put it on the element the exception genuinely belongs to: that is where the next reader will look for the explanation, and a comment beside it is worth more than the annotation. - Nothing at all. Load and schema errors are not suppressible by design.
The order of application is ignore first, then the severity override, then
--strict — so a rule listed in ignore is never reported whatever its severity
says, and a rule re-graded down to warning is still promoted back to error
under --strict.
Naming a schema rule is a usage error rather than a setting that quietly applies to nothing, and the message lists what you could have meant:
$ netviz -i examples/quickstart validate --disable NV-D005
error: --disable: 'NV-D005' is not a known rule id; expected one of E001, E002, E003, E004, E005, E006, E007, E008, E009, E010, E011, E012, E013, E014, E015, E016, E017, E018, E019, E020, E021, E022, E023, E024, E025, E026, E027, E028, E029, E030, E031, E032, E033, E034, E035, E036, E037, E038, E039, E040, E041, E042, E043, E044, E045, E046, E047, E048, E049, E050, W101, W102, W103, W104, W105, W106, W107, W108, W109, W110, W111, W112, W113, W114, W115, W116, W117, W118, W119, W120, W121, W122, W123, W124, W125, W126, W127, W128, W129, W130, W131, W132, W133, W134, W135, W136, W137, W138, W139, W140, W141, W142, W143, W144, W145, W146, W147, W148, W149, W150, W151, W152, W153, W154, I001, I002, I003, I004, I005, an NV-* alias from docs/schema.md §10, or '*'
An unknown id in an annotation is ignored rather than fatal — inventory data must not be able to abort a run — and therefore simply fails to suppress anything.
Suppressing a rule has all four in
full: the exact TOML, the annotation grammar, the accepted separators, and what
happens to an unknown key inside [validate]. Each rule's own section ends with
a Suppress with line naming its ids and the elements worth annotating.
Repairing what can be repaired
Suppression is one answer to a finding. The other is to fix it, and for a good part of the catalogue the inventory itself determines what the fix is: a layout document placing an element that has been deleted, a port trunking a VLAN its device's database does not declare, a VRF nothing is bound to.
netviz validate --fix # apply them, and report the rest
netviz validate --fix --dry-run # ... printing the diff instead
netviz validate --fix --choose W114=list # decide a rule that has two repairs
Each repair is applied on its own and the tree is validated again; it is kept
only if the finding it was aimed at is gone and no rule reports more than it did
before, so --fix cannot make an inventory worse. Where two repairs are equally
plausible the command names them and applies neither. The web editor puts the
same repairs on a button beside each diagnostic.
Fixing a finding lists which rules are
repairable and what each repair does, generated from the table in
netviz.fixes so it cannot drift; netviz rules --fixable prints it too.
Output for a machine
-F, --output-format selects one of four:
| Format | For |
|---|---|
text (default) |
Reading. Findings grouped by severity, most severe first. |
json |
A step of your own — jq, a dashboard, a diff of two runs. |
sarif |
SARIF 2.1.0, for github/codeql-action/upload-sarif or any SARIF viewer. |
github |
GitHub Actions workflow commands that annotate a pull request in place. |
The three structured formats put their document on stdout and move the human
summary to stderr, so the output stays pipeable:
netviz validate -F sarif > netviz.sarif writes a file a code-scanning
upload accepts while a person watching the run still sees what happened.
--quiet drops that summary and never the document. All four honour --strict
and --disable, and re-grading a rule in netviz.toml changes the JSON
severity, the SARIF level and the workflow command alike.
netviz -i inventory validate -F sarif --strict > netviz.sarif
netviz -i inventory validate -F github
docs/ci.md is the reference for all of this: the key-by-key JSON
envelope, what goes into the SARIF run, the composite GitHub Action and the
pre-commit hook this repository ships, and complete workflows for the two ways
findings can reach a pull request.
Every rule
The table below is generated from netviz.rules.RULES, the same catalogue the
validator and netviz rules read, so it describes the
build in this repository and cannot drift from it. Each id links to the rule's
section in docs/validation-rules.md, where the write-up
says why the rule exists, what it deliberately exempts, and how to suppress it.
| Id | Schema id | Severity | Rule |
|---|---|---|---|
E001 |
NV-C002, NV-C003 |
error | unknown cable endpoint |
E002 |
NV-C005 |
error | interface terminated by more than one cable |
E003 |
NV-I008 |
error | duplicate MAC address |
E004 |
NV-A004 |
error | duplicate IP address |
E005 |
NV-C011 |
error | VLAN mismatch across a link |
E006 |
NV-X008 |
error | adapter over capacity |
E007 |
NV-I004 |
error | cyclic interface stacking |
E008 |
NV-I005 |
error | a member is not free to be aggregated |
E009 |
NV-V005 |
error | sub-interface VLAN not carried by its parent |
E010 |
NV-I009 |
error | multicast MAC address |
E011 |
NV-C006 |
error | medium disagrees with the endpoint type |
E012 |
NV-C009 |
error | cable terminates on an interface with no socket |
E013 |
NV-X005 |
error | host attachment declared twice |
E014 |
NV-X006 |
error | cyclic adapter attachment |
E015 |
NV-X001 |
error | attached_to names nothing that could host the adapter |
E016 |
NV-T002 |
error | unknown tunnel endpoint |
E017 |
NV-T003 |
error | tunnel endpoint is not a tunnel interface |
E018 |
NV-T004 |
error | over names no tunnel |
E019 |
NV-T005 |
error | cyclic tunnel encapsulation |
E020 |
NV-A013 |
error | first hop is not on-link |
E021 |
NV-P001 |
error | cable on a position the patch panel does not have |
E022 |
NV-P003 |
error | patch-panel position terminated twice |
E023 |
NV-P004 |
error | patch panel where an active element is required |
E024 |
NV-P005 |
error | patch run loops back into its own panel |
E025 |
NV-U001 |
error | two elements occupy the same rack unit |
E026 |
NV-U002 |
error | element mounted above the top of its rack |
E027 |
NV-U003 |
error | rack declared with two heights |
E028 |
NV-W007 |
error | wireless link is not an association |
E029 |
NV-W008 |
error | duplicate BSSID |
E030 |
NV-W009 |
error | SSID VLAN is carried nowhere on the access point |
E031 |
NV-W010 |
error | associated to an SSID the access point does not advertise |
E032 |
NV-F008 |
error | next hop is not on-link |
E033 |
NV-F009 |
error | route sends out of an unknown interface |
E034 |
NV-F010 |
error | OSPF runs on an interface the device does not have |
E035 |
NV-F011 |
error | BGP session disagrees about an AS number |
E036 |
NV-F012 |
error | duplicate router id |
E037 |
NV-E010 |
error | PDU outlet claimed twice |
E038 |
NV-E011 |
error | power input names no outlet that exists |
E039 |
NV-E012 |
error | PDU load exceeds its capacity |
E040 |
NV-E013 |
error | PoE allocation exceeds the budget |
E041 |
NV-E014 |
error | PoE-powered device has no PoE uplink |
E042 |
NV-E015 |
error | redundant power that is not redundant |
E043 |
NV-S010 |
error | group member does not exist |
E044 |
NV-S011 |
error | group member is not an identity |
E045 |
NV-S012 |
error | group membership cycle |
E046 |
NV-S013 |
error | duplicate account identifier |
E047 |
— | error | declared gateway redundancy is not met |
E048 |
— | error | declared power redundancy is not met |
E049 |
NV-N024 |
error | cable on a virtual interface |
E050 |
NV-N025 |
error | aggregate spans network namespaces |
W101 |
NV-I013 |
warning | interface neither routes nor switches |
W102 |
NV-C010 |
warning | MTU mismatch across a link |
W103 |
NV-C016 |
warning | orphan device |
W104 |
NV-V009 |
warning | IP address on an access port |
W105 |
NV-A008 |
warning | subnet with a single member |
W106 |
NV-A009 |
warning | one address claimed twice in a subnet |
W107 |
NV-I006 |
warning | addresses on an aggregate member |
W108 |
NV-I007 |
warning | MAC address on a loopback |
W109 |
NV-I012 |
warning | device that cannot be cabled |
W110 |
NV-A005 |
warning | network or broadcast address assigned |
W111 |
NV-A006 |
warning | overlapping prefixes on one element |
W112 |
NV-A007 |
warning | loopback with a non-host prefix |
W113 |
NV-V004 |
warning | undeclared VLAN referenced |
W114 |
NV-V006 |
warning | native VLAN missing from trunk_vlans |
W115 |
NV-V007 |
warning | every VLAN trunked to a host |
W116 |
NV-V008 |
warning | LAG member contradicts its aggregate |
W117 |
NV-C004 |
warning | both ends of a cable on one element |
W118 |
NV-C008 |
warning | cable and endpoint disagree about speed |
W119 |
NV-C012 |
warning | cable terminates on a LAG aggregate |
W120 |
NV-C013 |
warning | half duplex without a hub |
W121 |
NV-C014 |
warning | disconnected topology |
W122 |
NV-H005 |
warning | one hub, two subnets |
W123 |
NV-X002 |
warning | cabled adapter with no host |
W124 |
NV-X007 |
warning | adapter attached to a hub or a switch |
W125 |
NV-T006 |
warning | overlay reaches past its underlay |
W126 |
NV-T011 |
warning | tunnel MTU does not fit its underlay |
W127 |
NV-T012 |
warning | tunnel carries traffic in the clear |
W128 |
NV-T013 |
warning | tunnel interface named by no tunnel |
W129 |
NV-T014 |
warning | two tunnels share a VNI on one element |
W130 |
NV-A010 |
warning | prefix claimed by two broadcast domains |
W131 |
NV-A011 |
warning | nested prefix in a different broadcast domain |
W132 |
NV-A012 |
warning | address outside every prefix on its link |
W133 |
NV-P002 |
warning | patch run stops inside the panel |
W134 |
NV-W011 |
warning | access points on overlapping channels |
W135 |
NV-F013 |
warning | BGP neighbour is not in the inventory |
W136 |
NV-F014 |
warning | VRF with no interface bound to it |
W137 |
NV-E016 |
warning | declared draw with no power path |
W138 |
NV-Y001 |
warning | stale diagram geometry |
W139 |
NV-S014 |
warning | group with no members |
W140 |
NV-S015 |
warning | departed user still in a group |
W141 |
— | warning | unknown redundancy expectation |
W142 |
NV-G001 |
warning | annotation about something that is gone |
W143 |
NV-G004 |
warning | area that encloses nothing |
W144 |
NV-Z003 |
warning | element styled invisible |
W145 |
NV-Z005 |
warning | unreadable label colour |
W146 |
NV-N026 |
warning | network namespace with no interface |
W147 |
NV-F022 |
warning | policy rule looks up an empty table |
W148 |
NV-F023 |
warning | routing table nothing selects |
W149 |
NV-F024 |
warning | unreachable policy rule |
W150 |
NV-B010 |
warning | security zone with no interface |
W151 |
NV-B011 |
warning | interface in no zone |
W152 |
NV-B012 |
warning | firewall mark nothing reads |
W153 |
NV-B013 |
warning | firewall mark nothing writes |
W154 |
NV-B014 |
warning | unreachable firewall rule |
I001 |
NV-I010 |
info | locally administered MAC address |
I002 |
NV-C015 |
info | enabled interface terminates no cable |
I003 |
NV-T015 |
info | tunnel on a non-standard port |
I004 |
NV-S016 |
info | person in no group |
I005 |
NV-N027 |
info | veth pair crosses no boundary |
See also
docs/validation-rules.md— the normative catalogue, one section per rule, and the full treatment of suppression.netviz validate— the command reference, with the flag tables and worked transcripts.docs/ci.md— the machine-readable envelopes, the GitHub Action and the pre-commit hook.docs/configuration.md—[validate]and[validate.severity]innetviz.toml.