diff --git a/README.md b/README.md index 45a3e6a..4e63282 100644 --- a/README.md +++ b/README.md @@ -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" diff --git a/netbox_better_ips/__init__.py b/netbox_better_ips/__init__.py index 7a0bac7..16e81ed 100644 --- a/netbox_better_ips/__init__.py +++ b/netbox_better_ips/__init__.py @@ -1,4 +1,4 @@ from .config import BetterIPsConfig -__version__ = "1.1.0" +__version__ = "1.2.0" config = BetterIPsConfig diff --git a/netbox_better_ips/config.py b/netbox_better_ips/config.py index ce4e3ee..07dfdea 100644 --- a/netbox_better_ips/config.py +++ b/netbox_better_ips/config.py @@ -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, } diff --git a/netbox_better_ips/management/commands/reconcile_ip_prefixes.py b/netbox_better_ips/management/commands/reconcile_ip_prefixes.py index d62ab36..1e87b44 100644 --- a/netbox_better_ips/management/commands/reconcile_ip_prefixes.py +++ b/netbox_better_ips/management/commands/reconcile_ip_prefixes.py @@ -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.")) diff --git a/netbox_better_ips/management/commands/reconcile_ip_ranges.py b/netbox_better_ips/management/commands/reconcile_ip_ranges.py new file mode 100644 index 0000000..16f46f8 --- /dev/null +++ b/netbox_better_ips/management/commands/reconcile_ip_ranges.py @@ -0,0 +1 @@ +from .reconcile_ip_prefixes import Command diff --git a/netbox_better_ips/services.py b/netbox_better_ips/services.py index 840d236..450eb35 100644 --- a/netbox_better_ips/services.py +++ b/netbox_better_ips/services.py @@ -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 diff --git a/netbox_better_ips/signals.py b/netbox_better_ips/signals.py index 28e676f..696118f 100644 --- a/netbox_better_ips/signals.py +++ b/netbox_better_ips/signals.py @@ -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) diff --git a/netbox_better_ips/tables.py b/netbox_better_ips/tables.py index b1c08e3..e54b536 100644 --- a/netbox_better_ips/tables.py +++ b/netbox_better_ips/tables.py @@ -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") diff --git a/netbox_better_ips/template_content.py b/netbox_better_ips/template_content.py index cd886ac..042cffc 100644 --- a/netbox_better_ips/template_content.py +++ b/netbox_better_ips/template_content.py @@ -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,) diff --git a/netbox_better_ips/templates/netbox_better_ips/inc/iprange_list_buttons.html b/netbox_better_ips/templates/netbox_better_ips/inc/iprange_list_buttons.html new file mode 100644 index 0000000..bca9897 --- /dev/null +++ b/netbox_better_ips/templates/netbox_better_ips/inc/iprange_list_buttons.html @@ -0,0 +1,15 @@ +{% if perms.ipam.view_prefix %} + + + Erweiterte Netzübersicht + +{% endif %} +{% if perms.ipam.view_ipaddress and perms.ipam.add_iprange %} +
+ {% csrf_token %} + +
+{% endif %} diff --git a/netbox_better_ips/templates/netbox_better_ips/inc/prefix_list_buttons.html b/netbox_better_ips/templates/netbox_better_ips/inc/prefix_list_buttons.html deleted file mode 100644 index 584bad4..0000000 --- a/netbox_better_ips/templates/netbox_better_ips/inc/prefix_list_buttons.html +++ /dev/null @@ -1,9 +0,0 @@ -{% if perms.ipam.view_ipaddress and perms.ipam.add_prefix %} -
- {% csrf_token %} - -
-{% endif %} diff --git a/netbox_better_ips/urls.py b/netbox_better_ips/urls.py index aef728e..e0a3870 100644 --- a/netbox_better_ips/urls.py +++ b/netbox_better_ips/urls.py @@ -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"), ] diff --git a/netbox_better_ips/views.py b/netbox_better_ips/views.py index e37ec69..104c6b3 100644 --- a/netbox_better_ips/views.py +++ b/netbox_better_ips/views.py @@ -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") diff --git a/pyproject.toml b/pyproject.toml index d0f5004..ea58737 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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"