netviz GitHub

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

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.

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:

  1. the namespace of the referring document,
  2. each ancestor namespace, nearest first, the root last,
  3. 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):

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:

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/0ge-1/0/3 — and the width of a span's low bound is its zero padding, so [01-12] yields 0112. 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