Power distribution
Power distribution¶
The plugin computes how each planned device's power draw distributes across the
rack's PDUs, feeds, and banks for a per-bank power heatmap. The projection
reads existing dcim data (real PDUs, feeds, device draws) and remains read-only
— your NetBox records are never modified. The heatmap overlay shows load per bank
(colored health bar), feed-leg redundancy (header colors), and overload warnings.
Configuration¶
All settings live in PLUGINS_CONFIG:
PLUGINS_CONFIG = {
"netbox_rack_design": {
# one of: "none" (default), "builtin", "script"
"distribution_mode": "none",
# dotted path to a callable, used when distribution_mode == "script"
"distribution_script": "",
# Custom-field bridge for planning dialogs (optional; empty by default)
"planning_fields": {},
},
}
Mode: none (default)¶
Per-device rack-share gradient — the current behavior. PDUs are excluded infrastructure. The heatmap colors each tile by the device's share of total rack power.
Mode: builtin¶
Zero-configuration distribution across PDUs and banks, built on two universal naming conventions that work on any NetBox instance:
"distribution_mode": "builtin",
The two universal conventions¶
The built-in mode works without a script or custom config, iff:
-
Bank = first segment of the outlet port name. A PDU outlet is named
"<bank>/<port>"; the part before/is the bank ID (1/1→ bank 1,2/3→ bank 2). This is how NetBox's native outlet naming works — no custom parsing. -
Feed-leg = the feed a PDU is bound to. A PDU draws from one feed (real
dcim.PowerFeedor plannedDesignPowerFeed); the feed's identity determines the redundancy leg (A or B), never the PDU's device name. Binding is explicit — you choose which feed each planned PDU uses when you add it to the rack.
Under these conventions, the plugin distributes each device's load per bank:
- Cabled device (real or planned, with a power outlet on a PDU) → charge the outlet's bank directly.
- Uncabled device (planned, not yet cabled) → its position in the rack determines which bank feeds it; redundant devices (2+ PSUs) charge both feed-legs in full (worst-case failover).
Per-bank breaker = pdu_input_draw / power_bank_count (where
power_bank_count is the distinct bank IDs on that PDU).
"builtin" reads no custom fields at all — no rack power ceiling, no PDU
orientation — and always splits rack units across banks symmetrically in a
fixed direction. Sites that need either of those need distribution_mode =
"script" (below).
Mode: script¶
For distribution behavior a configuration cannot express, point distribution_script at any
importable callable with the signature fn(rack, devices) -> Distribution | None.
The callable receives the planned dcim.Rack and the list of planned consumers
+ PDUs, and returns a distribution dict (see
pdu-distribution-spec.md §3 for the full schema),
or None when the rack has no PDUs to distribute across (heatmap stays
per-device).
It runs inside the NetBox process with full ORM read access — by convention,
read-only: compute a structure, never write to dcim.
Ready-to-run example (ships with the plugin)¶
The plugin ships a stock-runnable minimal example that reproduces the built-in distribution verbatim under the two conventions, with no site-specific data or custom field names:
"distribution_mode": "script",
"distribution_script": "netbox_rack_design.distribution_example.build",
It is a copyable template: enable it as-is to see the distribution in action,
then copy and edit for your site's behavior customization (direction, breaker
ceilings, PSU redundancy schemes). Like the naming example, it reads custom fields
generically via the planning_fields config bridge (§ Planning fields below)
— no site-specific cf names are hardcoded in the shipped code.
Fully-worked example¶
The plugin also ships a richer reference that fills in three customization surfaces the minimal example leaves stubbed:
"distribution_mode": "script",
"distribution_script": "netbox_rack_design.distribution_advanced_example.build",
# Optional: override the bank health-bar thresholds
"power_warn_pct": 75,
"power_critical_pct": 95,
"planning_fields": {
"rack": [
{"key": "power_limitation", "label": "Power limitation (W)",
"type": "number", "source": "cf.power_limitation"},
{"key": "pdu_location", "label": "PDU location", "type": "choice",
"choices": ["top", "bottom"], "source": "cf.pdu_location"},
],
"pdu": [
{"key": "pdu_scheme", "label": "PDU topology label",
"type": "text", "source": "cf.pdu_scheme"},
],
},
It demonstrates:
- Topology scheme label — computes a human-readable topology string
(e.g.,
"2x1PH2Banks") from the per-PDU bank-count signature, and allows a per-PDU override via a custom field. - Config-driven thresholds — reads bank utilization thresholds
(
power_warn_pct/power_critical_pct) from plugin config, matching the naming engine's pattern, instead of hardcoding constants. - Per-PDU custom field — reads the
planning_fields["pdu"]config to show how a site can layer in PDU-specific planning data via the config bridge.
Both examples share the same core algorithm and helper functions with the
built-in — you are not re-implementing distribution; you are customizing
behaviour (direction, ceilings, scheme labels) by reading data through the
config bridge. Start from distribution_example.py and adapt it to your needs.
Notes¶
- Robust to misconfiguration. If
distribution_scriptcan't be resolved (wrong or empty dotted path, module not importable, target not callable) — or the script raises while computing a distribution — the engine logs a warning and falls back to thenoneheatmap (per-device gradient). A broken or not-yet-loaded script therefore degrades the overlay gracefully rather than blocking the editor. Fixdistribution_scriptto restore custom distribution. - The structure the script returns remains read-only — the heatmap reads it,
never writes. No
dcimmutations, no design dirty flag. rack.pkis always set (a distribution runs on a saved rack). A script never receives a preview rack.
Feed binding & planned PDUs¶
A real PDU cabled to a real dcim.PowerFeed sizes its breaker from that feed
electricals (voltage × amperage × phase, the native path). A planned PDU (a
placement add, not yet realized in dcim) has no real feed yet, so the plugin
models planned feeds the same way: a DesignPowerFeed row carries the same
electrical fields (voltage, amperage, phase, supply), and a planned PDU binds
to it just like a real PDU binds to a real feed.
Dialog flows¶
When you add a PDU to a rack:
- If the rack has real feeds (a provisioned rack with
dcim.PowerFeedrecords) — a bind-to-feed picker appears, listing the rack's feeds (real first, then any planned feeds you've defined). Pick one to bind the PDU. - If the rack has no real feeds (greenfield planning) — a per-rack Power button opens the planned-power flow where you define feeds manually (name, voltage, amperage, phase) or copy them from another rack's real feeds.
After feeds are defined, each PDU binding travels with the design and is restored on load. A PDU with no binding is omitted from the distribution (logged, no error).
A planned PDU's custom fields — e.g., pdu_scheme (a site-specific topology
label; used by a distribution_script, not by "builtin") — come from one
source:
- Live device reference — if you link the PDU to an existing real device via
power_source_device, its custom fields are read live on every heatmap render. Edit the source device's fields and the heatmap updates immediately. - Manual entry — via the planning dialog, driven by
planning_fields["pdu"]entries. The values persist inpower_configon Save.
The two are mutually exclusive per placement.
Note pdu_location (top/bottom, for directing which bank claims which units)
is a rack-level custom field, not a PDU-level one — it is declared under
planning_fields["rack"] (see the example above) and reaches a
distribution_script via DesignRackPower merged over rack.cf, the same
path as power_limitation. It has nothing to do with power_source_device /
power_config, which resolve a PDU's custom fields.
Planning fields — custom-field bridge (script tier only)¶
Most sites keep power policy in custom fields: power limitations (rack
ceiling), PDU locations (direction), or future site-specific fields. These
reach the engine only under distribution_mode = "script" — the "builtin"
mode reads no custom fields at all. A distribution_script never hardcodes a
site's custom-field names — instead, planning_fields in PLUGINS_CONFIG maps
them generically, resolved by name through each entry's source key, so the
shipped code never changes per deployment. The rack/PDU planning dialogs'
manual cf inputs are therefore a script-tier feature too: with "none" or
"builtin", planning_fields has nothing to feed and the dialogs show only
native inputs.
"planning_fields": {
"rack": [
{"key": "power_limitation", "label": "Power limitation (W)",
"type": "number", "source": "cf.power_limitation"},
{"key": "pdu_location", "label": "PDU location", "type": "choice",
"choices": ["top", "bottom"], "source": "cf.pdu_location"},
],
"pdu": [
{"key": "pdu_scheme", "label": "Custom PDU scheme",
"type": "text", "source": "cf.pdu_scheme"},
],
}
Each entry declares:
key— the name the script/dialog knows it by (e.g.,power_limitation).label— what the UI shows.type—number,text, orchoice;choicerequireschoices.source— where to read the value:cf.<fieldname>for custom fields. Dotted paths are self-documenting, matching the naming-template grammar.
The plugin reads from custom fields only (never native fields) and never writes
back to dcim. An empty planning_fields (the default) is fine — the base
builtin feature needs none. The dialogs then show only native inputs (copy-from-rack,
bind-to-feed).
Writing and adapting a script¶
Copy netbox_rack_design/distribution_example.py and edit the parts that matter
to your site: the direction (unit-to-bank mapping if PDU placement is atypical),
breaker ceilings (per-bank limits), and redundancy scheme (which PSU lands
on which feed for a given device type).
Key points¶
- Contract:
build(rack, devices) -> Distribution | None. Therackis a real saveddcim.Rack;devicesis a normalized list of planned consumers and PDUs. Return theDistributiondict (orNone). It's read-only — compute, never write. - Config bridge: read custom fields through
read_planning_fields(role, obj)— this abstracts theplanning_fieldsmap, so a site'srack_power_capmaps to the keypower_limitationyour algorithm knows. Never hardcode cf names. - Shared helpers: both shipped examples reuse
distribution._collect_pdusanddistribution_example's_unit_to_bank,_legs_for,_chargefunctions. You are not reinventing the algorithm — you're configuring its behavior. - Absolute imports: use
from netbox_rack_design.distribution import ...(not relative imports) so the script keeps working if you copy it out of the package intoSCRIPTS_ROOT. NetBox'snetbox_rack_designpackage is always importable.
Keeping the script in NetBox's SCRIPTS_ROOT¶
You can keep the script where NetBox already manages Python: SCRIPTS_ROOT
(default <netbox>/scripts/). A file there is importable as scripts.<module>,
so reference it as:
"distribution_mode": "script",
"distribution_script": "scripts.power_distribution.build",
Set the config once. Edit configuration.py with the config above and
restart NetBox — this is the only step that needs a restart. While the script
isn't present yet, the distribution safely falls back to none (per-device
heatmap).
After that one-time config change, adding or editing the script itself needs
no restart. NetBox picks it up and the plugin imports the build function on
the next heatmap render.
Variant 1 — through the UI (Customization → Scripts → Add)¶
Use this to view and edit the script from the NetBox UI. NetBox lists a module
under Scripts only when it contains an extras.scripts.Script subclass, so wrap
the example with a tiny one:
from extras.scripts import Script
class PowerDistributionScript(Script):
class Meta:
name = "Rack Design power distribution"
def run(self, data, commit):
self.log_info("Distribution script for the rack-design editor; nothing to run.")
# ... paste the helpers and build() function from distribution_example.py below ...
Customization → Scripts → Add, name the module power_distribution, and
paste the wrapped script. No restart — reopen a design and the new distribution
appears immediately. Edit it later the same way.
Variant 2 — copy the file into SCRIPTS_ROOT¶
If you don't need UI editing, just drop the file in:
cp .../netbox_rack_design/distribution_example.py $SCRIPTS_ROOT/power_distribution.py
No restart needed; the plugin imports it on the next heatmap render.
Referencing the package copy directly,
netbox_rack_design.distribution_example.build, also works and needs no copy at all — use that if you only want the shipped defaults and no per-instance editing.
Verify on your instance¶
A quick end-to-end check that distribution works the way you expect:
- Configure and restart. Set
distribution_modeinPLUGINS_CONFIG:
"distribution_mode": "builtin",
Restart NetBox so the config is loaded.
-
Open a rack with PDUs and planned devices. Open a design in the editor and add devices (or view an existing design with devices already placed). Ensure the rack has real or planned
PowerFeedrecords, and each PDU is bound to one. -
Toggle the power heatmap. At the top of the editor, find the power (lightning bolt) icon / heatmap toggle. Turn it on. You should see:
- Each PDU column labeled with the feed-leg header (A or B).
- Inside each PDU, stacked colored banks (green → orange → red by load %).
- Device tiles with A/B feed-leg edges (if the device is redundant).
- Instant tooltips on hover (used W / breaker W per bank).
-
Overload warnings in red if any bank exceeds its breaker.
-
Drag a device to a different rack unit. The bank colors should update to reflect the new load distribution. If the device is cabled to a specific PDU outlet, it charges that bank; if uncabled (planned), it charges the bank corresponding to its new U position.
-
Check custom fields (script tier only). If you've switched to
distribution_mode = "script"withplanning_fieldsconfigured, add a rack with apower_limitationcustom field. Open the Power dialog and verify the field appears as configured. Same for PDU-role fields if your script reads them. (Skip this step under"builtin"— it reads no custom fields.)
Nothing here writes to dcim: distribution only ever runs on the read-only
projection, never on page load or on Save.
Switching between modes¶
none→builtin: Restart NetBox. The heatmap now shows per-bank colors instead of per-device.builtin→script: Setdistribution_scriptand restart. The script replaces the built-in; behavior may differ if the script implements customizations.script→none: Setdistribution_modetononeand restart. The heatmap reverts to per-device gradient.
A misconfigured or broken script (wrong path, unimportable module, runtime error)
degrades to none gracefully — logs a warning, never errors the editor.
Troubleshooting¶
I see a per-device gradient, not per-bank colors.
- Check
distribution_modeinPLUGINS_CONFIG— if it's"none", that's expected (the default). Set it to"builtin"or"script"and restart. - If using
"script": check thatdistribution_scriptis a valid importable path and the function exists. Enable DEBUG logging to see import/execution errors logged on each heatmap render.
The heatmap looks incomplete; some PDUs are missing.
- Check that each PDU is bound to a feed (real or planned). A PDU with no binding is omitted from the distribution (logged at debug level).
- Check that feeds exist: real feeds for a provisioned rack, or planned feeds defined via the Power button for greenfield racks.
- Check that outlet ports follow the
"<bank>/<port>"convention (e.g.,1/1,2/3). An outlet namedpdu-outlet-1with no/has no bank and is ignored.
The distribution looks wrong; devices are charged to the wrong banks.
- Verify cabling: a cabled device should charge the PDU outlet's bank directly
(the first segment before
/). - Verify uncabled devices are placed at their intended U positions. The rack distribution assumes a unit-to-bank mapping based on direction and bank count; a device at U5 might charge a different bank than U7.
distribution_mode = "builtin"always splits in a fixed direction (bank 1 at the bottom) — it does not read the rack custom fieldpdu_locationor any other cf.distribution_mode = "script"may read the rack custom fieldpdu_location("top"or"bottom") through theplanning_fieldsconfig bridge to flip the direction (default"bottom"when the field is absent) — check your script and itsplanning_fields["rack"]entry if the direction is wrong.- Inspect the debug logs:
logger.debugmessages indistribution_example.py/distribution_advanced_example.pytrace each device charge and unit mapping.
Warnings or errors appear at the top of the editor.
- If a warning mentions an overload or power limitation breach, check the
specific bank/PDU and device draws. Thresholds (
power_warn_pct,power_critical_pct) default to 80% / 100%; override them inPLUGINS_CONFIGor in a custom script. - If a warning mentions an unresolvable
power_source_device, the planned PDU linked to a real device whose custom fields were requested, but the device was deleted. The distribution falls back to manualpower_configfields (or{}if none); no error, just logged.
How do I troubleshoot a custom script?
- Enable DEBUG logging on the plugin
(
netbox_rack_design.distribution*). Each heatmap render logs entry, per-device charges, and the finalDistributionobject. - Add
logger.debug()calls to your script to trace its own logic. - Test in a dev/staging environment first; a broken script degrades the heatmap but never breaks the editor.
- Restart NetBox once to load a config change (
distribution_scriptpath); after that, editing the script file alone needs no restart.
See also¶
- PDU distribution specification — the full design
reference, feed model details, and
Distributiondata contract. - Device naming — the naming engine, which follows the same configuration patterns (mode selection, built-in, script, config bridge).