Laying out an inventory
An inventory is a directory tree of YAML documents, and netviz imposes almost nothing on its shape. This page is for the point just after the tutorial: you know what a document looks like, and now you have to decide how many files there are, what goes in each one, and which folder each one sits in — before the answer is decided for you by forty documents nobody wants to move.
The normative rules live in the specification;
docs/schema.md §2 is the section
this page paraphrases, and it is the one to read when two statements disagree.
Contents
- One document per file, or several
- Folders are namespaces
- How a reference is resolved
- Which files the loader reads
- The thirteen kinds
- A layout for a small network
- A layout for an estate
- Declaring a 48-port switch without typing it 48 times
- Annotations
- Rearranging the tree, from the diagram
- Keeping the tree tidy
One document per file, or several
Both are accepted. A file MAY hold one document, or several separated by ---
(NV-L004), and the loader treats the two the same way: a document is a document
wherever it was found, and every diagnostic quotes the file and the document
index within it, sites/hq/switches/sw-access-01.yaml#0:17.
So the choice is a review choice, not a technical one.
- One document per file is what you want for anything somebody will edit on
its own. A 48-port switch is a hundred lines; giving it a file named after it
means a diff touching that switch touches one file, and
git logon the file is the history of the switch. - Several documents in one file is what you want for things that are only
ever true together. Cables are the obvious case: a cable is meaningless
without the two devices it joins, and twenty of them in
cables/links.yamlread as a patch record. The same goes for a handful of near-identical hosts.
The examples in this repository use both, in exactly that split — examples/campus
gives every core and distribution device its own file and collects each site's
cables and hosts into one apiece.
Empty documents are skipped silently but still consume an index, which is why
netviz fmt writes an empty document as an explicit null
rather than deleting it: dropping it would renumber every document after it and
move the line every diagnostic points at.
Folders are namespaces
An element's fully-qualified name is the directory holding its document,
relative to the inventory root, plus its metadata.name. A switch named sw1
in sites/berlin/rack1/sw1.yaml is sites/berlin/rack1/sw1; one declared at the
root is just sw1. metadata.name has to be unique only within its own
namespace (NV-N002), so two racks may each hold a sw1.
Folders are otherwise for humans. Group by site, by rack, by tenant, by whatever the team already says out loud — cross-references work across any file in the tree, and the only thing a folder contributes is the namespace. Nothing about a folder changes how a device is validated or drawn.
The qualified names are what netviz list prints, and they are the names you
give to any command that takes one:
$ netviz -i examples/campus list devices
NAME KIND PORTS ADDRESS VLANS
----------------------------------------- -------- ----- ------------- -------------
sites/north/access/sw-north-acc-01 switch 6 10.1.99.11/24 1,10,20,30,99
...
sites/west/hosts/pc-west-02 computer 2 10.3.10.52/24 10
How a reference is resolved
Cables, adapters and tunnels point at interfaces with a two-part reference,
sw-access-01:GigabitEthernet1/0/1 — the device part, a colon, the interface
name (§4.2). The device part is a plain
name, and plain names are resolved outwards:
- the namespace of the referring document,
- each ancestor namespace, nearest first, the root last,
- the inventory as a whole — but only when exactly one element carries that
name; otherwise the reference is ambiguous and every candidate is named in the
diagnostic (
NV-N002).
A reference MAY also be written fully qualified, sites/berlin/rack1/sw1, which
is tried relative to the current namespace first and as an absolute name second.
§2.2 states the rule normatively.
Two consequences are worth designing around. An element at the root is visible
from everywhere, which makes the root the right place for the handful of things
every site refers to. And a name that is unique across the whole inventory never
needs qualifying at all, however deep it sits — which is why the campus example
prefixes device names with their site: sw-north-acc-01 is reachable from
sites/north/cables/ because steps 1 and 2 miss and step 3 finds exactly one
match.
Which files the loader reads
The walk is recursive from the inventory root and the rules are few (§2.1):
- only
*.yamland*.yml, compared case-insensitively; every other file is ignored (NV-L001). AREADME.mdor anetviz.tomlbeside your documents is not a problem. - nothing under a path component whose basename starts with
.or_, directories included (NV-L002)._drafts/and_scratch/are the idiom for work in progress, and.git/costs nothing to skip. - nothing a
.netvizignoreexcludes (NV-L006). - symbolic links are followed, but one that escapes the root, forms a cycle or
reaches an already-loaded directory is an error (
NV-L003).
Load order is deterministic — files sorted by their byte-wise POSIX path relative
to the root, then by document index — so renderers produce stable output
(NV-L005).
Loading is total: an unreadable file, a YAML syntax error, a schema violation and a duplicate name are all reported with their location and the walk continues. One broken file cannot hide the rest of the inventory.
.netvizignore
Optional, one per directory, applying to that directory and everything below it;
a file in a subdirectory overrides its parents. The syntax is the .gitignore
subset described in §2.3:
vendor/ # a directory, anywhere below this file
*.bak.yaml # a basename pattern, at any depth
/staging.yaml # only in this directory
generated/** # everything below generated/
!generated/keep.yaml # ... except this one (the parent is not excluded)
Reach for it when the tree is shared with something else — a Kubernetes overlay,
a vendor export, an Ansible role — and the _-prefix trick would mean renaming
directories that another tool owns.
Discovery is one implementation, used by every command, so a file the inventory
would not read is also a file fmt will not rewrite and validate will not
complain about.
The thirteen kinds
Every document declares a kind, and the kind decides the shape of its spec.
Twelve kinds are elements — each becomes a node or an edge of the graph. The
thirteenth, template, is not.
kind |
What it is for | Specification |
|---|---|---|
switch |
A VLAN-aware bridge. Layer-2 by default: it does not forward IP unless told to. | §6 |
router |
A device that forwards IP; forwarding is true for both families by default. |
§6 |
hub |
A layer-1 repeater. Takes no VLANs, no addresses and no bridge — one collision domain. | §6.5 |
computer |
An end host, drawn as a workstation. | §6 |
server |
An end host, drawn as a rack-mount server. Structurally identical to computer. |
§6 |
cable |
One undirected physical link between exactly two interfaces. Owns no interfaces of its own. | §7 |
adapter |
Interfaces presented over a non-network host port — a USB dock, a Thunderbolt bridge. | §8 |
tunnel |
An undirected logical link between two or more tunnel interfaces; over nests one inside another. |
§14 |
patchpanel |
A passive cross-connect. Its front and rear ports are derived from ports, and it is not a hop. |
§15 |
pdu |
A power distribution unit. Its numbered outlets are derived from outlets; a device names one in power.inputs. |
§17.1 |
user |
One identity: a person, a service account or a shared login. Owns no interfaces; drawn only in the identity view. |
§19.1 |
group |
A named set of identities. members may name a user or another group, so a hierarchy is expressible. |
§19.2 |
template |
A named partial device spec, merged into every device that names it in spec.from. Not an element. |
§6.6 |
docs/schema-reference.md is the generated
field-by-field table for each of them, and
netviz schema emits the JSON Schema an editor can
check a document against as you type.
There is no rule that one folder holds one kind, and no rule that it does not.
Grouping cables into cables/ is a habit worth having because it puts the patch
record in one place; grouping every switch in the estate into switches/ is
usually a mistake, because it puts two sites in one namespace and buys nothing.
A layout for a small network
For a house, a lab bench or a single office, one level is enough. Name the folders after what the things are, and let every name be globally unique — with a dozen devices, nothing will ever be ambiguous:
home-lab/
├── routers/rtr-home.yaml
├── switches/sw-home.yaml
├── wireless/ap-home.yaml
├── hosts/
│ ├── pc-desk.yaml
│ ├── laptop.yaml
│ ├── phone.yaml
│ ├── srv-nas.yaml
│ └── adp-usb-eth.yaml # an adapter is a document like any other
└── cables/links.yaml # every cable, one file
That is examples/home-lab exactly. The qualified name
of the switch is switches/sw-home, and no cable in cables/links.yaml ever
writes that prefix: the inventory-wide lookup finds the one sw-home.
If even that is more structure than you want, put every document in one file at the root and split it later. Nothing in a document mentions its own path, so moving a file only ever changes qualified names — and only the references that were already qualified need touching.
A layout for an estate
Once there is more than one site, make the site the top-level namespace and repeat
one shape inside it. Two things follow: a reference written inside a site resolves
inside that site first, and a rendering can be narrowed to one site with
--namespace sites/north (docs/rendering.md).
examples/campus is three sites, 22 devices and 22 cables
in that shape:
campus/
├── netviz.toml # per-inventory configuration
├── backbone/cables.yaml # the three inter-site fibres
├── templates/access-switch.yaml # a 48-port access switch, declared once
└── sites/
├── north/
│ ├── core/rtr-north-core-01.yaml
│ ├── distribution/sw-north-dist-01.yaml
│ ├── access/switches.yaml # three documents; the third uses the template
│ ├── hosts/hosts.yaml # three documents
│ └── cables/links.yaml # seven documents
├── south/ # same shape, two access switches
└── west/ # same shape, two access switches
Read it as a set of decisions:
- The site is the namespace, and the tier is the folder below it.
core,distribution,access,hosts,cables— five folders that mean the same thing in every site, which is what makessites/northandsites/southdiffable against each other. Anything that differs beyond the site index is a mistake, and that is a review technique the layout gives you for free. - Cables live in the site whose devices they join, and the three that join
two sites live in
backbone/at the root, because they belong to neither. - Shared things live at the root, where every site can see them:
templates/here, and anetviz.tomlfor the settings the whole inventory should share (docs/configuration.md). - Room is a namespace too, when a site is big enough to have them:
sites/hq/mdf/,sites/hq/idf-3/. That is a different question from where the hardware physically is, which ismetadata.location(§3.2) and drives the rack elevation, not the name.
Per-file device documents in the tiers you edit rarely and carefully, multi-document files for hosts and cables you edit in batches: that is the split described above, applied.
Declaring a 48-port switch without typing it 48 times
A real access layer is dozens of near-identical documents, each of which is dozens of near-identical ports. Two mechanisms remove the repetition without weakening any check: both are applied by the loader before validation, so the validator, the graph and every renderer still see plain interfaces and plain devices.
Interface ranges
One entry may declare range instead of name
(§6.2.5):
interfaces:
- range: GigabitEthernet1/0/[1-48]
type: ethernet
description: Access port {} # {} and %d are the port number
enabled: false
vlan: {mode: access, access_vlan: 10}
Several spans expand as an odometer, rightmost fastest — ge-[0-1]/0/[0-3]
yields ge-0/0/0 … ge-1/0/3 — and the width of a span's low bound is its
zero padding, so [01-12] yields 01 … 12. A document expands to at most
4096 interfaces; eth[1-99999999] is a diagnostic, not an out-of-memory kill.
Device templates
A kind: template document is a named partial device spec; a device merges it
in with spec.from (§6.6):
apiVersion: netviz.dev/v1alpha1
kind: switch
metadata:
name: sw-north-acc-03
spec:
from: templates/c9200l-48p
bridge: {address: '00:1b:0d:01:a3:ff'}
interfaces:
- name: Vlan99
ipv4: [10.1.99.13/24]
Nine lines instead of two hundred, and
examples/campus has that
switch next to two written out longhand so the two can be compared. The merge
rules are stated exactly in §6.6.1 and are worth
knowing in full, but in short: the device's own keys win, mappings merge key by
key, interfaces merge by name, and every other list the device declares
replaces the template's outright.
Templates are not elements — they never appear in a graph, in netviz list,
or in validation output. The one place a template does surface is as the source
location of a field it contributed: a value the template got wrong is reported
against the template's file and line, with a note naming the device that
inherited it, so fifty devices do not report fifty copies of one mistake.
templates/access-switch.yaml#0:52 NV-I011 spec.interfaces[2].mtu: mtu 1000 is below
the IPv6 minimum of 1280 but the interface carries IPv6 addresses (inherited by
'sw-north-acc-03' through 'spec.from: templates/c9200l-48p')
The file and the line are the template's; the note says who tripped over it.
Use netviz show NAME --raw to read a device as written
and netviz show NAME to read it merged:
$ netviz -i examples/campus show sw-north-acc-03 --raw
apiVersion: netviz.dev/v1alpha1
kind: switch
metadata:
name: sw-north-acc-03
description: Access switch 03, North campus - third floor riser.
labels:
site: north
role: access
env: prod
spec:
location: Building A, Hauptstrasse 1 / floor 3 / IDF-3
interfaces:
- name: Vlan99
ipv4:
addresses:
- 10.1.99.13/24
- name: TenGigabitEthernet1/1/1
mac: 00:1b:0d:01:a3:11
from: templates/c9200l-48p
bridge:
address: 00:1b:0d:01:a3:ff
Without --raw the same command prints 51 interfaces.
Annotations
metadata.labels are yours and drive filtering and grouping —
netviz render --select site=hq, --group-by rack — so a small consistent key
set (site, rack, role, env, owner) pays off. metadata.annotations are
the opposite: they are read by the tool, not by you, and never affect the graph.
The one annotation this revision defines is netviz/ignore, which suppresses
validation rules on the element carrying it:
metadata:
name: spare-switch
annotations:
netviz/ignore: "W103, E004" # or "*" for every rule
Ids may be separated by commas, semicolons or spaces, and * means every rule.
It is documented as a field in §3.1, and as one of the
three ways to silence a rule in
docs/validation-rules.md
— which is also where you will find the argument for annotating the element the
exception genuinely belongs to rather than the nearest one.
Rearranging the tree, from the diagram
Because a folder is a namespace, the tree has a picture: every namespace is a
box, and every box is a directory. netviz web draws them
— one frame per level, captioned with the namespace and how many elements are
under it — whenever the diagram is grouped by namespace (the group box, or
Alt-G).
The frames are editable, and every gesture on one lands in the file system:
| On the diagram | On disk |
|---|---|
| Drag an element into a frame | its document is rewritten into that directory |
| Drag a selection into one | all of them, as one change and one Ctrl-Z |
| Drag a frame into another frame | the whole subtree, keeping its own shape |
| Drop on empty canvas | the document moves to the inventory root |
| Paste into a frame | the copies are written into that directory |
| New namespace… | the directory, made by putting something in it |
So sites/north/access/sw-north-acc-01 dropped into the sites/north/racks/r1
box becomes sites/north/racks/r1/sw-north-acc-01 — a new folder if there was
none, the document moved verbatim into it, and every cable, tunnel, group,
layout and annotation that referred to it re-spelled so it still resolves. It is
exactly netviz edit move, and it is refused — before
anything is written — when the target namespace already holds that name, because
NV-N002 says two elements in one namespace cannot share one.
There is deliberately no way to make an empty namespace. A folder netviz
reads is one holding a document (which files),
so an empty directory declares nothing and would not survive a git clone
anyway. New namespace… makes the folder by putting the first document in it.
The frames themselves can be resized, and the rectangle is stored in the
groups section of a kind: layout
document keyed by namespace — the one place a folder acquires a coordinate.
Folding a frame with the triangle on its header draws the whole namespace as a
single node, which is netviz render --collapse and writes nothing.
docs/editing.md
has the operations behind all of this, and what each one is refused for.
Keeping the tree tidy
Three habits stop a growing tree from becoming a review problem.
Format it. netviz fmt rewrites every document in one
canonical form — two-space indent, keys in schema order, one quoting rule,
comments and blank lines untouched — so a diff is never about layout. It uses the
loader's discovery, so it rewrites exactly the files the inventory reads and
nothing else.
$ netviz fmt --check examples/campus
0 file(s) would be reformatted, 19 already formatted
Let your editor check it. netviz schema writes a JSON Schema, and a
one-line modeline at the top of a file gives you completion and inline errors
before you ever run the tool. See
Editor setup
and docs/schema.md §13.
Validate it in CI. netviz validate is the check
that a cable's far end exists, that two devices do not claim one address, that
both ends of a link agree about VLANs — the things no per-file schema can see.
docs/ci.md has the workflow and the pre-commit hooks; the --check
form of fmt belongs in the same job.
See also
docs/schema.md§2 — the normative discovery, namespace and provenance rules this page paraphrases.docs/getting-started.md— the eight-step tutorial that builds the first tree.netviz show— read one element as written, or as netviz resolved it.docs/importing.md— generating a first tree from LLDP, anipdump, a CSV or a packet capture rather than typing one.