diff --git a/Makefile b/Makefile
index 9605e62..143f3e1 100644
--- a/Makefile
+++ b/Makefile
@@ -24,7 +24,17 @@ docs-check:
python3 scripts/gen_overview.py --category hardware
python3 scripts/gen_overview.py --category services
python3 scripts/gen_rack.py
- git diff --exit-code docs/hardware/index.md docs/services/index.md docs/infrastructure/racks/
+ @git diff --exit-code docs/hardware/index.md docs/services/index.md docs/infrastructure/racks/ \
+ || { \
+ echo; \
+ echo "✗ The generated docs are out of date with the source files."; \
+ echo " The diff above is what 'make docs-index' just regenerated."; \
+ echo " This is what CI checks on push. To fix it:"; \
+ echo " 1. run 'make docs-index'"; \
+ echo " 2. commit the changed files (including the generated ones)"; \
+ echo " Guide: https://docs.makerfloss.eu/guides/editing-hardware-docs/"; \
+ exit 1; \
+ }
slides:
./build-slides.sh
diff --git a/docs/guides/hardware-naming-scheme.md b/docs/guides/hardware-naming-scheme.md
index 1cd0506..5977fac 100644
--- a/docs/guides/hardware-naming-scheme.md
+++ b/docs/guides/hardware-naming-scheme.md
@@ -52,6 +52,7 @@ The prefix equals the `kind` field, so the name is self-describing.
| PDU / power strip | `pdu` | `pdu` | `pdu01` |
| UPS | `ups` | `ups` | `ups01` |
| Shelf | `shelf` | `shf` | `shf01` |
+| WAN uplink / ISP demarcation | `wan` | `wan` | `wan01` |
Existing enum kinds not yet in the rack reuse their natural short form when
first used (`ap` → `ap`, `kvm` → `kvm`, `sbc` → `sbc`, `laptop` → `lt`,
diff --git a/docs/hardware/index.md b/docs/hardware/index.md
index 201e07d..5579dbb 100644
--- a/docs/hardware/index.md
+++ b/docs/hardware/index.md
@@ -25,7 +25,7 @@ _Auto-generated from `docs/hardware/*.md` — do not edit by hand. Run `make doc
| [makerfloss.eu](makerfloss.eu.md) | Hetzner HEL1 (cloud) | AMD EPYC (shared vCPU) · 2c | 4 GB | 40 GB NVME | 1 GbE | in-use |
| [nas01](nas01.md) | The pile | ? | ? | ? | ? | staging |
| [nas02](nas02.md) | The pile | ? | ? | ? | ? | staging |
-| [srv01](srv01.md) | The pile | ? | ? | ? | ? | staging |
+| [srv01](srv01.md) | The pile | ? | ? | ? | ? | in-use |
| [srv02](srv02.md) | The pile | Intel Core i5-8500 @ 3.00GHz · 6c | 8 GB | 40 GB NVME | 1 GbE | staging |
| [srv03](srv03.md) | The pile | Intel Core i5-8500 @ 3.00GHz · 6c | 16 GB | 40 GB NVME | 1 GbE | staging |
| [srv04](srv04.md) | The pile | Intel Core i5-3570K @ 3.40GHz · 4c | 8 GB | 500 GB HDD | 1 GbE | staging |
@@ -38,13 +38,30 @@ _Auto-generated from `docs/hardware/*.md` — do not edit by hand. Run `make doc
| Hostname | Location | CPU | RAM | Storage | NIC | Status |
|---|---|---|---|---|---|---|
| [shf01](shf01.md) | | | | | | in-use |
+| [shf02](shf02.md) | | | | | | in-use |
+| [shf03](shf03.md) | | | | | | in-use |
+| [shf04](shf04.md) | | | | | | in-use |
## Switches
| Hostname | Location | CPU | RAM | Storage | NIC | Status |
|---|---|---|---|---|---|---|
| [sw01](sw01.md) | | | | | | in-use |
-| [sw02](sw02.md) | | | | | | in-use |
-| [sw03](sw03.md) | | | | | | in-use |
-| [sw04](sw04.md) | | | | | | in-use |
-| [sw05](sw05.md) | | | | | | in-use |
+| [sw02](sw02.md) | | | | | | staging |
+| [sw03](sw03.md) | | | | | | staging |
+| [sw04](sw04.md) | | | | | | staging |
+| [sw05](sw05.md) | | | | | | staging |
+
+## UPS
+
+| Hostname | Location | CPU | RAM | Storage | NIC | Status |
+|---|---|---|---|---|---|---|
+| [ups01](ups01.md) | | | | | | staging |
+| [ups02](ups02.md) | | | | | | staging |
+
+## WAN uplinks
+
+| Hostname | Location | CPU | RAM | Storage | NIC | Status |
+|---|---|---|---|---|---|---|
+| [wan01](wan01.md) | ISP demarcation | | | | | in-use |
+| [wan02](wan02.md) | ISP demarcation | | | | | staging |
diff --git a/docs/hardware/nas01.md b/docs/hardware/nas01.md
index 5fcc112..6ac505a 100644
--- a/docs/hardware/nas01.md
+++ b/docs/hardware/nas01.md
@@ -11,14 +11,13 @@ ram_gb: "?"
storage: "?"
nic_gbps: "?"
rack: rack01
-mounted_on: shf01
-shelf_face: front
-shelf_slot: 8
+rack_u: 6
+u_height: 1
+rack_face: front
power:
- { pdu: pdu01, outlet: 1 }
- { pdu: pdu02, outlet: 1 }
-links:
- - { local: eth0, peer: sw01, peer_port: 1, speed_gbps: 1 }
+# links: cabling TBD — to be documented during network wiring
---
## Notes
diff --git a/docs/hardware/nas02.md b/docs/hardware/nas02.md
index 62a4f3d..0dfa34e 100644
--- a/docs/hardware/nas02.md
+++ b/docs/hardware/nas02.md
@@ -11,14 +11,13 @@ ram_gb: "?"
storage: "?"
nic_gbps: "?"
rack: rack01
-mounted_on: shf01
-shelf_face: front
-shelf_slot: 9
+rack_u: 7
+u_height: 1
+rack_face: front
power:
- { pdu: pdu01, outlet: 1 }
- { pdu: pdu02, outlet: 1 }
-links:
- - { local: eth0, peer: sw01, peer_port: 1, speed_gbps: 1 }
+# links: cabling TBD — to be documented during network wiring
---
## Notes
diff --git a/docs/hardware/pdu01.md b/docs/hardware/pdu01.md
index aad64e8..7541d6c 100644
--- a/docs/hardware/pdu01.md
+++ b/docs/hardware/pdu01.md
@@ -4,7 +4,7 @@ kind: pdu
status: in-use
rack: rack01
rack_face: rear
-outlets: 8
+outlets: 9
rack_u: 1
u_height: 1
---
diff --git a/docs/hardware/pdu02.md b/docs/hardware/pdu02.md
index 4ef2943..d5ff0ae 100644
--- a/docs/hardware/pdu02.md
+++ b/docs/hardware/pdu02.md
@@ -4,7 +4,7 @@ kind: pdu
status: in-use
rack: rack01
rack_face: rear
-outlets: 8
+outlets: 5
rack_u: 12
u_height: 1
---
diff --git a/docs/hardware/pdu03.md b/docs/hardware/pdu03.md
index 2a7cf02..ac8d01d 100644
--- a/docs/hardware/pdu03.md
+++ b/docs/hardware/pdu03.md
@@ -4,8 +4,8 @@ kind: pdu
status: in-use
rack: rack01
rack_face: rear
-outlets: 12
-rack_u: 33
+outlets: 11
+rack_u: 34
u_height: 1
---
diff --git a/docs/hardware/pdu04.md b/docs/hardware/pdu04.md
index 3470a22..786fb70 100644
--- a/docs/hardware/pdu04.md
+++ b/docs/hardware/pdu04.md
@@ -5,7 +5,7 @@ status: in-use
rack: rack01
rack_face: front
outlets: 5
-rack_u: 11
+rack_u: 12
u_height: 1
---
diff --git a/docs/hardware/pp01.md b/docs/hardware/pp01.md
index 0aeddc5..3080481 100644
--- a/docs/hardware/pp01.md
+++ b/docs/hardware/pp01.md
@@ -3,14 +3,18 @@ hostname: pp01
kind: patch-panel
status: in-use
rack: rack01
-rack_u: 25
+rack_u: 24
u_height: 1
rack_face: front
-ports: 16
+ports: 9
links:
- - { local: uplink, peer: sw01, peer_port: 24, speed_gbps: 1 }
+ - { local: "1", peer: wan01, peer_port: 1 }
---
## Notes
-- Link are placeholder values
+- Port 1 → wan01 (WAN line 1, active). Fed from pp02:1 (srv01 eth0).
+- Port 2 → wan02 (WAN line 2), set up but non-active.
+- Port 3 → working table (white cable), non-active.
+- Port 4 ← sw01:8 (switch management), patched out to the working table
+ (black cable), non-active.
diff --git a/docs/hardware/pp02.md b/docs/hardware/pp02.md
index fb3489e..fbc3455 100644
--- a/docs/hardware/pp02.md
+++ b/docs/hardware/pp02.md
@@ -3,13 +3,24 @@ hostname: pp02
kind: patch-panel
status: in-use
rack: rack01
-rack_u: 26
+rack_u: 25
u_height: 1
rack_face: front
+ports: 24
links:
- - { local: uplink, peer: sw01, peer_port: 24, speed_gbps: 1 }
+ - { local: "1", peer: pp01, peer_port: 1 }
+ - { local: "2", peer: sw01, peer_port: 1 }
+ - { local: "3", peer: sw01, peer_port: 2 }
+ - { local: "4", peer: sw01, peer_port: 3 }
+ - { local: "5", peer: sw01, peer_port: 4 }
+ - { local: "6", peer: sw01, peer_port: 5 }
+ - { local: "7", peer: sw01, peer_port: 6 }
+ - { local: "8", peer: sw01, peer_port: 7 }
---
## Notes
-- Reconstructed from committed rack artifacts; placeholder values.
+- 24-port patch panel; ports 1–8 are the live feeds.
+- Port 1 → pp01:1 → wan01 (srv01 eth0, WAN).
+- Ports 2–8 → sw01:1–7 (LAN): srv01 eth1 (p2), srv02 (p3), srv03 (p4),
+ srv04 (p5), srv05 (p6), srv06 (p7), srv07 (p8).
diff --git a/docs/hardware/shf01.md b/docs/hardware/shf01.md
index fb56738..4b0af23 100644
--- a/docs/hardware/shf01.md
+++ b/docs/hardware/shf01.md
@@ -3,12 +3,15 @@ hostname: shf01
kind: shelf
status: in-use
rack: rack01
-rack_u: 37
-u_height: 10
+rack_u: 46
+u_height: 1
rack_face: both
cluster: tappaas
---
## Notes
-- Provisional placeholder shelf holding the TaPPaaS nodes (srv01/srv02 front, srv03 rear).
+- 1U full-depth tray at U46. Tower PCs stand on it and rise above U46; they are
+ not rail-mounted, so the U's above are not consumed in the rack model.
+- Front: srv01 (stands ~U37–U46), srv02 (~U39–U46).
+- Rear: srv03 (~U40–U46).
diff --git a/docs/hardware/shf02.md b/docs/hardware/shf02.md
new file mode 100644
index 0000000..dc4854f
--- /dev/null
+++ b/docs/hardware/shf02.md
@@ -0,0 +1,17 @@
+---
+hostname: shf02
+kind: shelf
+status: in-use
+rack: rack01
+rack_u: 35
+u_height: 1
+rack_face: both
+---
+
+## Notes
+
+- 1U full-depth tray at U35. Tower PCs stand on it and rise above U35; they are
+ not rail-mounted, so the U's above are not consumed in the rack model
+ (e.g. pdu03 sits at U34, just above this shelf).
+- Front: srv07 (stands ~U29–U35), srv04 (~U27–U35).
+- Rear: srv05 (~U27–U35), srv06 (~U27–U35).
diff --git a/docs/hardware/shf03.md b/docs/hardware/shf03.md
new file mode 100644
index 0000000..2b46f01
--- /dev/null
+++ b/docs/hardware/shf03.md
@@ -0,0 +1,14 @@
+---
+hostname: shf03
+kind: shelf
+status: in-use
+rack: rack01
+rack_u: 21
+u_height: 1
+rack_face: front
+---
+
+## Notes
+
+- Half-depth shelf at U21 (front), currently empty.
+- Paired with shf04 (rear half-depth at the same U).
diff --git a/docs/hardware/shf04.md b/docs/hardware/shf04.md
new file mode 100644
index 0000000..d533e07
--- /dev/null
+++ b/docs/hardware/shf04.md
@@ -0,0 +1,14 @@
+---
+hostname: shf04
+kind: shelf
+status: in-use
+rack: rack01
+rack_u: 21
+u_height: 1
+rack_face: rear
+---
+
+## Notes
+
+- Half-depth shelf at U21 (rear), currently empty.
+- Paired with shf03 (front half-depth at the same U).
diff --git a/docs/hardware/srv01.md b/docs/hardware/srv01.md
index 2c5a072..991fd0f 100644
--- a/docs/hardware/srv01.md
+++ b/docs/hardware/srv01.md
@@ -1,7 +1,7 @@
---
hostname: srv01
kind: server
-status: staging
+status: in-use
cluster: tappaas
location: The pile
cpu: "?"
@@ -16,9 +16,9 @@ shelf_face: front
shelf_slot: 1
power:
- { pdu: pdu01, outlet: 1 }
- - { pdu: pdu02, outlet: 1 }
links:
- - { local: eth0, peer: sw01, peer_port: 1, speed_gbps: 1 }
+ - { local: eth0, peer: pp02, peer_port: 1, speed_gbps: 1 }
+ - { local: eth1, peer: pp02, peer_port: 2, speed_gbps: 1 }
---
## Notes
diff --git a/docs/hardware/srv02.md b/docs/hardware/srv02.md
index fcfc938..be16066 100644
--- a/docs/hardware/srv02.md
+++ b/docs/hardware/srv02.md
@@ -19,7 +19,7 @@ shelf_slot: 2
power:
- { pdu: pdu01, outlet: 2 }
links:
- - { local: eth0, peer: pp01, peer_port: 1, speed_gbps: 1 }
+ - { local: eth0, peer: pp02, peer_port: 3, speed_gbps: 1 }
---
## Notes
diff --git a/docs/hardware/srv03.md b/docs/hardware/srv03.md
index 7cd2e3d..6a64f4c 100644
--- a/docs/hardware/srv03.md
+++ b/docs/hardware/srv03.md
@@ -19,7 +19,7 @@ shelf_slot: 1
power:
- { pdu: pdu01, outlet: 3 }
links:
- - { local: eth0, peer: pp01, peer_port: 2, speed_gbps: 1 }
+ - { local: eth0, peer: pp02, peer_port: 4, speed_gbps: 1 }
---
## Notes
diff --git a/docs/hardware/srv04.md b/docs/hardware/srv04.md
index 2cd2da7..f0f62f1 100644
--- a/docs/hardware/srv04.md
+++ b/docs/hardware/srv04.md
@@ -11,13 +11,13 @@ storage_gb: 500
storage_type: hdd
nic_gbps: 1
rack: rack01
-rack_u: 5
-u_height: 2
-rack_face: front
+mounted_on: shf02
+shelf_face: front
+shelf_slot: 2
power:
- { pdu: pdu01, outlet: 4 }
links:
- - { local: eth0, peer: pp01, peer_port: 3, speed_gbps: 1 }
+ - { local: eth0, peer: pp02, peer_port: 5, speed_gbps: 1 }
---
## Notes
diff --git a/docs/hardware/srv05.md b/docs/hardware/srv05.md
index 0dc505f..3d6f819 100644
--- a/docs/hardware/srv05.md
+++ b/docs/hardware/srv05.md
@@ -11,13 +11,13 @@ storage_gb: 500
storage_type: hdd
nic_gbps: 1
rack: rack01
-rack_u: 5
-u_height: 2
-rack_face: rear
+mounted_on: shf02
+shelf_face: rear
+shelf_slot: 1
power:
- { pdu: pdu01, outlet: 5 }
links:
- - { local: eth0, peer: pp01, peer_port: 4, speed_gbps: 1 }
+ - { local: eth0, peer: pp02, peer_port: 6, speed_gbps: 1 }
---
## Notes
diff --git a/docs/hardware/srv06.md b/docs/hardware/srv06.md
index 1dc4abc..c688f0d 100644
--- a/docs/hardware/srv06.md
+++ b/docs/hardware/srv06.md
@@ -11,14 +11,14 @@ ram_gb: "?"
storage: "?"
nic_gbps: "?"
rack: rack01
-mounted_on: shf01
-shelf_face: front
-shelf_slot: 6
+mounted_on: shf02
+shelf_face: rear
+shelf_slot: 2
power:
- { pdu: pdu01, outlet: 1 }
- { pdu: pdu02, outlet: 1 }
links:
- - { local: eth0, peer: sw01, peer_port: 1, speed_gbps: 1 }
+ - { local: eth0, peer: pp02, peer_port: 7, speed_gbps: 1 }
---
## Notes
diff --git a/docs/hardware/srv07.md b/docs/hardware/srv07.md
index 925338e..ab29b9c 100644
--- a/docs/hardware/srv07.md
+++ b/docs/hardware/srv07.md
@@ -11,14 +11,14 @@ ram_gb: "?"
storage: "?"
nic_gbps: "?"
rack: rack01
-mounted_on: shf01
+mounted_on: shf02
shelf_face: front
-shelf_slot: 7
+shelf_slot: 1
power:
- { pdu: pdu01, outlet: 1 }
- { pdu: pdu02, outlet: 1 }
links:
- - { local: eth0, peer: sw01, peer_port: 1, speed_gbps: 1 }
+ - { local: eth0, peer: pp02, peer_port: 8, speed_gbps: 1 }
---
## Notes
diff --git a/docs/hardware/sw01.md b/docs/hardware/sw01.md
index aa6a6ca..4e22ff0 100644
--- a/docs/hardware/sw01.md
+++ b/docs/hardware/sw01.md
@@ -3,12 +3,15 @@ hostname: sw01
kind: switch
status: in-use
rack: rack01
-rack_u: 8
+rack_u: 23
u_height: 1
rack_face: front
-ports: 32
+ports: 10
---
## Notes
-- Provisional placeholder switch. Port assignments are not yet real.
+- 10 ports: p1–p8 are 1 GbE; sfp1–sfp2 are 2.5 GbE SFP+ (unused today).
+- p1–p7 carry server uplinks via pp02 (pp02:2–8 → sw01:1–7).
+- p8 is the management port, patched out via pp01:4 to the working table
+ (black cable) — non-active.
diff --git a/docs/hardware/sw02.md b/docs/hardware/sw02.md
index 52adb65..40f70f2 100644
--- a/docs/hardware/sw02.md
+++ b/docs/hardware/sw02.md
@@ -1,7 +1,7 @@
---
hostname: sw02
kind: switch
-status: in-use
+status: staging
rack: rack01
rack_u: 9
u_height: 1
diff --git a/docs/hardware/sw03.md b/docs/hardware/sw03.md
index d8d882e..ba30070 100644
--- a/docs/hardware/sw03.md
+++ b/docs/hardware/sw03.md
@@ -1,7 +1,7 @@
---
hostname: sw03
kind: switch
-status: in-use
+status: staging
rack: rack01
rack_u: 10
u_height: 1
diff --git a/docs/hardware/sw04.md b/docs/hardware/sw04.md
index a80acea..9a787be 100644
--- a/docs/hardware/sw04.md
+++ b/docs/hardware/sw04.md
@@ -1,9 +1,9 @@
---
hostname: sw04
kind: switch
-status: in-use
+status: staging
rack: rack01
-rack_u: 32
+rack_u: 5
u_height: 1
rack_face: front
---
diff --git a/docs/hardware/sw05.md b/docs/hardware/sw05.md
index 490ad0c..0676219 100644
--- a/docs/hardware/sw05.md
+++ b/docs/hardware/sw05.md
@@ -1,9 +1,9 @@
---
hostname: sw05
kind: switch
-status: in-use
+status: staging
rack: rack01
-rack_u: 36
+rack_u: 8
u_height: 1
rack_face: front
---
diff --git a/docs/hardware/ups01.md b/docs/hardware/ups01.md
new file mode 100644
index 0000000..5f18221
--- /dev/null
+++ b/docs/hardware/ups01.md
@@ -0,0 +1,13 @@
+---
+hostname: ups01
+kind: ups
+status: staging
+rack: rack01
+rack_u: 4
+u_height: 1
+rack_face: front
+---
+
+## Notes
+
+-
diff --git a/docs/hardware/ups02.md b/docs/hardware/ups02.md
new file mode 100644
index 0000000..f7c2fb8
--- /dev/null
+++ b/docs/hardware/ups02.md
@@ -0,0 +1,13 @@
+---
+hostname: ups02
+kind: ups
+status: staging
+rack: rack01
+rack_u: 3
+u_height: 1
+rack_face: front
+---
+
+## Notes
+
+-
diff --git a/docs/hardware/wan01.md b/docs/hardware/wan01.md
new file mode 100644
index 0000000..3b51868
--- /dev/null
+++ b/docs/hardware/wan01.md
@@ -0,0 +1,19 @@
+---
+hostname: wan01
+kind: wan
+status: in-use
+location: ISP demarcation
+ports: 1
+---
+
+## Notes
+
+- External WAN uplink — the upstream/ISP side of the WAN cable, where the
+ MakerFLOSS network meets the provider.
+- Not racked (no `rack:`), like the cloud-FQDN exception in the naming scheme.
+ It exists so the WAN cable has a real peer to terminate on.
+- Path into the rack: `wan01:1 ← pp01:1 ← pp02:1 ← srv01 eth0`.
+
+## ToDo
+
+- Confirm provider / circuit details for this handoff.
diff --git a/docs/hardware/wan02.md b/docs/hardware/wan02.md
new file mode 100644
index 0000000..2bb46c9
--- /dev/null
+++ b/docs/hardware/wan02.md
@@ -0,0 +1,13 @@
+---
+hostname: wan02
+kind: wan
+status: staging
+location: ISP demarcation
+ports: 1
+---
+
+## Notes
+
+- Second WAN uplink line ("wan 2"), set up but not yet active.
+- Patched from pp01:2 (non-active), so it does not yet appear in the live
+ network diagram.
diff --git a/docs/infrastructure/racks/rack01-elevation.svg b/docs/infrastructure/racks/rack01-elevation.svg
index cc338d9..7a6d89a 100644
--- a/docs/infrastructure/racks/rack01-elevation.svg
+++ b/docs/infrastructure/racks/rack01-elevation.svg
@@ -202,107 +202,135 @@
pdu01 (U1)
-
-srv04 · server · staging · cluster: — · U5–U6
-
-srv04 (U5–U6)
+
+ups02 · ups · staging · cluster: — · U3
+
+ups02 (U3)
-
-srv05 · server · staging · cluster: — · U5–U6
-
-srv05 (U5–U6)
+
+ups01 · ups · staging · cluster: — · U4
+
+ups01 (U4)
-
-sw01 · switch · in-use · cluster: — · U8
-
-sw01 (U8)
+
+sw04 · switch · staging · cluster: — · U5
+
+sw04 (U5)
+
+
+nas01 · server · staging · cluster: tappaas · U6
+
+nas01 (U6)
+
+
+nas02 · server · staging · cluster: tappaas · U7
+
+nas02 (U7)
+
+
+sw05 · switch · staging · cluster: — · U8
+
+sw05 (U8)
-sw02 · switch · in-use · cluster: — · U9
-
+sw02 · switch · staging · cluster: — · U9
+
sw02 (U9)
-sw03 · switch · in-use · cluster: — · U10
-
+sw03 · switch · staging · cluster: — · U10
+
sw03 (U10)
-
-pdu04 · pdu · in-use · cluster: — · U11
-
-pdu04 (U11)
-
pdu02 · pdu · in-use · cluster: — · U12
pdu02 (U12)
+
+pdu04 · pdu · in-use · cluster: — · U12
+
+pdu04 (U12)
+
+
+sw01 · switch · in-use · cluster: — · U23
+
+sw01 (U23)
+
-pp01 · patch-panel · in-use · cluster: — · U25
-
-pp01 (U25)
+pp01 · patch-panel · in-use · cluster: — · U24
+
+pp01 (U24)
-pp02 · patch-panel · in-use · cluster: — · U26
-
-pp02 (U26)
-
-
-sw04 · switch · in-use · cluster: — · U32
-
-sw04 (U32)
+pp02 · patch-panel · in-use · cluster: — · U25
+
+pp02 (U25)
-pdu03 · pdu · in-use · cluster: — · U33
-
-pdu03 (U33)
-
-
-sw05 · switch · in-use · cluster: — · U36
-
-sw05 (U36)
+pdu03 · pdu · in-use · cluster: — · U34
+
+pdu03 (U34)
-srv01 · server · staging · cluster: tappaas · shf01/front/slot 1
-
-srv01
+srv01 · server · in-use · cluster: tappaas · shf01/front/slot 1
+
+srv01
srv02 · server · staging · cluster: tappaas · shf01/front/slot 2
-
-srv02
-
-
-srv06 · server · staging · cluster: tappaas · shf01/front/slot 6
-
-srv06
-
-
-srv07 · server · staging · cluster: tappaas · shf01/front/slot 7
-
-srv07
-
-
-nas01 · server · staging · cluster: tappaas · shf01/front/slot 8
-
-nas01
-
-
-nas02 · server · staging · cluster: tappaas · shf01/front/slot 9
-
-nas02
+
+srv02
srv03 · server · staging · cluster: tappaas · shf01/rear/slot 1
-
-srv03
+
+srv03
-shf01 · shelf · in-use · cluster: tappaas · U37–U46
+shf01 · shelf · in-use · cluster: tappaas · U46
shf01
+
+srv07 · server · staging · cluster: tappaas · shf02/front/slot 1
+
+srv07
+
+
+srv04 · server · staging · cluster: — · shf02/front/slot 2
+
+srv04
+
+
+srv05 · server · staging · cluster: — · shf02/rear/slot 1
+
+srv05
+
+
+srv06 · server · staging · cluster: tappaas · shf02/rear/slot 2
+
+srv06
+
+
+shf02 · shelf · in-use · cluster: — · U35
+
+
+shf02
+
+
+shf03 · shelf · in-use · cluster: — · U21
+
+
+shf03
+
+
+shf04 · shelf · in-use · cluster: — · U21
+
+
+shf04
+
Legend
patch-panel
@@ -314,6 +342,8 @@
shelf
switch
+
+ups
in-use
diff --git a/docs/infrastructure/racks/rack01.md b/docs/infrastructure/racks/rack01.md
index 6acce9a..27d9b11 100644
--- a/docs/infrastructure/racks/rack01.md
+++ b/docs/infrastructure/racks/rack01.md
@@ -209,107 +209,135 @@ _Auto-generated from `docs/hardware/*.md` (items with `rack: rack01`) — do not
pdu01 (U1)
-
-srv04 · server · staging · cluster: — · U5–U6
-
-srv04 (U5–U6)
+
+ups02 · ups · staging · cluster: — · U3
+
+ups02 (U3)
-
-srv05 · server · staging · cluster: — · U5–U6
-
-srv05 (U5–U6)
+
+ups01 · ups · staging · cluster: — · U4
+
+ups01 (U4)
-
-sw01 · switch · in-use · cluster: — · U8
-
-sw01 (U8)
+
+sw04 · switch · staging · cluster: — · U5
+
+sw04 (U5)
+
+
+nas01 · server · staging · cluster: tappaas · U6
+
+nas01 (U6)
+
+
+nas02 · server · staging · cluster: tappaas · U7
+
+nas02 (U7)
+
+
+sw05 · switch · staging · cluster: — · U8
+
+sw05 (U8)
-sw02 · switch · in-use · cluster: — · U9
-
+sw02 · switch · staging · cluster: — · U9
+
sw02 (U9)
-sw03 · switch · in-use · cluster: — · U10
-
+sw03 · switch · staging · cluster: — · U10
+
sw03 (U10)
-
-pdu04 · pdu · in-use · cluster: — · U11
-
-pdu04 (U11)
-
pdu02 · pdu · in-use · cluster: — · U12
pdu02 (U12)
+
+pdu04 · pdu · in-use · cluster: — · U12
+
+pdu04 (U12)
+
+
+sw01 · switch · in-use · cluster: — · U23
+
+sw01 (U23)
+
-pp01 · patch-panel · in-use · cluster: — · U25
-
-pp01 (U25)
+pp01 · patch-panel · in-use · cluster: — · U24
+
+pp01 (U24)
-pp02 · patch-panel · in-use · cluster: — · U26
-
-pp02 (U26)
-
-
-sw04 · switch · in-use · cluster: — · U32
-
-sw04 (U32)
+pp02 · patch-panel · in-use · cluster: — · U25
+
+pp02 (U25)
-pdu03 · pdu · in-use · cluster: — · U33
-
-pdu03 (U33)
-
-
-sw05 · switch · in-use · cluster: — · U36
-
-sw05 (U36)
+pdu03 · pdu · in-use · cluster: — · U34
+
+pdu03 (U34)
-srv01 · server · staging · cluster: tappaas · shf01/front/slot 1
-
-srv01
+srv01 · server · in-use · cluster: tappaas · shf01/front/slot 1
+
+srv01
srv02 · server · staging · cluster: tappaas · shf01/front/slot 2
-
-srv02
-
-
-srv06 · server · staging · cluster: tappaas · shf01/front/slot 6
-
-srv06
-
-
-srv07 · server · staging · cluster: tappaas · shf01/front/slot 7
-
-srv07
-
-
-nas01 · server · staging · cluster: tappaas · shf01/front/slot 8
-
-nas01
-
-
-nas02 · server · staging · cluster: tappaas · shf01/front/slot 9
-
-nas02
+
+srv02
srv03 · server · staging · cluster: tappaas · shf01/rear/slot 1
-
-srv03
+
+srv03
-shf01 · shelf · in-use · cluster: tappaas · U37–U46
+shf01 · shelf · in-use · cluster: tappaas · U46
shf01
+
+srv07 · server · staging · cluster: tappaas · shf02/front/slot 1
+
+srv07
+
+
+srv04 · server · staging · cluster: — · shf02/front/slot 2
+
+srv04
+
+
+srv05 · server · staging · cluster: — · shf02/rear/slot 1
+
+srv05
+
+
+srv06 · server · staging · cluster: tappaas · shf02/rear/slot 2
+
+srv06
+
+
+shf02 · shelf · in-use · cluster: — · U35
+
+
+shf02
+
+
+shf03 · shelf · in-use · cluster: — · U21
+
+
+shf03
+
+
+shf04 · shelf · in-use · cluster: — · U21
+
+
+shf04
+
Legend
patch-panel
@@ -321,6 +349,8 @@ _Auto-generated from `docs/hardware/*.md` (items with `rack: rack01`) — do not
shelf
switch
+
+ups
in-use
@@ -338,19 +368,19 @@ _Auto-generated from `docs/hardware/*.md` (items with `rack: rack01`) — do not
```mermaid
flowchart LR
- pdu01["pdu01
8 outlets"]
- pdu02["pdu02
8 outlets"]
- pdu03["pdu03
12 outlets"]
+ pdu01["pdu01
9 outlets"]
+ pdu02["pdu02
5 outlets"]
+ pdu03["pdu03
11 outlets"]
pdu04["pdu04
5 outlets"]
- nas01["nas01"]
- nas02["nas02"]
srv01["srv01"]
srv02["srv02"]
srv03["srv03"]
- srv06["srv06"]
- srv07["srv07"]
srv04["srv04"]
srv05["srv05"]
+ srv06["srv06"]
+ srv07["srv07"]
+ nas01["nas01"]
+ nas02["nas02"]
pdu01 -->|outlet 1| nas01
pdu01 -->|outlet 1| nas02
pdu01 -->|outlet 1| srv01
@@ -362,7 +392,6 @@ flowchart LR
pdu01 -->|outlet 5| srv05
pdu02 -->|outlet 1| nas01
pdu02 -->|outlet 1| nas02
- pdu02 -->|outlet 1| srv01
pdu02 -->|outlet 1| srv06
pdu02 -->|outlet 1| srv07
style nas01 fill:#4c78a8,stroke:#333,color:#ffffff
@@ -397,8 +426,6 @@ flowchart LR
```mermaid
flowchart LR
- nas01["nas01"]
- nas02["nas02"]
pp01["pp01
patch-panel"]
pp02["pp02
patch-panel"]
srv01["srv01"]
@@ -409,21 +436,24 @@ flowchart LR
srv06["srv06"]
srv07["srv07"]
sw01["sw01
switch"]
- nas01 -->|eth0 → p1 · 1G| sw01
- nas02 -->|eth0 → p1 · 1G| sw01
- pp01 -->|uplink → p24 · 1G| sw01
- pp02 -->|uplink → p24 · 1G| sw01
- srv01 -->|eth0 → p1 · 1G| sw01
- srv02 -->|eth0 → p1 · 1G| pp01
- srv03 -->|eth0 → p2 · 1G| pp01
- srv04 -->|eth0 → p3 · 1G| pp01
- srv05 -->|eth0 → p4 · 1G| pp01
- srv06 -->|eth0 → p1 · 1G| sw01
- srv07 -->|eth0 → p1 · 1G| sw01
- style nas01 fill:#4c78a8,stroke:#333,color:#ffffff
- click nas01 "/hardware/nas01/"
- style nas02 fill:#4c78a8,stroke:#333,color:#ffffff
- click nas02 "/hardware/nas02/"
+ wan01["wan01"]
+ pp01 -->|1 → p1| wan01
+ pp02 -->|1 → p1| pp01
+ pp02 -->|2 → p1| sw01
+ pp02 -->|3 → p2| sw01
+ pp02 -->|4 → p3| sw01
+ pp02 -->|5 → p4| sw01
+ pp02 -->|6 → p5| sw01
+ pp02 -->|7 → p6| sw01
+ pp02 -->|8 → p7| sw01
+ srv01 -->|eth0 → p1 · 1G| pp02
+ srv01 -->|eth1 → p2 · 1G| pp02
+ srv02 -->|eth0 → p3 · 1G| pp02
+ srv03 -->|eth0 → p4 · 1G| pp02
+ srv04 -->|eth0 → p5 · 1G| pp02
+ srv05 -->|eth0 → p6 · 1G| pp02
+ srv06 -->|eth0 → p7 · 1G| pp02
+ srv07 -->|eth0 → p8 · 1G| pp02
style pp01 fill:#9c755f,stroke:#333,color:#ffffff
click pp01 "/hardware/pp01/"
style pp02 fill:#9c755f,stroke:#333,color:#ffffff
@@ -444,6 +474,7 @@ flowchart LR
click srv07 "/hardware/srv07/"
style sw01 fill:#59a14f,stroke:#333,color:#ffffff
click sw01 "/hardware/sw01/"
+ style wan01 fill:#888888,stroke:#333,color:#ffffff
```
## Occupancy
@@ -451,23 +482,28 @@ flowchart LR
| U | Device | Kind | Face | Status |
|---|---|---|---|---|
| U1 | [pdu01](../../hardware/pdu01.md) | pdu | rear | in-use |
-| U5–U6 | [srv04](../../hardware/srv04.md) | server | front | staging |
-| U5–U6 | [srv05](../../hardware/srv05.md) | server | rear | staging |
-| U8 | [sw01](../../hardware/sw01.md) | switch | front | in-use |
-| U9 | [sw02](../../hardware/sw02.md) | switch | front | in-use |
-| U10 | [sw03](../../hardware/sw03.md) | switch | front | in-use |
-| U11 | [pdu04](../../hardware/pdu04.md) | pdu | front | in-use |
+| U3 | [ups02](../../hardware/ups02.md) | ups | front | staging |
+| U4 | [ups01](../../hardware/ups01.md) | ups | front | staging |
+| U5 | [sw04](../../hardware/sw04.md) | switch | front | staging |
+| U6 | [nas01](../../hardware/nas01.md) | server | front | staging |
+| U7 | [nas02](../../hardware/nas02.md) | server | front | staging |
+| U8 | [sw05](../../hardware/sw05.md) | switch | front | staging |
+| U9 | [sw02](../../hardware/sw02.md) | switch | front | staging |
+| U10 | [sw03](../../hardware/sw03.md) | switch | front | staging |
| U12 | [pdu02](../../hardware/pdu02.md) | pdu | rear | in-use |
-| U25 | [pp01](../../hardware/pp01.md) | patch-panel | front | in-use |
-| U26 | [pp02](../../hardware/pp02.md) | patch-panel | front | in-use |
-| U32 | [sw04](../../hardware/sw04.md) | switch | front | in-use |
-| U33 | [pdu03](../../hardware/pdu03.md) | pdu | rear | in-use |
-| U36 | [sw05](../../hardware/sw05.md) | switch | front | in-use |
-| U37–U46 | [shf01](../../hardware/shf01.md) | shelf | both | in-use |
-| U37–U46 | [srv01](../../hardware/srv01.md) | server | front · shf01/1 | staging |
-| U37–U46 | [srv02](../../hardware/srv02.md) | server | front · shf01/2 | staging |
-| U37–U46 | [srv06](../../hardware/srv06.md) | server | front · shf01/6 | staging |
-| U37–U46 | [srv07](../../hardware/srv07.md) | server | front · shf01/7 | staging |
-| U37–U46 | [nas01](../../hardware/nas01.md) | server | front · shf01/8 | staging |
-| U37–U46 | [nas02](../../hardware/nas02.md) | server | front · shf01/9 | staging |
-| U37–U46 | [srv03](../../hardware/srv03.md) | server | rear · shf01/1 | staging |
+| U12 | [pdu04](../../hardware/pdu04.md) | pdu | front | in-use |
+| U21 | [shf03](../../hardware/shf03.md) | shelf | front | in-use |
+| U21 | [shf04](../../hardware/shf04.md) | shelf | rear | in-use |
+| U23 | [sw01](../../hardware/sw01.md) | switch | front | in-use |
+| U24 | [pp01](../../hardware/pp01.md) | patch-panel | front | in-use |
+| U25 | [pp02](../../hardware/pp02.md) | patch-panel | front | in-use |
+| U34 | [pdu03](../../hardware/pdu03.md) | pdu | rear | in-use |
+| U35 | [shf02](../../hardware/shf02.md) | shelf | both | in-use |
+| U35 | [srv07](../../hardware/srv07.md) | server | front · shf02/1 | staging |
+| U35 | [srv04](../../hardware/srv04.md) | server | front · shf02/2 | staging |
+| U35 | [srv05](../../hardware/srv05.md) | server | rear · shf02/1 | staging |
+| U35 | [srv06](../../hardware/srv06.md) | server | rear · shf02/2 | staging |
+| U46 | [shf01](../../hardware/shf01.md) | shelf | both | in-use |
+| U46 | [srv01](../../hardware/srv01.md) | server | front · shf01/1 | in-use |
+| U46 | [srv02](../../hardware/srv02.md) | server | front · shf01/2 | staging |
+| U46 | [srv03](../../hardware/srv03.md) | server | rear · shf01/1 | staging |
diff --git a/scripts/gen_overview.py b/scripts/gen_overview.py
index d4e86da..6425987 100755
--- a/scripts/gen_overview.py
+++ b/scripts/gen_overview.py
@@ -22,11 +22,39 @@ CONFIG_PATH = REPO_ROOT / "scripts" / "overview_config.yml"
FRONTMATTER_RE = re.compile(r"^---\s*\n(.*?)\n---\s*\n", re.DOTALL)
+# Shown at the bottom of every error report so a newcomer knows where to look.
+GUIDE_URL = "https://docs.makerfloss.eu/guides/editing-hardware-docs/"
+
class SchemaError(Exception):
pass
+def _allowed_hint(field: str, enums: dict) -> str:
+ allowed = enums.get(field)
+ return f" Allowed values: {', '.join(map(str, allowed))}." if allowed else ""
+
+
+def _example_value(field: str, enums: dict) -> str:
+ allowed = enums.get(field)
+ return str(allowed[0]) if allowed else "..."
+
+
+def report_errors(errors: list[str], category: str) -> None:
+ """Print a collected list of problems with orientation for newcomers."""
+ print(
+ f"\ngen_overview: found {len(errors)} problem(s) in the {category} docs:",
+ file=sys.stderr,
+ )
+ for err in errors:
+ print(f" ✗ {err}", file=sys.stderr)
+ print(
+ "\nFix the field(s) named above, then run 'make docs-index' again.\n"
+ f"Guide: {GUIDE_URL}",
+ file=sys.stderr,
+ )
+
+
def parse_frontmatter(path: Path) -> dict | None:
text = path.read_text(encoding="utf-8")
m = FRONTMATTER_RE.match(text)
@@ -42,24 +70,33 @@ def parse_frontmatter(path: Path) -> dict | None:
def validate(path: Path, fm: dict, cfg: dict) -> None:
+ enums = cfg.get("enums", {})
+ name = path.name
for field in cfg["required_fields"]:
if field not in fm:
- raise SchemaError(f"{path}: missing required field '{field}'")
- for field, allowed in cfg.get("enums", {}).items():
+ raise SchemaError(
+ f"{name}: missing required field '{field}'. Add a line like "
+ f"'{field}: {_example_value(field, enums)}' to the frontmatter."
+ f"{_allowed_hint(field, enums)}"
+ )
+ for field, allowed in enums.items():
if field in fm and fm[field] not in allowed:
raise SchemaError(
- f"{path}: {field}={fm[field]!r} not in {allowed}"
+ f"{name}: {field} {fm[field]!r} is not allowed. "
+ f"Use one of: {', '.join(map(str, allowed))}."
)
key_field = cfg.get("key_field", "hostname")
if key_field not in fm:
raise SchemaError(
- f"{path}: missing key field {key_field!r}"
+ f"{name}: missing the '{key_field}' field (the device's id). It must "
+ f"match the filename, e.g. '{key_field}: {path.stem}'."
)
stem = path.stem
value = fm[key_field]
if stem != value:
raise SchemaError(
- f"{path}: filename stem {stem!r} != {key_field} {value!r}"
+ f"{name}: '{key_field}: {value}' does not match the filename '{name}'. "
+ f"Rename the file to '{value}.md', or set {key_field} to '{stem}'."
)
@@ -222,7 +259,11 @@ def main() -> int:
errors.append(str(e))
continue
if fm is None:
- print(f"WARNING: {path}: no YAML frontmatter, skipping", file=sys.stderr)
+ print(
+ f"WARNING: {path.name}: no '---' frontmatter block — skipping "
+ f"(it will not appear in the {args.category} index).",
+ file=sys.stderr,
+ )
continue
try:
validate(path, fm, cfg)
@@ -232,8 +273,7 @@ def main() -> int:
items.append(fm)
if errors:
- for err in errors:
- print(f"ERROR: {err}", file=sys.stderr)
+ report_errors(errors, args.category)
return 1
output_file.parent.mkdir(parents=True, exist_ok=True)
diff --git a/scripts/gen_rack.py b/scripts/gen_rack.py
index 1efaa44..5bdbaf8 100644
--- a/scripts/gen_rack.py
+++ b/scripts/gen_rack.py
@@ -29,6 +29,9 @@ FACES = {"front", "rear", "both", "left", "right"}
ZERO_U_FACES = {"left", "right"}
SHELF_FACES = {"front", "rear"}
+# Shown at the bottom of every error report so a newcomer knows where to look.
+GUIDE_URL = "https://docs.makerfloss.eu/guides/editing-hardware-docs/"
+
KIND_COLORS = {
"server": "#4c78a8",
"switch": "#59a14f",
@@ -76,45 +79,65 @@ def validate_item(fm: dict) -> None:
name = fm.get("hostname") or fm.get("_path", "?")
rack = fm.get("rack")
if not isinstance(rack, str) or not rack:
- raise SchemaError(f"{name}: rack must be a non-empty string")
+ raise SchemaError(f"{name}: 'rack' must name a rack, e.g. 'rack: rack01'.")
if "mounted_on" in fm:
mounted_on = fm.get("mounted_on")
if not isinstance(mounted_on, str) or not mounted_on:
- raise SchemaError(f"{name}: mounted_on must be a non-empty string")
+ raise SchemaError(
+ f"{name}: 'mounted_on' must name the shelf it sits on, "
+ f"e.g. 'mounted_on: shf01'."
+ )
for forbidden in ("rack_u", "u_height", "rack_face"):
if forbidden in fm:
raise SchemaError(
- f"{name}: mounted item must omit {forbidden}"
+ f"{name}: a shelf-mounted device must not set '{forbidden}' — "
+ f"it takes its position from the shelf. Use 'shelf_face' and "
+ f"'shelf_slot' instead."
)
sface = fm.get("shelf_face")
if sface not in SHELF_FACES:
raise SchemaError(
- f"{name}: shelf_face={sface!r} not in {sorted(SHELF_FACES)}"
+ f"{name}: shelf_face {sface!r} must be 'front' or 'rear'."
)
slot = fm.get("shelf_slot")
if not isinstance(slot, int) or slot < 1:
- raise SchemaError(f"{name}: shelf_slot must be an integer >= 1")
+ raise SchemaError(
+ f"{name}: 'shelf_slot' must be a whole number 1 or higher "
+ f"(got {slot!r})."
+ )
return
face = fm.get("rack_face")
if face not in FACES:
- raise SchemaError(f"{name}: rack_face={face!r} not in {sorted(FACES)}")
+ raise SchemaError(
+ f"{name}: rack_face {face!r} is not valid. Use 'front', 'rear' or "
+ f"'both' for a U-mounted device, or 'left'/'right' for a 0U side rail."
+ )
if face in ZERO_U_FACES:
if "rack_u" in fm or "u_height" in fm:
raise SchemaError(
- f"{name}: 0U item (face={face}) must omit rack_u/u_height"
+ f"{name}: a side-rail device (rack_face: {face}) is 0U — remove "
+ f"'rack_u' and 'u_height'."
)
return
u = fm.get("rack_u")
h = fm.get("u_height")
if not isinstance(u, int) or not isinstance(h, int):
- raise SchemaError(f"{name}: rack_u and u_height must be integers")
+ raise SchemaError(
+ f"{name}: a {face}-mounted device needs whole-number 'rack_u' and "
+ f"'u_height' (e.g. 'rack_u: 12' and 'u_height: 2')."
+ )
if u < 1 or u > RACK_UNITS:
- raise SchemaError(f"{name}: rack_u={u} out of range 1..{RACK_UNITS}")
+ raise SchemaError(
+ f"{name}: rack_u={u} is outside the rack — it must be between 1 "
+ f"and {RACK_UNITS}."
+ )
if h < 1:
- raise SchemaError(f"{name}: u_height={h} must be >= 1")
+ raise SchemaError(f"{name}: u_height={h} must be at least 1.")
if u + h - 1 > RACK_UNITS:
raise SchemaError(
- f"{name}: occupies U{u}..U{u + h - 1}, exceeds {RACK_UNITS}U"
+ f"{name}: a {h}U device starting at U{u} runs off the top of the rack "
+ f"(it would need U{u}–U{u + h - 1}, but the rack is only {RACK_UNITS}U). "
+ f"Lower 'rack_u' or 'u_height'."
)
@@ -136,7 +159,9 @@ def check_overlaps(items: list[dict]) -> None:
key = (f, uu)
if key in occupied:
raise SchemaError(
- f"U{uu} {f}: {name} overlaps {occupied[key]}"
+ f"U{uu} {f}: {name} overlaps {occupied[key]} — two devices "
+ f"can't share the same U on the same face. Move one to a "
+ f"free U or to the other face."
)
occupied[key] = name
@@ -157,23 +182,27 @@ def check_shelves(items: list[dict]) -> None:
target = by_host.get(shelf_name)
if target is None:
raise SchemaError(
- f"{name}: mounted_on={shelf_name!r} is not in this rack"
+ f"{name}: mounted_on={shelf_name!r} — no device with that id is in "
+ f"this rack. Check the shelf's hostname."
)
if target.get("kind") != "shelf":
raise SchemaError(
- f"{name}: mounted_on={shelf_name!r} is not a kind:shelf item"
+ f"{name}: mounted_on={shelf_name!r} is a {target.get('kind')!r}, "
+ f"not a shelf. Only kind:shelf devices can hold mounted gear."
)
if not isinstance(target.get("rack_u"), int) or not isinstance(
target.get("u_height"), int
):
raise SchemaError(
- f"{name}: shelf {shelf_name!r} is not placed (needs rack_u/u_height)"
+ f"{name}: the shelf {shelf_name!r} has no position yet — give it "
+ f"'rack_u' and 'u_height' first."
)
key = (shelf_name, fm["shelf_face"], fm["shelf_slot"])
if key in occupied:
raise SchemaError(
f"{shelf_name} {fm['shelf_face']} slot {fm['shelf_slot']}: "
- f"{name} overlaps {occupied[key]}"
+ f"{name} overlaps {occupied[key]} — each shelf face and slot holds "
+ f"one device."
)
occupied[key] = name
@@ -206,30 +235,44 @@ def validate_links(items: list[dict], hw_index: dict[str, dict]) -> None:
continue
name = fm.get("hostname", "?")
if not isinstance(links, list):
- raise SchemaError(f"{name}: links must be a list")
+ raise SchemaError(
+ f"{name}: 'links' must be a list of cables like "
+ f"'- {{ local: eth0, peer: sw01, peer_port: 1 }}'."
+ )
for link in links:
if not isinstance(link, dict):
- raise SchemaError(f"{name}: links entry must be a mapping")
+ raise SchemaError(
+ f"{name}: each 'links' entry must look like "
+ f"'{{ local: eth0, peer: sw01, peer_port: 1 }}'."
+ )
local = link.get("local")
peer = link.get("peer")
peer_port = link.get("peer_port")
if not isinstance(local, str) or not local:
- raise SchemaError(f"{name}: links entry needs a non-empty 'local'")
+ raise SchemaError(
+ f"{name}: a 'links' entry needs a 'local' port name, "
+ f"e.g. 'local: eth0'."
+ )
if not isinstance(peer, str) or not peer:
- raise SchemaError(f"{name}: links entry needs a non-empty 'peer'")
+ raise SchemaError(
+ f"{name}: a 'links' entry needs a 'peer' device, "
+ f"e.g. 'peer: sw01'."
+ )
if not isinstance(peer_port, int):
raise SchemaError(
- f"{name}: links entry for {peer} needs an integer 'peer_port'"
+ f"{name}: the link to {peer} needs a whole-number 'peer_port'."
)
target = hw_index.get(peer)
if target is None:
raise SchemaError(
- f"{name}: links peer={peer!r} is not a known hardware file"
+ f"{name}: link points at peer={peer!r}, but no hardware file "
+ f"has that id. Check the peer hostname."
)
ports = target.get("ports")
if isinstance(ports, int) and (peer_port < 1 or peer_port > ports):
raise SchemaError(
- f"{name}: peer_port {peer_port} out of range 1..{ports} on {peer}"
+ f"{name}: peer_port {peer_port} doesn't exist on {peer} — it "
+ f"has {ports} port(s) (valid 1–{ports})."
)
@@ -253,7 +296,7 @@ def validate_power(items: list[dict]) -> None:
outlets = fm.get("outlets")
if not isinstance(outlets, int) or outlets < 1:
raise SchemaError(
- f"{name}: kind:pdu must declare a positive integer 'outlets'"
+ f"{name}: a PDU must say how many outlets it has, e.g. 'outlets: 8'."
)
for fm in items:
feeds = fm.get("power")
@@ -261,27 +304,38 @@ def validate_power(items: list[dict]) -> None:
continue
name = fm.get("hostname", "?")
if not isinstance(feeds, list):
- raise SchemaError(f"{name}: power must be a list")
+ raise SchemaError(
+ f"{name}: 'power' must be a list of feeds like "
+ f"'- {{ pdu: pdu01, outlet: 1 }}'."
+ )
for feed in feeds:
if not isinstance(feed, dict):
- raise SchemaError(f"{name}: power entry must be a mapping")
+ raise SchemaError(
+ f"{name}: each 'power' feed must look like "
+ f"'{{ pdu: pdu01, outlet: 1 }}'."
+ )
pdu = feed.get("pdu")
outlet = feed.get("outlet")
if not isinstance(pdu, str) or not pdu:
- raise SchemaError(f"{name}: power entry needs a non-empty 'pdu'")
+ raise SchemaError(
+ f"{name}: a 'power' feed needs a 'pdu' name, "
+ f"e.g. '{{ pdu: pdu01, outlet: 1 }}'."
+ )
if not isinstance(outlet, int):
raise SchemaError(
- f"{name}: power entry for {pdu} needs an integer 'outlet'"
+ f"{name}: the 'power' feed to {pdu} needs a whole-number 'outlet'."
)
target = pdus.get(pdu)
if target is None:
raise SchemaError(
- f"{name}: power pdu={pdu!r} is not a known kind:pdu file"
+ f"{name}: power feed points at pdu={pdu!r}, but no kind:pdu "
+ f"device has that id. Check the PDU hostname."
)
count = target["outlets"]
if outlet < 1 or outlet > count:
raise SchemaError(
- f"{name}: outlet {outlet} out of range 1..{count} on {pdu}"
+ f"{name}: outlet {outlet} doesn't exist on {pdu} — it has "
+ f"{count} outlet(s) (valid 1–{count})."
)
@@ -752,6 +806,22 @@ def render_page(rack: str, items: list[dict]) -> str:
return "\n".join(lines).rstrip() + "\n"
+def report_errors(errors: list[str]) -> None:
+ """Print a collected list of problems with orientation for newcomers."""
+ print(
+ f"\ngen_rack: found {len(errors)} problem(s) in docs/hardware/:",
+ file=sys.stderr,
+ )
+ for err in errors:
+ print(f" ✗ {err}", file=sys.stderr)
+ print(
+ "\nEach line is ': what's wrong'. Fix the named frontmatter "
+ "field(s),\nthen run 'make docs-index' again.\n"
+ f"Guide: {GUIDE_URL}",
+ file=sys.stderr,
+ )
+
+
def generate(hardware_dir: Path, output_dir: Path) -> int:
items = load_rack_items(hardware_dir)
hw_index = load_hardware_index(hardware_dir)
@@ -778,8 +848,7 @@ def generate(hardware_dir: Path, output_dir: Path) -> int:
errors.append(f"{rack}: {e}")
if errors:
- for err in errors:
- print(f"ERROR: {err}", file=sys.stderr)
+ report_errors(errors)
return 1
output_dir.mkdir(parents=True, exist_ok=True)
diff --git a/scripts/overview_config.yml b/scripts/overview_config.yml
index be8f16e..93bdb2a 100644
--- a/scripts/overview_config.yml
+++ b/scripts/overview_config.yml
@@ -14,7 +14,7 @@ hardware:
- kind
- status
enums:
- kind: [server, laptop, sbc, switch, ap, desktop, pdu, patch-panel, shelf, blank, ups, kvm]
+ kind: [server, laptop, sbc, switch, ap, desktop, pdu, patch-panel, shelf, blank, ups, kvm, wan]
status: [in-use, staging, spare, broken, donated]
storage_type: [nvme, ssd, hdd, mixed]
group_by: kind
@@ -33,6 +33,7 @@ hardware:
blank: Blank panels
ups: UPS
kvm: KVM
+ wan: WAN uplinks
sort_by: hostname
columns:
- { header: Hostname, kind: key-link, field: hostname }
diff --git a/tests/test_gen_overview.py b/tests/test_gen_overview.py
new file mode 100644
index 0000000..445be05
--- /dev/null
+++ b/tests/test_gen_overview.py
@@ -0,0 +1,48 @@
+import pytest
+
+import gen_overview
+
+
+CFG = {
+ "required_fields": ["hostname", "kind", "status"],
+ "enums": {
+ "kind": ["server", "switch", "pdu"],
+ "status": ["in-use", "staging", "spare"],
+ },
+ "key_field": "hostname",
+}
+
+
+def validate(stem, fm):
+ from pathlib import Path
+ gen_overview.validate(Path(f"{stem}.md"), fm, CFG)
+
+
+def test_missing_required_field_names_field_and_lists_allowed():
+ with pytest.raises(gen_overview.SchemaError) as ei:
+ validate("srv06", {"hostname": "srv06", "kind": "server"})
+ msg = str(ei.value)
+ assert "status" in msg # which field
+ assert "in-use" in msg # an allowed value, so a novice knows what to type
+
+
+def test_enum_violation_lists_allowed_values():
+ with pytest.raises(gen_overview.SchemaError) as ei:
+ validate("x", {"hostname": "x", "kind": "router", "status": "in-use"})
+ msg = str(ei.value)
+ assert "router" in msg # the offending value
+ assert "server" in msg # an allowed value
+
+
+def test_filename_mismatch_explains_the_rename():
+ with pytest.raises(gen_overview.SchemaError) as ei:
+ validate("srv06", {"hostname": "srv07", "kind": "server", "status": "in-use"})
+ msg = str(ei.value).lower()
+ assert "srv07.md" in msg or "rename" in msg
+
+
+def test_error_report_points_to_the_guide(capsys):
+ gen_overview.report_errors(["srv06.md: missing required field 'status'"], "hardware")
+ err = capsys.readouterr().err
+ assert "make docs-index" in err
+ assert gen_overview.GUIDE_URL in err
diff --git a/tests/test_gen_rack.py b/tests/test_gen_rack.py
index 106fe9e..abccc49 100644
--- a/tests/test_gen_rack.py
+++ b/tests/test_gen_rack.py
@@ -744,3 +744,47 @@ def test_network_graph_off_rack_peer_has_no_click():
assert "style router0 fill:" in out # off-rack peer is still colored
assert 'click router0 "' not in out # but it is NOT clickable
assert 'click srv01 "/hardware/srv01/"' in out
+
+
+# --- novice-friendly error messages ---------------------------------------
+
+def test_overlap_message_explains_the_conflict():
+ items = [
+ item(hostname="a", rack_u=1, u_height=2, rack_face="front"),
+ item(hostname="b", rack_u=2, u_height=1, rack_face="front"),
+ ]
+ with pytest.raises(gen_rack.SchemaError) as ei:
+ gen_rack.check_overlaps(items)
+ msg = str(ei.value).lower()
+ assert "overlap" in msg
+ assert "same u" in msg or "free u" in msg or "other face" in msg # tells them how to fix
+
+
+def test_zero_u_message_tells_user_to_drop_units():
+ with pytest.raises(gen_rack.SchemaError) as ei:
+ gen_rack.validate_item(item(rack_face="left", rack_u=1, u_height=1))
+ msg = str(ei.value).lower()
+ assert "rack_u" in msg and "remove" in msg
+
+
+def test_bad_face_message_lists_valid_faces():
+ with pytest.raises(gen_rack.SchemaError) as ei:
+ gen_rack.validate_item(item(rack_u=1, u_height=1, rack_face="sideways"))
+ msg = str(ei.value)
+ assert "front" in msg and "left" in msg # both U-mounted and 0U options shown
+
+
+def test_generate_error_output_is_novice_friendly(tmp_path, capsys):
+ hw = tmp_path / "hardware"
+ out = tmp_path / "out"
+ hw.mkdir()
+ _write_item(
+ hw, "srv01",
+ "---\nhostname: srv01\nkind: server\nstatus: in-use\n"
+ "rack: rack01\nrack_face: front\n---\n", # front face but no rack_u/u_height
+ )
+ rc = gen_rack.generate(hw, out)
+ err = capsys.readouterr().err
+ assert rc == 1
+ assert "make docs-index" in err
+ assert gen_rack.GUIDE_URL in err