A neutral, extensible fleet inventory model for swamp — the foundational record of what exists and its declared attributes.
It is deliberately not a transport, an access layer, or a telemetry store. Consumer abstractions (telemetry, management, config-drift, capacity) are separate models that depend on this one, reading a device's facets by CEL:
data.latest("<inventory-instance>", "<device-id>").attributes.facets.access
Declarative, not live. The device list lives in the model's
globalArguments.devices; apply materialises one device resource per id.
The source of truth is the declared record — no host need be reachable to
record it.
Uniform core, variable depth. An atomic smart plug and a multi-part server
are both first-class items validating against the same schema. What they share
is the core (id + purpose); their difference in complexity lives in
components (empty for the plug, rich for the server) and facets — never in a
different shape.
Extensible in two axes, both deliberate:
- Data —
facetsis an open map (catchall), andcomponents/relationscarry open vocabularies, so a new facet (telemetry source, firmware, warranty…) or field needs no core edit. - Behaviour — new consumers are separate dependent models; swamp's
export const extensioncan also graft methods onto this type later.
| Field | Purpose |
|---|---|
id |
Stable slug; the resource key and relation target |
name, kind, purpose |
Identity (kind is an open vocabulary: host | switch | router | ups | pdu | smartplug | nas | …) |
components[] |
A device's moving parts ({ type, spec, qty?, ref? }) — ref promotes a part to its own item |
relations[] |
The item's place in the larger system ({ rel, target }: fedBy | meters | hosts | uplinkTo | …) |
facets |
Optional, layered dimensions — access, power, network, config, management, firmware, interfaces, plus any custom |
Facets double as the scoping unit: a consumer reads only the slice it needs, so the boundary that decides "how often does this change / get re-verified" is also the query boundary. Roughly:
| Tier | Rate | Facets |
|---|---|---|
| identity | static (verify on physical change) | core fields + components |
| firmware / cabling | occasional (scheduled check) | firmware, interfaces |
| os / network / config | frequent (drift target) | config, network |
firmware— version-tracked components (os,bios,bmc, … open) as{ vendor, product, version?, releaseDate?, eol?, channel? }.vendor+productare required so a scheduled release-currency check can resolve the upstream feed and flag "update available / behind / EOL".interfaces— network interfaces + physical cabling:{ name, mac?, speed?, role?, spec?, uplink?: { device, port } }.uplinkties a NIC to the device/port it plugs into.
swamp model create @stateless/inventory fleet
# edit globalArguments.devices, then:
swamp model method run fleet applyglobalArguments:
name: fleet
devices:
- id: ups-1
name: Rack UPS
kind: ups
purpose: Battery ride-through for the rack
facets:
power: { ratedW: 900 }
access:
methods:
- { kind: nut, target: "usb (host)" }
- id: host-1
name: Compute host
kind: host
purpose: Virtualisation node
components:
- { type: cpu, spec: "8c/16t" }
- { type: ram, spec: "64 GB" }
- { type: disk, spec: "2 TB NVMe" }
relations:
- { rel: fedBy, target: ups-1 }
facets:
access:
methods:
- { kind: ssh, target: "host-1.example" }
- { kind: rest, ref: "${{ vault.get('infra', 'api_token') }}" }
power: { drawEstW: 100 }
network:
addresses:
- { kind: ipv4, value: "203.0.113.10" }
firmware:
os: { vendor: Debian, product: Debian, version: "12" }
bios: { vendor: ExampleCorp, product: "MB-X1", version: "2.6", releaseDate: "2026-01-21" }
interfaces:
- { name: eth0, mac: "00:00:5e:00:53:01", speed: "1GbE", uplink: { device: switch-1, port: 12 } }
- id: plug-1
name: Smart plug
kind: smartplug
purpose: Meter host-1's power draw
relations:
- { rel: meters, target: host-1 }The materialised device resources are queryable server-side. Use --select
with CEL (null-safe via has()), which is more robust than projecting client-side:
# list
swamp data query 'modelName == "fleet" && specName == "device"' \
--select '{"id": attributes.id, "kind": attributes.kind}'
# dependency graph — everything fed by a given UPS
swamp data query 'modelName == "fleet" && specName == "device"' \
--select '{"id": attributes.id,
"onUps": attributes.relations.exists(r, r.rel == "fedBy" && r.target == "ups-1")}'
# capacity — estimated draw per device
swamp data query 'modelName == "fleet" && specName == "device"' \
--select '{"id": attributes.id,
"draw": has(attributes.facets) && has(attributes.facets.power)
? attributes.facets.power.drawEstW : 0}'- The extension carries no fleet data and is publish-safe. A populated instance is a recon map (topology + access paths) — treat it as private.
- Credentials are never embedded.
access.refholds only a${{ vault.get(...) }}reference and is marked sensitive so swamp redacts it. - Published examples use neutral placeholders and TEST-NET addresses, never real names or IPs.
| Method | Description |
|---|---|
apply |
Materialise each declared device as a device resource (one per id). Re-running records a new version, so record history (the trend) is retained. |
prune |
Reconcile: soft-retire stored devices no longer in devices by recording a final version with status set (default removed, configurable via --input status=retired). No hard delete — the record + version history survive, so the lifecycle (active → removed → active on re-declare) reads back as the trend. Idempotent. |
apply is additive/idempotent; prune is the deliberate reconcile. A device's
arc is recorded as the version history of its device resource:
apply (declared) → v1 status: active
apply (re-run) → v2 status: active
prune (dropped from list) → v3 status: removed
apply (re-declared) → v4 status: active
MIT — see LICENSE.txt.