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:
@@ -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,4 +1,4 @@
|
||||
from .config import BetterIPsConfig
|
||||
|
||||
__version__ = "1.1.0"
|
||||
__version__ = "1.2.0"
|
||||
config = BetterIPsConfig
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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 %}
|
||||
@@ -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"),
|
||||
]
|
||||
|
||||
@@ -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
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user