fix: IP-Bereiche statt Prefixe automatisch verwalten

- nutzbare Hostbereiche aus der CIDR-Maske erzeugen
- Netzwerk- und Broadcastadressen bei IPv4 ausschließen
- Abgleich-Aktion in die IP-Bereichsliste verschieben
- Gerät oder VM in der IP-Adressansicht anzeigen
- Bestandsabgleich für IP-Bereiche ergänzen
- Plugin-Version auf 1.2.0 erhöhen
This commit is contained in:
2026-07-23 12:08:59 +02:00
parent acb4e449bc
commit 8be6978d81
14 changed files with 88 additions and 107 deletions
+10 -11
View File
@@ -2,10 +2,10 @@
Plugin für **NetBox 4.6.5** mit zwei Funktionen:
- Beim Speichern einer IP-Adresse wird das durch deren CIDR-Maske beschriebene Prefix gesucht und, falls es fehlt, automatisch erstellt. NetBox ordnet die IP danach nativ anhand von Prefix und VRF ein.
- Beim Speichern einer IP-Adresse wird der durch deren CIDR-Maske beschriebene nutzbare IP-Bereich gesucht und, falls er fehlt, automatisch erstellt. Bei normalen IPv4-Subnetzen werden Netzwerk- und Broadcastadresse nicht in den Bereich aufgenommen.
- Eine zusätzliche IP-Netzübersicht kann nach Organisation/Region, Standortgruppe, Standort, Lokation, Mandantengruppe und Mandant gefiltert werden.
- Unter **IPAM → Prefixes/IP-Bereiche** legt die Aktion **Alle fehlenden Bereiche anlegen** die Netze für sämtliche bereits vorhandenen, sichtbaren IP-Adressen an.
- Unter **Prefix/IP-Bereich → IP-Adressen** wird das zugehörige Gerät beziehungsweise die VM standardmäßig als eigene Spalte angezeigt.
- Unter **IPAM → IP-Bereiche** legt die Aktion **Alle fehlenden Bereiche anlegen** die Bereiche für sämtliche bereits vorhandenen, sichtbaren IP-Adressen an.
- Unter **IP-Bereich → IP-Adressen** wird das zugehörige Gerät beziehungsweise die VM standardmäßig als eigene Spalte angezeigt.
## Installation
@@ -24,9 +24,8 @@ PLUGINS = ["netbox_better_ips"]
PLUGINS_CONFIG = {
"netbox_better_ips": {
"auto_create_prefix": True,
"prefix_status": "active",
"inherit_scope": True,
"auto_create_range": True,
"range_status": "active",
"inherit_tenant": True,
"ignore_host_prefixes": True,
}
@@ -37,22 +36,22 @@ Danach NetBox neu starten. Das Plugin hat keine eigenen Datenbankmodelle und ben
## Verhalten
Aus `192.0.2.17/24` wird bei Bedarf `192.0.2.0/24` in derselben VRF erstellt. Bei einer Interface-Zuweisung wird bevorzugt die Lokation, sonst der Standort als Prefix-Scope übernommen. Der Mandant wird von der IP oder dem zugewiesenen Gerät/der VM übernommen. Vorhandene Prefix-Metadaten werden nie überschrieben.
Aus `192.0.2.17/24` wird bei Bedarf der IP-Bereich `192.0.2.1/24` bis `192.0.2.254/24` in derselben VRF erstellt. Der Mandant wird von der IP oder dem zugewiesenen Gerät/der VM übernommen. Vorhandene Bereichsmetadaten werden nie überschrieben.
Hostmasken (`/32`, `/128`) werden standardmäßig nicht als Prefix erstellt. Dies kann mit `ignore_host_prefixes=False` geändert werden.
Hostmasken (`/32`, `/128`) werden standardmäßig nicht als eigener Bereich erstellt. Dies kann mit `ignore_host_prefixes=False` geändert werden.
Bereits vorhandene IP-Adressen lassen sich abgleichen:
```bash
python /opt/netbox/netbox/manage.py reconcile_ip_prefixes --dry-run
python /opt/netbox/netbox/manage.py reconcile_ip_prefixes
python /opt/netbox/netbox/manage.py reconcile_ip_ranges --dry-run
python /opt/netbox/netbox/manage.py reconcile_ip_ranges
```
## Berechtigungen
Für die Übersicht ist die NetBox-Berechtigung `ipam.view_prefix` erforderlich. Die automatische Erstellung läuft serverseitig; stellen Sie sicher, dass dies zu Ihrem Berechtigungs- und Change-Control-Konzept passt.
Die Aktion für Bestandsdaten benötigt `ipam.view_ipaddress` und `ipam.add_prefix`. Sie verarbeitet nur IP-Adressen, die der ausführende Benutzer sehen darf.
Die Aktion für Bestandsdaten benötigt `ipam.view_ipaddress` und `ipam.add_iprange`. Sie verarbeitet nur IP-Adressen, die der ausführende Benutzer sehen darf.
## Hinweis zu „Organisation"
+1 -1
View File
@@ -1,4 +1,4 @@
from .config import BetterIPsConfig
__version__ = "1.1.0"
__version__ = "1.2.0"
config = BetterIPsConfig
+3 -4
View File
@@ -6,14 +6,13 @@ class BetterIPsConfig(PluginConfig):
verbose_name = "NetBox Better IPs"
description = "Automatische Prefix-Zuordnung und erweiterte IP-Netzübersicht"
author = "LKE"
version = "1.1.0"
version = "1.2.0"
base_url = "better-ips"
min_version = "4.6.5"
max_version = "4.6.99"
default_settings = {
"auto_create_prefix": True,
"prefix_status": "active",
"inherit_scope": True,
"auto_create_range": True,
"range_status": "active",
"inherit_tenant": True,
"ignore_host_prefixes": True,
}
@@ -1,11 +1,11 @@
from django.core.management.base import BaseCommand
from ipam.models import IPAddress
from netbox_better_ips.services import ensure_prefix_for_ip
from netbox_better_ips.services import ensure_range_for_ip
class Command(BaseCommand):
help = "Create missing exact prefixes for existing IP addresses"
help = "Create missing usable-host IP ranges for existing IP addresses"
def add_arguments(self, parser):
parser.add_argument("--dry-run", action="store_true")
@@ -13,11 +13,10 @@ class Command(BaseCommand):
def handle(self, *args, **options):
created = skipped = 0
for address in IPAddress.objects.select_related("tenant", "assigned_object_type").iterator():
prefix, was_created = ensure_prefix_for_ip(address)
ip_range, was_created = ensure_range_for_ip(address)
if options["dry_run"] and was_created:
prefix.delete()
ip_range.delete()
created += int(was_created)
skipped += int(prefix is None)
skipped += int(ip_range is None)
verb = "würden erstellt" if options["dry_run"] else "erstellt"
self.stdout.write(self.style.SUCCESS(f"{created} Prefixe {verb}; {skipped} Host-Präfixe ignoriert."))
self.stdout.write(self.style.SUCCESS(f"{created} IP-Bereiche {verb}; {skipped} Hostmasken ignoriert."))
@@ -0,0 +1 @@
from .reconcile_ip_prefixes import Command
+24 -48
View File
@@ -1,9 +1,8 @@
import ipaddress
import logging
from django.contrib.contenttypes.models import ContentType
from django.db import transaction
from ipam.models import Prefix
from ipam.models import IPRange
from netbox.plugins import get_plugin_config
logger = logging.getLogger(__name__)
@@ -14,32 +13,6 @@ def network_for_address(address):
return ipaddress.ip_interface(str(address)).network
def _scope_from_ip(ip):
"""Derive the most specific supported Prefix scope from an IP assignment."""
assigned = getattr(ip, "assigned_object", None)
if assigned is None:
return None
parent = (
getattr(assigned, "parent_object", None)
or getattr(assigned, "device", None)
or getattr(assigned, "virtual_machine", None)
)
if parent is None:
return None
location = getattr(parent, "location", None)
if location is not None:
return location
site = getattr(parent, "site", None)
if site is not None:
return site
cluster = getattr(parent, "cluster", None)
return getattr(cluster, "scope", None) if cluster is not None else None
def _tenant_from_ip(ip):
tenant = getattr(ip, "tenant", None)
if tenant is not None:
@@ -55,37 +28,40 @@ def _tenant_from_ip(ip):
@transaction.atomic
def ensure_prefix_for_ip(ip, *, config=None):
"""Get or create the exact Prefix represented by an IP address and its mask.
NetBox derives hierarchy from address/prefix + VRF. There is deliberately no
parent FK to update on IPAddress; creating the missing Prefix is the assignment.
"""
def ensure_range_for_ip(ip, *, config=None):
"""Get or create the usable-host IPRange represented by an IP and its mask."""
settings = config or {
key: get_plugin_config("netbox_better_ips", key)
for key in (
"prefix_status", "inherit_scope", "inherit_tenant", "ignore_host_prefixes"
)
for key in ("range_status", "inherit_tenant", "ignore_host_prefixes")
}
network = network_for_address(ip.address)
if settings.get("ignore_host_prefixes", True) and network.prefixlen == network.max_prefixlen:
return None, False
lookup = {"prefix": str(network), "vrf_id": ip.vrf_id}
existing = Prefix.objects.filter(**lookup).first()
first = int(network.network_address)
last = int(network.broadcast_address)
if network.version == 4 and network.prefixlen < 31:
first += 1
last -= 1
elif network.version == 6 and network.prefixlen < 127:
first += 1
start_address = f"{ipaddress.ip_address(first)}/{network.prefixlen}"
end_address = f"{ipaddress.ip_address(last)}/{network.prefixlen}"
lookup = {
"start_address": start_address,
"end_address": end_address,
"vrf_id": ip.vrf_id,
}
existing = IPRange.objects.filter(**lookup).first()
if existing:
return existing, False
defaults = {"status": settings.get("prefix_status", "active")}
defaults = {"status": settings.get("range_status", "active")}
if settings.get("inherit_tenant", True):
defaults["tenant"] = _tenant_from_ip(ip)
if settings.get("inherit_scope", True):
scope = _scope_from_ip(ip)
if scope is not None:
defaults["scope_type"] = ContentType.objects.get_for_model(scope)
defaults["scope_id"] = scope.pk
prefix, created = Prefix.objects.get_or_create(defaults=defaults, **lookup)
ip_range, created = IPRange.objects.get_or_create(defaults=defaults, **lookup)
if created:
logger.info("Created missing prefix %s for IP address %s", prefix, ip)
return prefix, created
logger.info("Created missing IP range %s for IP address %s", ip_range, ip)
return ip_range, created
+6 -7
View File
@@ -5,18 +5,17 @@ from django.dispatch import receiver
from ipam.models import IPAddress
from netbox.plugins import get_plugin_config
from .services import ensure_prefix_for_ip
from .services import ensure_range_for_ip
logger = logging.getLogger(__name__)
@receiver(post_save, sender=IPAddress, dispatch_uid="netbox_better_ips.ensure_prefix")
def ensure_ip_prefix(sender, instance, raw=False, **kwargs):
if raw or not get_plugin_config("netbox_better_ips", "auto_create_prefix"):
@receiver(post_save, sender=IPAddress, dispatch_uid="netbox_better_ips.ensure_range")
def ensure_ip_range(sender, instance, raw=False, **kwargs):
if raw or not get_plugin_config("netbox_better_ips", "auto_create_range"):
return
try:
ensure_prefix_for_ip(instance)
ensure_range_for_ip(instance)
except Exception:
# The IP save must not be rolled back by an optional convenience feature.
logger.exception("Could not ensure a prefix for IP address %s", instance)
logger.exception("Could not ensure an IP range for IP address %s", instance)
+9 -7
View File
@@ -1,5 +1,5 @@
import django_tables2 as tables
from ipam.tables import AnnotatedIPAddressTable
from ipam.tables import IPAddressTable
from utilities.tables import register_table_column
@@ -11,13 +11,15 @@ device_or_vm = tables.Column(
empty_values=(),
)
register_table_column(device_or_vm, "device_or_vm", AnnotatedIPAddressTable)
register_table_column(device_or_vm, "device_or_vm", IPAddressTable)
# Registered columns are selectable but not displayed by default. The annotated
# table is used specifically by Prefix -> IP Addresses, where this information
# should be visible immediately.
if "device_or_vm" not in AnnotatedIPAddressTable.Meta.default_columns:
AnnotatedIPAddressTable.Meta.default_columns = (
*AnnotatedIPAddressTable.Meta.default_columns,
# table is used by IP Range -> IP Addresses, where this information should be
# visible immediately.
if "device_or_vm" not in IPAddressTable.Meta.default_columns:
IPAddressTable.Meta.default_columns = (
*IPAddressTable.Meta.default_columns,
"device_or_vm",
)
if "device_or_vm" not in IPAddressTable.exempt_columns:
IPAddressTable.exempt_columns = (*IPAddressTable.exempt_columns, "device_or_vm")
+4 -4
View File
@@ -1,11 +1,11 @@
from netbox.plugins import PluginTemplateExtension
class PrefixListActions(PluginTemplateExtension):
model = "ipam.prefix"
class IPRangeListActions(PluginTemplateExtension):
model = "ipam.iprange"
def list_buttons(self):
return self.render("netbox_better_ips/inc/prefix_list_buttons.html")
return self.render("netbox_better_ips/inc/iprange_list_buttons.html")
template_extensions = (PrefixListActions,)
template_extensions = (IPRangeListActions,)
@@ -0,0 +1,15 @@
{% if perms.ipam.view_prefix %}
<a href="{% url 'plugins:netbox_better_ips:network_overview' %}" class="btn btn-outline-primary">
<i class="mdi mdi-view-list" aria-hidden="true"></i>
Erweiterte Netzübersicht
</a>
{% endif %}
{% if perms.ipam.view_ipaddress and perms.ipam.add_iprange %}
<form method="post" action="{% url 'plugins:netbox_better_ips:reconcile_missing_ranges' %}" class="d-inline">
{% csrf_token %}
<button type="submit" class="btn btn-primary">
<i class="mdi mdi-auto-fix" aria-hidden="true"></i>
Alle fehlenden Bereiche anlegen
</button>
</form>
{% endif %}
@@ -1,9 +0,0 @@
{% if perms.ipam.view_ipaddress and perms.ipam.add_prefix %}
<form method="post" action="{% url 'plugins:netbox_better_ips:reconcile_missing_prefixes' %}" class="d-inline">
{% csrf_token %}
<button type="submit" class="btn btn-primary">
<i class="mdi mdi-auto-fix" aria-hidden="true"></i>
Alle fehlenden Bereiche anlegen
</button>
</form>
{% endif %}
+2 -2
View File
@@ -1,10 +1,10 @@
from django.urls import path
from .views import NetworkOverviewView, reconcile_missing_prefixes
from .views import NetworkOverviewView, reconcile_missing_ranges
app_name = "netbox_better_ips"
urlpatterns = [
path("networks/", NetworkOverviewView.as_view(), name="network_overview"),
path("reconcile/", reconcile_missing_prefixes, name="reconcile_missing_prefixes"),
path("reconcile/", reconcile_missing_ranges, name="reconcile_missing_ranges"),
]
+6 -6
View File
@@ -12,7 +12,7 @@ from ipam.models import IPAddress, Prefix
import netaddr
from .forms import NetworkOverviewFilterForm
from .services import ensure_prefix_for_ip
from .services import ensure_range_for_ip
def _descendant_ids(obj):
@@ -95,17 +95,17 @@ class NetworkOverviewView(PermissionRequiredMixin, TemplateView):
@require_POST
@permission_required(("ipam.view_ipaddress", "ipam.add_prefix"), raise_exception=True)
def reconcile_missing_prefixes(request):
@permission_required(("ipam.view_ipaddress", "ipam.add_iprange"), raise_exception=True)
def reconcile_missing_ranges(request):
created = skipped = failed = 0
addresses = IPAddress.objects.restrict(request.user, "view").select_related(
"tenant", "assigned_object_type"
)
for address in addresses.iterator():
try:
prefix, was_created = ensure_prefix_for_ip(address)
ip_range, was_created = ensure_range_for_ip(address)
created += int(was_created)
skipped += int(prefix is None)
skipped += int(ip_range is None)
except Exception:
failed += 1
@@ -119,4 +119,4 @@ def reconcile_missing_prefixes(request):
request,
f"{created} fehlende Bereiche wurden angelegt. {skipped} Host-Adressen wurden ignoriert.",
)
return redirect("ipam:prefix_list")
return redirect("ipam:iprange_list")
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "netbox-better-ips"
version = "1.1.0"
version = "1.2.0"
description = "Automatic prefix creation and an organization-aware IP network overview for NetBox 4.6"
readme = "README.md"
requires-python = ">=3.12"