Compare commits
6 commits
9253d1ca0d
...
1b5e8316ea
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1b5e8316ea | ||
|
|
5c3dab55a4 | ||
|
|
d2744db4ee | ||
|
|
ed4e7c751a | ||
|
|
a45d6d0266 | ||
|
|
f4022edf3b |
15 changed files with 1201 additions and 159 deletions
|
|
@ -2,6 +2,13 @@
|
||||||
|
|
||||||
_Auto-generated from `docs/hardware/*.md` — do not edit by hand. Run `make docs-index` after changing a file._
|
_Auto-generated from `docs/hardware/*.md` — do not edit by hand. Run `make docs-index` after changing a file._
|
||||||
|
|
||||||
|
## PDUs
|
||||||
|
|
||||||
|
| Hostname | Location | CPU | RAM | Storage | NIC | Status |
|
||||||
|
|---|---|---|---|---|---|---|
|
||||||
|
| [pdu01](pdu01.md) | | | | | | in-use |
|
||||||
|
| [pdu02](pdu02.md) | | | | | | in-use |
|
||||||
|
|
||||||
## Servers
|
## Servers
|
||||||
|
|
||||||
| Hostname | Location | CPU | RAM | Storage | NIC | Status |
|
| Hostname | Location | CPU | RAM | Storage | NIC | Status |
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,9 @@ rack: rack01
|
||||||
rack_u: 1
|
rack_u: 1
|
||||||
u_height: 1
|
u_height: 1
|
||||||
rack_face: front
|
rack_face: front
|
||||||
|
power:
|
||||||
|
- { pdu: pdu01, outlet: 1 }
|
||||||
|
- { pdu: pdu02, outlet: 1 }
|
||||||
---
|
---
|
||||||
|
|
||||||
## Notes
|
## Notes
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,8 @@ rack: rack01
|
||||||
rack_u: 2
|
rack_u: 2
|
||||||
u_height: 1
|
u_height: 1
|
||||||
rack_face: front
|
rack_face: front
|
||||||
|
power:
|
||||||
|
- { pdu: pdu01, outlet: 2 }
|
||||||
---
|
---
|
||||||
|
|
||||||
## Notes
|
## Notes
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,8 @@ rack: rack01
|
||||||
rack_u: 3
|
rack_u: 3
|
||||||
u_height: 1
|
u_height: 1
|
||||||
rack_face: front
|
rack_face: front
|
||||||
|
power:
|
||||||
|
- { pdu: pdu01, outlet: 3 }
|
||||||
---
|
---
|
||||||
|
|
||||||
## Notes
|
## Notes
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,8 @@ rack: rack01
|
||||||
rack_u: 5
|
rack_u: 5
|
||||||
u_height: 2
|
u_height: 2
|
||||||
rack_face: front
|
rack_face: front
|
||||||
|
power:
|
||||||
|
- { pdu: pdu01, outlet: 4 }
|
||||||
---
|
---
|
||||||
|
|
||||||
## Notes
|
## Notes
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,8 @@ rack: rack01
|
||||||
rack_u: 5
|
rack_u: 5
|
||||||
u_height: 2
|
u_height: 2
|
||||||
rack_face: rear
|
rack_face: rear
|
||||||
|
power:
|
||||||
|
- { pdu: pdu01, outlet: 5 }
|
||||||
---
|
---
|
||||||
|
|
||||||
## Notes
|
## Notes
|
||||||
|
|
|
||||||
12
docs/hardware/pdu01.md
Normal file
12
docs/hardware/pdu01.md
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
---
|
||||||
|
hostname: pdu01
|
||||||
|
kind: pdu
|
||||||
|
status: in-use
|
||||||
|
rack: rack01
|
||||||
|
rack_face: left
|
||||||
|
outlets: 8
|
||||||
|
---
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- Provisional placeholder PDU (left rail). Outlet assignments are not yet real.
|
||||||
12
docs/hardware/pdu02.md
Normal file
12
docs/hardware/pdu02.md
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
---
|
||||||
|
hostname: pdu02
|
||||||
|
kind: pdu
|
||||||
|
status: in-use
|
||||||
|
rack: rack01
|
||||||
|
rack_face: right
|
||||||
|
outlets: 8
|
||||||
|
---
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- Provisional placeholder PDU (right rail). Provides redundant feeds.
|
||||||
|
|
@ -1,160 +1,164 @@
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="584" height="1012" viewBox="0 0 584 1012" font-family="sans-serif" font-size="11">
|
<svg xmlns="http://www.w3.org/2000/svg" width="616" height="1012" viewBox="0 0 616 1012" font-family="sans-serif" font-size="11">
|
||||||
<rect width="584" height="1012" fill="#ffffff"/>
|
<rect width="616" height="1012" fill="#ffffff"/>
|
||||||
<text x="12" y="28" font-size="16" font-weight="bold">Rack rack01</text>
|
<text x="12" y="28" font-size="16" font-weight="bold">Rack rack01</text>
|
||||||
<text x="162" y="34" text-anchor="middle" font-weight="bold">front</text>
|
<text x="178" y="34" text-anchor="middle" font-weight="bold">front</text>
|
||||||
<rect x="42" y="40" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
|
<rect x="58" y="40" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
|
||||||
<rect x="42" y="60" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
|
<rect x="58" y="60" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
|
||||||
<rect x="42" y="80" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
|
<rect x="58" y="80" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
|
||||||
<rect x="42" y="100" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
|
<rect x="58" y="100" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
|
||||||
<rect x="42" y="120" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
|
<rect x="58" y="120" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
|
||||||
<rect x="42" y="140" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
|
<rect x="58" y="140" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
|
||||||
<rect x="42" y="160" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
|
<rect x="58" y="160" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
|
||||||
<rect x="42" y="180" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
|
<rect x="58" y="180" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
|
||||||
<rect x="42" y="200" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
|
<rect x="58" y="200" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
|
||||||
<rect x="42" y="220" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
|
<rect x="58" y="220" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
|
||||||
<rect x="42" y="240" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
|
<rect x="58" y="240" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
|
||||||
<rect x="42" y="260" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
|
<rect x="58" y="260" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
|
||||||
<rect x="42" y="280" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
|
<rect x="58" y="280" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
|
||||||
<rect x="42" y="300" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
|
<rect x="58" y="300" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
|
||||||
<rect x="42" y="320" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
|
<rect x="58" y="320" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
|
||||||
<rect x="42" y="340" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
|
<rect x="58" y="340" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
|
||||||
<rect x="42" y="360" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
|
<rect x="58" y="360" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
|
||||||
<rect x="42" y="380" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
|
<rect x="58" y="380" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
|
||||||
<rect x="42" y="400" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
|
<rect x="58" y="400" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
|
||||||
<rect x="42" y="420" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
|
<rect x="58" y="420" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
|
||||||
<rect x="42" y="440" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
|
<rect x="58" y="440" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
|
||||||
<rect x="42" y="460" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
|
<rect x="58" y="460" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
|
||||||
<rect x="42" y="480" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
|
<rect x="58" y="480" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
|
||||||
<rect x="42" y="500" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
|
<rect x="58" y="500" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
|
||||||
<rect x="42" y="520" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
|
<rect x="58" y="520" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
|
||||||
<rect x="42" y="540" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
|
<rect x="58" y="540" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
|
||||||
<rect x="42" y="560" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
|
<rect x="58" y="560" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
|
||||||
<rect x="42" y="580" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
|
<rect x="58" y="580" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
|
||||||
<rect x="42" y="600" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
|
<rect x="58" y="600" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
|
||||||
<rect x="42" y="620" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
|
<rect x="58" y="620" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
|
||||||
<rect x="42" y="640" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
|
<rect x="58" y="640" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
|
||||||
<rect x="42" y="660" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
|
<rect x="58" y="660" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
|
||||||
<rect x="42" y="680" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
|
<rect x="58" y="680" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
|
||||||
<rect x="42" y="700" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
|
<rect x="58" y="700" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
|
||||||
<rect x="42" y="720" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
|
<rect x="58" y="720" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
|
||||||
<rect x="42" y="740" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
|
<rect x="58" y="740" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
|
||||||
<rect x="42" y="760" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
|
<rect x="58" y="760" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
|
||||||
<rect x="42" y="780" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
|
<rect x="58" y="780" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
|
||||||
<rect x="42" y="800" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
|
<rect x="58" y="800" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
|
||||||
<rect x="42" y="820" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
|
<rect x="58" y="820" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
|
||||||
<rect x="42" y="840" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
|
<rect x="58" y="840" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
|
||||||
<rect x="42" y="860" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
|
<rect x="58" y="860" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
|
||||||
<rect x="42" y="880" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
|
<rect x="58" y="880" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
|
||||||
<rect x="42" y="900" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
|
<rect x="58" y="900" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
|
||||||
<rect x="42" y="920" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
|
<rect x="58" y="920" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
|
||||||
<rect x="42" y="940" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
|
<rect x="58" y="940" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
|
||||||
<rect x="42" y="960" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
|
<rect x="58" y="960" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
|
||||||
<rect x="42" y="980" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
|
<rect x="58" y="980" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
|
||||||
<text x="452" y="34" text-anchor="middle" font-weight="bold">rear</text>
|
<text x="468" y="34" text-anchor="middle" font-weight="bold">rear</text>
|
||||||
<rect x="332" y="40" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
|
<rect x="348" y="40" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
|
||||||
<rect x="332" y="60" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
|
<rect x="348" y="60" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
|
||||||
<rect x="332" y="80" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
|
<rect x="348" y="80" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
|
||||||
<rect x="332" y="100" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
|
<rect x="348" y="100" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
|
||||||
<rect x="332" y="120" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
|
<rect x="348" y="120" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
|
||||||
<rect x="332" y="140" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
|
<rect x="348" y="140" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
|
||||||
<rect x="332" y="160" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
|
<rect x="348" y="160" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
|
||||||
<rect x="332" y="180" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
|
<rect x="348" y="180" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
|
||||||
<rect x="332" y="200" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
|
<rect x="348" y="200" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
|
||||||
<rect x="332" y="220" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
|
<rect x="348" y="220" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
|
||||||
<rect x="332" y="240" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
|
<rect x="348" y="240" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
|
||||||
<rect x="332" y="260" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
|
<rect x="348" y="260" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
|
||||||
<rect x="332" y="280" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
|
<rect x="348" y="280" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
|
||||||
<rect x="332" y="300" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
|
<rect x="348" y="300" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
|
||||||
<rect x="332" y="320" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
|
<rect x="348" y="320" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
|
||||||
<rect x="332" y="340" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
|
<rect x="348" y="340" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
|
||||||
<rect x="332" y="360" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
|
<rect x="348" y="360" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
|
||||||
<rect x="332" y="380" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
|
<rect x="348" y="380" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
|
||||||
<rect x="332" y="400" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
|
<rect x="348" y="400" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
|
||||||
<rect x="332" y="420" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
|
<rect x="348" y="420" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
|
||||||
<rect x="332" y="440" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
|
<rect x="348" y="440" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
|
||||||
<rect x="332" y="460" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
|
<rect x="348" y="460" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
|
||||||
<rect x="332" y="480" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
|
<rect x="348" y="480" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
|
||||||
<rect x="332" y="500" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
|
<rect x="348" y="500" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
|
||||||
<rect x="332" y="520" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
|
<rect x="348" y="520" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
|
||||||
<rect x="332" y="540" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
|
<rect x="348" y="540" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
|
||||||
<rect x="332" y="560" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
|
<rect x="348" y="560" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
|
||||||
<rect x="332" y="580" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
|
<rect x="348" y="580" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
|
||||||
<rect x="332" y="600" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
|
<rect x="348" y="600" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
|
||||||
<rect x="332" y="620" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
|
<rect x="348" y="620" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
|
||||||
<rect x="332" y="640" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
|
<rect x="348" y="640" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
|
||||||
<rect x="332" y="660" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
|
<rect x="348" y="660" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
|
||||||
<rect x="332" y="680" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
|
<rect x="348" y="680" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
|
||||||
<rect x="332" y="700" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
|
<rect x="348" y="700" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
|
||||||
<rect x="332" y="720" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
|
<rect x="348" y="720" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
|
||||||
<rect x="332" y="740" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
|
<rect x="348" y="740" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
|
||||||
<rect x="332" y="760" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
|
<rect x="348" y="760" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
|
||||||
<rect x="332" y="780" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
|
<rect x="348" y="780" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
|
||||||
<rect x="332" y="800" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
|
<rect x="348" y="800" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
|
||||||
<rect x="332" y="820" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
|
<rect x="348" y="820" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
|
||||||
<rect x="332" y="840" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
|
<rect x="348" y="840" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
|
||||||
<rect x="332" y="860" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
|
<rect x="348" y="860" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
|
||||||
<rect x="332" y="880" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
|
<rect x="348" y="880" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
|
||||||
<rect x="332" y="900" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
|
<rect x="348" y="900" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
|
||||||
<rect x="332" y="920" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
|
<rect x="348" y="920" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
|
||||||
<rect x="332" y="940" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
|
<rect x="348" y="940" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
|
||||||
<rect x="332" y="960" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
|
<rect x="348" y="960" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
|
||||||
<rect x="332" y="980" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
|
<rect x="348" y="980" width="240" height="20" fill="#f5f5f5" stroke="#e0e0e0"/>
|
||||||
<text x="38" y="54" text-anchor="end" fill="#999">1</text>
|
<text x="54" y="54" text-anchor="end" fill="#999">1</text>
|
||||||
<text x="38" y="74" text-anchor="end" fill="#999">2</text>
|
<text x="54" y="74" text-anchor="end" fill="#999">2</text>
|
||||||
<text x="38" y="94" text-anchor="end" fill="#999">3</text>
|
<text x="54" y="94" text-anchor="end" fill="#999">3</text>
|
||||||
<text x="38" y="114" text-anchor="end" fill="#999">4</text>
|
<text x="54" y="114" text-anchor="end" fill="#999">4</text>
|
||||||
<text x="38" y="134" text-anchor="end" fill="#999">5</text>
|
<text x="54" y="134" text-anchor="end" fill="#999">5</text>
|
||||||
<text x="38" y="154" text-anchor="end" fill="#999">6</text>
|
<text x="54" y="154" text-anchor="end" fill="#999">6</text>
|
||||||
<text x="38" y="174" text-anchor="end" fill="#999">7</text>
|
<text x="54" y="174" text-anchor="end" fill="#999">7</text>
|
||||||
<text x="38" y="194" text-anchor="end" fill="#999">8</text>
|
<text x="54" y="194" text-anchor="end" fill="#999">8</text>
|
||||||
<text x="38" y="214" text-anchor="end" fill="#999">9</text>
|
<text x="54" y="214" text-anchor="end" fill="#999">9</text>
|
||||||
<text x="38" y="234" text-anchor="end" fill="#999">10</text>
|
<text x="54" y="234" text-anchor="end" fill="#999">10</text>
|
||||||
<text x="38" y="254" text-anchor="end" fill="#999">11</text>
|
<text x="54" y="254" text-anchor="end" fill="#999">11</text>
|
||||||
<text x="38" y="274" text-anchor="end" fill="#999">12</text>
|
<text x="54" y="274" text-anchor="end" fill="#999">12</text>
|
||||||
<text x="38" y="294" text-anchor="end" fill="#999">13</text>
|
<text x="54" y="294" text-anchor="end" fill="#999">13</text>
|
||||||
<text x="38" y="314" text-anchor="end" fill="#999">14</text>
|
<text x="54" y="314" text-anchor="end" fill="#999">14</text>
|
||||||
<text x="38" y="334" text-anchor="end" fill="#999">15</text>
|
<text x="54" y="334" text-anchor="end" fill="#999">15</text>
|
||||||
<text x="38" y="354" text-anchor="end" fill="#999">16</text>
|
<text x="54" y="354" text-anchor="end" fill="#999">16</text>
|
||||||
<text x="38" y="374" text-anchor="end" fill="#999">17</text>
|
<text x="54" y="374" text-anchor="end" fill="#999">17</text>
|
||||||
<text x="38" y="394" text-anchor="end" fill="#999">18</text>
|
<text x="54" y="394" text-anchor="end" fill="#999">18</text>
|
||||||
<text x="38" y="414" text-anchor="end" fill="#999">19</text>
|
<text x="54" y="414" text-anchor="end" fill="#999">19</text>
|
||||||
<text x="38" y="434" text-anchor="end" fill="#999">20</text>
|
<text x="54" y="434" text-anchor="end" fill="#999">20</text>
|
||||||
<text x="38" y="454" text-anchor="end" fill="#999">21</text>
|
<text x="54" y="454" text-anchor="end" fill="#999">21</text>
|
||||||
<text x="38" y="474" text-anchor="end" fill="#999">22</text>
|
<text x="54" y="474" text-anchor="end" fill="#999">22</text>
|
||||||
<text x="38" y="494" text-anchor="end" fill="#999">23</text>
|
<text x="54" y="494" text-anchor="end" fill="#999">23</text>
|
||||||
<text x="38" y="514" text-anchor="end" fill="#999">24</text>
|
<text x="54" y="514" text-anchor="end" fill="#999">24</text>
|
||||||
<text x="38" y="534" text-anchor="end" fill="#999">25</text>
|
<text x="54" y="534" text-anchor="end" fill="#999">25</text>
|
||||||
<text x="38" y="554" text-anchor="end" fill="#999">26</text>
|
<text x="54" y="554" text-anchor="end" fill="#999">26</text>
|
||||||
<text x="38" y="574" text-anchor="end" fill="#999">27</text>
|
<text x="54" y="574" text-anchor="end" fill="#999">27</text>
|
||||||
<text x="38" y="594" text-anchor="end" fill="#999">28</text>
|
<text x="54" y="594" text-anchor="end" fill="#999">28</text>
|
||||||
<text x="38" y="614" text-anchor="end" fill="#999">29</text>
|
<text x="54" y="614" text-anchor="end" fill="#999">29</text>
|
||||||
<text x="38" y="634" text-anchor="end" fill="#999">30</text>
|
<text x="54" y="634" text-anchor="end" fill="#999">30</text>
|
||||||
<text x="38" y="654" text-anchor="end" fill="#999">31</text>
|
<text x="54" y="654" text-anchor="end" fill="#999">31</text>
|
||||||
<text x="38" y="674" text-anchor="end" fill="#999">32</text>
|
<text x="54" y="674" text-anchor="end" fill="#999">32</text>
|
||||||
<text x="38" y="694" text-anchor="end" fill="#999">33</text>
|
<text x="54" y="694" text-anchor="end" fill="#999">33</text>
|
||||||
<text x="38" y="714" text-anchor="end" fill="#999">34</text>
|
<text x="54" y="714" text-anchor="end" fill="#999">34</text>
|
||||||
<text x="38" y="734" text-anchor="end" fill="#999">35</text>
|
<text x="54" y="734" text-anchor="end" fill="#999">35</text>
|
||||||
<text x="38" y="754" text-anchor="end" fill="#999">36</text>
|
<text x="54" y="754" text-anchor="end" fill="#999">36</text>
|
||||||
<text x="38" y="774" text-anchor="end" fill="#999">37</text>
|
<text x="54" y="774" text-anchor="end" fill="#999">37</text>
|
||||||
<text x="38" y="794" text-anchor="end" fill="#999">38</text>
|
<text x="54" y="794" text-anchor="end" fill="#999">38</text>
|
||||||
<text x="38" y="814" text-anchor="end" fill="#999">39</text>
|
<text x="54" y="814" text-anchor="end" fill="#999">39</text>
|
||||||
<text x="38" y="834" text-anchor="end" fill="#999">40</text>
|
<text x="54" y="834" text-anchor="end" fill="#999">40</text>
|
||||||
<text x="38" y="854" text-anchor="end" fill="#999">41</text>
|
<text x="54" y="854" text-anchor="end" fill="#999">41</text>
|
||||||
<text x="38" y="874" text-anchor="end" fill="#999">42</text>
|
<text x="54" y="874" text-anchor="end" fill="#999">42</text>
|
||||||
<text x="38" y="894" text-anchor="end" fill="#999">43</text>
|
<text x="54" y="894" text-anchor="end" fill="#999">43</text>
|
||||||
<text x="38" y="914" text-anchor="end" fill="#999">44</text>
|
<text x="54" y="914" text-anchor="end" fill="#999">44</text>
|
||||||
<text x="38" y="934" text-anchor="end" fill="#999">45</text>
|
<text x="54" y="934" text-anchor="end" fill="#999">45</text>
|
||||||
<text x="38" y="954" text-anchor="end" fill="#999">46</text>
|
<text x="54" y="954" text-anchor="end" fill="#999">46</text>
|
||||||
<text x="38" y="974" text-anchor="end" fill="#999">47</text>
|
<text x="54" y="974" text-anchor="end" fill="#999">47</text>
|
||||||
<text x="38" y="994" text-anchor="end" fill="#999">48</text>
|
<text x="54" y="994" text-anchor="end" fill="#999">48</text>
|
||||||
<rect x="43" y="41" width="238" height="18" rx="3" fill="#4c78a8" stroke="#333"/>
|
<rect x="59" y="41" width="238" height="18" rx="3" fill="#4c78a8" stroke="#333"/>
|
||||||
<text x="162" y="54" text-anchor="middle" fill="#ffffff">mf00 (U1)</text>
|
<text x="178" y="54" text-anchor="middle" fill="#ffffff">mf00 (U1)</text>
|
||||||
<rect x="43" y="61" width="238" height="18" rx="3" fill="#4c78a8" stroke="#333"/>
|
<rect x="59" y="61" width="238" height="18" rx="3" fill="#4c78a8" stroke="#333"/>
|
||||||
<text x="162" y="74" text-anchor="middle" fill="#ffffff">mf01 (U2)</text>
|
<text x="178" y="74" text-anchor="middle" fill="#ffffff">mf01 (U2)</text>
|
||||||
<rect x="43" y="81" width="238" height="18" rx="3" fill="#4c78a8" stroke="#333"/>
|
<rect x="59" y="81" width="238" height="18" rx="3" fill="#4c78a8" stroke="#333"/>
|
||||||
<text x="162" y="94" text-anchor="middle" fill="#ffffff">mf02 (U3)</text>
|
<text x="178" y="94" text-anchor="middle" fill="#ffffff">mf02 (U3)</text>
|
||||||
<rect x="43" y="121" width="238" height="38" rx="3" fill="#4c78a8" stroke="#333"/>
|
<rect x="59" y="121" width="238" height="38" rx="3" fill="#4c78a8" stroke="#333"/>
|
||||||
<text x="162" y="144" text-anchor="middle" fill="#ffffff">mf03 (U5–U6)</text>
|
<text x="178" y="144" text-anchor="middle" fill="#ffffff">mf03 (U5–U6)</text>
|
||||||
<rect x="333" y="121" width="238" height="38" rx="3" fill="#4c78a8" stroke="#333"/>
|
<rect x="349" y="121" width="238" height="38" rx="3" fill="#4c78a8" stroke="#333"/>
|
||||||
<text x="452" y="144" text-anchor="middle" fill="#ffffff">mf04 (U5–U6)</text>
|
<text x="468" y="144" text-anchor="middle" fill="#ffffff">mf04 (U5–U6)</text>
|
||||||
|
<rect x="12" y="40" width="16" height="960" fill="#e15759" stroke="#333"/>
|
||||||
|
<text x="20" y="520" text-anchor="middle" fill="#ffffff" transform="rotate(-90 20 520)">pdu01</text>
|
||||||
|
<rect x="588" y="40" width="16" height="960" fill="#e15759" stroke="#333"/>
|
||||||
|
<text x="596" y="520" text-anchor="middle" fill="#ffffff" transform="rotate(-90 596 520)">pdu02</text>
|
||||||
</svg>
|
</svg>
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
|
|
@ -6,6 +6,25 @@ _Auto-generated from `docs/hardware/*.md` (items with `rack: rack01`) — do not
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
## Power
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart LR
|
||||||
|
pdu01["pdu01<br/>8 outlets"]
|
||||||
|
pdu02["pdu02<br/>8 outlets"]
|
||||||
|
mf00["mf00"]
|
||||||
|
mf01["mf01"]
|
||||||
|
mf02["mf02"]
|
||||||
|
mf03["mf03"]
|
||||||
|
mf04["mf04"]
|
||||||
|
pdu01 -->|outlet 1| mf00
|
||||||
|
pdu01 -->|outlet 2| mf01
|
||||||
|
pdu01 -->|outlet 3| mf02
|
||||||
|
pdu01 -->|outlet 4| mf03
|
||||||
|
pdu01 -->|outlet 5| mf04
|
||||||
|
pdu02 -->|outlet 1| mf00
|
||||||
|
```
|
||||||
|
|
||||||
## Occupancy
|
## Occupancy
|
||||||
|
|
||||||
| U | Device | Kind | Face | Status |
|
| U | Device | Kind | Face | Status |
|
||||||
|
|
@ -15,3 +34,5 @@ _Auto-generated from `docs/hardware/*.md` (items with `rack: rack01`) — do not
|
||||||
| U3 | [mf02](../../hardware/mf02.md) | server | front | staging |
|
| U3 | [mf02](../../hardware/mf02.md) | server | front | staging |
|
||||||
| U5–U6 | [mf03](../../hardware/mf03.md) | server | front | staging |
|
| U5–U6 | [mf03](../../hardware/mf03.md) | server | front | staging |
|
||||||
| U5–U6 | [mf04](../../hardware/mf04.md) | server | rear | staging |
|
| U5–U6 | [mf04](../../hardware/mf04.md) | server | rear | staging |
|
||||||
|
| 0U | [pdu01](../../hardware/pdu01.md) | pdu | left | in-use |
|
||||||
|
| 0U | [pdu02](../../hardware/pdu02.md) | pdu | right | in-use |
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,11 @@ markdown_extensions:
|
||||||
- tables
|
- tables
|
||||||
- attr_list
|
- attr_list
|
||||||
- md_in_html
|
- md_in_html
|
||||||
- pymdownx.superfences
|
- pymdownx.superfences:
|
||||||
|
custom_fences:
|
||||||
|
- name: mermaid
|
||||||
|
class: mermaid
|
||||||
|
format: !!python/name:pymdownx.superfences.fence_code_format
|
||||||
- pymdownx.highlight:
|
- pymdownx.highlight:
|
||||||
anchor_linenums: true
|
anchor_linenums: true
|
||||||
- pymdownx.inlinehilite
|
- pymdownx.inlinehilite
|
||||||
|
|
|
||||||
553
notes/dev/plans/2026-06-24-rack-power.md
Normal file
553
notes/dev/plans/2026-06-24-rack-power.md
Normal file
|
|
@ -0,0 +1,553 @@
|
||||||
|
# Rack Power (Phase 2) Implementation Plan
|
||||||
|
|
||||||
|
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||||
|
|
||||||
|
**Goal:** Add power-distribution data (`power:` feeds + PDU files) to the rack pipeline, validate it, and render a mermaid power graph on the generated rack page — reusing every Phase 1 mechanism.
|
||||||
|
|
||||||
|
**Architecture:** Extend the existing `scripts/gen_rack.py` with `validate_power` (rule 3 from the spec) and `render_power` (a `flowchart LR` with the outlet number as the edge label); insert a `## Power` section into `render_page`. PDU files are 0U `left`/`right` items that Phase 1 already renders as side-rails and `gen_overview.py` already lists. Pull the mermaid superfences fence forward into `mkdocs.yml` so the graph renders.
|
||||||
|
|
||||||
|
**Tech Stack:** Python 3 (stdlib + PyYAML only), pytest, MkDocs Material, Forgejo Actions CI.
|
||||||
|
|
||||||
|
**Spec:** `notes/dev/specs/2026-06-24-rack-power-design.md`.
|
||||||
|
|
||||||
|
## Global Constraints
|
||||||
|
|
||||||
|
- Scripts use **stdlib + PyYAML only**; deterministic and offline (copy `gen_rack.py`/`gen_overview.py` style). No randomness/time in generated output.
|
||||||
|
- `re` and `yaml` are already imported in `scripts/gen_rack.py`; do not add new imports.
|
||||||
|
- Validation failures raise `SchemaError`; `generate` prints `ERROR: …` to stderr and returns `1`, **writing nothing** on failure (existing Phase 1 behaviour).
|
||||||
|
- Generated files keep the existing `_Auto-generated … do not edit by hand_` banner.
|
||||||
|
- PDU files are **0U**: `rack_face: left|right`, **no** `rack_u`/`u_height`, and a positive-int `outlets`.
|
||||||
|
- Power data added here is **provisional placeholder data** (like the existing `mfNN` U positions), not real values.
|
||||||
|
- The Makefile `docs-check` and CI drift step already diff the whole `docs/infrastructure/racks/` dir — **do not edit** `Makefile`, `.forgejo/workflows/docs.yml`, or `scripts/overview_config.yml`.
|
||||||
|
- `mkdocs build --strict` must pass; `make docs-check` must exit 0 after regeneration.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 1: `validate_power` — rule 3 validation (TDD)
|
||||||
|
|
||||||
|
Add PDU lookup + power-feed validation and wire it into `generate`. This task is testable on validation alone (no rendering needed).
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `scripts/gen_rack.py` (add `_pdu_index`, `validate_power`; call in `generate`)
|
||||||
|
- Modify: `tests/test_gen_rack.py` (append tests)
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
- Consumes: `SchemaError`, the `item()`/`_write_item` test helpers, `generate`.
|
||||||
|
- Produces:
|
||||||
|
- `_pdu_index(items: list[dict]) -> dict[str, dict]` — `{hostname: fm}` for `kind == "pdu"` items.
|
||||||
|
- `validate_power(items: list[dict]) -> None` — raises `SchemaError` on a bad PDU `outlets` declaration or a bad `power` feed.
|
||||||
|
|
||||||
|
- [ ] **Step 1: Append failing tests to `tests/test_gen_rack.py`**
|
||||||
|
|
||||||
|
```python
|
||||||
|
def test_validate_power_accepts_valid_feed():
|
||||||
|
items = [
|
||||||
|
item(hostname="pdu01", kind="pdu", rack_face="left", outlets=8),
|
||||||
|
item(hostname="mf00", rack_u=1, u_height=1, rack_face="front",
|
||||||
|
power=[{"pdu": "pdu01", "outlet": 1}]),
|
||||||
|
]
|
||||||
|
gen_rack.validate_power(items)
|
||||||
|
|
||||||
|
|
||||||
|
def test_validate_power_rejects_unknown_pdu():
|
||||||
|
items = [item(hostname="mf00", rack_u=1, u_height=1, rack_face="front",
|
||||||
|
power=[{"pdu": "ghost", "outlet": 1}])]
|
||||||
|
with pytest.raises(gen_rack.SchemaError):
|
||||||
|
gen_rack.validate_power(items)
|
||||||
|
|
||||||
|
|
||||||
|
def test_validate_power_rejects_non_pdu_target():
|
||||||
|
items = [
|
||||||
|
item(hostname="sw01", kind="switch", rack_u=1, u_height=1,
|
||||||
|
rack_face="front"),
|
||||||
|
item(hostname="mf00", rack_u=2, u_height=1, rack_face="front",
|
||||||
|
power=[{"pdu": "sw01", "outlet": 1}]),
|
||||||
|
]
|
||||||
|
with pytest.raises(gen_rack.SchemaError):
|
||||||
|
gen_rack.validate_power(items)
|
||||||
|
|
||||||
|
|
||||||
|
def test_validate_power_rejects_outlet_over_count():
|
||||||
|
items = [
|
||||||
|
item(hostname="pdu01", kind="pdu", rack_face="left", outlets=8),
|
||||||
|
item(hostname="mf00", rack_u=1, u_height=1, rack_face="front",
|
||||||
|
power=[{"pdu": "pdu01", "outlet": 9}]),
|
||||||
|
]
|
||||||
|
with pytest.raises(gen_rack.SchemaError):
|
||||||
|
gen_rack.validate_power(items)
|
||||||
|
|
||||||
|
|
||||||
|
def test_validate_power_rejects_outlet_zero():
|
||||||
|
items = [
|
||||||
|
item(hostname="pdu01", kind="pdu", rack_face="left", outlets=8),
|
||||||
|
item(hostname="mf00", rack_u=1, u_height=1, rack_face="front",
|
||||||
|
power=[{"pdu": "pdu01", "outlet": 0}]),
|
||||||
|
]
|
||||||
|
with pytest.raises(gen_rack.SchemaError):
|
||||||
|
gen_rack.validate_power(items)
|
||||||
|
|
||||||
|
|
||||||
|
def test_validate_power_rejects_malformed_entry():
|
||||||
|
items = [
|
||||||
|
item(hostname="pdu01", kind="pdu", rack_face="left", outlets=8),
|
||||||
|
item(hostname="mf00", rack_u=1, u_height=1, rack_face="front",
|
||||||
|
power=["pdu01"]),
|
||||||
|
]
|
||||||
|
with pytest.raises(gen_rack.SchemaError):
|
||||||
|
gen_rack.validate_power(items)
|
||||||
|
|
||||||
|
|
||||||
|
def test_validate_power_rejects_pdu_without_outlets():
|
||||||
|
items = [
|
||||||
|
item(hostname="pdu01", kind="pdu", rack_face="left"),
|
||||||
|
item(hostname="mf00", rack_u=1, u_height=1, rack_face="front",
|
||||||
|
power=[{"pdu": "pdu01", "outlet": 1}]),
|
||||||
|
]
|
||||||
|
with pytest.raises(gen_rack.SchemaError):
|
||||||
|
gen_rack.validate_power(items)
|
||||||
|
|
||||||
|
|
||||||
|
def test_generate_returns_1_on_bad_power_ref(tmp_path):
|
||||||
|
hw = tmp_path / "hardware"
|
||||||
|
out = tmp_path / "out"
|
||||||
|
hw.mkdir()
|
||||||
|
_write_item(
|
||||||
|
hw,
|
||||||
|
"mf00",
|
||||||
|
"---\nhostname: mf00\nkind: server\nstatus: in-use\n"
|
||||||
|
"rack: rack01\nrack_u: 1\nu_height: 1\nrack_face: front\n"
|
||||||
|
"power:\n - { pdu: ghost, outlet: 1 }\n---\n",
|
||||||
|
)
|
||||||
|
rc = gen_rack.generate(hw, out)
|
||||||
|
assert rc == 1
|
||||||
|
assert not (out / "rack01.md").exists()
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run to verify failure**
|
||||||
|
|
||||||
|
Run: `pytest tests/test_gen_rack.py -q`
|
||||||
|
Expected: FAIL — `AttributeError: module 'gen_rack' has no attribute 'validate_power'`.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Add `_pdu_index` and `validate_power` after `check_overlaps` in `scripts/gen_rack.py`**
|
||||||
|
|
||||||
|
```python
|
||||||
|
def _pdu_index(items: list[dict]) -> dict[str, dict]:
|
||||||
|
"""Map hostname -> frontmatter for every kind:pdu item."""
|
||||||
|
return {
|
||||||
|
fm.get("hostname"): fm
|
||||||
|
for fm in items
|
||||||
|
if fm.get("kind") == "pdu"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def validate_power(items: list[dict]) -> None:
|
||||||
|
"""Validate PDU outlet declarations and `power` feeds within one rack.
|
||||||
|
|
||||||
|
Rule 3: every power[].pdu resolves to a kind:pdu file, and outlet is
|
||||||
|
within that PDU's `outlets` count.
|
||||||
|
"""
|
||||||
|
pdus = _pdu_index(items)
|
||||||
|
for name, fm in pdus.items():
|
||||||
|
outlets = fm.get("outlets")
|
||||||
|
if not isinstance(outlets, int) or outlets < 1:
|
||||||
|
raise SchemaError(
|
||||||
|
f"{name}: kind:pdu must declare a positive integer 'outlets'"
|
||||||
|
)
|
||||||
|
for fm in items:
|
||||||
|
feeds = fm.get("power")
|
||||||
|
if feeds is None:
|
||||||
|
continue
|
||||||
|
name = fm.get("hostname", "?")
|
||||||
|
if not isinstance(feeds, list):
|
||||||
|
raise SchemaError(f"{name}: power must be a list")
|
||||||
|
for feed in feeds:
|
||||||
|
if not isinstance(feed, dict):
|
||||||
|
raise SchemaError(f"{name}: power entry must be a mapping")
|
||||||
|
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'")
|
||||||
|
if not isinstance(outlet, int):
|
||||||
|
raise SchemaError(
|
||||||
|
f"{name}: power entry for {pdu} needs an integer 'outlet'"
|
||||||
|
)
|
||||||
|
target = pdus.get(pdu)
|
||||||
|
if target is None:
|
||||||
|
raise SchemaError(
|
||||||
|
f"{name}: power pdu={pdu!r} is not a known kind:pdu file"
|
||||||
|
)
|
||||||
|
count = target["outlets"]
|
||||||
|
if outlet < 1 or outlet > count:
|
||||||
|
raise SchemaError(
|
||||||
|
f"{name}: outlet {outlet} out of range 1..{count} on {pdu}"
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: Wire `validate_power` into `generate` in `scripts/gen_rack.py`**
|
||||||
|
|
||||||
|
Change the overlap loop so it also validates power. Replace:
|
||||||
|
|
||||||
|
```python
|
||||||
|
if not errors: # only check overlaps once placements are individually valid
|
||||||
|
for rack, ritems in racks.items():
|
||||||
|
try:
|
||||||
|
check_overlaps(ritems)
|
||||||
|
except SchemaError as e:
|
||||||
|
errors.append(f"{rack}: {e}")
|
||||||
|
```
|
||||||
|
|
||||||
|
with:
|
||||||
|
|
||||||
|
```python
|
||||||
|
if not errors: # only check overlaps once placements are individually valid
|
||||||
|
for rack, ritems in racks.items():
|
||||||
|
try:
|
||||||
|
check_overlaps(ritems)
|
||||||
|
validate_power(ritems)
|
||||||
|
except SchemaError as e:
|
||||||
|
errors.append(f"{rack}: {e}")
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 5: Run to verify pass**
|
||||||
|
|
||||||
|
Run: `pytest tests/test_gen_rack.py -q`
|
||||||
|
Expected: PASS (all prior tests + 8 new).
|
||||||
|
|
||||||
|
- [ ] **Step 6: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add scripts/gen_rack.py tests/test_gen_rack.py
|
||||||
|
git commit -m "feat(rack): validate power feeds against PDU outlets"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 2: `render_power` + page section (TDD)
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `scripts/gen_rack.py` (add `_node_id`, `render_power`; edit `render_page`)
|
||||||
|
- Modify: `tests/test_gen_rack.py` (append tests)
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
- Consumes: `_pdu_index` (Task 1), `render_page`, `generate`.
|
||||||
|
- Produces:
|
||||||
|
- `_node_id(name: str) -> str` — hostname with non-alphanumeric chars replaced by `_`.
|
||||||
|
- `render_power(rack: str, items: list[dict]) -> str` — a fenced `mermaid` `flowchart LR` ending in a newline, or `""` when no item has a `power` feed.
|
||||||
|
|
||||||
|
- [ ] **Step 1: Append failing tests to `tests/test_gen_rack.py`**
|
||||||
|
|
||||||
|
```python
|
||||||
|
def test_render_power_has_nodes_and_edge_labels():
|
||||||
|
items = [
|
||||||
|
item(hostname="pdu01", kind="pdu", rack_face="left", outlets=8),
|
||||||
|
item(hostname="mf00", rack_u=1, u_height=1, rack_face="front",
|
||||||
|
power=[{"pdu": "pdu01", "outlet": 3}]),
|
||||||
|
]
|
||||||
|
out = gen_rack.render_power("rack01", items)
|
||||||
|
assert "```mermaid" in out
|
||||||
|
assert "flowchart LR" in out
|
||||||
|
assert "pdu01" in out
|
||||||
|
assert "8 outlets" in out
|
||||||
|
assert "outlet 3" in out
|
||||||
|
assert "mf00" in out
|
||||||
|
|
||||||
|
|
||||||
|
def test_render_power_redundant_device_has_two_edges():
|
||||||
|
items = [
|
||||||
|
item(hostname="pdu01", kind="pdu", rack_face="left", outlets=8),
|
||||||
|
item(hostname="pdu02", kind="pdu", rack_face="right", outlets=8),
|
||||||
|
item(hostname="mf00", rack_u=1, u_height=1, rack_face="front",
|
||||||
|
power=[{"pdu": "pdu01", "outlet": 1},
|
||||||
|
{"pdu": "pdu02", "outlet": 1}]),
|
||||||
|
]
|
||||||
|
out = gen_rack.render_power("rack01", items)
|
||||||
|
assert out.count("-->|outlet") == 2
|
||||||
|
|
||||||
|
|
||||||
|
def test_render_power_empty_when_no_feeds():
|
||||||
|
items = [item(hostname="mf00", rack_u=1, u_height=1, rack_face="front")]
|
||||||
|
assert gen_rack.render_power("rack01", items) == ""
|
||||||
|
|
||||||
|
|
||||||
|
def test_render_power_is_deterministic():
|
||||||
|
a = item(hostname="pdu01", kind="pdu", rack_face="left", outlets=8)
|
||||||
|
b = item(hostname="mf01", rack_u=2, u_height=1, rack_face="front",
|
||||||
|
power=[{"pdu": "pdu01", "outlet": 2}])
|
||||||
|
c = item(hostname="mf00", rack_u=1, u_height=1, rack_face="front",
|
||||||
|
power=[{"pdu": "pdu01", "outlet": 1}])
|
||||||
|
assert gen_rack.render_power("rack01", [a, b, c]) == \
|
||||||
|
gen_rack.render_power("rack01", [c, b, a])
|
||||||
|
|
||||||
|
|
||||||
|
def test_generate_includes_power_section(tmp_path):
|
||||||
|
hw = tmp_path / "hardware"
|
||||||
|
out = tmp_path / "out"
|
||||||
|
hw.mkdir()
|
||||||
|
_write_item(
|
||||||
|
hw,
|
||||||
|
"pdu01",
|
||||||
|
"---\nhostname: pdu01\nkind: pdu\nstatus: in-use\n"
|
||||||
|
"rack: rack01\nrack_face: left\noutlets: 8\n---\n",
|
||||||
|
)
|
||||||
|
_write_item(
|
||||||
|
hw,
|
||||||
|
"mf00",
|
||||||
|
"---\nhostname: mf00\nkind: server\nstatus: in-use\n"
|
||||||
|
"rack: rack01\nrack_u: 1\nu_height: 1\nrack_face: front\n"
|
||||||
|
"power:\n - { pdu: pdu01, outlet: 1 }\n---\n",
|
||||||
|
)
|
||||||
|
rc = gen_rack.generate(hw, out)
|
||||||
|
assert rc == 0
|
||||||
|
page = (out / "rack01.md").read_text()
|
||||||
|
assert "## Power" in page
|
||||||
|
assert "```mermaid" in page
|
||||||
|
assert "outlet 1" in page
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run to verify failure**
|
||||||
|
|
||||||
|
Run: `pytest tests/test_gen_rack.py -q`
|
||||||
|
Expected: FAIL — `AttributeError: module 'gen_rack' has no attribute 'render_power'`.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Add `_node_id` and `render_power` after `render_svg` in `scripts/gen_rack.py`**
|
||||||
|
|
||||||
|
```python
|
||||||
|
def _node_id(name: str) -> str:
|
||||||
|
"""A mermaid-safe node id derived from a hostname."""
|
||||||
|
return re.sub(r"[^0-9A-Za-z]", "_", str(name))
|
||||||
|
|
||||||
|
|
||||||
|
def render_power(rack: str, items: list[dict]) -> str:
|
||||||
|
"""Return a mermaid power-distribution flowchart, or '' if no feeds."""
|
||||||
|
powered = [fm for fm in items if fm.get("power")]
|
||||||
|
if not powered:
|
||||||
|
return ""
|
||||||
|
pdus = _pdu_index(items)
|
||||||
|
|
||||||
|
edges: list[tuple[str, int, str]] = []
|
||||||
|
for fm in powered:
|
||||||
|
device = fm.get("hostname", "?")
|
||||||
|
for feed in fm["power"]:
|
||||||
|
edges.append((feed["pdu"], feed["outlet"], device))
|
||||||
|
edges.sort()
|
||||||
|
|
||||||
|
lines: list[str] = ["```mermaid", "flowchart LR"]
|
||||||
|
for pdu in sorted(pdus):
|
||||||
|
outlets = pdus[pdu].get("outlets")
|
||||||
|
lines.append(f' {_node_id(pdu)}["{pdu}<br/>{outlets} outlets"]')
|
||||||
|
devices = sorted(
|
||||||
|
powered,
|
||||||
|
key=lambda i: (
|
||||||
|
i.get("rack_u", 0) if isinstance(i.get("rack_u"), int) else 0,
|
||||||
|
i.get("hostname", ""),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
for fm in devices:
|
||||||
|
device = fm.get("hostname", "?")
|
||||||
|
lines.append(f' {_node_id(device)}["{device}"]')
|
||||||
|
for pdu, outlet, device in edges:
|
||||||
|
lines.append(
|
||||||
|
f" {_node_id(pdu)} -->|outlet {outlet}| {_node_id(device)}"
|
||||||
|
)
|
||||||
|
lines.append("```")
|
||||||
|
return "\n".join(lines) + "\n"
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: Insert the `## Power` section in `render_page` in `scripts/gen_rack.py`**
|
||||||
|
|
||||||
|
In `render_page`, replace this block:
|
||||||
|
|
||||||
|
```python
|
||||||
|
lines.append(f"")
|
||||||
|
lines.append("")
|
||||||
|
lines.append("## Occupancy")
|
||||||
|
```
|
||||||
|
|
||||||
|
with:
|
||||||
|
|
||||||
|
```python
|
||||||
|
lines.append(f"")
|
||||||
|
lines.append("")
|
||||||
|
power = render_power(rack, items)
|
||||||
|
if power:
|
||||||
|
lines.append("## Power")
|
||||||
|
lines.append("")
|
||||||
|
lines.append(power.rstrip())
|
||||||
|
lines.append("")
|
||||||
|
lines.append("## Occupancy")
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 5: Run to verify pass**
|
||||||
|
|
||||||
|
Run: `pytest tests/test_gen_rack.py -q`
|
||||||
|
Expected: PASS (all prior tests + 5 new).
|
||||||
|
|
||||||
|
- [ ] **Step 6: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add scripts/gen_rack.py tests/test_gen_rack.py
|
||||||
|
git commit -m "feat(rack): render mermaid power graph into the rack page"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 3: Enable mermaid, populate provisional power data, regenerate
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `mkdocs.yml` (mermaid superfences fence)
|
||||||
|
- Create: `docs/hardware/pdu01.md`, `docs/hardware/pdu02.md`
|
||||||
|
- Modify: `docs/hardware/mf00.md`..`mf04.md` (add `power:`)
|
||||||
|
- Regenerate: `docs/hardware/index.md`, `docs/infrastructure/racks/rack01.md`, `docs/infrastructure/racks/rack01-elevation.svg`
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
- Consumes: `python3 scripts/gen_rack.py` / `make docs-index`, `mkdocs build --strict`, `make docs-check`.
|
||||||
|
|
||||||
|
> **Operator note — provisional data.** The PDU placements and outlet assignments below are placeholders proving the feature, matching the existing fictional `mfNN` U positions. Replace with real values when known; `validate_power` will reject dangling/over-count feeds loudly.
|
||||||
|
|
||||||
|
- [ ] **Step 1: Enable the mermaid custom fence in `mkdocs.yml`**
|
||||||
|
|
||||||
|
In `mkdocs.yml`, replace the bare superfences line:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- pymdownx.superfences
|
||||||
|
```
|
||||||
|
|
||||||
|
with:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- pymdownx.superfences:
|
||||||
|
custom_fences:
|
||||||
|
- name: mermaid
|
||||||
|
class: mermaid
|
||||||
|
format: !!python/name:pymdownx.superfences.fence_code_format
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Create the two PDU files**
|
||||||
|
|
||||||
|
Create `docs/hardware/pdu01.md`:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
---
|
||||||
|
hostname: pdu01
|
||||||
|
kind: pdu
|
||||||
|
status: in-use
|
||||||
|
rack: rack01
|
||||||
|
rack_face: left
|
||||||
|
outlets: 8
|
||||||
|
---
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- Provisional placeholder PDU (left rail). Outlet assignments are not yet real.
|
||||||
|
```
|
||||||
|
|
||||||
|
Create `docs/hardware/pdu02.md`:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
---
|
||||||
|
hostname: pdu02
|
||||||
|
kind: pdu
|
||||||
|
status: in-use
|
||||||
|
rack: rack01
|
||||||
|
rack_face: right
|
||||||
|
outlets: 8
|
||||||
|
---
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- Provisional placeholder PDU (right rail). Provides redundant feeds.
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: Add `power:` to the five host files**
|
||||||
|
|
||||||
|
In `docs/hardware/mf00.md`, add to the frontmatter (before the closing `---`) — note mf00 has **two** feeds (the redundant demonstration):
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
power:
|
||||||
|
- { pdu: pdu01, outlet: 1 }
|
||||||
|
- { pdu: pdu02, outlet: 1 }
|
||||||
|
```
|
||||||
|
|
||||||
|
In `docs/hardware/mf01.md` add:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
power:
|
||||||
|
- { pdu: pdu01, outlet: 2 }
|
||||||
|
```
|
||||||
|
|
||||||
|
In `docs/hardware/mf02.md` add:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
power:
|
||||||
|
- { pdu: pdu01, outlet: 3 }
|
||||||
|
```
|
||||||
|
|
||||||
|
In `docs/hardware/mf03.md` add:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
power:
|
||||||
|
- { pdu: pdu01, outlet: 4 }
|
||||||
|
```
|
||||||
|
|
||||||
|
In `docs/hardware/mf04.md` add:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
power:
|
||||||
|
- { pdu: pdu01, outlet: 5 }
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: Regenerate all indices and rack artifacts**
|
||||||
|
|
||||||
|
Run: `make docs-index`
|
||||||
|
Expected: `gen_overview.py` rewrites `docs/hardware/index.md` (now listing pdu01/pdu02 under "PDUs"); `gen_rack.py` prints `Wrote rack01.md + rack01-elevation.svg (7 item(s))`.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Confirm the generated page has a rendered power graph and PDU rails**
|
||||||
|
|
||||||
|
Run: `grep -c "outlet" docs/infrastructure/racks/rack01.md`
|
||||||
|
Expected: ≥ 6 (one edge per feed: mf00 ×2, mf01..mf04 ×1).
|
||||||
|
|
||||||
|
Run: `grep -c "pdu0" docs/infrastructure/racks/rack01-elevation.svg`
|
||||||
|
Expected: ≥ 2 (pdu01 + pdu02 drawn as side-rails).
|
||||||
|
|
||||||
|
- [ ] **Step 6: Run the full test suite**
|
||||||
|
|
||||||
|
Run: `make test`
|
||||||
|
Expected: PASS (all tests).
|
||||||
|
|
||||||
|
- [ ] **Step 7: Build the site strictly**
|
||||||
|
|
||||||
|
Run: `mkdocs build --strict`
|
||||||
|
Expected: build succeeds with no warnings-as-errors; `site/infrastructure/racks/rack01/index.html` contains a `<pre class="mermaid">` (or `<div class="mermaid">`) block rather than a plain `<code>` fence.
|
||||||
|
|
||||||
|
Verify: `grep -c "mermaid" site/infrastructure/racks/rack01/index.html`
|
||||||
|
Expected: ≥ 1.
|
||||||
|
|
||||||
|
- [ ] **Step 8: Confirm the drift guard is satisfied**
|
||||||
|
|
||||||
|
Run: `make docs-check`
|
||||||
|
Expected: exit 0 — committed artifacts match a fresh regeneration.
|
||||||
|
|
||||||
|
- [ ] **Step 9: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add mkdocs.yml docs/hardware/ docs/infrastructure/racks/
|
||||||
|
git commit -m "feat(rack): enable mermaid, populate provisional power data"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Self-Review
|
||||||
|
|
||||||
|
**Spec coverage (`2026-06-24-rack-power-design.md`):**
|
||||||
|
- `power:` frontmatter on devices — Task 3 (populate); validated Task 1. ✔
|
||||||
|
- PDU files (`kind: pdu`, `outlets`, 0U `left`/`right`) — Task 3; outlets validated Task 1. ✔
|
||||||
|
- Validation rule 3 (pdu resolves to kind:pdu; outlet in range; pdu declares outlets) — Task 1 (`validate_power`), wired into `generate`. ✔
|
||||||
|
- Mermaid power graph, outlet as edge label, redundancy as two edges, omit-when-empty, deterministic — Task 2 (`render_power`), inserted in `render_page`. ✔
|
||||||
|
- Node-id sanitization — Task 2 (`_node_id`). ✔
|
||||||
|
- Mermaid pulled forward in `mkdocs.yml` — Task 3 Step 1. ✔
|
||||||
|
- No Makefile/CI/overview_config changes — honored (Global Constraints); drift covered by existing `racks/` diff — Task 3 Steps 4/8. ✔
|
||||||
|
- Provisional data (pdu01 left → mf00..mf04 o1..o5; pdu02 right → mf00 o1) — Task 3 Steps 2–3. ✔
|
||||||
|
|
||||||
|
**Placeholder scan:** No "TBD"/"handle edge cases"/"similar to Task N". The only operator-judgement item is provisional power values, explicitly bounded and guarded by `validate_power`.
|
||||||
|
|
||||||
|
**Type consistency:** `_pdu_index` → `dict[str, dict]`; `validate_power`/`check_overlaps` → `None` (raise `SchemaError`); `render_power`/`render_page`/`render_svg`/`_node_id` → `str`; `generate` → `int` (0/1). `validate_power(ritems)` is called per-rack alongside `check_overlaps(ritems)`. `render_power` consumes `_pdu_index` and feeds `render_page`. Names match across tasks and tests.
|
||||||
163
notes/dev/specs/2026-06-24-rack-power-design.md
Normal file
163
notes/dev/specs/2026-06-24-rack-power-design.md
Normal file
|
|
@ -0,0 +1,163 @@
|
||||||
|
# Rack Power (Phase 2) Design
|
||||||
|
|
||||||
|
**Date:** 2026-06-24
|
||||||
|
**Status:** Approved
|
||||||
|
**Parent spec:** `notes/dev/specs/2026-06-24-rack-documentation-design.md` (Phase 2)
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
Add power-distribution data to the rack documentation pipeline and render it as a
|
||||||
|
mermaid graph on the generated rack page, so the published page at
|
||||||
|
`docs.makerfloss.eu` shows which PDU/outlet feeds each device and which devices
|
||||||
|
have redundant (dual-PSU) feeds. This reuses every Phase 1 mechanism: the same
|
||||||
|
`scripts/gen_rack.py` generator, the same generated files under
|
||||||
|
`docs/infrastructure/racks/`, and the same CI drift guard.
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
- Phase 1 (rack elevation) is merged. `scripts/gen_rack.py` reads
|
||||||
|
`docs/hardware/*.md` files carrying a `rack:` field, validates placement
|
||||||
|
(U range, overlap, 0U rules), and writes `<rack>-elevation.svg` +
|
||||||
|
`<rack>.md` per rack. Tests in `tests/test_gen_rack.py`.
|
||||||
|
- The `pdu` value is already in the `hardware` `kind` enum
|
||||||
|
(`scripts/overview_config.yml`), so PDU files already validate and already
|
||||||
|
appear in the hardware index under "PDUs".
|
||||||
|
- Phase 1 already renders 0U `rack_face: left|right` items as side-rails in the
|
||||||
|
SVG, so PDU files need **no new SVG code** to appear in the elevation.
|
||||||
|
- The Makefile `docs-check` target and CI `Fail on drift` step already diff the
|
||||||
|
**entire** `docs/infrastructure/racks/` directory, so a regenerated page with
|
||||||
|
a power graph is already drift-covered — **no Makefile/CI edits required**.
|
||||||
|
- Mermaid is **not yet enabled** in `mkdocs.yml` (superfences has no mermaid
|
||||||
|
custom fence). Enabling it was nominally a Phase 3 item; Phase 2's graph needs
|
||||||
|
it, so it is **pulled forward** into this phase (decided during brainstorming).
|
||||||
|
- The `mfNN` rack positions are fictional placeholders proving the pipeline. The
|
||||||
|
power data added here is **similarly provisional** until real values are given.
|
||||||
|
|
||||||
|
## Data model
|
||||||
|
|
||||||
|
### Powered devices — `power:` frontmatter
|
||||||
|
|
||||||
|
Each powered device gains a `power` list; each entry is one feed:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
power:
|
||||||
|
- { pdu: pdu01, outlet: 1 }
|
||||||
|
- { pdu: pdu02, outlet: 1 } # a second entry = a redundant PSU feed
|
||||||
|
```
|
||||||
|
|
||||||
|
`power` is optional — a device with no `power` field simply contributes no power
|
||||||
|
edges.
|
||||||
|
|
||||||
|
### PDU files — new lightweight hardware items
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# docs/hardware/pdu01.md
|
||||||
|
hostname: pdu01
|
||||||
|
kind: pdu
|
||||||
|
status: in-use
|
||||||
|
rack: rack01
|
||||||
|
rack_face: left
|
||||||
|
outlets: 8
|
||||||
|
```
|
||||||
|
|
||||||
|
PDUs are 0U side-rail items (`rack_face: left|right`, no `rack_u`/`u_height`),
|
||||||
|
exactly the shape Phase 1's validator and SVG already handle. `outlets` is a new
|
||||||
|
field, validated by `gen_rack.py` (below). No `overview_config.yml` change is
|
||||||
|
needed: `kind: pdu` is already an enum value, and `outlets` is an extra field
|
||||||
|
that `gen_overview.py` ignores.
|
||||||
|
|
||||||
|
### Provisional data populated by this phase
|
||||||
|
|
||||||
|
- `pdu01` — `rack_face: left`, `outlets: 8`.
|
||||||
|
- `pdu02` — `rack_face: right`, `outlets: 8`.
|
||||||
|
- `mf00..mf04` — fed from `pdu01` outlets 1..5 respectively.
|
||||||
|
- `mf00` — additionally fed from `pdu02` outlet 1 (the redundant demonstration).
|
||||||
|
|
||||||
|
## Validation (rule 3 from the parent spec)
|
||||||
|
|
||||||
|
A new `validate_power(items: list[dict]) -> None` in `gen_rack.py`, called from
|
||||||
|
`generate()` after per-item placement validation and before/with overlap
|
||||||
|
checking. It raises `SchemaError` (→ stderr + exit 1, nothing written) when:
|
||||||
|
|
||||||
|
1. A `kind: pdu` file does not declare `outlets` as a positive integer.
|
||||||
|
2. A `power` value is not a list, or an entry is not a mapping.
|
||||||
|
3. An entry lacks a non-empty string `pdu` or an integer `outlet`.
|
||||||
|
4. An entry's `pdu` does not resolve to a loaded file whose `kind == pdu`.
|
||||||
|
5. An entry's `outlet` is outside `1..outlets` of the referenced PDU.
|
||||||
|
|
||||||
|
PDU resolution is by `hostname` against all loaded rack items (a PDU lookup map
|
||||||
|
`{hostname: fm for fm in items if kind == pdu}`).
|
||||||
|
|
||||||
|
## Rendering
|
||||||
|
|
||||||
|
### `render_power(rack, items) -> str`
|
||||||
|
|
||||||
|
Returns a fenced mermaid block, or `""` when no device in the rack has any
|
||||||
|
`power` entry (so the `## Power` section is omitted for power-less racks).
|
||||||
|
|
||||||
|
- ` ```mermaid ` + `flowchart LR`.
|
||||||
|
- One node per PDU that is referenced or placed in the rack:
|
||||||
|
label `pdu01<br/>8 outlets`.
|
||||||
|
- One node per powered device: label = hostname.
|
||||||
|
- One edge per feed: `pduNode -->|outlet N| deviceNode`.
|
||||||
|
- Node ids are the hostname with non-alphanumeric characters replaced by `_`
|
||||||
|
(display text keeps the real hostname via the quoted label), guarding against
|
||||||
|
ids that mermaid would reject.
|
||||||
|
- Deterministic: PDU nodes sorted by hostname, device nodes by
|
||||||
|
`(rack_u, hostname)`, edges sorted by `(pdu, outlet, device)`.
|
||||||
|
|
||||||
|
Redundant feeds render naturally as two edges into one device node, from two
|
||||||
|
different PDUs.
|
||||||
|
|
||||||
|
### `render_page` change
|
||||||
|
|
||||||
|
Insert a `## Power` section containing `render_power(...)` **between** the
|
||||||
|
Elevation and Occupancy sections — only when `render_power` returns non-empty.
|
||||||
|
|
||||||
|
## mkdocs (pulled forward)
|
||||||
|
|
||||||
|
Add the mermaid custom fence to the existing `pymdownx.superfences` entry in
|
||||||
|
`mkdocs.yml`:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- pymdownx.superfences:
|
||||||
|
custom_fences:
|
||||||
|
- name: mermaid
|
||||||
|
class: mermaid
|
||||||
|
format: !!python/name:pymdownx.superfences.fence_code_format
|
||||||
|
```
|
||||||
|
|
||||||
|
Material ships `mermaid.js` and activates it on this fence, so
|
||||||
|
`mkdocs build --strict` renders the graph as a diagram.
|
||||||
|
|
||||||
|
## Integration
|
||||||
|
|
||||||
|
| File | Change |
|
||||||
|
|------|--------|
|
||||||
|
| `scripts/gen_rack.py` | Add `validate_power`; call it in `generate`; add `render_power`; insert `## Power` in `render_page` |
|
||||||
|
| `tests/test_gen_rack.py` | Add `validate_power` + `render_power` + `generate` power cases |
|
||||||
|
| `mkdocs.yml` | Enable mermaid via superfences custom fence |
|
||||||
|
| `docs/hardware/pdu01.md`, `pdu02.md` | New 0U PDU files (`kind: pdu`, `outlets: 8`) |
|
||||||
|
| `docs/hardware/mf00.md`..`mf04.md` | Add `power:` lists |
|
||||||
|
| `docs/hardware/index.md` | Regenerated (PDUs now listed) |
|
||||||
|
| `docs/infrastructure/racks/rack01.md`, `rack01-elevation.svg` | Regenerated (power section + PDU side-rails) |
|
||||||
|
|
||||||
|
No `Makefile`, `.forgejo/workflows/docs.yml`, or `overview_config.yml` changes.
|
||||||
|
|
||||||
|
## Test plan
|
||||||
|
|
||||||
|
- **Unit — `validate_power`:** accept a valid feed; reject unknown `pdu`,
|
||||||
|
`pdu` pointing at a non-`pdu` kind, `outlet` of 0 / above `outlets`, a
|
||||||
|
malformed entry (non-mapping / missing keys), and a `pdu` file with
|
||||||
|
missing/zero/non-int `outlets`.
|
||||||
|
- **Unit — `render_power`:** PDU and device nodes present; a redundant device
|
||||||
|
has two incoming edges; returns `""` when no device has power; deterministic
|
||||||
|
for reordered input.
|
||||||
|
- **Integration — `generate`:** with valid fixtures the page contains the
|
||||||
|
`## Power` section and the mermaid fence; with a dangling `pdu` reference it
|
||||||
|
returns `1` and writes nothing.
|
||||||
|
- **Drift:** `make docs-check` exits 0 after regeneration (existing guard,
|
||||||
|
unchanged).
|
||||||
|
- **Visual:** `mkdocs build --strict` succeeds and the rack page shows the power
|
||||||
|
graph as a rendered diagram (not a raw code block), with mf00 showing two
|
||||||
|
incoming feeds.
|
||||||
|
|
@ -120,6 +120,58 @@ def check_overlaps(items: list[dict]) -> None:
|
||||||
occupied[key] = name
|
occupied[key] = name
|
||||||
|
|
||||||
|
|
||||||
|
def _pdu_index(items: list[dict]) -> dict[str, dict]:
|
||||||
|
"""Map hostname -> frontmatter for every kind:pdu item."""
|
||||||
|
return {
|
||||||
|
fm.get("hostname"): fm
|
||||||
|
for fm in items
|
||||||
|
if fm.get("kind") == "pdu"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def validate_power(items: list[dict]) -> None:
|
||||||
|
"""Validate PDU outlet declarations and `power` feeds within one rack.
|
||||||
|
|
||||||
|
Rule 3: every power[].pdu resolves to a kind:pdu file, and outlet is
|
||||||
|
within that PDU's `outlets` count.
|
||||||
|
"""
|
||||||
|
pdus = _pdu_index(items)
|
||||||
|
for name, fm in pdus.items():
|
||||||
|
outlets = fm.get("outlets")
|
||||||
|
if not isinstance(outlets, int) or outlets < 1:
|
||||||
|
raise SchemaError(
|
||||||
|
f"{name}: kind:pdu must declare a positive integer 'outlets'"
|
||||||
|
)
|
||||||
|
for fm in items:
|
||||||
|
feeds = fm.get("power")
|
||||||
|
if feeds is None:
|
||||||
|
continue
|
||||||
|
name = fm.get("hostname", "?")
|
||||||
|
if not isinstance(feeds, list):
|
||||||
|
raise SchemaError(f"{name}: power must be a list")
|
||||||
|
for feed in feeds:
|
||||||
|
if not isinstance(feed, dict):
|
||||||
|
raise SchemaError(f"{name}: power entry must be a mapping")
|
||||||
|
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'")
|
||||||
|
if not isinstance(outlet, int):
|
||||||
|
raise SchemaError(
|
||||||
|
f"{name}: power entry for {pdu} needs an integer 'outlet'"
|
||||||
|
)
|
||||||
|
target = pdus.get(pdu)
|
||||||
|
if target is None:
|
||||||
|
raise SchemaError(
|
||||||
|
f"{name}: power pdu={pdu!r} is not a known kind:pdu file"
|
||||||
|
)
|
||||||
|
count = target["outlets"]
|
||||||
|
if outlet < 1 or outlet > count:
|
||||||
|
raise SchemaError(
|
||||||
|
f"{name}: outlet {outlet} out of range 1..{count} on {pdu}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _esc(s: object) -> str:
|
def _esc(s: object) -> str:
|
||||||
return str(s).replace("&", "&").replace("<", "<").replace(">", ">")
|
return str(s).replace("&", "&").replace("<", "<").replace(">", ">")
|
||||||
|
|
||||||
|
|
@ -240,6 +292,52 @@ def render_svg(rack: str, items: list[dict]) -> str:
|
||||||
return "\n".join(p) + "\n"
|
return "\n".join(p) + "\n"
|
||||||
|
|
||||||
|
|
||||||
|
def _node_id(name: str) -> str:
|
||||||
|
"""A mermaid-safe node id derived from a hostname."""
|
||||||
|
return re.sub(r"[^0-9A-Za-z]", "_", str(name))
|
||||||
|
|
||||||
|
|
||||||
|
def render_power(rack: str, items: list[dict]) -> str:
|
||||||
|
"""Return a mermaid power-distribution flowchart, or '' if no feeds.
|
||||||
|
|
||||||
|
Assumes `validate_power(items)` has already passed: every referenced PDU
|
||||||
|
resolves to a kind:pdu item with a positive-int `outlets`. `generate`
|
||||||
|
guarantees this by validating before any render call.
|
||||||
|
"""
|
||||||
|
powered = [fm for fm in items if fm.get("power")]
|
||||||
|
if not powered:
|
||||||
|
return ""
|
||||||
|
pdus = _pdu_index(items)
|
||||||
|
|
||||||
|
edges: list[tuple[str, int, str]] = []
|
||||||
|
for fm in powered:
|
||||||
|
device = fm.get("hostname", "?")
|
||||||
|
for feed in fm["power"]:
|
||||||
|
edges.append((feed["pdu"], feed["outlet"], device))
|
||||||
|
edges.sort()
|
||||||
|
|
||||||
|
lines: list[str] = ["```mermaid", "flowchart LR"]
|
||||||
|
for pdu in sorted(pdus):
|
||||||
|
outlets = pdus[pdu].get("outlets")
|
||||||
|
lines.append(f' {_node_id(pdu)}["{pdu}<br/>{outlets} outlets"]')
|
||||||
|
devices = sorted(
|
||||||
|
powered,
|
||||||
|
key=lambda i: (
|
||||||
|
i.get("rack_u", 0) if isinstance(i.get("rack_u"), int) else 0,
|
||||||
|
i.get("hostname", ""),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
for fm in devices:
|
||||||
|
device = fm.get("hostname", "?")
|
||||||
|
lines.append(f' {_node_id(device)}["{device}"]')
|
||||||
|
for pdu, outlet, device in edges:
|
||||||
|
lines.append(
|
||||||
|
f" {_node_id(pdu)} -->|outlet {outlet}| {_node_id(device)}"
|
||||||
|
)
|
||||||
|
lines.append("```")
|
||||||
|
return "\n".join(lines) + "\n"
|
||||||
|
|
||||||
|
|
||||||
def render_page(rack: str, items: list[dict]) -> str:
|
def render_page(rack: str, items: list[dict]) -> str:
|
||||||
items = _sorted_items(items)
|
items = _sorted_items(items)
|
||||||
lines: list[str] = []
|
lines: list[str] = []
|
||||||
|
|
@ -255,6 +353,12 @@ def render_page(rack: str, items: list[dict]) -> str:
|
||||||
lines.append("")
|
lines.append("")
|
||||||
lines.append(f"")
|
lines.append(f"")
|
||||||
lines.append("")
|
lines.append("")
|
||||||
|
power = render_power(rack, items)
|
||||||
|
if power:
|
||||||
|
lines.append("## Power")
|
||||||
|
lines.append("")
|
||||||
|
lines.append(power.rstrip())
|
||||||
|
lines.append("")
|
||||||
lines.append("## Occupancy")
|
lines.append("## Occupancy")
|
||||||
lines.append("")
|
lines.append("")
|
||||||
lines.append("| U | Device | Kind | Face | Status |")
|
lines.append("| U | Device | Kind | Face | Status |")
|
||||||
|
|
@ -295,6 +399,7 @@ def generate(hardware_dir: Path, output_dir: Path) -> int:
|
||||||
for rack, ritems in racks.items():
|
for rack, ritems in racks.items():
|
||||||
try:
|
try:
|
||||||
check_overlaps(ritems)
|
check_overlaps(ritems)
|
||||||
|
validate_power(ritems)
|
||||||
except SchemaError as e:
|
except SchemaError as e:
|
||||||
errors.append(f"{rack}: {e}")
|
errors.append(f"{rack}: {e}")
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -192,3 +192,153 @@ def test_generate_returns_1_on_overlap(tmp_path):
|
||||||
|
|
||||||
assert rc == 1
|
assert rc == 1
|
||||||
assert not (out / "rack01.md").exists()
|
assert not (out / "rack01.md").exists()
|
||||||
|
|
||||||
|
|
||||||
|
def test_validate_power_accepts_valid_feed():
|
||||||
|
items = [
|
||||||
|
item(hostname="pdu01", kind="pdu", rack_face="left", outlets=8),
|
||||||
|
item(hostname="mf00", rack_u=1, u_height=1, rack_face="front",
|
||||||
|
power=[{"pdu": "pdu01", "outlet": 1}]),
|
||||||
|
]
|
||||||
|
gen_rack.validate_power(items)
|
||||||
|
|
||||||
|
|
||||||
|
def test_validate_power_rejects_unknown_pdu():
|
||||||
|
items = [item(hostname="mf00", rack_u=1, u_height=1, rack_face="front",
|
||||||
|
power=[{"pdu": "ghost", "outlet": 1}])]
|
||||||
|
with pytest.raises(gen_rack.SchemaError):
|
||||||
|
gen_rack.validate_power(items)
|
||||||
|
|
||||||
|
|
||||||
|
def test_validate_power_rejects_non_pdu_target():
|
||||||
|
items = [
|
||||||
|
item(hostname="sw01", kind="switch", rack_u=1, u_height=1,
|
||||||
|
rack_face="front"),
|
||||||
|
item(hostname="mf00", rack_u=2, u_height=1, rack_face="front",
|
||||||
|
power=[{"pdu": "sw01", "outlet": 1}]),
|
||||||
|
]
|
||||||
|
with pytest.raises(gen_rack.SchemaError):
|
||||||
|
gen_rack.validate_power(items)
|
||||||
|
|
||||||
|
|
||||||
|
def test_validate_power_rejects_outlet_over_count():
|
||||||
|
items = [
|
||||||
|
item(hostname="pdu01", kind="pdu", rack_face="left", outlets=8),
|
||||||
|
item(hostname="mf00", rack_u=1, u_height=1, rack_face="front",
|
||||||
|
power=[{"pdu": "pdu01", "outlet": 9}]),
|
||||||
|
]
|
||||||
|
with pytest.raises(gen_rack.SchemaError):
|
||||||
|
gen_rack.validate_power(items)
|
||||||
|
|
||||||
|
|
||||||
|
def test_validate_power_rejects_outlet_zero():
|
||||||
|
items = [
|
||||||
|
item(hostname="pdu01", kind="pdu", rack_face="left", outlets=8),
|
||||||
|
item(hostname="mf00", rack_u=1, u_height=1, rack_face="front",
|
||||||
|
power=[{"pdu": "pdu01", "outlet": 0}]),
|
||||||
|
]
|
||||||
|
with pytest.raises(gen_rack.SchemaError):
|
||||||
|
gen_rack.validate_power(items)
|
||||||
|
|
||||||
|
|
||||||
|
def test_validate_power_rejects_malformed_entry():
|
||||||
|
items = [
|
||||||
|
item(hostname="pdu01", kind="pdu", rack_face="left", outlets=8),
|
||||||
|
item(hostname="mf00", rack_u=1, u_height=1, rack_face="front",
|
||||||
|
power=["pdu01"]),
|
||||||
|
]
|
||||||
|
with pytest.raises(gen_rack.SchemaError):
|
||||||
|
gen_rack.validate_power(items)
|
||||||
|
|
||||||
|
|
||||||
|
def test_validate_power_rejects_pdu_without_outlets():
|
||||||
|
items = [
|
||||||
|
item(hostname="pdu01", kind="pdu", rack_face="left"),
|
||||||
|
item(hostname="mf00", rack_u=1, u_height=1, rack_face="front",
|
||||||
|
power=[{"pdu": "pdu01", "outlet": 1}]),
|
||||||
|
]
|
||||||
|
with pytest.raises(gen_rack.SchemaError):
|
||||||
|
gen_rack.validate_power(items)
|
||||||
|
|
||||||
|
|
||||||
|
def test_generate_returns_1_on_bad_power_ref(tmp_path):
|
||||||
|
hw = tmp_path / "hardware"
|
||||||
|
out = tmp_path / "out"
|
||||||
|
hw.mkdir()
|
||||||
|
_write_item(
|
||||||
|
hw,
|
||||||
|
"mf00",
|
||||||
|
"---\nhostname: mf00\nkind: server\nstatus: in-use\n"
|
||||||
|
"rack: rack01\nrack_u: 1\nu_height: 1\nrack_face: front\n"
|
||||||
|
"power:\n - { pdu: ghost, outlet: 1 }\n---\n",
|
||||||
|
)
|
||||||
|
rc = gen_rack.generate(hw, out)
|
||||||
|
assert rc == 1
|
||||||
|
assert not (out / "rack01.md").exists()
|
||||||
|
|
||||||
|
|
||||||
|
def test_render_power_has_nodes_and_edge_labels():
|
||||||
|
items = [
|
||||||
|
item(hostname="pdu01", kind="pdu", rack_face="left", outlets=8),
|
||||||
|
item(hostname="mf00", rack_u=1, u_height=1, rack_face="front",
|
||||||
|
power=[{"pdu": "pdu01", "outlet": 3}]),
|
||||||
|
]
|
||||||
|
out = gen_rack.render_power("rack01", items)
|
||||||
|
assert "```mermaid" in out
|
||||||
|
assert "flowchart LR" in out
|
||||||
|
assert "pdu01" in out
|
||||||
|
assert "8 outlets" in out
|
||||||
|
assert "outlet 3" in out
|
||||||
|
assert "mf00" in out
|
||||||
|
|
||||||
|
|
||||||
|
def test_render_power_redundant_device_has_two_edges():
|
||||||
|
items = [
|
||||||
|
item(hostname="pdu01", kind="pdu", rack_face="left", outlets=8),
|
||||||
|
item(hostname="pdu02", kind="pdu", rack_face="right", outlets=8),
|
||||||
|
item(hostname="mf00", rack_u=1, u_height=1, rack_face="front",
|
||||||
|
power=[{"pdu": "pdu01", "outlet": 1},
|
||||||
|
{"pdu": "pdu02", "outlet": 1}]),
|
||||||
|
]
|
||||||
|
out = gen_rack.render_power("rack01", items)
|
||||||
|
assert out.count("-->|outlet") == 2
|
||||||
|
|
||||||
|
|
||||||
|
def test_render_power_empty_when_no_feeds():
|
||||||
|
items = [item(hostname="mf00", rack_u=1, u_height=1, rack_face="front")]
|
||||||
|
assert gen_rack.render_power("rack01", items) == ""
|
||||||
|
|
||||||
|
|
||||||
|
def test_render_power_is_deterministic():
|
||||||
|
a = item(hostname="pdu01", kind="pdu", rack_face="left", outlets=8)
|
||||||
|
b = item(hostname="mf01", rack_u=2, u_height=1, rack_face="front",
|
||||||
|
power=[{"pdu": "pdu01", "outlet": 2}])
|
||||||
|
c = item(hostname="mf00", rack_u=1, u_height=1, rack_face="front",
|
||||||
|
power=[{"pdu": "pdu01", "outlet": 1}])
|
||||||
|
assert gen_rack.render_power("rack01", [a, b, c]) == \
|
||||||
|
gen_rack.render_power("rack01", [c, b, a])
|
||||||
|
|
||||||
|
|
||||||
|
def test_generate_includes_power_section(tmp_path):
|
||||||
|
hw = tmp_path / "hardware"
|
||||||
|
out = tmp_path / "out"
|
||||||
|
hw.mkdir()
|
||||||
|
_write_item(
|
||||||
|
hw,
|
||||||
|
"pdu01",
|
||||||
|
"---\nhostname: pdu01\nkind: pdu\nstatus: in-use\n"
|
||||||
|
"rack: rack01\nrack_face: left\noutlets: 8\n---\n",
|
||||||
|
)
|
||||||
|
_write_item(
|
||||||
|
hw,
|
||||||
|
"mf00",
|
||||||
|
"---\nhostname: mf00\nkind: server\nstatus: in-use\n"
|
||||||
|
"rack: rack01\nrack_u: 1\nu_height: 1\nrack_face: front\n"
|
||||||
|
"power:\n - { pdu: pdu01, outlet: 1 }\n---\n",
|
||||||
|
)
|
||||||
|
rc = gen_rack.generate(hw, out)
|
||||||
|
assert rc == 0
|
||||||
|
page = (out / "rack01.md").read_text()
|
||||||
|
assert "## Power" in page
|
||||||
|
assert "```mermaid" in page
|
||||||
|
assert "outlet 1" in page
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue