Device naming
Device naming¶
The plugin proposes a name for every planned device (palette adds and moved
devices) through a configurable naming engine. The proposal appears in the
editor's rename dialog and is stored on the placement as proposed_name — your
live dcim data is never modified.
Configuration¶
All settings live in PLUGINS_CONFIG:
PLUGINS_CONFIG = {
"netbox_rack_design": {
# one of: "sequence" (default), "template", "script"
"naming_mode": "sequence",
# used when naming_mode == "template"
"naming_template": "{design.name}-{n}",
# dotted path to a callable, used when naming_mode == "script"
"naming_script": "",
},
}
Mode: sequence (default)¶
<design title>-<n> where n is the placement's 1-based ordinal within its
design. Zero configuration, always safe.
Mode: template¶
A str.format-style template over dotted attribute paths on real NetBox
model objects — not flat aliases. Available roots:
| Token root | Resolves to |
|---|---|
{design...} |
the Design ({design.name} is an alias for its title; any real attribute works: {design.site.name}, …) |
{device...} |
for moves/removes: the real dcim.Device (full attribute tree). For adds: a placement-backed proxy exposing the same paths ({device.site.name}, {device.device_type.model}, {device.rack.name}, {device.role.name}, {device.tenant.name}, {device.position}, {device.face}) |
{n} |
the placement's ordinal |
Traversal is safe: a missing attribute renders as an empty string and never raises.
"naming_template": "{design.name}-{device.site.name}-{device.role.name}-{n}"
# -> "Migration-AMS1-Server-3"
Mode: script¶
For conventions a template cannot express, point naming_script at any
importable callable with the signature fn(placement) -> str. The callable
receives the (possibly unsaved) DesignPlacement and returns the proposed
name. It runs inside the NetBox process with full ORM access — read-only by
convention: compute a string, never write.
Ready-to-run example (ships with the plugin)¶
The plugin ships a small, stock-runnable example you can enable as-is — no extra data, no custom role slugs, no lookup tables:
"naming_mode": "script",
"naming_script": "netbox_rack_design.naming_example.build_name",
It demonstrates the two patterns a template cannot express:
- Family counter — continue a numbered family (
ams1-leaf-switch-1→ next is-2) by asking NetBox for the highest existing number, so you never hand-pick the next digit. It also counts names proposed by other unsaved tiles in the same editor session, so two quick drops get consecutive numbers instead of colliding. - Phase pairs — PDUs run
a1, b1, a2, b2, …(an A/B phase pair per index) instead of a flat counter.
Copy netbox_rack_design/naming_example.py and edit the rules to match your
convention. The fully-worked, heavily-commented version below shows a richer
multi-rule corporate scheme built the same way.
Fully-commented example¶
The example below implements a realistic multi-rule corporate convention and
demonstrates the most valuable script-only trick: continuing a numbered
family (ams1-sw7050-3 → next device becomes …-4) by querying NetBox for
the highest existing number — the lookup engineers otherwise do by hand — via
naming.chain_placement_names(), which also counts every APPROVED ancestor
design's placements — each under its effective name, which for a move that
keeps the device's name is that device's real name — so the family counter
stays correct when this design is part of a
design chain.
Save it anywhere importable by NetBox (e.g. next to manage.py) and set:
"naming_mode": "script",
"naming_script": "my_naming.build_name",
"""Example naming script for netbox-rack-design (naming_mode = "script")."""
import re
# ---------------------------------------------------------------------------
# Convention tables. In a real deployment these encode your naming standard;
# extend them freely — this is plain Python.
# ---------------------------------------------------------------------------
# Device-type part number -> short type code used inside the name.
TYPE_CODES = {
"DCS-7050CX3-32S-R": "sw7050",
"DCS-7010T-48-R": "sw7010",
"AP8853": "pdu",
# ... add your fleet here
}
# Role slug -> the role token embedded in the name (network gear only).
ROLE_CODES = {
"core-switch": "core",
"leaf-switch": "leaf",
"oob-switch": "oob",
}
# Roles whose devices are numbered per PROJECT rather than per family.
PROJECT_NUMBERED_ROLES = {"server", "disk-enclosure"}
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
def _next_number(placement, prefix):
"""Continue a numbered family: find the highest existing <prefix><digits>
device name in NetBox — plus any names reserved by a design chain (see
below) and any names already proposed in this editing session (the plugin
passes them on the placement) — and return max + 1.
This automates the manual "open NetBox and find the last number" step.
"""
from dcim.models import Device
from netbox_rack_design.naming import chain_placement_names, pending_names
tail = re.compile(r"^" + re.escape(prefix) + r"(\d+)$")
highest = 0
# Saved devices in NetBox:
for name in Device.objects.filter(name__startswith=prefix).values_list(
"name", flat=True
):
m = tail.match(name)
if m:
highest = max(highest, int(m.group(1)))
# This design's own placements PLUS every APPROVED ancestor's placements,
# matched under their SETTLED names (see docs/design-chains.md) — never
# query `DesignPlacement.objects.filter(design=placement.design)`
# yourself, or a design based on another one will hand out a number its
# ancestor already reserved:
for name in chain_placement_names(placement):
m = tail.match(name or "")
if m:
highest = max(highest, int(m.group(1)))
# Names proposed by OTHER unsaved tiles in the same editor session, so two
# pending adds get consecutive numbers instead of colliding:
for name in pending_names(placement):
m = tail.match(name or "")
if m:
highest = max(highest, int(m.group(1)))
return str(highest + 1)
def _role_slug(placement):
"""The placement's role slug: the chosen role for adds, the real device's
role for moves/removes."""
role = placement.device_role or (
placement.device.role if placement.device else None
)
return (role.slug if role else "").lower()
def _site_and_rack(placement):
"""Site/rack come from the TARGET rack (falling back to the real device)."""
rack = placement.target_rack or (
placement.device.rack if placement.device else None
)
site = rack.site.name if (rack and rack.site) else ""
return site.lower(), (rack.name if rack else "")
# ---------------------------------------------------------------------------
# The entry point: fn(placement) -> str
# ---------------------------------------------------------------------------
def build_name(placement):
role = _role_slug(placement)
site, rack_name = _site_and_rack(placement)
device_type = placement.device_type or (
placement.device.device_type if placement.device else None
)
part = (device_type.part_number or device_type.model) if device_type else ""
type_code = TYPE_CODES.get(part, "dev")
# Rule 1 — project-numbered gear: PRJ-<project number>-<n>.
# The project number is parsed from the design title (e.g. "PRJ-1042 ...").
if role in PROJECT_NUMBERED_ROLES:
m = re.search(r"PRJ-?(\d+)", placement.design.title, re.IGNORECASE)
project = m.group(1) if m else placement.design.title
prefix = f"PRJ-{project}-"
return prefix + _next_number(placement, prefix)
# Rule 2 — PDUs embed the rack: <site>-pdu-r<rack, cleaned>-<n>.
if role in ("pdu", "unmanageable-pdu"):
cleaned = re.sub(r"[./\-_:]", "", rack_name).lower()
prefix = f"{site}-pdu-r{cleaned}-"
return prefix + _next_number(placement, prefix)
# Rule 3 — network gear: <site>-<type code>-<role code>-<n>,
# numbering continued per family across all of NetBox.
role_code = ROLE_CODES.get(role, role or "misc")
prefix = f"{site}-{type_code}-{role_code}-"
return prefix + _next_number(placement, prefix)
Notes¶
- Robust to misconfiguration. If
naming_scriptcan't be resolved (wrong or empty dotted path, module not importable, target not callable) — or the script raises while computing a name — the engine logs a warning and falls back to the defaultsequencename (<design title>-<n>). A broken or not-yet-loaded script therefore degrades to sensible default names rather than blocking planning. Fixnaming_scriptto restore custom naming. - Everything the script proposes remains editable in the rename dialog — the engine suggests, the user decides.
placement.pkisNonefor previews; never rely on the placement being saved.
Verify on your test instance¶
A quick end-to-end check that naming works the way you expect:
- Configure and restart. Set
naming_mode/naming_script(ornaming_template) inPLUGINS_CONFIGand restart NetBox so the new config is loaded. To start from the shipped example:
"naming_mode": "script",
"naming_script": "netbox_rack_design.naming_example.build_name",
-
Open a design in the editor and drag a device from the palette onto a rack. The proposed name appears on the tile immediately (and in the rename dialog for a moved device) — e.g.
ams1-leaf-switch-1. -
Check the family counter. Drop a second device of the same family; it should get the next number (
…-2), and two quick drops before saving get consecutive numbers rather than colliding. -
Confirm the fallback (optional). Point
naming_scriptat a name that does not exist and restart: dropping a device now yields the default<design title>-<n>instead of erroring — proof a bad config can't break the editor. Restore the correct path afterwards.
Nothing here writes to dcim: naming only ever runs on the read-only
name-preview request, never on page load or on Save.
Keeping the script in NetBox's SCRIPTS_ROOT¶
Because naming_script is any importable dotted path, you can keep the script
where NetBox already manages Python: SCRIPTS_ROOT (default <netbox>/scripts/).
A file there is importable as scripts.<module>, so you reference it as
scripts.<module>.build_name. Two ways to get it there — both verified:
The shipped netbox_rack_design/naming_example.py uses absolute imports
precisely so it keeps working when copied out of the package. Start from it
rather than writing one from scratch.
Set the config once. Point naming_script at the module path and restart
NetBox — this is the only step that needs a restart (it edits
configuration.py). While the script isn't present yet, naming safely falls
back to the default sequence name.
"naming_mode": "script",
"naming_script": "scripts.device_naming.build_name",
After that one-time config change, adding or editing the script itself needs
no restart — NetBox picks it up and the plugin imports build_name live on
the next name preview.
Variant 1 — through the UI (Customization → Scripts → Add)¶
Use this to view and edit the convention from the NetBox UI. NetBox lists a
module under Scripts only when it contains an extras.scripts.Script subclass —
and the shipped naming_example.py already includes one (DeviceNamingScript),
purely so the module appears under Scripts. The plugin still calls the module-level
build_name directly; the Script class's run() is a no-op and build_name
stays read-only. So there's nothing to hand-wrap:
Customization → Scripts → Add, name the module to match your naming_script
config path (e.g. naming_example for scripts.naming_example.build_name), and
paste the file's contents. No restart — reopen a design and the new names appear
immediately; edit it later via the Edit button the same way.
Variant 2 — copy the file into SCRIPTS_ROOT¶
If you don't need UI editing, just drop the file in — no Script subclass
required, because the plugin imports build_name directly:
cp .../netbox_rack_design/naming_example.py $SCRIPTS_ROOT/device_naming.py
No restart needed; the plugin imports it on the next name preview.
Referencing the package copy directly,
netbox_rack_design.naming_example.build_name, also works and needs no copy at all — use that if you only want the defaults and no per-instance editing.