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