feat: automatische Prefix-Zuordnung und IP-Netzübersicht hinzufügen
- fehlende Prefixe beim Speichern von IP-Adressen automatisch erstellen - VRF, Mandant, Standort und Lokation übernehmen - bestehende IP-Adressen per Management-Command abgleichen - filterbare IP-Netzübersicht für Regionen, Standorte und Mandanten ergänzen - NetBox 4.6.5 unterstützen
This commit is contained in:
@@ -0,0 +1,7 @@
|
|||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*.egg-info/
|
||||||
|
.build-wheel/
|
||||||
|
build/
|
||||||
|
dist/
|
||||||
|
.pytest_cache/
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
Apache License 2.0
|
||||||
|
|
||||||
|
Copyright 2026 NetBox Better IPs contributors
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
# NetBox Better IPs
|
||||||
|
|
||||||
|
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.
|
||||||
|
- Eine zusätzliche IP-Netzübersicht kann nach Organisation/Region, Standortgruppe, Standort, Lokation, Mandantengruppe und Mandant gefiltert werden.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
/opt/netbox/venv/bin/pip install /pfad/zu/Netbox-Better-IPs
|
||||||
|
```
|
||||||
|
|
||||||
|
In `configuration.py`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
PLUGINS = ["netbox_better_ips"]
|
||||||
|
|
||||||
|
PLUGINS_CONFIG = {
|
||||||
|
"netbox_better_ips": {
|
||||||
|
"auto_create_prefix": True,
|
||||||
|
"prefix_status": "active",
|
||||||
|
"inherit_scope": True,
|
||||||
|
"inherit_tenant": True,
|
||||||
|
"ignore_host_prefixes": True,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Danach NetBox neu starten. Das Plugin hat keine eigenen Datenbankmodelle und benötigt daher keine Migration.
|
||||||
|
|
||||||
|
## 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.
|
||||||
|
|
||||||
|
Hostmasken (`/32`, `/128`) werden standardmäßig nicht als Prefix 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
|
||||||
|
```
|
||||||
|
|
||||||
|
## 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.
|
||||||
|
|
||||||
|
## Hinweis zu „Organisation"
|
||||||
|
|
||||||
|
NetBox besitzt kein separates Core-Modell namens Organisation. Die Übersicht bildet Organisation auf NetBox-`Region` ab und bezieht untergeordnete Regionen sowie deren Standorte und Lokationen ein.
|
||||||
|
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
from .config import BetterIPsConfig
|
||||||
|
|
||||||
|
__version__ = "1.0.0"
|
||||||
|
config = BetterIPsConfig
|
||||||
|
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
from netbox.plugins import PluginConfig
|
||||||
|
|
||||||
|
|
||||||
|
class BetterIPsConfig(PluginConfig):
|
||||||
|
name = "netbox_better_ips"
|
||||||
|
verbose_name = "NetBox Better IPs"
|
||||||
|
description = "Automatische Prefix-Zuordnung und erweiterte IP-Netzübersicht"
|
||||||
|
author = "LKE"
|
||||||
|
version = "1.0.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,
|
||||||
|
"inherit_tenant": True,
|
||||||
|
"ignore_host_prefixes": True,
|
||||||
|
}
|
||||||
|
|
||||||
|
def ready(self):
|
||||||
|
super().ready()
|
||||||
|
from . import signals # noqa: F401
|
||||||
|
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
from django import forms
|
||||||
|
from dcim.models import Location, Region, Site, SiteGroup
|
||||||
|
from tenancy.models import Tenant, TenantGroup
|
||||||
|
|
||||||
|
|
||||||
|
class NetworkOverviewFilterForm(forms.Form):
|
||||||
|
q = forms.CharField(required=False, label="Suche")
|
||||||
|
organization = forms.ModelChoiceField(
|
||||||
|
queryset=Region.objects.all(), required=False, label="Organisation / Region"
|
||||||
|
)
|
||||||
|
site_group = forms.ModelChoiceField(
|
||||||
|
queryset=SiteGroup.objects.all(), required=False, label="Standortgruppe"
|
||||||
|
)
|
||||||
|
site = forms.ModelChoiceField(queryset=Site.objects.all(), required=False, label="Standort")
|
||||||
|
location = forms.ModelChoiceField(
|
||||||
|
queryset=Location.objects.all(), required=False, label="Lokation"
|
||||||
|
)
|
||||||
|
tenant_group = forms.ModelChoiceField(
|
||||||
|
queryset=TenantGroup.objects.all(), required=False, label="Mandantengruppe"
|
||||||
|
)
|
||||||
|
tenant = forms.ModelChoiceField(
|
||||||
|
queryset=Tenant.objects.all(), required=False, label="Mandant"
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
for field in self.fields.values():
|
||||||
|
css_class = "form-select" if isinstance(field.widget, forms.Select) else "form-control"
|
||||||
|
field.widget.attrs["class"] = css_class
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
from django.core.management.base import BaseCommand
|
||||||
|
from ipam.models import IPAddress
|
||||||
|
|
||||||
|
from netbox_better_ips.services import ensure_prefix_for_ip
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
help = "Create missing exact prefixes for existing IP addresses"
|
||||||
|
|
||||||
|
def add_arguments(self, parser):
|
||||||
|
parser.add_argument("--dry-run", action="store_true")
|
||||||
|
|
||||||
|
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)
|
||||||
|
if options["dry_run"] and was_created:
|
||||||
|
prefix.delete()
|
||||||
|
created += int(was_created)
|
||||||
|
skipped += int(prefix 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."))
|
||||||
|
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
from netbox.plugins import PluginMenuItem
|
||||||
|
|
||||||
|
menu_items = (
|
||||||
|
PluginMenuItem(
|
||||||
|
link="plugins:netbox_better_ips:network_overview",
|
||||||
|
link_text="IP-Netzübersicht",
|
||||||
|
permissions=["ipam.view_prefix"],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
import ipaddress
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from django.contrib.contenttypes.models import ContentType
|
||||||
|
from django.db import transaction
|
||||||
|
from ipam.models import Prefix
|
||||||
|
from netbox.plugins import get_plugin_config
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def network_for_address(address):
|
||||||
|
"""Return the canonical network encoded by an IPAddress.address value."""
|
||||||
|
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:
|
||||||
|
return tenant
|
||||||
|
assigned = getattr(ip, "assigned_object", None)
|
||||||
|
parent = (
|
||||||
|
getattr(assigned, "parent_object", None)
|
||||||
|
or getattr(assigned, "device", None)
|
||||||
|
or getattr(assigned, "virtual_machine", None)
|
||||||
|
or assigned
|
||||||
|
)
|
||||||
|
return getattr(parent, "tenant", None)
|
||||||
|
|
||||||
|
|
||||||
|
@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.
|
||||||
|
"""
|
||||||
|
settings = config or {
|
||||||
|
key: get_plugin_config("netbox_better_ips", key)
|
||||||
|
for key in (
|
||||||
|
"prefix_status", "inherit_scope", "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()
|
||||||
|
if existing:
|
||||||
|
return existing, False
|
||||||
|
|
||||||
|
defaults = {"status": settings.get("prefix_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)
|
||||||
|
if created:
|
||||||
|
logger.info("Created missing prefix %s for IP address %s", prefix, ip)
|
||||||
|
return prefix, created
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import logging
|
||||||
|
|
||||||
|
from django.db.models.signals import post_save
|
||||||
|
from django.dispatch import receiver
|
||||||
|
from ipam.models import IPAddress
|
||||||
|
from netbox.plugins import get_plugin_config
|
||||||
|
|
||||||
|
from .services import ensure_prefix_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"):
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
ensure_prefix_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)
|
||||||
|
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
{% extends 'base/layout.html' %}
|
||||||
|
{% load helpers %}
|
||||||
|
|
||||||
|
{% block title %}IP-Netzübersicht{% endblock %}
|
||||||
|
|
||||||
|
{% block header %}
|
||||||
|
<div class="row align-items-center">
|
||||||
|
<div class="col"><h1>IP-Netzübersicht</h1></div>
|
||||||
|
<div class="col-auto text-secondary">{{ total_count }} Netze</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-xl-9">
|
||||||
|
<div class="card">
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-hover mb-0">
|
||||||
|
<thead><tr><th>Netz</th><th>VRF</th><th>Scope</th><th>Mandant</th><th>Status</th><th>Beschreibung</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
{% for prefix in page_obj %}
|
||||||
|
<tr>
|
||||||
|
<td><a href="{{ prefix.get_absolute_url }}"><code>{{ prefix.prefix }}</code></a></td>
|
||||||
|
<td>{{ prefix.vrf|placeholder }}</td>
|
||||||
|
<td>{{ prefix.scope|placeholder }}</td>
|
||||||
|
<td>{{ prefix.tenant|placeholder }}</td>
|
||||||
|
<td><span class="badge text-bg-{{ prefix.get_status_color }}">{{ prefix.get_status_display }}</span></td>
|
||||||
|
<td>{{ prefix.description|placeholder }}</td>
|
||||||
|
</tr>
|
||||||
|
{% empty %}
|
||||||
|
<tr><td colspan="6" class="text-center text-secondary py-4">Keine passenden Netze gefunden.</td></tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% if page_obj.has_other_pages %}
|
||||||
|
<nav class="mt-3"><ul class="pagination">
|
||||||
|
{% if page_obj.has_previous %}<li class="page-item"><a class="page-link" href="?{% querystring request page=page_obj.previous_page_number %}">Zurück</a></li>{% endif %}
|
||||||
|
<li class="page-item disabled"><span class="page-link">Seite {{ page_obj.number }} / {{ page_obj.paginator.num_pages }}</span></li>
|
||||||
|
{% if page_obj.has_next %}<li class="page-item"><a class="page-link" href="?{% querystring request page=page_obj.next_page_number %}">Weiter</a></li>{% endif %}
|
||||||
|
</ul></nav>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<div class="col-xl-3">
|
||||||
|
<div class="card">
|
||||||
|
<h2 class="card-header">Filter</h2>
|
||||||
|
<div class="card-body">
|
||||||
|
<form method="get">
|
||||||
|
{% for field in filter_form %}<div class="mb-3">{{ field.label_tag }}{{ field }}</div>{% endfor %}
|
||||||
|
<div class="d-flex gap-2"><button class="btn btn-primary" type="submit">Filtern</button><a class="btn btn-outline-secondary" href=".">Zurücksetzen</a></div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
from django.urls import path
|
||||||
|
|
||||||
|
from .views import NetworkOverviewView
|
||||||
|
|
||||||
|
app_name = "netbox_better_ips"
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
path("networks/", NetworkOverviewView.as_view(), name="network_overview"),
|
||||||
|
]
|
||||||
|
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
from django.contrib.contenttypes.models import ContentType
|
||||||
|
from django.contrib.auth.mixins import PermissionRequiredMixin
|
||||||
|
from django.core.paginator import Paginator
|
||||||
|
from django.db.models import Q
|
||||||
|
from django.views.generic import TemplateView
|
||||||
|
from dcim.models import Location, Region, Site, SiteGroup
|
||||||
|
from ipam.models import Prefix
|
||||||
|
import netaddr
|
||||||
|
|
||||||
|
from .forms import NetworkOverviewFilterForm
|
||||||
|
|
||||||
|
|
||||||
|
def _descendant_ids(obj):
|
||||||
|
"""Return an object's ID plus nested-tree descendants across NetBox versions."""
|
||||||
|
try:
|
||||||
|
return list(obj.get_descendants(include_self=True).values_list("pk", flat=True))
|
||||||
|
except (AttributeError, TypeError):
|
||||||
|
ids = list(obj.get_descendants().values_list("pk", flat=True))
|
||||||
|
return [obj.pk, *ids]
|
||||||
|
|
||||||
|
|
||||||
|
def _scope_query(model, ids):
|
||||||
|
content_type = ContentType.objects.get_for_model(model)
|
||||||
|
return Q(scope_type=content_type, scope_id__in=ids)
|
||||||
|
|
||||||
|
|
||||||
|
def apply_overview_filters(queryset, data):
|
||||||
|
if q := data.get("q"):
|
||||||
|
search = Q(description__icontains=q) | Q(comments__icontains=q)
|
||||||
|
try:
|
||||||
|
search |= Q(prefix__net_contains_or_equals=str(netaddr.IPNetwork(q)))
|
||||||
|
except (netaddr.AddrFormatError, ValueError):
|
||||||
|
pass
|
||||||
|
queryset = queryset.filter(search)
|
||||||
|
if tenant := data.get("tenant"):
|
||||||
|
queryset = queryset.filter(tenant=tenant)
|
||||||
|
if group := data.get("tenant_group"):
|
||||||
|
queryset = queryset.filter(tenant__group_id__in=_descendant_ids(group))
|
||||||
|
if location := data.get("location"):
|
||||||
|
queryset = queryset.filter(_scope_query(Location, _descendant_ids(location)))
|
||||||
|
if site := data.get("site"):
|
||||||
|
location_ids = Location.objects.filter(site=site).values_list("pk", flat=True)
|
||||||
|
queryset = queryset.filter(
|
||||||
|
_scope_query(Site, [site.pk]) | _scope_query(Location, location_ids)
|
||||||
|
)
|
||||||
|
if site_group := data.get("site_group"):
|
||||||
|
site_ids = Site.objects.filter(group_id__in=_descendant_ids(site_group)).values_list(
|
||||||
|
"pk", flat=True
|
||||||
|
)
|
||||||
|
location_ids = Location.objects.filter(site_id__in=site_ids).values_list("pk", flat=True)
|
||||||
|
queryset = queryset.filter(
|
||||||
|
_scope_query(SiteGroup, _descendant_ids(site_group))
|
||||||
|
| _scope_query(Site, site_ids)
|
||||||
|
| _scope_query(Location, location_ids)
|
||||||
|
)
|
||||||
|
if region := data.get("organization"):
|
||||||
|
region_ids = _descendant_ids(region)
|
||||||
|
site_ids = Site.objects.filter(region_id__in=region_ids).values_list("pk", flat=True)
|
||||||
|
location_ids = Location.objects.filter(site_id__in=site_ids).values_list("pk", flat=True)
|
||||||
|
queryset = queryset.filter(
|
||||||
|
_scope_query(Region, region_ids)
|
||||||
|
| _scope_query(Site, site_ids)
|
||||||
|
| _scope_query(Location, location_ids)
|
||||||
|
)
|
||||||
|
return queryset
|
||||||
|
|
||||||
|
|
||||||
|
class NetworkOverviewView(PermissionRequiredMixin, TemplateView):
|
||||||
|
template_name = "netbox_better_ips/network_overview.html"
|
||||||
|
permission_required = "ipam.view_prefix"
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
context = super().get_context_data(**kwargs)
|
||||||
|
form = NetworkOverviewFilterForm(self.request.GET or None)
|
||||||
|
prefixes = Prefix.objects.restrict(self.request.user, "view").select_related(
|
||||||
|
"vrf", "tenant", "scope_type"
|
||||||
|
)
|
||||||
|
if form.is_valid():
|
||||||
|
prefixes = apply_overview_filters(prefixes, form.cleaned_data)
|
||||||
|
prefixes = prefixes.order_by("prefix", "vrf")
|
||||||
|
paginator = Paginator(prefixes, 50)
|
||||||
|
context.update(
|
||||||
|
{
|
||||||
|
"filter_form": form,
|
||||||
|
"page_obj": paginator.get_page(self.request.GET.get("page")),
|
||||||
|
"total_count": prefixes.count(),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return context
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
[build-system]
|
||||||
|
requires = ["setuptools>=68", "wheel"]
|
||||||
|
build-backend = "setuptools.build_meta"
|
||||||
|
|
||||||
|
[project]
|
||||||
|
name = "netbox-better-ips"
|
||||||
|
version = "1.0.0"
|
||||||
|
description = "Automatic prefix creation and an organization-aware IP network overview for NetBox 4.6"
|
||||||
|
readme = "README.md"
|
||||||
|
requires-python = ">=3.12"
|
||||||
|
license = {text = "Apache-2.0"}
|
||||||
|
authors = [{name = "NetBox Better IPs contributors"}]
|
||||||
|
dependencies = []
|
||||||
|
classifiers = [
|
||||||
|
"Framework :: Django",
|
||||||
|
"Programming Language :: Python :: 3",
|
||||||
|
"License :: OSI Approved :: Apache Software License",
|
||||||
|
]
|
||||||
|
|
||||||
|
[tool.setuptools.packages.find]
|
||||||
|
include = ["netbox_better_ips*"]
|
||||||
|
|
||||||
|
[tool.setuptools.package-data]
|
||||||
|
netbox_better_ips = ["templates/**/*.html"]
|
||||||
Reference in New Issue
Block a user