netviz GitHub

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

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 assignedE 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:

  1. --disable RULE on the command line. Repeatable, accepts either spelling of an id and the wildcards *, all and any. It adds to whatever netviz.toml already 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.
  2. ignore and [validate.severity] in netviz.toml. The per-inventory decision, versioned with the tree, and the only place that can re-grade a rule rather than silence it. See docs/configuration.md.
  3. The netviz/ignore annotation on an element. The per-element exception — netviz/ignore: "W103, E004" in an element's metadata.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.
  4. 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