From 27cc25f648b4605bc6238d1cf34ffba96d8e0330 Mon Sep 17 00:00:00 2001 From: Louis Date: Thu, 23 Jul 2026 10:36:57 +0200 Subject: [PATCH] feat: add site prefix column to device and rack tables - derive prefix from the first word of the site name - display prefix as the first data column - support NetBox 4.6.5 through 4.6.x - add packaging, installation documentation, and tests --- .gitignore | 8 ++++++ README.md | 48 ++++++++++++++++++++++++++++++++++ netbox_site_prefix/__init__.py | 21 +++++++++++++++ netbox_site_prefix/tables.py | 47 +++++++++++++++++++++++++++++++++ netbox_site_prefix/urls.py | 2 ++ pyproject.toml | 23 ++++++++++++++++ tests/__init__.py | 1 + tests/test_tables.py | 32 +++++++++++++++++++++++ 8 files changed, 182 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 netbox_site_prefix/__init__.py create mode 100644 netbox_site_prefix/tables.py create mode 100644 netbox_site_prefix/urls.py create mode 100644 pyproject.toml create mode 100644 tests/__init__.py create mode 100644 tests/test_tables.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c626806 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +__pycache__/ +*.py[cod] +*.egg-info/ +.pytest_cache/ +build/ +dist/ +.venv/ + diff --git a/README.md b/README.md new file mode 100644 index 0000000..9f1d653 --- /dev/null +++ b/README.md @@ -0,0 +1,48 @@ +# NetBox Site Prefix + +Plugin für NetBox 4.6.5, das in den Geräte- und Rack-Listen eine Spalte +**Prefix** als erste Datenspalte anzeigt. Der Wert ist immer das erste Wort +des zugewiesenen Standortnamens (`Site.name`). + +Beispiele: + +| Standort | Prefix | +|---|---| +| `Berlin Campus West` | `Berlin` | +| `DC01 Frankfurt` | `DC01` | +| kein Standort | `—` | + +Das Plugin benötigt keine Datenbankmigration und speichert keine eigenen +Daten. + +## Installation + +Im Python-Virtualenv von NetBox installieren: + +```shell +/opt/netbox/venv/bin/pip install /pfad/zu/Netbox-Prefixes +``` + +Danach in `configuration.py` aktivieren: + +```python +PLUGINS = [ + # weitere Plugins ... + "netbox_site_prefix", +] +``` + +Anschließend NetBox neu starten. `upgrade.sh` ist nicht erforderlich, kann +aber wie bei anderen Plugin-Installationen ausgeführt werden. + +Bei bestehenden Benutzern kann eine bereits gespeicherte persönliche +Spaltenauswahl Vorrang vor den Standardspalten haben. In diesem Fall in der +Geräte- bzw. Rack-Liste **Spalten konfigurieren** öffnen, **Prefix** auswählen +und an die erste Position schieben oder die gespeicherte Tabellenkonfiguration +zurücksetzen. + +## Kompatibilität + +- NetBox: 4.6.5 bis einschließlich 4.6.x +- Python: ab 3.12 + diff --git a/netbox_site_prefix/__init__.py b/netbox_site_prefix/__init__.py new file mode 100644 index 0000000..57cbda8 --- /dev/null +++ b/netbox_site_prefix/__init__.py @@ -0,0 +1,21 @@ +from netbox.plugins import PluginConfig + + +class SitePrefixConfig(PluginConfig): + name = "netbox_site_prefix" + verbose_name = "Site Prefix" + author = "LKE" + description = "Shows the first word of the site name in device and rack tables." + version = "1.0.0" + min_version = "4.6.5" + max_version = "4.6.99" + + def ready(self): + super().ready() + + # Importing the module registers its columns with NetBox. + from . import tables # noqa: F401 + + +config = SitePrefixConfig + diff --git a/netbox_site_prefix/tables.py b/netbox_site_prefix/tables.py new file mode 100644 index 0000000..7d543cf --- /dev/null +++ b/netbox_site_prefix/tables.py @@ -0,0 +1,47 @@ +import django_tables2 as tables + +from dcim.tables import DeviceTable, RackTable +from utilities.tables import register_table_column + + +COLUMN_NAME = "site_prefix" + + +def first_word(value): + """Return the first whitespace-delimited word of a value.""" + words = str(value or "").split() + return words[0] if words else None + + +class SitePrefixColumn(tables.Column): + """Render the first word of a record's site name.""" + + def render(self, value): + if value is None: + return None + return first_word(value.name) + + +site_prefix = SitePrefixColumn( + accessor=tables.A("site"), + verbose_name="Prefix", + empty_values=(), + orderable=False, +) + +register_table_column(site_prefix, COLUMN_NAME, DeviceTable, RackTable) + + +def _prepend_to_defaults(table): + """Show the registered column first for users without saved preferences.""" + defaults = tuple(table.Meta.default_columns) + table.Meta.default_columns = ( + "pk", + COLUMN_NAME, + *(name for name in defaults if name not in {"pk", COLUMN_NAME}), + ) + + +_prepend_to_defaults(DeviceTable) +_prepend_to_defaults(RackTable) + diff --git a/netbox_site_prefix/urls.py b/netbox_site_prefix/urls.py new file mode 100644 index 0000000..f2303d4 --- /dev/null +++ b/netbox_site_prefix/urls.py @@ -0,0 +1,2 @@ +urlpatterns = [] + diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..03668eb --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,23 @@ +[build-system] +requires = ["setuptools>=77"] +build-backend = "setuptools.build_meta" + +[project] +name = "netbox-site-prefix" +version = "1.0.0" +description = "Adds a site-name prefix column to NetBox device and rack tables." +readme = "README.md" +requires-python = ">=3.12" +license = "MIT" +authors = [ + { name = "NetBox Site Prefix contributors" }, +] +classifiers = [ + "Framework :: Django", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.12", +] + +[tool.setuptools.packages.find] +include = ["netbox_site_prefix*"] + diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1 @@ + diff --git a/tests/test_tables.py b/tests/test_tables.py new file mode 100644 index 0000000..f203ea3 --- /dev/null +++ b/tests/test_tables.py @@ -0,0 +1,32 @@ +import ast +from pathlib import Path + + +MODULE = Path(__file__).parents[1] / "netbox_site_prefix" / "tables.py" + + +def _load_first_word(): + """Load the pure helper without requiring a local NetBox installation.""" + module = ast.parse(MODULE.read_text(encoding="utf-8")) + function = next( + node for node in module.body + if isinstance(node, ast.FunctionDef) and node.name == "first_word" + ) + namespace = {} + exec(compile(ast.Module(body=[function], type_ignores=[]), str(MODULE), "exec"), namespace) + return namespace["first_word"] + + +def test_first_word(): + first_word = _load_first_word() + + assert first_word("Berlin Campus West") == "Berlin" + assert first_word(" DC01\tFrankfurt ") == "DC01" + + +def test_first_word_handles_missing_or_blank_value(): + first_word = _load_first_word() + + assert first_word(None) is None + assert first_word(" ") is None +