mirror of
https://github.com/netbox-community/netbox.git
synced 2026-02-12 11:57:43 +01:00
Compare commits
12 Commits
feature-ip
...
21196-q-fi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6bfe1e7b89 | ||
|
|
4b22be03a0 | ||
|
|
24769ce127 | ||
|
|
164e9db98d | ||
|
|
23f1c86e9c | ||
|
|
02ffdd9d5d | ||
|
|
5013297326 | ||
|
|
584e0a9b8c | ||
|
|
3ac9d0b8bf | ||
|
|
b387ea5f58 | ||
|
|
ba9f6bf359 | ||
|
|
ee6cbdcefe |
@@ -35,11 +35,6 @@ django-mptt==0.17.0
|
||||
# https://github.com/Xof/django-pglocks/blob/master/CHANGES.txt
|
||||
django-pglocks
|
||||
|
||||
|
||||
# Manager for managing PostgreSQL triggers
|
||||
# https://github.com/AmbitionEng/django-pgtrigger/blob/main/CHANGELOG.md
|
||||
django-pgtrigger
|
||||
|
||||
# Prometheus metrics library for Django
|
||||
# https://github.com/korfuri/django-prometheus/blob/master/CHANGELOG.md
|
||||
django-prometheus
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import json
|
||||
import platform
|
||||
|
||||
from copy import deepcopy
|
||||
from django import __version__ as django_version
|
||||
from django.conf import settings
|
||||
from django.contrib import messages
|
||||
@@ -310,6 +311,22 @@ class ConfigRevisionListView(generic.ObjectListView):
|
||||
class ConfigRevisionView(generic.ObjectView):
|
||||
queryset = ConfigRevision.objects.all()
|
||||
|
||||
def get_extra_context(self, request, instance):
|
||||
"""
|
||||
Retrieve additional context for a given request and instance.
|
||||
"""
|
||||
# Copy the revision data to avoid modifying the original
|
||||
config = deepcopy(instance.data or {})
|
||||
|
||||
# Serialize any JSON-based classes
|
||||
for attr in ['CUSTOM_VALIDATORS', 'DEFAULT_USER_PREFERENCES', 'PROTECTION_RULES']:
|
||||
if attr in config:
|
||||
config[attr] = json.dumps(config[attr], cls=ConfigJSONEncoder, indent=4)
|
||||
|
||||
return {
|
||||
'config': config,
|
||||
}
|
||||
|
||||
|
||||
@register_model_view(ConfigRevision, 'add', detail=False)
|
||||
class ConfigRevisionEditView(generic.ObjectEditView):
|
||||
@@ -617,8 +634,8 @@ class SystemView(UserPassesTestMixin, View):
|
||||
response['Content-Disposition'] = 'attachment; filename="netbox.json"'
|
||||
return response
|
||||
|
||||
# Serialize any CustomValidator classes
|
||||
for attr in ['CUSTOM_VALIDATORS', 'PROTECTION_RULES']:
|
||||
# Serialize any JSON-based classes
|
||||
for attr in ['CUSTOM_VALIDATORS', 'DEFAULT_USER_PREFERENCES', 'PROTECTION_RULES']:
|
||||
if hasattr(config, attr) and getattr(config, attr, None):
|
||||
setattr(config, attr, json.dumps(getattr(config, attr), cls=ConfigJSONEncoder, indent=4))
|
||||
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import django_filters
|
||||
import netaddr
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.utils.translation import gettext as _
|
||||
from drf_spectacular.types import OpenApiTypes
|
||||
from drf_spectacular.utils import extend_schema_field
|
||||
from netaddr.core import AddrFormatError
|
||||
|
||||
from circuits.models import CircuitTermination, VirtualCircuit, VirtualCircuitTermination
|
||||
from extras.filtersets import LocalConfigContextFilterSet
|
||||
@@ -1329,16 +1331,24 @@ class DeviceFilterSet(
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
return queryset
|
||||
return queryset.filter(
|
||||
qs_filter = (
|
||||
Q(name__icontains=value) |
|
||||
Q(virtual_chassis__name__icontains=value) |
|
||||
Q(serial__icontains=value.strip()) |
|
||||
Q(asset_tag__icontains=value.strip()) |
|
||||
Q(description__icontains=value.strip()) |
|
||||
Q(comments__icontains=value) |
|
||||
Q(primary_ip4__address__startswith=value) |
|
||||
Q(primary_ip6__address__startswith=value)
|
||||
).distinct()
|
||||
Q(comments__icontains=value)
|
||||
)
|
||||
# If the given value looks like an IP address, look for primary IPv4/IPv6 assignments
|
||||
try:
|
||||
ipaddress = netaddr.IPNetwork(value)
|
||||
if ipaddress.version == 4:
|
||||
qs_filter |= Q(primary_ip4__address__host__inet=ipaddress.ip)
|
||||
elif ipaddress.version == 6:
|
||||
qs_filter |= Q(primary_ip6__address__host__inet=ipaddress.ip)
|
||||
except (AddrFormatError, ValueError):
|
||||
pass
|
||||
return queryset.filter(qs_filter)
|
||||
|
||||
def _has_primary_ip(self, queryset, name, value):
|
||||
params = Q(primary_ip4__isnull=False) | Q(primary_ip6__isnull=False)
|
||||
|
||||
@@ -373,7 +373,7 @@ class Rack(ContactsMixin, ImageAttachmentsMixin, TrackingModelMixin, RackBase):
|
||||
super().clean()
|
||||
|
||||
# Validate location/site assignment
|
||||
if self.site and self.location and self.location.site != self.site:
|
||||
if self.site_id and self.location_id and self.location.site_id != self.site_id:
|
||||
raise ValidationError(_("Assigned location must belong to parent site ({site}).").format(site=self.site))
|
||||
|
||||
# Validate outer dimensions and unit
|
||||
|
||||
@@ -584,6 +584,15 @@ class BaseInterfaceTable(NetBoxTable):
|
||||
orderable=False,
|
||||
verbose_name=_('IP Addresses')
|
||||
)
|
||||
primary_mac_address = tables.Column(
|
||||
verbose_name=_('Primary MAC'),
|
||||
linkify=True
|
||||
)
|
||||
mac_addresses = columns.ManyToManyColumn(
|
||||
orderable=False,
|
||||
linkify_item=True,
|
||||
verbose_name=_('MAC Addresses')
|
||||
)
|
||||
fhrp_groups = tables.TemplateColumn(
|
||||
accessor=Accessor('fhrp_group_assignments'),
|
||||
template_code=INTERFACE_FHRPGROUPS,
|
||||
@@ -615,10 +624,6 @@ class BaseInterfaceTable(NetBoxTable):
|
||||
verbose_name=_('Q-in-Q SVLAN'),
|
||||
linkify=True
|
||||
)
|
||||
primary_mac_address = tables.Column(
|
||||
verbose_name=_('MAC Address'),
|
||||
linkify=True
|
||||
)
|
||||
|
||||
def value_ip_addresses(self, value):
|
||||
return ",".join([str(obj.address) for obj in value.all()])
|
||||
@@ -681,11 +686,12 @@ class InterfaceTable(BaseInterfaceTable, ModularDeviceComponentTable, PathEndpoi
|
||||
model = models.Interface
|
||||
fields = (
|
||||
'pk', 'id', 'name', 'device', 'module_bay', 'module', 'label', 'enabled', 'type', 'mgmt_only', 'mtu',
|
||||
'speed', 'speed_formatted', 'duplex', 'mode', 'primary_mac_address', 'wwn', 'poe_mode', 'poe_type',
|
||||
'rf_role', 'rf_channel', 'rf_channel_frequency', 'rf_channel_width', 'tx_power', 'description',
|
||||
'mark_connected', 'cable', 'cable_color', 'wireless_link', 'wireless_lans', 'link_peer', 'connection',
|
||||
'tags', 'vdcs', 'vrf', 'l2vpn', 'tunnel', 'ip_addresses', 'fhrp_groups', 'untagged_vlan', 'tagged_vlans',
|
||||
'qinq_svlan', 'inventory_items', 'created', 'last_updated', 'vlan_translation_policy'
|
||||
'speed', 'speed_formatted', 'duplex', 'mode', 'mac_addresses', 'primary_mac_address', 'wwn',
|
||||
'poe_mode', 'poe_type', 'rf_role', 'rf_channel', 'rf_channel_frequency', 'rf_channel_width', 'tx_power',
|
||||
'description', 'mark_connected', 'cable', 'cable_color', 'wireless_link', 'wireless_lans', 'link_peer',
|
||||
'connection', 'tags', 'vdcs', 'vrf', 'l2vpn', 'tunnel', 'ip_addresses', 'fhrp_groups',
|
||||
'untagged_vlan', 'tagged_vlans', 'qinq_svlan', 'inventory_items', 'created', 'last_updated',
|
||||
'vlan_translation_policy',
|
||||
)
|
||||
default_columns = ('pk', 'name', 'device', 'label', 'enabled', 'type', 'description')
|
||||
|
||||
@@ -746,10 +752,11 @@ class DeviceInterfaceTable(InterfaceTable):
|
||||
model = models.Interface
|
||||
fields = (
|
||||
'pk', 'id', 'name', 'module_bay', 'module', 'label', 'enabled', 'type', 'parent', 'bridge', 'lag',
|
||||
'mgmt_only', 'mtu', 'mode', 'primary_mac_address', 'wwn', 'rf_role', 'rf_channel', 'rf_channel_frequency',
|
||||
'rf_channel_width', 'tx_power', 'description', 'mark_connected', 'cable', 'cable_color', 'wireless_link',
|
||||
'wireless_lans', 'link_peer', 'connection', 'tags', 'vdcs', 'vrf', 'l2vpn', 'tunnel', 'ip_addresses',
|
||||
'fhrp_groups', 'untagged_vlan', 'tagged_vlans', 'qinq_svlan', 'actions',
|
||||
'mgmt_only', 'mtu', 'mode', 'mac_addresses', 'primary_mac_address', 'wwn', 'rf_role', 'rf_channel',
|
||||
'rf_channel_frequency', 'rf_channel_width', 'tx_power', 'description', 'mark_connected', 'cable',
|
||||
'cable_color', 'wireless_link', 'wireless_lans', 'link_peer', 'connection', 'tags', 'vdcs', 'vrf',
|
||||
'l2vpn', 'tunnel', 'ip_addresses', 'fhrp_groups', 'untagged_vlan', 'tagged_vlans', 'qinq_svlan',
|
||||
'actions',
|
||||
)
|
||||
default_columns = (
|
||||
'pk', 'name', 'label', 'enabled', 'type', 'parent', 'lag', 'mtu', 'mode', 'description', 'ip_addresses',
|
||||
@@ -880,24 +887,36 @@ class DeviceBayTable(DeviceComponentTable):
|
||||
'args': [Accessor('device_id')],
|
||||
}
|
||||
)
|
||||
role = columns.ColoredLabelColumn(
|
||||
accessor=Accessor('installed_device__role'),
|
||||
verbose_name=_('Role')
|
||||
)
|
||||
device_type = tables.Column(
|
||||
accessor=Accessor('installed_device__device_type'),
|
||||
linkify=True,
|
||||
verbose_name=_('Type')
|
||||
)
|
||||
status = tables.TemplateColumn(
|
||||
verbose_name=_('Status'),
|
||||
template_code=DEVICEBAY_STATUS,
|
||||
order_by=Accessor('installed_device__status')
|
||||
)
|
||||
installed_device = tables.Column(
|
||||
verbose_name=_('Installed device'),
|
||||
verbose_name=_('Installed Device'),
|
||||
linkify=True
|
||||
)
|
||||
installed_role = columns.ColoredLabelColumn(
|
||||
accessor=Accessor('installed_device__role'),
|
||||
verbose_name=_('Installed Role')
|
||||
)
|
||||
installed_device_type = tables.Column(
|
||||
accessor=Accessor('installed_device__device_type'),
|
||||
linkify=True,
|
||||
verbose_name=_('Installed Type')
|
||||
)
|
||||
installed_description = tables.Column(
|
||||
accessor=Accessor('installed_device__description'),
|
||||
verbose_name=_('Installed Description')
|
||||
)
|
||||
installed_serial = tables.Column(
|
||||
accessor=Accessor('installed_device__serial'),
|
||||
verbose_name=_('Installed Serial')
|
||||
)
|
||||
installed_asset_tag = tables.Column(
|
||||
accessor=Accessor('installed_device__asset_tag'),
|
||||
verbose_name=_('Installed Asset Tag')
|
||||
)
|
||||
tags = columns.TagColumn(
|
||||
url_name='dcim:devicebay_list'
|
||||
)
|
||||
@@ -905,8 +924,9 @@ class DeviceBayTable(DeviceComponentTable):
|
||||
class Meta(DeviceComponentTable.Meta):
|
||||
model = models.DeviceBay
|
||||
fields = (
|
||||
'pk', 'id', 'name', 'device', 'label', 'status', 'role', 'device_type', 'installed_device', 'description',
|
||||
'tags', 'created', 'last_updated',
|
||||
'pk', 'id', 'name', 'device', 'label', 'status', 'description', 'installed_device', 'installed_role',
|
||||
'installed_device_type', 'installed_description', 'installed_serial', 'installed_asset_tag', 'tags',
|
||||
'created', 'last_updated',
|
||||
)
|
||||
|
||||
default_columns = ('pk', 'name', 'device', 'label', 'status', 'installed_device', 'description')
|
||||
@@ -1199,4 +1219,6 @@ class MACAddressTable(PrimaryModelTable):
|
||||
'pk', 'id', 'mac_address', 'assigned_object_parent', 'assigned_object', 'description', 'is_primary',
|
||||
'comments', 'tags', 'created', 'last_updated',
|
||||
)
|
||||
default_columns = ('pk', 'mac_address', 'assigned_object_parent', 'assigned_object', 'description')
|
||||
default_columns = (
|
||||
'pk', 'mac_address', 'is_primary', 'assigned_object_parent', 'assigned_object', 'description',
|
||||
)
|
||||
|
||||
@@ -90,7 +90,6 @@ class DevicePanel(panels.ObjectAttributesPanel):
|
||||
parent_device = attrs.TemplatedAttr('parent_bay', template_name='dcim/device/attrs/parent_device.html')
|
||||
gps_coordinates = attrs.GPSCoordinatesAttr()
|
||||
tenant = attrs.RelatedObjectAttr('tenant', linkify=True, grouped_by='group')
|
||||
device_type = attrs.RelatedObjectAttr('device_type', linkify=True, grouped_by='manufacturer')
|
||||
description = attrs.TextAttr('description')
|
||||
airflow = attrs.ChoiceAttr('airflow')
|
||||
serial = attrs.TextAttr('serial', label=_('Serial number'), style='font-monospace', copy_button=True)
|
||||
@@ -122,10 +121,19 @@ class DeviceManagementPanel(panels.ObjectAttributesPanel):
|
||||
cluster = attrs.RelatedObjectAttr('cluster', linkify=True)
|
||||
|
||||
|
||||
class DeviceDeviceTypePanel(panels.ObjectAttributesPanel):
|
||||
title = _('Device Type')
|
||||
|
||||
manufacturer = attrs.RelatedObjectAttr('device_type.manufacturer', linkify=True)
|
||||
model = attrs.RelatedObjectAttr('device_type', linkify=True)
|
||||
height = attrs.TextAttr('device_type.u_height', format_string='{}U')
|
||||
front_image = attrs.ImageAttr('device_type.front_image')
|
||||
rear_image = attrs.ImageAttr('device_type.rear_image')
|
||||
|
||||
|
||||
class DeviceDimensionsPanel(panels.ObjectAttributesPanel):
|
||||
title = _('Dimensions')
|
||||
|
||||
height = attrs.TextAttr('device_type.u_height', format_string='{}U')
|
||||
total_weight = attrs.TemplatedAttr('total_weight', template_name='dcim/device/attrs/total_weight.html')
|
||||
|
||||
|
||||
|
||||
@@ -13,7 +13,6 @@ from django.utils.translation import gettext_lazy as _
|
||||
from django.views.generic import View
|
||||
|
||||
from circuits.models import Circuit, CircuitTermination
|
||||
from dcim.ui import panels
|
||||
from extras.ui.panels import CustomFieldsPanel, ImageAttachmentsPanel, TagsPanel
|
||||
from extras.views import ObjectConfigContextView, ObjectRenderConfigView
|
||||
from ipam.models import ASN, IPAddress, Prefix, VLAN, VLANGroup
|
||||
@@ -44,6 +43,7 @@ from .choices import DeviceFaceChoices, InterfaceModeChoices
|
||||
from .models import *
|
||||
from .models.device_components import PortMapping
|
||||
from .object_actions import BulkAddComponents, BulkDisconnect
|
||||
from .ui import panels
|
||||
|
||||
CABLE_TERMINATION_TYPES = {
|
||||
'dcim.consoleport': ConsolePort,
|
||||
@@ -2470,6 +2470,7 @@ class DeviceView(generic.ObjectView):
|
||||
],
|
||||
),
|
||||
ImageAttachmentsPanel(),
|
||||
panels.DeviceDeviceTypePanel(),
|
||||
panels.DeviceDimensionsPanel(),
|
||||
TemplatePanel('dcim/panels/device_rack_elevations.html'),
|
||||
],
|
||||
|
||||
@@ -39,9 +39,20 @@ __all__ = (
|
||||
)
|
||||
|
||||
IMAGEATTACHMENT_IMAGE = """
|
||||
{% load thumbnail %}
|
||||
{% if record.image %}
|
||||
<a href="{{ record.image.url }}" target="_blank" class="image-preview" data-bs-placement="top">
|
||||
<i class="mdi mdi-image"></i></a>
|
||||
{% thumbnail record.image "400x400" as tn %}
|
||||
<a href="{{ record.get_absolute_url }}"
|
||||
class="image-preview"
|
||||
data-preview-url="{{ tn.url }}"
|
||||
data-bs-placement="left"
|
||||
title="{{ record.filename }}"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
aria-label="{{ record.filename }}">
|
||||
<i class="mdi mdi-image"></i>
|
||||
</a>
|
||||
{% endthumbnail %}
|
||||
{% endif %}
|
||||
<a href="{{ record.get_absolute_url }}">{{ record.filename|truncate_middle:16 }}</a>
|
||||
"""
|
||||
|
||||
@@ -60,24 +60,18 @@ class PrefixSerializer(PrimaryModelSerializer):
|
||||
vlan = VLANSerializer(nested=True, required=False, allow_null=True)
|
||||
status = ChoiceField(choices=PrefixStatusChoices, required=False)
|
||||
role = RoleSerializer(nested=True, required=False, allow_null=True)
|
||||
_children = serializers.IntegerField(read_only=True)
|
||||
children = serializers.IntegerField(read_only=True)
|
||||
_depth = serializers.IntegerField(read_only=True)
|
||||
prefix = IPNetworkField()
|
||||
|
||||
class Meta:
|
||||
model = Prefix
|
||||
fields = [
|
||||
'id', 'url', 'display_url', 'display', 'family', 'aggregate', 'parent', 'prefix', 'vrf', 'scope_type',
|
||||
'scope_id', 'scope', 'tenant', 'vlan', 'status', 'role', 'is_pool', 'mark_utilized', 'description',
|
||||
'owner', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', '_children', '_depth',
|
||||
'id', 'url', 'display_url', 'display', 'family', 'prefix', 'vrf', 'scope_type', 'scope_id', 'scope',
|
||||
'tenant', 'vlan', 'status', 'role', 'is_pool', 'mark_utilized', 'description', 'owner', 'comments', 'tags',
|
||||
'custom_fields', 'created', 'last_updated', 'children', '_depth',
|
||||
]
|
||||
brief_fields = ('id', 'url', 'display', 'family', 'aggregate', 'parent', 'prefix', 'description', '_depth')
|
||||
|
||||
def get_fields(self):
|
||||
fields = super(PrefixSerializer, self).get_fields()
|
||||
fields['parent'] = PrefixSerializer(nested=True, read_only=True)
|
||||
|
||||
return fields
|
||||
brief_fields = ('id', 'url', 'display', 'family', 'prefix', 'description', '_depth')
|
||||
|
||||
|
||||
class PrefixLengthSerializer(serializers.Serializer):
|
||||
@@ -131,9 +125,7 @@ class AvailablePrefixSerializer(serializers.Serializer):
|
||||
# IP ranges
|
||||
#
|
||||
|
||||
|
||||
class IPRangeSerializer(PrimaryModelSerializer):
|
||||
prefix = PrefixSerializer(nested=True, required=False, allow_null=True)
|
||||
family = ChoiceField(choices=IPAddressFamilyChoices, read_only=True)
|
||||
start_address = IPAddressField()
|
||||
end_address = IPAddressField()
|
||||
@@ -145,11 +137,11 @@ class IPRangeSerializer(PrimaryModelSerializer):
|
||||
class Meta:
|
||||
model = IPRange
|
||||
fields = [
|
||||
'id', 'url', 'display_url', 'display', 'family', 'prefix', 'start_address', 'end_address', 'size', 'vrf',
|
||||
'tenant', 'status', 'role', 'description', 'owner', 'comments', 'tags', 'custom_fields', 'created',
|
||||
'last_updated', 'mark_populated', 'mark_utilized',
|
||||
'id', 'url', 'display_url', 'display', 'family', 'start_address', 'end_address', 'size', 'vrf', 'tenant',
|
||||
'status', 'role', 'description', 'owner', 'comments', 'tags', 'custom_fields', 'created', 'last_updated',
|
||||
'mark_populated', 'mark_utilized',
|
||||
]
|
||||
brief_fields = ('id', 'url', 'display', 'family', 'prefix', 'start_address', 'end_address', 'description')
|
||||
brief_fields = ('id', 'url', 'display', 'family', 'start_address', 'end_address', 'description')
|
||||
|
||||
|
||||
#
|
||||
@@ -194,7 +186,6 @@ class AvailableIPRequestSerializer(serializers.Serializer):
|
||||
|
||||
|
||||
class IPAddressSerializer(PrimaryModelSerializer):
|
||||
prefix = PrefixSerializer(nested=True, required=False, allow_null=True)
|
||||
family = ChoiceField(choices=IPAddressFamilyChoices, read_only=True)
|
||||
address = IPAddressField()
|
||||
vrf = VRFSerializer(nested=True, required=False, allow_null=True)
|
||||
@@ -213,11 +204,11 @@ class IPAddressSerializer(PrimaryModelSerializer):
|
||||
class Meta:
|
||||
model = IPAddress
|
||||
fields = [
|
||||
'id', 'url', 'display_url', 'display', 'family', 'prefix', 'address', 'vrf', 'tenant', 'status', 'role',
|
||||
'id', 'url', 'display_url', 'display', 'family', 'address', 'vrf', 'tenant', 'status', 'role',
|
||||
'assigned_object_type', 'assigned_object_id', 'assigned_object', 'nat_inside', 'nat_outside',
|
||||
'dns_name', 'description', 'owner', 'comments', 'tags', 'custom_fields', 'created', 'last_updated',
|
||||
]
|
||||
brief_fields = ('id', 'url', 'display', 'family', 'prefix', 'address', 'description')
|
||||
brief_fields = ('id', 'url', 'display', 'family', 'address', 'description')
|
||||
|
||||
|
||||
class AvailableIPSerializer(serializers.Serializer):
|
||||
|
||||
@@ -340,26 +340,6 @@ class PrefixFilterSet(PrimaryModelFilterSet, ScopedFilterSet, TenancyFilterSet,
|
||||
field_name='prefix',
|
||||
lookup_expr='net_mask_length__lte'
|
||||
)
|
||||
aggregate_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=Aggregate.objects.all(),
|
||||
label=_('Aggregate'),
|
||||
)
|
||||
aggregate = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='aggregate__prefix',
|
||||
queryset=Aggregate.objects.all(),
|
||||
to_field_name='prefix',
|
||||
label=_('Aggregate (Prefix)'),
|
||||
)
|
||||
parent_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=Prefix.objects.all(),
|
||||
label=_('Parent Prefix'),
|
||||
)
|
||||
parent = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='parent__prefix',
|
||||
queryset=Prefix.objects.all(),
|
||||
to_field_name='prefix',
|
||||
label=_('Parent Prefix (Prefix)'),
|
||||
)
|
||||
vrf_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=VRF.objects.all(),
|
||||
label=_('VRF'),
|
||||
@@ -504,16 +484,6 @@ class IPRangeFilterSet(PrimaryModelFilterSet, TenancyFilterSet, ContactModelFilt
|
||||
method='search_contains',
|
||||
label=_('Ranges which contain this prefix or IP'),
|
||||
)
|
||||
prefix_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=Prefix.objects.all(),
|
||||
label=_('Prefix (ID)'),
|
||||
)
|
||||
prefix = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='prefix__prefix',
|
||||
queryset=Prefix.objects.all(),
|
||||
to_field_name='prefix',
|
||||
label=_('Prefix'),
|
||||
)
|
||||
vrf_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=VRF.objects.all(),
|
||||
label=_('VRF'),
|
||||
@@ -599,16 +569,6 @@ class IPAddressFilterSet(PrimaryModelFilterSet, TenancyFilterSet, ContactModelFi
|
||||
method='search_by_parent',
|
||||
label=_('Parent prefix'),
|
||||
)
|
||||
prefix_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=Prefix.objects.all(),
|
||||
label=_('Prefix (ID)'),
|
||||
)
|
||||
prefix = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='prefix__prefix',
|
||||
queryset=Prefix.objects.all(),
|
||||
to_field_name='prefix',
|
||||
label=_('Prefix (prefix)'),
|
||||
)
|
||||
address = MultiValueCharFilter(
|
||||
method='filter_address',
|
||||
label=_('Address'),
|
||||
|
||||
@@ -168,11 +168,6 @@ class RoleBulkEditForm(OrganizationalModelBulkEditForm):
|
||||
|
||||
|
||||
class PrefixBulkEditForm(ScopedBulkEditForm, PrimaryModelBulkEditForm):
|
||||
parent = DynamicModelChoiceField(
|
||||
queryset=Prefix.objects.all(),
|
||||
required=False,
|
||||
label=_('Parent Prefix')
|
||||
)
|
||||
vlan_group = DynamicModelChoiceField(
|
||||
queryset=VLANGroup.objects.all(),
|
||||
required=False,
|
||||
@@ -226,7 +221,7 @@ class PrefixBulkEditForm(ScopedBulkEditForm, PrimaryModelBulkEditForm):
|
||||
model = Prefix
|
||||
fieldsets = (
|
||||
FieldSet('tenant', 'status', 'role', 'description'),
|
||||
FieldSet('parent', 'vrf', 'prefix_length', 'is_pool', 'mark_utilized', name=_('Addressing')),
|
||||
FieldSet('vrf', 'prefix_length', 'is_pool', 'mark_utilized', name=_('Addressing')),
|
||||
FieldSet('scope_type', 'scope', name=_('Scope')),
|
||||
FieldSet('vlan_group', 'vlan', name=_('VLAN Assignment')),
|
||||
)
|
||||
@@ -236,11 +231,6 @@ class PrefixBulkEditForm(ScopedBulkEditForm, PrimaryModelBulkEditForm):
|
||||
|
||||
|
||||
class IPRangeBulkEditForm(PrimaryModelBulkEditForm):
|
||||
prefix = DynamicModelChoiceField(
|
||||
queryset=Prefix.objects.all(),
|
||||
required=False,
|
||||
label=_('Prefix')
|
||||
)
|
||||
vrf = DynamicModelChoiceField(
|
||||
queryset=VRF.objects.all(),
|
||||
required=False,
|
||||
@@ -282,16 +272,6 @@ class IPRangeBulkEditForm(PrimaryModelBulkEditForm):
|
||||
|
||||
|
||||
class IPAddressBulkEditForm(PrimaryModelBulkEditForm):
|
||||
prefix = DynamicModelChoiceField(
|
||||
queryset=Prefix.objects.all(),
|
||||
required=False,
|
||||
label=_('Prefix')
|
||||
)
|
||||
prefix = DynamicModelChoiceField(
|
||||
queryset=Prefix.objects.all(),
|
||||
required=False,
|
||||
label=_('Prefix')
|
||||
)
|
||||
vrf = DynamicModelChoiceField(
|
||||
queryset=VRF.objects.all(),
|
||||
required=False,
|
||||
@@ -327,10 +307,10 @@ class IPAddressBulkEditForm(PrimaryModelBulkEditForm):
|
||||
model = IPAddress
|
||||
fieldsets = (
|
||||
FieldSet('status', 'role', 'tenant', 'description'),
|
||||
FieldSet('prefix', 'vrf', 'mask_length', 'dns_name', name=_('Addressing')),
|
||||
FieldSet('vrf', 'mask_length', 'dns_name', name=_('Addressing')),
|
||||
)
|
||||
nullable_fields = (
|
||||
'prefix', 'vrf', 'role', 'tenant', 'dns_name', 'description', 'comments',
|
||||
'vrf', 'role', 'tenant', 'dns_name', 'description', 'comments',
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -343,8 +343,8 @@ class IPAddressImportForm(PrimaryModelImportForm):
|
||||
class Meta:
|
||||
model = IPAddress
|
||||
fields = [
|
||||
'prefix', 'address', 'vrf', 'tenant', 'status', 'role', 'device', 'virtual_machine', 'interface',
|
||||
'fhrp_group', 'is_primary', 'is_oob', 'dns_name', 'owner', 'description', 'comments', 'tags',
|
||||
'address', 'vrf', 'tenant', 'status', 'role', 'device', 'virtual_machine', 'interface', 'fhrp_group',
|
||||
'is_primary', 'is_oob', 'dns_name', 'description', 'owner', 'comments', 'tags',
|
||||
]
|
||||
|
||||
def __init__(self, data=None, *args, **kwargs):
|
||||
|
||||
@@ -219,12 +219,6 @@ class PrefixFilterForm(ContactModelFilterForm, TenancyFilterForm, PrimaryModelFi
|
||||
choices=PREFIX_MASK_LENGTH_CHOICES,
|
||||
label=_('Mask length')
|
||||
)
|
||||
aggregate_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Aggregate.objects.all(),
|
||||
required=False,
|
||||
label=_('Aggregate'),
|
||||
null_option='Global'
|
||||
)
|
||||
vrf_id = DynamicModelMultipleChoiceField(
|
||||
queryset=VRF.objects.all(),
|
||||
required=False,
|
||||
@@ -298,20 +292,12 @@ class PrefixFilterForm(ContactModelFilterForm, TenancyFilterForm, PrimaryModelFi
|
||||
class IPRangeFilterForm(ContactModelFilterForm, TenancyFilterForm, PrimaryModelFilterSetForm):
|
||||
model = IPRange
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag', 'owner_id'),
|
||||
FieldSet(
|
||||
'prefix', 'family', 'vrf_id', 'status', 'role_id', 'mark_populated', 'mark_utilized', name=_('Attributes')
|
||||
),
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet('family', 'vrf_id', 'status', 'role_id', 'mark_populated', 'mark_utilized', name=_('Attributes')),
|
||||
FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
|
||||
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||
FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts')),
|
||||
)
|
||||
prefix = DynamicModelMultipleChoiceField(
|
||||
queryset=Prefix.objects.all(),
|
||||
required=False,
|
||||
label=_('Prefix'),
|
||||
null_option='None'
|
||||
)
|
||||
family = forms.ChoiceField(
|
||||
required=False,
|
||||
choices=add_blank_choice(IPAddressFamilyChoices),
|
||||
@@ -356,7 +342,7 @@ class IPAddressFilterForm(ContactModelFilterForm, TenancyFilterForm, PrimaryMode
|
||||
fieldsets = (
|
||||
FieldSet('q', 'filter_id', 'tag'),
|
||||
FieldSet(
|
||||
'prefix', 'parent', 'family', 'status', 'role', 'mask_length', 'assigned_to_interface', 'dns_name',
|
||||
'parent', 'family', 'status', 'role', 'mask_length', 'assigned_to_interface', 'dns_name',
|
||||
name=_('Attributes')
|
||||
),
|
||||
FieldSet('vrf_id', 'present_in_vrf_id', name=_('VRF')),
|
||||
@@ -365,7 +351,7 @@ class IPAddressFilterForm(ContactModelFilterForm, TenancyFilterForm, PrimaryMode
|
||||
FieldSet('owner_group_id', 'owner_id', name=_('Ownership')),
|
||||
FieldSet('contact', 'contact_role', 'contact_group', name=_('Contacts')),
|
||||
)
|
||||
selector_fields = ('filter_id', 'q', 'region_id', 'group_id', 'prefix_id', 'parent', 'status', 'role')
|
||||
selector_fields = ('filter_id', 'q', 'region_id', 'group_id', 'parent', 'status', 'role')
|
||||
parent = forms.CharField(
|
||||
required=False,
|
||||
widget=forms.TextInput(
|
||||
@@ -385,11 +371,6 @@ class IPAddressFilterForm(ContactModelFilterForm, TenancyFilterForm, PrimaryMode
|
||||
choices=IPADDRESS_MASK_LENGTH_CHOICES,
|
||||
label=_('Mask length')
|
||||
)
|
||||
prefix_id = DynamicModelMultipleChoiceField(
|
||||
queryset=Prefix.objects.all(),
|
||||
required=False,
|
||||
label=_('Prefix'),
|
||||
)
|
||||
vrf_id = DynamicModelMultipleChoiceField(
|
||||
queryset=VRF.objects.all(),
|
||||
required=False,
|
||||
|
||||
@@ -255,8 +255,8 @@ class IPRangeForm(TenancyForm, PrimaryModelForm):
|
||||
|
||||
fieldsets = (
|
||||
FieldSet(
|
||||
'vrf', 'start_address', 'end_address', 'role', 'status', 'mark_populated', 'mark_utilized',
|
||||
'description', 'tags', name=_('IP Range')
|
||||
'vrf', 'start_address', 'end_address', 'role', 'status', 'mark_populated', 'mark_utilized', 'description',
|
||||
'tags', name=_('IP Range')
|
||||
),
|
||||
FieldSet('tenant_group', 'tenant', name=_('Tenancy')),
|
||||
)
|
||||
@@ -264,8 +264,8 @@ class IPRangeForm(TenancyForm, PrimaryModelForm):
|
||||
class Meta:
|
||||
model = IPRange
|
||||
fields = [
|
||||
'vrf', 'start_address', 'end_address', 'status', 'role', 'tenant_group', 'tenant',
|
||||
'mark_populated', 'mark_utilized', 'description', 'owner', 'comments', 'tags',
|
||||
'vrf', 'start_address', 'end_address', 'status', 'role', 'tenant_group', 'tenant', 'mark_populated',
|
||||
'mark_utilized', 'description', 'owner', 'comments', 'tags',
|
||||
]
|
||||
|
||||
|
||||
@@ -331,8 +331,8 @@ class IPAddressForm(TenancyForm, PrimaryModelForm):
|
||||
class Meta:
|
||||
model = IPAddress
|
||||
fields = [
|
||||
'address', 'vrf', 'status', 'role', 'dns_name', 'primary_for_parent', 'oob_for_parent',
|
||||
'nat_inside', 'tenant_group', 'tenant', 'description', 'owner', 'comments', 'tags',
|
||||
'address', 'vrf', 'status', 'role', 'dns_name', 'primary_for_parent', 'oob_for_parent', 'nat_inside',
|
||||
'tenant_group', 'tenant', 'description', 'owner', 'comments', 'tags',
|
||||
]
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
|
||||
@@ -170,7 +170,6 @@ class FHRPGroupAssignmentFilter(ChangeLoggedModelFilter):
|
||||
|
||||
@strawberry_django.filter_type(models.IPAddress, lookups=True)
|
||||
class IPAddressFilter(ContactFilterMixin, TenancyFilterMixin, PrimaryModelFilter):
|
||||
prefix: Annotated['PrefixFilter', strawberry.lazy('ipam.graphql.filters')] | None = strawberry_django.filter_field()
|
||||
address: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
vrf: Annotated['VRFFilter', strawberry.lazy('ipam.graphql.filters')] | None = strawberry_django.filter_field()
|
||||
vrf_id: ID | None = strawberry_django.filter_field()
|
||||
@@ -222,7 +221,6 @@ class IPAddressFilter(ContactFilterMixin, TenancyFilterMixin, PrimaryModelFilter
|
||||
|
||||
@strawberry_django.filter_type(models.IPRange, lookups=True)
|
||||
class IPRangeFilter(ContactFilterMixin, TenancyFilterMixin, PrimaryModelFilter):
|
||||
prefix: Annotated['PrefixFilter', strawberry.lazy('ipam.graphql.filters')] | None = strawberry_django.filter_field()
|
||||
start_address: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
end_address: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
size: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = (
|
||||
@@ -277,10 +275,6 @@ class IPRangeFilter(ContactFilterMixin, TenancyFilterMixin, PrimaryModelFilter):
|
||||
|
||||
@strawberry_django.filter_type(models.Prefix, lookups=True)
|
||||
class PrefixFilter(ContactFilterMixin, ScopedFilterMixin, TenancyFilterMixin, PrimaryModelFilter):
|
||||
aggregate: Annotated['AggregateFilter', strawberry.lazy('ipam.graphql.filters')] | None = (
|
||||
strawberry_django.filter_field()
|
||||
)
|
||||
parent: Annotated['PrefixFilter', strawberry.lazy('ipam.graphql.filters')] | None = strawberry_django.filter_field()
|
||||
prefix: FilterLookup[str] | None = strawberry_django.filter_field()
|
||||
vrf: Annotated['VRFFilter', strawberry.lazy('ipam.graphql.filters')] | None = strawberry_django.filter_field()
|
||||
vrf_id: ID | None = strawberry_django.filter_field()
|
||||
|
||||
@@ -143,7 +143,6 @@ class FHRPGroupAssignmentType(BaseObjectType):
|
||||
)
|
||||
class IPAddressType(ContactsMixin, BaseIPAddressFamilyType, PrimaryObjectType):
|
||||
address: str
|
||||
prefix: Annotated["PrefixType", strawberry.lazy('ipam.graphql.types')] | None
|
||||
vrf: Annotated["VRFType", strawberry.lazy('ipam.graphql.types')] | None
|
||||
tenant: Annotated["TenantType", strawberry.lazy('tenancy.graphql.types')] | None
|
||||
nat_inside: Annotated["IPAddressType", strawberry.lazy('ipam.graphql.types')] | None
|
||||
@@ -168,7 +167,6 @@ class IPAddressType(ContactsMixin, BaseIPAddressFamilyType, PrimaryObjectType):
|
||||
pagination=True
|
||||
)
|
||||
class IPRangeType(ContactsMixin, PrimaryObjectType):
|
||||
prefix: Annotated["PrefixType", strawberry.lazy('ipam.graphql.types')] | None
|
||||
start_address: str
|
||||
end_address: str
|
||||
vrf: Annotated["VRFType", strawberry.lazy('ipam.graphql.types')] | None
|
||||
@@ -183,8 +181,6 @@ class IPRangeType(ContactsMixin, PrimaryObjectType):
|
||||
pagination=True
|
||||
)
|
||||
class PrefixType(ContactsMixin, BaseIPAddressFamilyType, PrimaryObjectType):
|
||||
aggregate: Annotated["AggregateType", strawberry.lazy('ipam.graphql.types')] | None
|
||||
parent: Annotated["PrefixType", strawberry.lazy('ipam.graphql.types')] | None
|
||||
prefix: str
|
||||
vrf: Annotated["VRFType", strawberry.lazy('ipam.graphql.types')] | None
|
||||
tenant: Annotated["TenantType", strawberry.lazy('tenancy.graphql.types')] | None
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
# Generated by Django 5.0.9 on 2025-02-20 16:49
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('ipam', '0086_gfk_indexes'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='prefix',
|
||||
name='parent',
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.DO_NOTHING,
|
||||
related_name='children',
|
||||
to='ipam.prefix',
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='ipaddress',
|
||||
name='prefix',
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name='ip_addresses',
|
||||
to='ipam.prefix',
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='iprange',
|
||||
name='prefix',
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name='ip_ranges',
|
||||
to='ipam.prefix',
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='prefix',
|
||||
name='aggregate',
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name='prefixes',
|
||||
to='ipam.aggregate',
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -1,132 +0,0 @@
|
||||
# Generated by Django 5.0.9 on 2025-02-20 16:49
|
||||
|
||||
import sys
|
||||
import time
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
from ipam.choices import PrefixStatusChoices
|
||||
|
||||
|
||||
def draw_progress(count, total, length=20):
|
||||
if total == 0:
|
||||
return
|
||||
progress = count / total
|
||||
percent = int(progress * 100)
|
||||
bar = int(progress * length)
|
||||
sys.stdout.write('\r')
|
||||
sys.stdout.write(f"[{'=' * bar:{length}s}] {percent}%")
|
||||
sys.stdout.flush()
|
||||
|
||||
|
||||
def set_prefix(apps, schema_editor, model, attr='address', parent_attr='prefix', parent_model='Prefix'):
|
||||
start = time.time()
|
||||
ChildModel = apps.get_model('ipam', model)
|
||||
ParentModel = apps.get_model('ipam', parent_model)
|
||||
|
||||
addresses = ChildModel.objects.all()
|
||||
total = addresses.count()
|
||||
if total == 0:
|
||||
return
|
||||
|
||||
print('\r\n')
|
||||
print(f'Migrating {parent_model}')
|
||||
print('\r\n')
|
||||
i = 0
|
||||
draw_progress(i, total, 50)
|
||||
for address in addresses:
|
||||
i += 1
|
||||
address_attr = getattr(address, attr)
|
||||
prefixes = ParentModel.objects.filter(
|
||||
prefix__net_contains_or_equals=str(address_attr.ip),
|
||||
prefix__net_mask_length__lte=address_attr.prefixlen,
|
||||
)
|
||||
|
||||
setattr(address, parent_attr, prefixes.last())
|
||||
try:
|
||||
address.save()
|
||||
except Exception as e:
|
||||
print(f'Error at {address}')
|
||||
raise e
|
||||
draw_progress(i, total, 50)
|
||||
|
||||
end = time.time()
|
||||
print(f"\r\nElapsed Time: {end - start:.2f}s")
|
||||
|
||||
|
||||
def set_ipaddress_prefix(apps, schema_editor):
|
||||
set_prefix(apps, schema_editor, 'IPAddress')
|
||||
|
||||
|
||||
def unset_ipaddress_prefix(apps, schema_editor):
|
||||
IPAddress = apps.get_model('ipam', 'IPAddress')
|
||||
IPAddress.objects.update(prefix=None)
|
||||
|
||||
|
||||
def set_iprange_prefix(apps, schema_editor):
|
||||
set_prefix(apps, schema_editor, 'IPRange', 'start_address')
|
||||
|
||||
|
||||
def unset_iprange_prefix(apps, schema_editor):
|
||||
IPRange = apps.get_model('ipam', 'IPRange')
|
||||
IPRange.objects.update(prefix=None)
|
||||
|
||||
|
||||
def set_prefix_aggregate(apps, schema_editor):
|
||||
set_prefix(apps, schema_editor, 'Prefix', 'prefix', 'aggregate', 'Aggregate')
|
||||
|
||||
|
||||
def unset_prefix_aggregate(apps, schema_editor):
|
||||
Prefix = apps.get_model('ipam', 'Prefix')
|
||||
Prefix.objects.update(aggregate=None)
|
||||
|
||||
|
||||
def set_prefix_parent(apps, schema_editor):
|
||||
Prefix = apps.get_model('ipam', 'Prefix')
|
||||
start = time.time()
|
||||
addresses = Prefix.objects.all()
|
||||
i = 0
|
||||
total = addresses.count()
|
||||
if total == 0:
|
||||
return
|
||||
|
||||
print('\r\n')
|
||||
draw_progress(i, total, 50)
|
||||
for address in addresses:
|
||||
i += 1
|
||||
prefixes = Prefix.objects.exclude(pk=address.pk).filter(
|
||||
models.Q(vrf=address.vrf, prefix__net_contains=str(address.prefix.ip))
|
||||
| models.Q(
|
||||
vrf=None,
|
||||
status=PrefixStatusChoices.STATUS_CONTAINER,
|
||||
prefix__net_contains=str(address.prefix.ip),
|
||||
)
|
||||
)
|
||||
if not prefixes.exists():
|
||||
draw_progress(i, total, 50)
|
||||
continue
|
||||
|
||||
address.parent = prefixes.last()
|
||||
address.save()
|
||||
draw_progress(i, total, 50)
|
||||
end = time.time()
|
||||
print(f"\r\nElapsed Time: {end - start:.2f}s")
|
||||
|
||||
|
||||
def unset_prefix_parent(apps, schema_editor):
|
||||
Prefix = apps.get_model('ipam', 'Prefix')
|
||||
Prefix.objects.update(parent=None)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('ipam', '0087_ipaddress_iprange_prefix_parent'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(set_ipaddress_prefix, unset_ipaddress_prefix),
|
||||
migrations.RunPython(set_iprange_prefix, unset_iprange_prefix),
|
||||
migrations.RunPython(set_prefix_aggregate, unset_prefix_aggregate),
|
||||
migrations.RunPython(set_prefix_parent, unset_prefix_parent),
|
||||
]
|
||||
@@ -1,57 +0,0 @@
|
||||
# Generated by Django 5.2.5 on 2026-02-06 21:30
|
||||
|
||||
import pgtrigger.compiler
|
||||
import pgtrigger.migrations
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('ipam', '0088_ipaddress_iprange_prefix_parent_data'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
pgtrigger.migrations.AddTrigger(
|
||||
model_name='prefix',
|
||||
trigger=pgtrigger.compiler.Trigger(
|
||||
name='ipam_prefix_delete',
|
||||
sql=pgtrigger.compiler.UpsertTriggerSql(
|
||||
func="\n-- Update Child Prefix's with Prefix's PARENT This is a safe assumption based on the fact that the parent would be the\n-- next direct parent for anything else that could contain this prefix\nUPDATE ipam_prefix SET parent_id=OLD.parent_id WHERE parent_id=OLD.id;\nRETURN OLD;\n",
|
||||
hash='ee3f890009c05a3617428158e7b6f3d77317885d',
|
||||
operation='DELETE',
|
||||
pgid='pgtrigger_ipam_prefix_delete_e7810',
|
||||
table='ipam_prefix',
|
||||
when='BEFORE',
|
||||
),
|
||||
),
|
||||
),
|
||||
pgtrigger.migrations.AddTrigger(
|
||||
model_name='prefix',
|
||||
trigger=pgtrigger.compiler.Trigger(
|
||||
name='ipam_prefix_insert',
|
||||
sql=pgtrigger.compiler.UpsertTriggerSql(
|
||||
func="\n-- Update the prefix with the new parent if the parent is the most appropriate prefix\nUPDATE ipam_prefix\nSET parent_id=NEW.id\nWHERE\n prefix << NEW.prefix\n AND\n (\n (vrf_id = NEW.vrf_id OR (vrf_id IS NULL AND NEW.vrf_id IS NULL))\n OR\n (\n NEW.vrf_id IS NULL\n AND\n NEW.status = 'container'\n AND\n NOT EXISTS(\n SELECT 1 FROM ipam_prefix p WHERE p.prefix >> ipam_prefix.prefix AND p.vrf_id = ipam_prefix.vrf_id\n )\n )\n )\n AND id != NEW.id\n AND NOT EXISTS (\n SELECT 1 FROM ipam_prefix p\n WHERE\n p.prefix >> ipam_prefix.prefix\n AND p.prefix << NEW.prefix\n AND (\n (p.vrf_id = ipam_prefix.vrf_id OR (p.vrf_id IS NULL AND ipam_prefix.vrf_id IS NULL))\n OR\n (p.vrf_id IS NULL AND p.status = 'container')\n )\n AND p.id != NEW.id\n )\n;\nRETURN NEW;\n",
|
||||
hash='1d71498f09e767183d3b0d29c06c9ac9e2cc000a',
|
||||
operation='INSERT',
|
||||
pgid='pgtrigger_ipam_prefix_insert_46c72',
|
||||
table='ipam_prefix',
|
||||
when='AFTER',
|
||||
),
|
||||
),
|
||||
),
|
||||
pgtrigger.migrations.AddTrigger(
|
||||
model_name='prefix',
|
||||
trigger=pgtrigger.compiler.Trigger(
|
||||
name='ipam_prefix_update',
|
||||
sql=pgtrigger.compiler.UpsertTriggerSql(
|
||||
func="\n-- When a prefix changes, reassign any child prefixes that no longer\n-- fall within the new prefix range to the parent prefix (or set null if no parent exists)\nUPDATE ipam_prefix\nSET parent_id = OLD.parent_id\nWHERE\n parent_id = NEW.id\n -- IP address no longer contained within the updated prefix\n AND NOT (prefix << NEW.prefix);\n\n-- When a prefix changes, reassign any ip addresses that no longer\n-- fall within the new prefix range to the parent prefix (or set null if no parent exists)\nUPDATE ipam_ipaddress\nSET prefix_id = OLD.parent_id\nWHERE\n prefix_id = NEW.id\n -- IP address no longer contained within the updated prefix\n AND\n NOT (address << NEW.prefix)\n;\n\n-- When a prefix changes, reassign any ip ranges that no longer\n-- fall within the new prefix range to the parent prefix (or set null if no parent exists)\nUPDATE ipam_iprange\nSET prefix_id = OLD.parent_id\nWHERE\n prefix_id = NEW.id\n -- IP address no longer contained within the updated prefix\n AND\n NOT (start_address << NEW.prefix)\n AND\n NOT (end_address << NEW.prefix)\n;\n\n-- When a prefix changes, reassign any ip addresses that are in-scope but\n-- no longer within the same VRF\nUPDATE ipam_ipaddress\n SET prefix_id = OLD.parent_id\n WHERE\n prefix_id = NEW.id\n AND\n address << OLD.prefix\n AND\n (\n NOT address << NEW.prefix\n OR\n (\n vrf_id is NULL\n AND\n NEW.vrf_id IS NOT NULL\n )\n OR\n (\n OLD.vrf_id IS NULL\n AND\n NEW.vrf_id IS NOT NULL\n AND\n NEW.vrf_id != vrf_id\n )\n )\n;\n\n-- When a prefix changes, reassign any ip ranges that are in-scope but\n-- no longer within the same VRF\nUPDATE ipam_iprange\n SET prefix_id = OLD.parent_id\n WHERE\n prefix_id = NEW.id\n AND\n start_address << OLD.prefix\n AND\n end_address << OLD.prefix\n AND\n (\n NOT start_address << NEW.prefix\n OR\n NOT end_address << NEW.prefix\n OR\n (\n vrf_id is NULL\n AND\n NEW.vrf_id IS NOT NULL\n )\n OR\n (\n OLD.vrf_id IS NULL\n AND\n NEW.vrf_id IS NOT NULL\n AND\n NEW.vrf_id != vrf_id\n )\n )\n;\n\n-- Update the prefix with the new parent if the parent is the most appropriate prefix\nUPDATE ipam_prefix\n SET parent_id=NEW.id\n WHERE\n prefix << NEW.prefix\n AND\n (\n (vrf_id = NEW.vrf_id OR (vrf_id IS NULL AND NEW.vrf_id IS NULL))\n OR\n (\n NEW.vrf_id IS NULL\n AND\n NEW.status = 'container'\n AND\n NOT EXISTS(\n SELECT 1 FROM ipam_prefix p WHERE p.prefix >> prefix AND p.vrf_id = vrf_id\n )\n )\n )\n AND id != NEW.id\n AND NOT EXISTS (\n SELECT 1 FROM ipam_prefix p\n WHERE\n p.prefix >> ipam_prefix.prefix\n AND p.prefix << NEW.prefix\n AND (\n (p.vrf_id = ipam_prefix.vrf_id OR (p.vrf_id IS NULL AND ipam_prefix.vrf_id IS NULL))\n OR\n (p.vrf_id IS NULL AND p.status = 'container')\n )\n AND p.id != NEW.id\n )\n;\nUPDATE ipam_ipaddress\n SET prefix_id = NEW.id\n WHERE\n prefix_id != NEW.id\n AND\n address << NEW.prefix\n AND (\n (vrf_id = NEW.vrf_id OR (vrf_id IS NULL AND NEW.vrf_id IS NULL))\n OR (\n NEW.vrf_id IS NULL\n AND\n NEW.status = 'container'\n AND\n NOT EXISTS(\n SELECT 1 FROM ipam_prefix p WHERE p.prefix >> address AND p.vrf_id = vrf_id\n )\n )\n )\n;\nUPDATE ipam_iprange\n SET prefix_id = NEW.id\n WHERE\n prefix_id != NEW.id\n AND\n start_address << NEW.prefix\n AND\n end_address << NEW.prefix\n AND (\n (vrf_id = NEW.vrf_id OR (vrf_id IS NULL AND NEW.vrf_id IS NULL))\n OR (\n NEW.vrf_id IS NULL\n AND\n NEW.status = 'container'\n AND\n NOT EXISTS(\n SELECT 1 FROM ipam_prefix p WHERE\n p.prefix >> start_address\n AND\n p.prefix >> end_address\n AND\n p.vrf_id = vrf_id\n )\n )\n )\n;\nRETURN NEW;\n",
|
||||
hash='7dce524151c88aa9864aad70a24cb5982b05aa28',
|
||||
operation='UPDATE',
|
||||
pgid='pgtrigger_ipam_prefix_update_e5fca',
|
||||
table='ipam_prefix',
|
||||
when='AFTER',
|
||||
),
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -1,5 +1,4 @@
|
||||
import netaddr
|
||||
import pgtrigger
|
||||
from django.contrib.contenttypes.fields import GenericForeignKey
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.contrib.postgres.indexes import GistIndex
|
||||
@@ -9,7 +8,6 @@ from django.db.models import F
|
||||
from django.db.models.functions import Cast
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from netaddr.ip import IPNetwork
|
||||
|
||||
from dcim.models.mixins import CachedScopeMixin
|
||||
from ipam.choices import *
|
||||
@@ -18,8 +16,6 @@ from ipam.fields import IPNetworkField, IPAddressField
|
||||
from ipam.lookups import Host
|
||||
from ipam.managers import IPAddressManager
|
||||
from ipam.querysets import PrefixQuerySet
|
||||
from ipam.triggers import ipam_prefix_delete_adjust_prefix_parent, ipam_prefix_insert_adjust_prefix_parent, \
|
||||
ipam_prefix_update_adjust_prefix_parent
|
||||
from ipam.validators import DNSValidator
|
||||
from netbox.config import get_config
|
||||
from netbox.models import OrganizationalModel, PrimaryModel
|
||||
@@ -189,28 +185,31 @@ class Aggregate(ContactsMixin, GetAvailablePrefixesMixin, PrimaryModel):
|
||||
return min(utilization, 100)
|
||||
|
||||
|
||||
class Role(OrganizationalModel):
|
||||
"""
|
||||
A Role represents the functional role of a Prefix or VLAN; for example, "Customer," "Infrastructure," or
|
||||
"Management."
|
||||
"""
|
||||
weight = models.PositiveSmallIntegerField(
|
||||
verbose_name=_('weight'),
|
||||
default=1000
|
||||
)
|
||||
|
||||
class Meta:
|
||||
ordering = ('weight', 'name')
|
||||
verbose_name = _('role')
|
||||
verbose_name_plural = _('roles')
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
class Prefix(ContactsMixin, GetAvailablePrefixesMixin, CachedScopeMixin, PrimaryModel):
|
||||
"""
|
||||
A Prefix represents an IPv4 or IPv6 network, including mask length. Prefixes can optionally be scoped to certain
|
||||
areas and/or assigned to VRFs. A Prefix must be assigned a status and may optionally be assigned a used-define Role.
|
||||
A Prefix can also be assigned to a VLAN where appropriate.
|
||||
"""
|
||||
aggregate = models.ForeignKey(
|
||||
to='ipam.Aggregate',
|
||||
on_delete=models.SET_NULL, # This is handled by triggers
|
||||
related_name='prefixes',
|
||||
blank=True,
|
||||
null=True,
|
||||
verbose_name=_('aggregate')
|
||||
)
|
||||
parent = models.ForeignKey(
|
||||
to='ipam.Prefix',
|
||||
on_delete=models.DO_NOTHING,
|
||||
related_name='children',
|
||||
blank=True,
|
||||
null=True,
|
||||
verbose_name=_('Prefix')
|
||||
)
|
||||
prefix = IPNetworkField(
|
||||
verbose_name=_('prefix'),
|
||||
help_text=_('IPv4 or IPv6 network with mask')
|
||||
@@ -285,32 +284,8 @@ class Prefix(ContactsMixin, GetAvailablePrefixesMixin, CachedScopeMixin, Primary
|
||||
verbose_name_plural = _('prefixes')
|
||||
indexes = (
|
||||
models.Index(fields=('scope_type', 'scope_id')),
|
||||
GistIndex(
|
||||
fields=['prefix'],
|
||||
name='ipam_prefix_gist_idx',
|
||||
opclasses=['inet_ops'],
|
||||
),
|
||||
)
|
||||
triggers = (
|
||||
pgtrigger.Trigger(
|
||||
name='ipam_prefix_delete',
|
||||
operation=pgtrigger.Delete,
|
||||
when=pgtrigger.Before,
|
||||
func=ipam_prefix_delete_adjust_prefix_parent,
|
||||
),
|
||||
pgtrigger.Trigger(
|
||||
name='ipam_prefix_insert',
|
||||
operation=pgtrigger.Insert,
|
||||
when=pgtrigger.After,
|
||||
func=ipam_prefix_insert_adjust_prefix_parent,
|
||||
),
|
||||
pgtrigger.Trigger(
|
||||
name='ipam_prefix_update',
|
||||
operation=pgtrigger.Update,
|
||||
when=pgtrigger.After,
|
||||
func=ipam_prefix_update_adjust_prefix_parent,
|
||||
),
|
||||
)
|
||||
GistIndex(fields=['prefix'], name='ipam_prefix_gist_idx', opclasses=['inet_ops']),
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
@@ -326,8 +301,6 @@ class Prefix(ContactsMixin, GetAvailablePrefixesMixin, CachedScopeMixin, Primary
|
||||
super().clean()
|
||||
|
||||
if self.prefix:
|
||||
if not isinstance(self.prefix, IPNetwork):
|
||||
self.prefix = IPNetwork(self.prefix)
|
||||
|
||||
# /0 masks are not acceptable
|
||||
if self.prefix.prefixlen == 0:
|
||||
@@ -349,10 +322,6 @@ class Prefix(ContactsMixin, GetAvailablePrefixesMixin, CachedScopeMixin, Primary
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
|
||||
if not self.pk or not self.parent or (self.prefix != self._prefix) or (self.vrf_id != self._vrf_id):
|
||||
parent = self.find_parent_prefix(networks=self.prefix, vrf=self.vrf, exclude=self.pk)
|
||||
self.parent = parent
|
||||
|
||||
if isinstance(self.prefix, netaddr.IPNetwork):
|
||||
|
||||
# Clear host bits from prefix
|
||||
@@ -377,11 +346,11 @@ class Prefix(ContactsMixin, GetAvailablePrefixesMixin, CachedScopeMixin, Primary
|
||||
return netaddr.IPAddress(self.prefix).format(netaddr.ipv6_full)
|
||||
|
||||
@property
|
||||
def depth_count(self):
|
||||
def depth(self):
|
||||
return self._depth
|
||||
|
||||
@property
|
||||
def children_count(self):
|
||||
def children(self):
|
||||
return self._children
|
||||
|
||||
def _set_prefix_length(self, value):
|
||||
@@ -521,63 +490,11 @@ class Prefix(ContactsMixin, GetAvailablePrefixesMixin, CachedScopeMixin, Primary
|
||||
|
||||
return min(utilization, 100)
|
||||
|
||||
@classmethod
|
||||
def find_parent_prefix(cls, networks, vrf=None, exclude=None):
|
||||
# TODO: Document
|
||||
if type(networks) in [netaddr.IPAddress, netaddr.IPNetwork, str]:
|
||||
networks = [networks, ]
|
||||
|
||||
network_filter = models.Q()
|
||||
for network in networks:
|
||||
network_filter &= models.Q(
|
||||
prefix__net_contains_or_equals=network
|
||||
)
|
||||
prefixes = Prefix.objects.filter(
|
||||
models.Q(
|
||||
network_filter,
|
||||
vrf=vrf
|
||||
) | models.Q(
|
||||
network_filter,
|
||||
vrf=None,
|
||||
status=PrefixStatusChoices.STATUS_CONTAINER,
|
||||
)
|
||||
)
|
||||
if exclude:
|
||||
prefixes = prefixes.exclude(pk=exclude)
|
||||
return prefixes.last()
|
||||
|
||||
|
||||
class Role(OrganizationalModel):
|
||||
"""
|
||||
A Role represents the functional role of a Prefix or VLAN; for example, "Customer," "Infrastructure," or
|
||||
"Management."
|
||||
"""
|
||||
weight = models.PositiveSmallIntegerField(
|
||||
verbose_name=_('weight'),
|
||||
default=1000
|
||||
)
|
||||
|
||||
class Meta:
|
||||
ordering = ('weight', 'name')
|
||||
verbose_name = _('role')
|
||||
verbose_name_plural = _('roles')
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
class IPRange(ContactsMixin, PrimaryModel):
|
||||
"""
|
||||
A range of IP addresses, defined by start and end addresses.
|
||||
"""
|
||||
prefix = models.ForeignKey(
|
||||
to='ipam.Prefix',
|
||||
on_delete=models.SET_NULL,
|
||||
related_name='ip_ranges',
|
||||
null=True,
|
||||
blank=True,
|
||||
verbose_name=_('prefix'),
|
||||
)
|
||||
start_address = IPAddressField(
|
||||
verbose_name=_('start address'),
|
||||
help_text=_('IPv4 or IPv6 address (with mask)')
|
||||
@@ -647,27 +564,6 @@ class IPRange(ContactsMixin, PrimaryModel):
|
||||
super().clean()
|
||||
|
||||
if self.start_address and self.end_address:
|
||||
# If prefix is set, validate suitability
|
||||
if self.prefix:
|
||||
# Check that start address and end address are within the prefix range
|
||||
if self.start_address not in self.prefix.prefix and self.end_address not in self.prefix.prefix:
|
||||
raise ValidationError({
|
||||
'start_address': _("Start address must be part of the selected prefix"),
|
||||
'end_address': _("End address must be part of the selected prefix.")
|
||||
})
|
||||
elif self.start_address not in self.prefix.prefix:
|
||||
raise ValidationError({
|
||||
'start_address': _("Start address must be part of the selected prefix")
|
||||
})
|
||||
elif self.end_address not in self.prefix.prefix:
|
||||
raise ValidationError({
|
||||
'end_address': _("End address must be part of the selected prefix.")
|
||||
})
|
||||
# Check that VRF matches prefix VRF
|
||||
if self.vrf != self.prefix.vrf:
|
||||
raise ValidationError({
|
||||
'vrf': _("VRF must match the prefix VRF.")
|
||||
})
|
||||
|
||||
# Check that start & end IP versions match
|
||||
if self.start_address.version != self.end_address.version:
|
||||
@@ -730,12 +626,6 @@ class IPRange(ContactsMixin, PrimaryModel):
|
||||
# Record the range's size (number of IP addresses)
|
||||
self.size = int(self.end_address.ip - self.start_address.ip) + 1
|
||||
|
||||
# Set the parent prefix
|
||||
self.prefix = Prefix.find_parent_prefix(
|
||||
networks=[self.start_address, self.end_address],
|
||||
vrf=self.vrf
|
||||
)
|
||||
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
@property
|
||||
@@ -842,14 +732,6 @@ class IPAddress(ContactsMixin, PrimaryModel):
|
||||
for example, when mapping public addresses to private addresses. When an Interface has been assigned an IPAddress
|
||||
which has a NAT outside IP, that Interface's Device can use either the inside or outside IP as its primary IP.
|
||||
"""
|
||||
prefix = models.ForeignKey(
|
||||
to='ipam.Prefix',
|
||||
on_delete=models.SET_NULL,
|
||||
related_name='ip_addresses',
|
||||
blank=True,
|
||||
null=True,
|
||||
verbose_name=_('Prefix')
|
||||
)
|
||||
address = IPAddressField(
|
||||
verbose_name=_('address'),
|
||||
help_text=_('IPv4 or IPv6 address (with mask)')
|
||||
@@ -937,7 +819,6 @@ class IPAddress(ContactsMixin, PrimaryModel):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self._address = self.address
|
||||
# Denote the original assigned object (if any) for validation in clean()
|
||||
self._original_assigned_object_id = self.__dict__.get('assigned_object_id')
|
||||
self._original_assigned_object_type_id = self.__dict__.get('assigned_object_type_id')
|
||||
@@ -984,16 +865,6 @@ class IPAddress(ContactsMixin, PrimaryModel):
|
||||
super().clean()
|
||||
|
||||
if self.address:
|
||||
# If prefix is set, validate suitability
|
||||
if self.prefix:
|
||||
if self.address not in self.prefix.prefix:
|
||||
raise ValidationError({
|
||||
'prefix': _("IP address must be part of the selected prefix.")
|
||||
})
|
||||
if self.vrf != self.prefix.vrf:
|
||||
raise ValidationError({
|
||||
'vrf': _("IP address VRF must match the prefix VRF.")
|
||||
})
|
||||
|
||||
# /0 masks are not acceptable
|
||||
if self.address.prefixlen == 0:
|
||||
@@ -1087,9 +958,6 @@ class IPAddress(ContactsMixin, PrimaryModel):
|
||||
# Force dns_name to lowercase
|
||||
self.dns_name = self.dns_name.lower()
|
||||
|
||||
# Set the parent prefix
|
||||
self.prefix = Prefix.find_parent_prefix(networks=self.address, vrf=self.vrf)
|
||||
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
def clone(self):
|
||||
@@ -1144,8 +1012,3 @@ class IPAddress(ContactsMixin, PrimaryModel):
|
||||
|
||||
def get_role_color(self):
|
||||
return IPAddressRoleChoices.colors.get(self.role)
|
||||
|
||||
@classmethod
|
||||
def find_prefix(self, address):
|
||||
prefixes = Prefix.objects.filter(prefix__net_contains=address.address, vrf=address.vrf)
|
||||
return prefixes.last()
|
||||
|
||||
@@ -53,12 +53,11 @@ class IPAddressIndex(SearchIndex):
|
||||
model = models.IPAddress
|
||||
fields = (
|
||||
('address', 100),
|
||||
('prefix', 200),
|
||||
('dns_name', 300),
|
||||
('description', 500),
|
||||
('comments', 5000),
|
||||
)
|
||||
display_attrs = ('prefix', 'vrf', 'tenant', 'status', 'role', 'description')
|
||||
display_attrs = ('vrf', 'tenant', 'status', 'role', 'description')
|
||||
|
||||
|
||||
@register_search
|
||||
@@ -67,11 +66,10 @@ class IPRangeIndex(SearchIndex):
|
||||
fields = (
|
||||
('start_address', 100),
|
||||
('end_address', 300),
|
||||
('prefix', 400),
|
||||
('description', 500),
|
||||
('comments', 5000),
|
||||
)
|
||||
display_attrs = ('prefix', 'vrf', 'tenant', 'status', 'role', 'description')
|
||||
display_attrs = ('vrf', 'tenant', 'status', 'role', 'description')
|
||||
|
||||
|
||||
@register_search
|
||||
@@ -79,12 +77,10 @@ class PrefixIndex(SearchIndex):
|
||||
model = models.Prefix
|
||||
fields = (
|
||||
('prefix', 110),
|
||||
('parent', 200),
|
||||
('aggregate', 300),
|
||||
('description', 500),
|
||||
('comments', 5000),
|
||||
)
|
||||
display_attrs = ('scope', 'aggregate', 'parent', 'vrf', 'tenant', 'vlan', 'status', 'role', 'description')
|
||||
display_attrs = ('scope', 'vrf', 'tenant', 'vlan', 'status', 'role', 'description')
|
||||
|
||||
|
||||
@register_search
|
||||
|
||||
@@ -152,10 +152,6 @@ class PrefixUtilizationColumn(columns.UtilizationColumn):
|
||||
|
||||
|
||||
class PrefixTable(TenancyColumnsMixin, ContactsColumnMixin, PrimaryModelTable):
|
||||
parent = tables.Column(
|
||||
verbose_name=_('Parent'),
|
||||
linkify=True
|
||||
)
|
||||
prefix = columns.TemplateColumn(
|
||||
verbose_name=_('Prefix'),
|
||||
template_code=PREFIX_LINK_WITH_DEPTH,
|
||||
@@ -234,9 +230,9 @@ class PrefixTable(TenancyColumnsMixin, ContactsColumnMixin, PrimaryModelTable):
|
||||
class Meta(PrimaryModelTable.Meta):
|
||||
model = Prefix
|
||||
fields = (
|
||||
'pk', 'id', 'prefix', 'status', 'parent', 'prefix', 'prefix_flat', 'children', 'vrf', 'utilization',
|
||||
'tenant', 'tenant_group', 'scope', 'scope_type', 'vlan_group', 'vlan', 'role', 'is_pool', 'mark_utilized',
|
||||
'contacts', 'description', 'comments', 'tags', 'created', 'last_updated',
|
||||
'pk', 'id', 'prefix', 'prefix_flat', 'status', 'children', 'vrf', 'utilization', 'tenant', 'tenant_group',
|
||||
'scope', 'scope_type', 'vlan_group', 'vlan', 'role', 'is_pool', 'mark_utilized', 'description', 'contacts',
|
||||
'comments', 'tags', 'created', 'last_updated',
|
||||
)
|
||||
default_columns = (
|
||||
'pk', 'prefix', 'status', 'children', 'vrf', 'utilization', 'tenant', 'scope', 'vlan', 'role',
|
||||
@@ -250,11 +246,8 @@ class PrefixTable(TenancyColumnsMixin, ContactsColumnMixin, PrimaryModelTable):
|
||||
#
|
||||
# IP ranges
|
||||
#
|
||||
|
||||
class IPRangeTable(TenancyColumnsMixin, ContactsColumnMixin, PrimaryModelTable):
|
||||
prefix = tables.Column(
|
||||
verbose_name=_('Prefix'),
|
||||
linkify=True
|
||||
)
|
||||
start_address = tables.Column(
|
||||
verbose_name=_('Start address'),
|
||||
linkify=True
|
||||
@@ -291,9 +284,9 @@ class IPRangeTable(TenancyColumnsMixin, ContactsColumnMixin, PrimaryModelTable):
|
||||
class Meta(PrimaryModelTable.Meta):
|
||||
model = IPRange
|
||||
fields = (
|
||||
'pk', 'id', 'start_address', 'end_address', 'prefix', 'size', 'vrf', 'status', 'role', 'tenant',
|
||||
'tenant_group', 'mark_populated', 'mark_utilized', 'utilization', 'description', 'contacts',
|
||||
'comments', 'tags', 'created', 'last_updated',
|
||||
'pk', 'id', 'start_address', 'end_address', 'size', 'vrf', 'status', 'role', 'tenant', 'tenant_group',
|
||||
'mark_populated', 'mark_utilized', 'utilization', 'description', 'contacts', 'comments', 'tags',
|
||||
'created', 'last_updated',
|
||||
)
|
||||
default_columns = (
|
||||
'pk', 'start_address', 'end_address', 'size', 'vrf', 'status', 'role', 'tenant', 'description',
|
||||
@@ -308,18 +301,10 @@ class IPRangeTable(TenancyColumnsMixin, ContactsColumnMixin, PrimaryModelTable):
|
||||
#
|
||||
|
||||
class IPAddressTable(TenancyColumnsMixin, ContactsColumnMixin, PrimaryModelTable):
|
||||
prefix = tables.Column(
|
||||
verbose_name=_('Prefix'),
|
||||
linkify=True
|
||||
)
|
||||
address = tables.TemplateColumn(
|
||||
template_code=IPADDRESS_LINK,
|
||||
verbose_name=_('IP Address')
|
||||
)
|
||||
prefix = tables.Column(
|
||||
linkify=True,
|
||||
verbose_name=_('Prefix')
|
||||
)
|
||||
vrf = tables.TemplateColumn(
|
||||
template_code=VRF_LINK,
|
||||
verbose_name=_('VRF')
|
||||
@@ -368,9 +353,8 @@ class IPAddressTable(TenancyColumnsMixin, ContactsColumnMixin, PrimaryModelTable
|
||||
class Meta(PrimaryModelTable.Meta):
|
||||
model = IPAddress
|
||||
fields = (
|
||||
'pk', 'id', 'address', 'vrf', 'prefix', 'status', 'role', 'tenant', 'tenant_group', 'nat_inside',
|
||||
'nat_outside', 'assigned', 'dns_name', 'description', 'comments', 'contacts', 'tags', 'created',
|
||||
'last_updated',
|
||||
'pk', 'id', 'address', 'vrf', 'status', 'role', 'tenant', 'tenant_group', 'nat_inside', 'nat_outside',
|
||||
'assigned', 'dns_name', 'description', 'comments', 'contacts', 'tags', 'created', 'last_updated',
|
||||
)
|
||||
default_columns = (
|
||||
'pk', 'address', 'vrf', 'status', 'role', 'tenant', 'assigned', 'dns_name', 'description',
|
||||
|
||||
@@ -16,20 +16,12 @@ PREFIX_COPY_BUTTON = """
|
||||
|
||||
PREFIX_LINK_WITH_DEPTH = """
|
||||
{% load helpers %}
|
||||
{% if record.depth_count %}
|
||||
{% if object %}
|
||||
<div class="record-depth">
|
||||
{% for i in record.depth_count|parent_depth:object|as_range %}
|
||||
<span>•</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="record-depth">
|
||||
{% for i in record.depth_count|as_range %}
|
||||
<span>•</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if record.depth %}
|
||||
<div class="record-depth">
|
||||
{% for i in record.depth|as_range %}
|
||||
<span>•</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
""" + PREFIX_LINK
|
||||
|
||||
|
||||
@@ -407,8 +407,7 @@ class RoleTest(APIViewTestCases.APIViewTestCase):
|
||||
|
||||
class PrefixTest(APIViewTestCases.APIViewTestCase):
|
||||
model = Prefix
|
||||
# TODO: Alter for parent prefix
|
||||
brief_fields = ['_depth', 'aggregate', 'description', 'display', 'family', 'id', 'parent', 'prefix', 'url']
|
||||
brief_fields = ['_depth', 'description', 'display', 'family', 'id', 'prefix', 'url']
|
||||
create_data = [
|
||||
{
|
||||
'prefix': '192.168.4.0/24',
|
||||
@@ -648,8 +647,7 @@ class PrefixTest(APIViewTestCases.APIViewTestCase):
|
||||
|
||||
class IPRangeTest(APIViewTestCases.APIViewTestCase):
|
||||
model = IPRange
|
||||
# TODO: Alter for parent prefix
|
||||
brief_fields = ['description', 'display', 'end_address', 'family', 'id', 'prefix', 'start_address', 'url']
|
||||
brief_fields = ['description', 'display', 'end_address', 'family', 'id', 'start_address', 'url']
|
||||
create_data = [
|
||||
{
|
||||
'start_address': '192.168.4.10/24',
|
||||
@@ -807,8 +805,7 @@ class IPRangeTest(APIViewTestCases.APIViewTestCase):
|
||||
|
||||
class IPAddressTest(APIViewTestCases.APIViewTestCase):
|
||||
model = IPAddress
|
||||
# TODO: Alter for parent prefix
|
||||
brief_fields = ['address', 'description', 'display', 'family', 'id', 'prefix', 'url']
|
||||
brief_fields = ['address', 'description', 'display', 'family', 'id', 'url']
|
||||
create_data = [
|
||||
{
|
||||
'address': '192.168.0.4/24',
|
||||
|
||||
@@ -901,10 +901,6 @@ class PrefixTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
params = {'description': ['foobar1', 'foobar2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
# TODO: Test for parent prefix
|
||||
# TODO: Test for children?
|
||||
# TODO: Test for aggregate
|
||||
|
||||
|
||||
class IPRangeTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
queryset = IPRange.objects.all()
|
||||
@@ -1083,7 +1079,6 @@ class IPRangeTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_parent(self):
|
||||
# TODO: Alter for prefix
|
||||
params = {'parent': ['10.0.1.0/24', '10.0.2.0/24']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
params = {'parent': ['10.0.1.0/25']} # Range 10.0.1.100-199 is not fully contained by 10.0.1.0/25
|
||||
@@ -1323,7 +1318,6 @@ class IPAddressTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_parent(self):
|
||||
# TODO: Alter for prefix
|
||||
params = {'parent': ['10.0.0.0/30', '2001:db8::/126']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 8)
|
||||
|
||||
|
||||
@@ -39,26 +39,6 @@ class TestAggregate(TestCase):
|
||||
|
||||
|
||||
class TestIPRange(TestCase):
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
cls.vrf = VRF.objects.create(name='VRF A', rd='1:1')
|
||||
|
||||
cls.prefixes = (
|
||||
|
||||
# IPv4
|
||||
Prefix(prefix='192.0.0.0/16'),
|
||||
Prefix(prefix='192.0.2.0/24'),
|
||||
Prefix(prefix='192.0.0.0/16', vrf=cls.vrf),
|
||||
|
||||
# IPv6
|
||||
Prefix(prefix='2001:db8::/32'),
|
||||
Prefix(prefix='2001:db8::/64'),
|
||||
|
||||
)
|
||||
|
||||
for prefix in cls.prefixes:
|
||||
prefix.clean()
|
||||
prefix.save()
|
||||
|
||||
def test_overlapping_range(self):
|
||||
iprange_192_168 = IPRange.objects.create(
|
||||
@@ -107,69 +87,6 @@ class TestIPRange(TestCase):
|
||||
)
|
||||
iprange_4_198_201.clean()
|
||||
|
||||
def test_parent_prefix(self):
|
||||
ranges = (
|
||||
IPRange(
|
||||
start_address=IPNetwork('192.0.0.1/24'),
|
||||
end_address=IPNetwork('192.0.0.254/24'),
|
||||
prefix=self.prefixes[0]
|
||||
),
|
||||
IPRange(
|
||||
start_address=IPNetwork('192.0.2.1/24'),
|
||||
end_address=IPNetwork('192.0.2.254/24'),
|
||||
prefix=self.prefixes[1]
|
||||
),
|
||||
IPRange(
|
||||
start_address=IPNetwork('192.0.2.1/24'),
|
||||
end_address=IPNetwork('192.0.2.254/24'),
|
||||
vrf=self.vrf,
|
||||
prefix=self.prefixes[2]
|
||||
),
|
||||
IPRange(
|
||||
start_address=IPNetwork('2001:db8::/64'),
|
||||
end_address=IPNetwork('2001:db8::ffff/64'),
|
||||
prefix=self.prefixes[4]
|
||||
),
|
||||
IPRange(
|
||||
start_address=IPNetwork('2001:db8:2::/64'),
|
||||
end_address=IPNetwork('2001:db8:2::ffff/64'),
|
||||
prefix=self.prefixes[3]
|
||||
),
|
||||
)
|
||||
|
||||
for range in ranges:
|
||||
range.clean()
|
||||
range.save()
|
||||
|
||||
self.assertEqual(ranges[0].prefix, self.prefixes[0])
|
||||
self.assertEqual(ranges[1].prefix, self.prefixes[1])
|
||||
self.assertEqual(ranges[2].prefix, self.prefixes[2])
|
||||
self.assertEqual(ranges[3].prefix, self.prefixes[4])
|
||||
|
||||
def test_parent_prefix_change(self):
|
||||
|
||||
range = IPRange(
|
||||
start_address=IPNetwork('192.0.1.1/24'),
|
||||
end_address=IPNetwork('192.0.1.254/24'),
|
||||
prefix=self.prefixes[0]
|
||||
)
|
||||
range.clean()
|
||||
range.save()
|
||||
|
||||
prefix = Prefix(prefix='192.0.0.0/17')
|
||||
prefix.clean()
|
||||
prefix.save()
|
||||
|
||||
range.refresh_from_db()
|
||||
|
||||
self.assertEqual(range.prefix, prefix)
|
||||
|
||||
# TODO: Prefix Altered
|
||||
# TODO: Prefix Deleted
|
||||
|
||||
# TODO: Prefix falls outside range
|
||||
# TODO: Prefix VRF does not match range VRF
|
||||
|
||||
|
||||
class TestPrefix(TestCase):
|
||||
|
||||
@@ -252,16 +169,23 @@ class TestPrefix(TestCase):
|
||||
prefix=IPNetwork('10.0.0.0/16'), status=PrefixStatusChoices.STATUS_CONTAINER
|
||||
)
|
||||
ips = IPAddress.objects.bulk_create((
|
||||
IPAddress(prefix=parent_prefix, address=IPNetwork('10.0.0.1/24'), vrf=None),
|
||||
IPAddress(prefix=parent_prefix, address=IPNetwork('10.0.1.1/24'), vrf=vrfs[0]),
|
||||
IPAddress(prefix=parent_prefix, address=IPNetwork('10.0.2.1/24'), vrf=vrfs[1]),
|
||||
IPAddress(prefix=parent_prefix, address=IPNetwork('10.0.3.1/24'), vrf=vrfs[2]),
|
||||
IPAddress(address=IPNetwork('10.0.0.1/24'), vrf=None),
|
||||
IPAddress(address=IPNetwork('10.0.1.1/24'), vrf=vrfs[0]),
|
||||
IPAddress(address=IPNetwork('10.0.2.1/24'), vrf=vrfs[1]),
|
||||
IPAddress(address=IPNetwork('10.0.3.1/24'), vrf=vrfs[2]),
|
||||
))
|
||||
child_ip_pks = {p.pk for p in parent_prefix.ip_addresses.all()}
|
||||
child_ip_pks = {p.pk for p in parent_prefix.get_child_ips()}
|
||||
|
||||
# Global container should return all children
|
||||
self.assertSetEqual(child_ip_pks, {ips[0].pk, ips[1].pk, ips[2].pk, ips[3].pk})
|
||||
|
||||
parent_prefix.vrf = vrfs[0]
|
||||
parent_prefix.save()
|
||||
child_ip_pks = {p.pk for p in parent_prefix.get_child_ips()}
|
||||
|
||||
# VRF container is limited to its own VRF
|
||||
self.assertSetEqual(child_ip_pks, {ips[1].pk})
|
||||
|
||||
def test_get_available_prefixes(self):
|
||||
|
||||
prefixes = Prefix.objects.bulk_create((
|
||||
@@ -408,62 +332,6 @@ class TestPrefix(TestCase):
|
||||
duplicate_prefix = Prefix(vrf=vrf, prefix=IPNetwork('192.0.2.0/24'))
|
||||
self.assertRaises(ValidationError, duplicate_prefix.clean)
|
||||
|
||||
def test_parent_container_prefix_change(self):
|
||||
vrfs = VRF.objects.bulk_create((
|
||||
VRF(name='VRF 1'),
|
||||
VRF(name='VRF 2'),
|
||||
VRF(name='VRF 3'),
|
||||
))
|
||||
parent_prefix = Prefix.objects.create(
|
||||
prefix=IPNetwork('10.0.0.0/16'), status=PrefixStatusChoices.STATUS_CONTAINER
|
||||
)
|
||||
ips = IPAddress.objects.bulk_create((
|
||||
IPAddress(prefix=parent_prefix, address=IPNetwork('10.0.0.1/24'), vrf=None),
|
||||
IPAddress(prefix=parent_prefix, address=IPNetwork('10.0.1.1/24'), vrf=vrfs[0]),
|
||||
IPAddress(prefix=parent_prefix, address=IPNetwork('10.0.2.1/24'), vrf=vrfs[1]),
|
||||
IPAddress(prefix=parent_prefix, address=IPNetwork('10.0.3.1/24'), vrf=vrfs[2]),
|
||||
))
|
||||
child_ip_pks = {p.pk for p in parent_prefix.ip_addresses.all()}
|
||||
|
||||
# Global container should return all children
|
||||
self.assertSetEqual(child_ip_pks, {ips[0].pk, ips[1].pk, ips[2].pk, ips[3].pk})
|
||||
|
||||
parent_prefix.vrf = vrfs[0]
|
||||
parent_prefix.save()
|
||||
|
||||
parent_prefix.refresh_from_db()
|
||||
child_ip_pks = {p.pk for p in parent_prefix.ip_addresses.all()}
|
||||
|
||||
# VRF container is limited to its own VRF
|
||||
self.assertSetEqual(child_ip_pks, {ips[1].pk})
|
||||
|
||||
def test_parent_container_vrf_change(self):
|
||||
vrfs = VRF.objects.bulk_create((
|
||||
VRF(name='VRF 1'),
|
||||
VRF(name='VRF 2'),
|
||||
VRF(name='VRF 3'),
|
||||
))
|
||||
parent_prefix = Prefix.objects.create(
|
||||
prefix=IPNetwork('10.0.0.0/16'), status=PrefixStatusChoices.STATUS_CONTAINER
|
||||
)
|
||||
ips = IPAddress.objects.bulk_create((
|
||||
IPAddress(prefix=parent_prefix, address=IPNetwork('10.0.0.1/24'), vrf=None),
|
||||
IPAddress(prefix=parent_prefix, address=IPNetwork('10.0.1.1/24'), vrf=vrfs[0]),
|
||||
IPAddress(prefix=parent_prefix, address=IPNetwork('10.0.2.1/24'), vrf=vrfs[1]),
|
||||
IPAddress(prefix=parent_prefix, address=IPNetwork('10.0.3.1/24'), vrf=vrfs[2]),
|
||||
))
|
||||
child_ip_pks = {p.pk for p in parent_prefix.ip_addresses.all()}
|
||||
|
||||
# Global container should return all children
|
||||
self.assertSetEqual(child_ip_pks, {ips[0].pk, ips[1].pk, ips[2].pk, ips[3].pk})
|
||||
|
||||
parent_prefix.prefix = '10.0.0.0/23'
|
||||
parent_prefix.save()
|
||||
|
||||
parent_prefix.refresh_from_db()
|
||||
child_ip_pks = {p.pk for p in parent_prefix.ip_addresses.all()}
|
||||
self.assertSetEqual(child_ip_pks, {ips[0].pk, ips[1].pk})
|
||||
|
||||
|
||||
class TestPrefixHierarchy(TestCase):
|
||||
"""
|
||||
@@ -476,21 +344,17 @@ class TestPrefixHierarchy(TestCase):
|
||||
prefixes = (
|
||||
|
||||
# IPv4
|
||||
Prefix(prefix='10.0.0.0/8'),
|
||||
Prefix(prefix='10.0.0.0/16'),
|
||||
Prefix(prefix='10.0.0.0/24'),
|
||||
Prefix(prefix='192.168.0.0/16'),
|
||||
Prefix(prefix='10.0.0.0/8', _depth=0, _children=2),
|
||||
Prefix(prefix='10.0.0.0/16', _depth=1, _children=1),
|
||||
Prefix(prefix='10.0.0.0/24', _depth=2, _children=0),
|
||||
|
||||
# IPv6
|
||||
Prefix(prefix='2001:db8::/32'),
|
||||
Prefix(prefix='2001:db8::/40'),
|
||||
Prefix(prefix='2001:db8::/48'),
|
||||
Prefix(prefix='2001:db8::/32', _depth=0, _children=2),
|
||||
Prefix(prefix='2001:db8::/40', _depth=1, _children=1),
|
||||
Prefix(prefix='2001:db8::/48', _depth=2, _children=0),
|
||||
|
||||
)
|
||||
|
||||
for prefix in prefixes:
|
||||
prefix.clean()
|
||||
prefix.save()
|
||||
Prefix.objects.bulk_create(prefixes)
|
||||
|
||||
def test_create_prefix4(self):
|
||||
# Create 10.0.0.0/12
|
||||
@@ -498,19 +362,15 @@ class TestPrefixHierarchy(TestCase):
|
||||
|
||||
prefixes = Prefix.objects.filter(prefix__family=4)
|
||||
self.assertEqual(prefixes[0].prefix, IPNetwork('10.0.0.0/8'))
|
||||
self.assertEqual(prefixes[0].parent, None)
|
||||
self.assertEqual(prefixes[0]._depth, 0)
|
||||
self.assertEqual(prefixes[0]._children, 3)
|
||||
self.assertEqual(prefixes[1].prefix, IPNetwork('10.0.0.0/12'))
|
||||
self.assertEqual(prefixes[1].parent.prefix, IPNetwork('10.0.0.0/8'))
|
||||
self.assertEqual(prefixes[1]._depth, 1)
|
||||
self.assertEqual(prefixes[1]._children, 2)
|
||||
self.assertEqual(prefixes[2].prefix, IPNetwork('10.0.0.0/16'))
|
||||
self.assertEqual(prefixes[2].parent.prefix, IPNetwork('10.0.0.0/12'))
|
||||
self.assertEqual(prefixes[2]._depth, 2)
|
||||
self.assertEqual(prefixes[2]._children, 1)
|
||||
self.assertEqual(prefixes[3].prefix, IPNetwork('10.0.0.0/24'))
|
||||
self.assertEqual(prefixes[3].parent.prefix, IPNetwork('10.0.0.0/16'))
|
||||
self.assertEqual(prefixes[3]._depth, 3)
|
||||
self.assertEqual(prefixes[3]._children, 0)
|
||||
|
||||
@@ -520,19 +380,15 @@ class TestPrefixHierarchy(TestCase):
|
||||
|
||||
prefixes = Prefix.objects.filter(prefix__family=6)
|
||||
self.assertEqual(prefixes[0].prefix, IPNetwork('2001:db8::/32'))
|
||||
self.assertEqual(prefixes[0].parent, None)
|
||||
self.assertEqual(prefixes[0]._depth, 0)
|
||||
self.assertEqual(prefixes[0]._children, 3)
|
||||
self.assertEqual(prefixes[1].prefix, IPNetwork('2001:db8::/36'))
|
||||
self.assertEqual(prefixes[1].parent.prefix, IPNetwork('2001:db8::/32'))
|
||||
self.assertEqual(prefixes[1]._depth, 1)
|
||||
self.assertEqual(prefixes[1]._children, 2)
|
||||
self.assertEqual(prefixes[2].prefix, IPNetwork('2001:db8::/40'))
|
||||
self.assertEqual(prefixes[2].parent.prefix, IPNetwork('2001:db8::/36'))
|
||||
self.assertEqual(prefixes[2]._depth, 2)
|
||||
self.assertEqual(prefixes[2]._children, 1)
|
||||
self.assertEqual(prefixes[3].prefix, IPNetwork('2001:db8::/48'))
|
||||
self.assertEqual(prefixes[3].parent.prefix, IPNetwork('2001:db8::/40'))
|
||||
self.assertEqual(prefixes[3]._depth, 3)
|
||||
self.assertEqual(prefixes[3]._children, 0)
|
||||
|
||||
@@ -544,15 +400,12 @@ class TestPrefixHierarchy(TestCase):
|
||||
|
||||
prefixes = Prefix.objects.filter(prefix__family=4)
|
||||
self.assertEqual(prefixes[0].prefix, IPNetwork('10.0.0.0/8'))
|
||||
self.assertEqual(prefixes[0].parent, None)
|
||||
self.assertEqual(prefixes[0]._depth, 0)
|
||||
self.assertEqual(prefixes[0]._children, 2)
|
||||
self.assertEqual(prefixes[1].prefix, IPNetwork('10.0.0.0/12'))
|
||||
self.assertEqual(prefixes[1].parent.prefix, IPNetwork('10.0.0.0/8'))
|
||||
self.assertEqual(prefixes[1]._depth, 1)
|
||||
self.assertEqual(prefixes[1]._children, 1)
|
||||
self.assertEqual(prefixes[2].prefix, IPNetwork('10.0.0.0/16'))
|
||||
self.assertEqual(prefixes[2].parent.prefix, IPNetwork('10.0.0.0/12'))
|
||||
self.assertEqual(prefixes[2]._depth, 2)
|
||||
self.assertEqual(prefixes[2]._children, 0)
|
||||
|
||||
@@ -564,15 +417,12 @@ class TestPrefixHierarchy(TestCase):
|
||||
|
||||
prefixes = Prefix.objects.filter(prefix__family=6)
|
||||
self.assertEqual(prefixes[0].prefix, IPNetwork('2001:db8::/32'))
|
||||
self.assertEqual(prefixes[0].parent, None)
|
||||
self.assertEqual(prefixes[0]._depth, 0)
|
||||
self.assertEqual(prefixes[0]._children, 2)
|
||||
self.assertEqual(prefixes[1].prefix, IPNetwork('2001:db8::/36'))
|
||||
self.assertEqual(prefixes[1].parent.prefix, IPNetwork('2001:db8::/32'))
|
||||
self.assertEqual(prefixes[1]._depth, 1)
|
||||
self.assertEqual(prefixes[1]._children, 1)
|
||||
self.assertEqual(prefixes[2].prefix, IPNetwork('2001:db8::/40'))
|
||||
self.assertEqual(prefixes[2].parent.prefix, IPNetwork('2001:db8::/36'))
|
||||
self.assertEqual(prefixes[2]._depth, 2)
|
||||
self.assertEqual(prefixes[2]._children, 0)
|
||||
|
||||
@@ -587,17 +437,14 @@ class TestPrefixHierarchy(TestCase):
|
||||
|
||||
prefixes = Prefix.objects.filter(vrf__isnull=True, prefix__family=4)
|
||||
self.assertEqual(prefixes[0].prefix, IPNetwork('10.0.0.0/8'))
|
||||
self.assertEqual(prefixes[0].parent, None)
|
||||
self.assertEqual(prefixes[0]._depth, 0)
|
||||
self.assertEqual(prefixes[0]._children, 1)
|
||||
self.assertEqual(prefixes[1].prefix, IPNetwork('10.0.0.0/24'))
|
||||
self.assertEqual(prefixes[1].parent.prefix, IPNetwork('10.0.0.0/8'))
|
||||
self.assertEqual(prefixes[1]._depth, 1)
|
||||
self.assertEqual(prefixes[1]._children, 0)
|
||||
|
||||
prefixes = Prefix.objects.filter(vrf=vrf)
|
||||
self.assertEqual(prefixes[0].prefix, IPNetwork('10.0.0.0/16'))
|
||||
self.assertEqual(prefixes[0].parent, None)
|
||||
self.assertEqual(prefixes[0]._depth, 0)
|
||||
self.assertEqual(prefixes[0]._children, 0)
|
||||
|
||||
@@ -612,17 +459,14 @@ class TestPrefixHierarchy(TestCase):
|
||||
|
||||
prefixes = Prefix.objects.filter(vrf__isnull=True, prefix__family=6)
|
||||
self.assertEqual(prefixes[0].prefix, IPNetwork('2001:db8::/32'))
|
||||
self.assertEqual(prefixes[0].parent, None)
|
||||
self.assertEqual(prefixes[0]._depth, 0)
|
||||
self.assertEqual(prefixes[0]._children, 1)
|
||||
self.assertEqual(prefixes[1].prefix, IPNetwork('2001:db8::/48'))
|
||||
self.assertEqual(prefixes[1].parent.prefix, IPNetwork('2001:db8::/32'))
|
||||
self.assertEqual(prefixes[1]._depth, 1)
|
||||
self.assertEqual(prefixes[1]._children, 0)
|
||||
|
||||
prefixes = Prefix.objects.filter(vrf=vrf)
|
||||
self.assertEqual(prefixes[0].prefix, IPNetwork('2001:db8::/40'))
|
||||
self.assertEqual(prefixes[0].parent, None)
|
||||
self.assertEqual(prefixes[0]._depth, 0)
|
||||
self.assertEqual(prefixes[0]._children, 0)
|
||||
|
||||
@@ -632,11 +476,9 @@ class TestPrefixHierarchy(TestCase):
|
||||
|
||||
prefixes = Prefix.objects.filter(prefix__family=4)
|
||||
self.assertEqual(prefixes[0].prefix, IPNetwork('10.0.0.0/8'))
|
||||
self.assertEqual(prefixes[0].parent, None)
|
||||
self.assertEqual(prefixes[0]._depth, 0)
|
||||
self.assertEqual(prefixes[0]._children, 1)
|
||||
self.assertEqual(prefixes[1].prefix, IPNetwork('10.0.0.0/24'))
|
||||
self.assertEqual(prefixes[1].parent.prefix, IPNetwork('10.0.0.0/8'))
|
||||
self.assertEqual(prefixes[1]._depth, 1)
|
||||
self.assertEqual(prefixes[1]._children, 0)
|
||||
|
||||
@@ -646,11 +488,9 @@ class TestPrefixHierarchy(TestCase):
|
||||
|
||||
prefixes = Prefix.objects.filter(prefix__family=6)
|
||||
self.assertEqual(prefixes[0].prefix, IPNetwork('2001:db8::/32'))
|
||||
self.assertEqual(prefixes[0].parent, None)
|
||||
self.assertEqual(prefixes[0]._depth, 0)
|
||||
self.assertEqual(prefixes[0]._children, 1)
|
||||
self.assertEqual(prefixes[1].prefix, IPNetwork('2001:db8::/48'))
|
||||
self.assertEqual(prefixes[1].parent.prefix, IPNetwork('2001:db8::/32'))
|
||||
self.assertEqual(prefixes[1]._depth, 1)
|
||||
self.assertEqual(prefixes[1]._children, 0)
|
||||
|
||||
@@ -660,20 +500,15 @@ class TestPrefixHierarchy(TestCase):
|
||||
|
||||
prefixes = Prefix.objects.filter(prefix__family=4)
|
||||
self.assertEqual(prefixes[0].prefix, IPNetwork('10.0.0.0/8'))
|
||||
self.assertEqual(prefixes[0].parent, None)
|
||||
self.assertEqual(prefixes[0]._depth, 0)
|
||||
self.assertEqual(prefixes[0]._children, 3)
|
||||
self.assertEqual(prefixes[1].prefix, IPNetwork('10.0.0.0/16'))
|
||||
self.assertEqual(prefixes[1].parent.prefix, IPNetwork('10.0.0.0/8'))
|
||||
self.assertEqual(prefixes[1]._depth, 1)
|
||||
self.assertEqual(prefixes[1]._children, 1)
|
||||
self.assertEqual(prefixes[2].prefix, IPNetwork('10.0.0.0/16'))
|
||||
self.assertEqual(prefixes[2].parent.prefix, IPNetwork('10.0.0.0/8'))
|
||||
self.assertEqual(prefixes[2]._depth, 1)
|
||||
self.assertEqual(prefixes[2]._children, 1)
|
||||
self.assertEqual(prefixes[3].prefix, IPNetwork('10.0.0.0/24'))
|
||||
# TODO: How to we resolve the parent for duplicate prefixes
|
||||
self.assertEqual(prefixes[3].parent.prefix, IPNetwork('10.0.0.0/16'))
|
||||
self.assertEqual(prefixes[3]._depth, 2)
|
||||
self.assertEqual(prefixes[3]._children, 0)
|
||||
|
||||
@@ -683,158 +518,20 @@ class TestPrefixHierarchy(TestCase):
|
||||
|
||||
prefixes = Prefix.objects.filter(prefix__family=6)
|
||||
self.assertEqual(prefixes[0].prefix, IPNetwork('2001:db8::/32'))
|
||||
self.assertEqual(prefixes[0].parent, None)
|
||||
self.assertEqual(prefixes[0]._depth, 0)
|
||||
self.assertEqual(prefixes[0]._children, 3)
|
||||
self.assertEqual(prefixes[1].prefix, IPNetwork('2001:db8::/40'))
|
||||
self.assertEqual(prefixes[1].parent.prefix, IPNetwork('2001:db8::/32'))
|
||||
self.assertEqual(prefixes[1]._depth, 1)
|
||||
self.assertEqual(prefixes[1]._children, 1)
|
||||
self.assertEqual(prefixes[2].prefix, IPNetwork('2001:db8::/40'))
|
||||
self.assertEqual(prefixes[2].parent.prefix, IPNetwork('2001:db8::/32'))
|
||||
self.assertEqual(prefixes[2]._depth, 1)
|
||||
self.assertEqual(prefixes[2]._children, 1)
|
||||
self.assertEqual(prefixes[3].prefix, IPNetwork('2001:db8::/48'))
|
||||
self.assertEqual(prefixes[3].parent.prefix, IPNetwork('2001:db8::/40'))
|
||||
self.assertEqual(prefixes[3]._depth, 2)
|
||||
self.assertEqual(prefixes[3]._children, 0)
|
||||
|
||||
|
||||
class TestTriggers(TestCase):
|
||||
"""
|
||||
Test the automatic updating of depth and child count in response to changes made within
|
||||
the prefix hierarchy.
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
|
||||
vrfs = (
|
||||
VRF(name='VRF A'),
|
||||
VRF(name='VRF B'),
|
||||
)
|
||||
|
||||
for vrf in vrfs:
|
||||
vrf.clean()
|
||||
vrf.save()
|
||||
|
||||
cls.prefixes = (
|
||||
# IPv4
|
||||
Prefix(prefix='10.0.0.0/8'),
|
||||
Prefix(prefix='10.0.0.0/16'),
|
||||
Prefix(prefix='10.0.0.0/22'),
|
||||
Prefix(prefix='10.0.0.0/23'),
|
||||
Prefix(prefix='10.0.2.0/23'),
|
||||
Prefix(prefix='10.0.0.0/24'),
|
||||
Prefix(prefix='10.0.1.0/24'),
|
||||
Prefix(prefix='10.0.2.0/24'),
|
||||
Prefix(prefix='10.0.3.0/24'),
|
||||
Prefix(prefix='10.1.0.0/16', status='container'),
|
||||
Prefix(prefix='10.1.0.0/22', vrf=vrfs[0]),
|
||||
Prefix(prefix='10.1.0.0/23', vrf=vrfs[0]),
|
||||
Prefix(prefix='10.1.2.0/23', vrf=vrfs[0]),
|
||||
Prefix(prefix='10.1.0.0/24', vrf=vrfs[0]),
|
||||
Prefix(prefix='10.1.1.0/24', vrf=vrfs[0]),
|
||||
Prefix(prefix='10.1.2.0/24', vrf=vrfs[0]),
|
||||
Prefix(prefix='10.1.3.0/24', vrf=vrfs[0]),
|
||||
)
|
||||
|
||||
for prefix in cls.prefixes:
|
||||
prefix.clean()
|
||||
prefix.save()
|
||||
|
||||
def test_current_hierarchy(self):
|
||||
self.assertIsNone(Prefix.objects.get(prefix='10.0.0.0/8').parent)
|
||||
self.assertEqual(Prefix.objects.get(prefix='10.0.0.0/16').parent, Prefix.objects.get(prefix='10.0.0.0/8'))
|
||||
self.assertEqual(Prefix.objects.get(prefix='10.0.0.0/22').parent, Prefix.objects.get(prefix='10.0.0.0/16'))
|
||||
self.assertEqual(Prefix.objects.get(prefix='10.0.0.0/23').parent, Prefix.objects.get(prefix='10.0.0.0/22'))
|
||||
self.assertEqual(Prefix.objects.get(prefix='10.0.2.0/23').parent, Prefix.objects.get(prefix='10.0.0.0/22'))
|
||||
self.assertEqual(Prefix.objects.get(prefix='10.0.0.0/24').parent, Prefix.objects.get(prefix='10.0.0.0/23'))
|
||||
self.assertEqual(Prefix.objects.get(prefix='10.0.1.0/24').parent, Prefix.objects.get(prefix='10.0.0.0/23'))
|
||||
self.assertEqual(Prefix.objects.get(prefix='10.0.2.0/24').parent, Prefix.objects.get(prefix='10.0.2.0/23'))
|
||||
self.assertEqual(Prefix.objects.get(prefix='10.0.3.0/24').parent, Prefix.objects.get(prefix='10.0.2.0/23'))
|
||||
|
||||
def test_basic_insert(self):
|
||||
pfx = Prefix.objects.create(prefix='10.0.0.0/21')
|
||||
self.assertIsNotNone(Prefix.objects.get(prefix='10.0.0.0/22').parent)
|
||||
self.assertEqual(Prefix.objects.get(prefix='10.0.0.0/22').parent, pfx)
|
||||
|
||||
def test_vrf_insert(self):
|
||||
vrf = VRF.objects.get(name='VRF A')
|
||||
pfx = Prefix.objects.create(prefix='10.1.0.0/21', vrf=vrf)
|
||||
parent = Prefix.objects.get(prefix='10.1.0.0/16')
|
||||
|
||||
self.assertIsNotNone(Prefix.objects.get(prefix='10.1.0.0/21').parent)
|
||||
self.assertEqual(Prefix.objects.get(prefix='10.1.0.0/21').parent, parent)
|
||||
self.assertIsNotNone(Prefix.objects.get(prefix='10.1.0.0/22').parent)
|
||||
self.assertEqual(Prefix.objects.get(prefix='10.1.0.0/22').parent, pfx)
|
||||
|
||||
def test_basic_delete(self):
|
||||
Prefix.objects.get(prefix='10.0.0.0/23').delete()
|
||||
parent = Prefix.objects.get(prefix='10.0.0.0/22')
|
||||
self.assertEqual(Prefix.objects.get(prefix='10.0.0.0/24').parent, parent)
|
||||
self.assertEqual(Prefix.objects.get(prefix='10.0.1.0/24').parent, parent)
|
||||
self.assertEqual(Prefix.objects.get(prefix='10.0.2.0/24').parent, Prefix.objects.get(prefix='10.0.2.0/23'))
|
||||
|
||||
def test_vrf_delete(self):
|
||||
Prefix.objects.get(prefix='10.1.0.0/23').delete()
|
||||
parent = Prefix.objects.get(prefix='10.1.0.0/22')
|
||||
self.assertEqual(Prefix.objects.get(prefix='10.1.0.0/24').parent, parent)
|
||||
self.assertEqual(Prefix.objects.get(prefix='10.1.1.0/24').parent, parent)
|
||||
self.assertEqual(Prefix.objects.get(prefix='10.1.2.0/24').parent, Prefix.objects.get(prefix='10.1.2.0/23'))
|
||||
|
||||
def test_basic_update(self):
|
||||
pfx = Prefix.objects.get(prefix='10.0.0.0/23')
|
||||
parent = Prefix.objects.get(prefix='10.0.0.0/22')
|
||||
pfx.prefix = '10.3.0.0/23'
|
||||
pfx.parent = Prefix.objects.get(prefix='10.0.0.0/8')
|
||||
pfx.clean()
|
||||
pfx.save()
|
||||
|
||||
self.assertEqual(Prefix.objects.get(prefix='10.0.0.0/24').parent, parent)
|
||||
self.assertEqual(Prefix.objects.get(prefix='10.0.1.0/24').parent, parent)
|
||||
self.assertEqual(Prefix.objects.get(prefix='10.0.2.0/24').parent, Prefix.objects.get(prefix='10.0.2.0/23'))
|
||||
|
||||
def test_vrf_update(self):
|
||||
pfx = Prefix.objects.get(prefix='10.1.0.0/23')
|
||||
parent = Prefix.objects.get(prefix='10.1.0.0/22')
|
||||
pfx.prefix = '10.3.0.0/23'
|
||||
pfx.parent = None
|
||||
pfx.clean()
|
||||
pfx.save()
|
||||
|
||||
self.assertEqual(Prefix.objects.get(prefix='10.1.0.0/24').parent, parent)
|
||||
self.assertEqual(Prefix.objects.get(prefix='10.1.1.0/24').parent, parent)
|
||||
self.assertEqual(Prefix.objects.get(prefix='10.1.2.0/24').parent, Prefix.objects.get(prefix='10.1.2.0/23'))
|
||||
|
||||
# TODO: Test VRF Changes
|
||||
|
||||
|
||||
class TestIPAddress(TestCase):
|
||||
"""
|
||||
Test the automatic updating of depth and child count in response to changes made within
|
||||
the prefix hierarchy.
|
||||
"""
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
cls.vrf = VRF.objects.create(name='VRF A', rd='1:1')
|
||||
|
||||
cls.prefixes = (
|
||||
|
||||
# IPv4
|
||||
Prefix(prefix='192.0.0.0/16'),
|
||||
Prefix(prefix='192.0.2.0/24'),
|
||||
Prefix(prefix='192.0.0.0/16', vrf=cls.vrf),
|
||||
|
||||
# IPv6
|
||||
Prefix(prefix='2001:db8::/32'),
|
||||
Prefix(prefix='2001:db8::/64'),
|
||||
|
||||
)
|
||||
|
||||
for prefix in cls.prefixes:
|
||||
prefix.clean()
|
||||
prefix.save()
|
||||
|
||||
def test_get_duplicates(self):
|
||||
ips = IPAddress.objects.bulk_create((
|
||||
@@ -846,44 +543,6 @@ class TestIPAddress(TestCase):
|
||||
|
||||
self.assertSetEqual(set(duplicate_ip_pks), {ips[1].pk, ips[2].pk})
|
||||
|
||||
def test_parent_prefix(self):
|
||||
ips = (
|
||||
IPAddress(address=IPNetwork('192.0.0.1/24'), prefix=self.prefixes[0]),
|
||||
IPAddress(address=IPNetwork('192.0.2.1/24'), prefix=self.prefixes[1]),
|
||||
IPAddress(address=IPNetwork('192.0.2.1/24'), vrf=self.vrf, prefix=self.prefixes[2]),
|
||||
IPAddress(address=IPNetwork('2001:db8::/64'), prefix=self.prefixes[4]),
|
||||
IPAddress(address=IPNetwork('2001:db8:2::/64')),
|
||||
)
|
||||
|
||||
for ip in ips:
|
||||
ip.clean()
|
||||
ip.save()
|
||||
|
||||
self.assertEqual(ips[0].prefix, self.prefixes[0])
|
||||
self.assertEqual(ips[1].prefix, self.prefixes[1])
|
||||
self.assertEqual(ips[2].prefix, self.prefixes[2])
|
||||
self.assertEqual(ips[3].prefix, self.prefixes[4])
|
||||
self.assertEqual(ips[4].prefix, self.prefixes[3])
|
||||
|
||||
def test_parent_prefix_change(self):
|
||||
ip = IPAddress(address=IPNetwork('192.0.1.1/24'), prefix=self.prefixes[0])
|
||||
ip.clean()
|
||||
ip.save()
|
||||
|
||||
prefix = Prefix(prefix='192.0.1.0/17')
|
||||
prefix.clean()
|
||||
prefix.save()
|
||||
|
||||
ip.refresh_from_db()
|
||||
|
||||
self.assertEqual(ip.prefix, prefix)
|
||||
|
||||
# TODO: Prefix Altered
|
||||
# TODO: Prefix Deleted
|
||||
|
||||
# TODO: Prefix does not contain IP Address
|
||||
# TODO: Prefix VRF does not match IP Address VRF
|
||||
|
||||
#
|
||||
# Uniqueness enforcement tests
|
||||
#
|
||||
@@ -900,20 +559,13 @@ class TestIPAddress(TestCase):
|
||||
self.assertRaises(ValidationError, duplicate_ip.clean)
|
||||
|
||||
def test_duplicate_vrf(self):
|
||||
vrf = VRF.objects.get(rd='1:1')
|
||||
vrf.enforce_unique = False
|
||||
vrf.clean()
|
||||
vrf.save()
|
||||
|
||||
vrf = VRF.objects.create(name='Test', rd='1:1', enforce_unique=False)
|
||||
IPAddress.objects.create(vrf=vrf, address=IPNetwork('192.0.2.1/24'))
|
||||
duplicate_ip = IPAddress(vrf=vrf, address=IPNetwork('192.0.2.1/24'))
|
||||
self.assertIsNone(duplicate_ip.clean())
|
||||
|
||||
def test_duplicate_vrf_unique(self):
|
||||
vrf = VRF.objects.get(rd='1:1')
|
||||
vrf.enforce_unique = True
|
||||
vrf.clean()
|
||||
vrf.save()
|
||||
vrf = VRF.objects.create(name='Test', rd='1:1', enforce_unique=True)
|
||||
IPAddress.objects.create(vrf=vrf, address=IPNetwork('192.0.2.1/24'))
|
||||
duplicate_ip = IPAddress(vrf=vrf, address=IPNetwork('192.0.2.1/24'))
|
||||
self.assertRaises(ValidationError, duplicate_ip.clean)
|
||||
|
||||
@@ -421,7 +421,6 @@ class PrefixTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
|
||||
tags = create_tags('Alpha', 'Bravo', 'Charlie')
|
||||
|
||||
# TODO: Alter for prefix
|
||||
cls.form_data = {
|
||||
'prefix': IPNetwork('192.0.2.0/24'),
|
||||
'scope_type': ContentType.objects.get_for_model(Site).pk,
|
||||
@@ -437,7 +436,6 @@ class PrefixTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
}
|
||||
|
||||
site = sites[0].pk
|
||||
# TODO: Alter for prefix
|
||||
cls.csv_data = (
|
||||
"vrf,prefix,status,scope_type,scope_id",
|
||||
f"VRF 1,10.4.0.0/16,active,dcim.site,{site}",
|
||||
@@ -445,7 +443,6 @@ class PrefixTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
f"VRF 1,10.6.0.0/16,active,dcim.site,{site}",
|
||||
)
|
||||
|
||||
# TODO: Alter for prefix
|
||||
cls.csv_update_data = (
|
||||
"id,description,status",
|
||||
f"{prefixes[0].pk},New description 7,{PrefixStatusChoices.STATUS_RESERVED}",
|
||||
@@ -453,7 +450,6 @@ class PrefixTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
f"{prefixes[2].pk},New description 9,{PrefixStatusChoices.STATUS_RESERVED}",
|
||||
)
|
||||
|
||||
# TODO: Alter for prefix
|
||||
cls.bulk_edit_data = {
|
||||
'vrf': vrfs[1].pk,
|
||||
'tenant': None,
|
||||
@@ -481,9 +477,9 @@ class PrefixTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
def test_prefix_ipranges(self):
|
||||
prefix = Prefix.objects.create(prefix=IPNetwork('192.168.0.0/16'))
|
||||
ip_ranges = (
|
||||
IPRange(prefix=prefix, start_address='192.168.0.1/24', end_address='192.168.0.100/24', size=99),
|
||||
IPRange(prefix=prefix, start_address='192.168.1.1/24', end_address='192.168.1.100/24', size=99),
|
||||
IPRange(prefix=prefix, start_address='192.168.2.1/24', end_address='192.168.2.100/24', size=99),
|
||||
IPRange(start_address='192.168.0.1/24', end_address='192.168.0.100/24', size=99),
|
||||
IPRange(start_address='192.168.1.1/24', end_address='192.168.1.100/24', size=99),
|
||||
IPRange(start_address='192.168.2.1/24', end_address='192.168.2.100/24', size=99),
|
||||
)
|
||||
IPRange.objects.bulk_create(ip_ranges)
|
||||
self.assertEqual(prefix.get_child_ranges().count(), 3)
|
||||
@@ -495,12 +491,12 @@ class PrefixTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
def test_prefix_ipaddresses(self):
|
||||
prefix = Prefix.objects.create(prefix=IPNetwork('192.168.0.0/16'))
|
||||
ip_addresses = (
|
||||
IPAddress(prefix=prefix, address=IPNetwork('192.168.0.1/16')),
|
||||
IPAddress(prefix=prefix, address=IPNetwork('192.168.0.2/16')),
|
||||
IPAddress(prefix=prefix, address=IPNetwork('192.168.0.3/16')),
|
||||
IPAddress(address=IPNetwork('192.168.0.1/16')),
|
||||
IPAddress(address=IPNetwork('192.168.0.2/16')),
|
||||
IPAddress(address=IPNetwork('192.168.0.3/16')),
|
||||
)
|
||||
IPAddress.objects.bulk_create(ip_addresses)
|
||||
self.assertEqual(prefix.ip_addresses.all().count(), 3)
|
||||
self.assertEqual(prefix.get_child_ips().count(), 3)
|
||||
|
||||
url = reverse('ipam:prefix_ipaddresses', kwargs={'pk': prefix.pk})
|
||||
self.assertHttpStatus(self.client.get(url), 200)
|
||||
@@ -674,7 +670,6 @@ class IPRangeTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
|
||||
tags = create_tags('Alpha', 'Bravo', 'Charlie')
|
||||
|
||||
# TODO: Alter for prefix
|
||||
cls.form_data = {
|
||||
'start_address': IPNetwork('192.0.5.10/24'),
|
||||
'end_address': IPNetwork('192.0.5.100/24'),
|
||||
@@ -688,7 +683,6 @@ class IPRangeTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
'tags': [t.pk for t in tags],
|
||||
}
|
||||
|
||||
# TODO: Alter for prefix
|
||||
cls.csv_data = (
|
||||
"vrf,start_address,end_address,status",
|
||||
"VRF 1,10.1.0.1/16,10.1.9.254/16,active",
|
||||
@@ -696,7 +690,6 @@ class IPRangeTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
"VRF 1,10.3.0.1/16,10.3.9.254/16,active",
|
||||
)
|
||||
|
||||
# TODO: Alter for prefix
|
||||
cls.csv_update_data = (
|
||||
"id,description,status",
|
||||
f"{ip_ranges[0].pk},New description 7,{IPRangeStatusChoices.STATUS_RESERVED}",
|
||||
@@ -704,7 +697,6 @@ class IPRangeTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
f"{ip_ranges[2].pk},New description 9,{IPRangeStatusChoices.STATUS_RESERVED}",
|
||||
)
|
||||
|
||||
# TODO: Alter for prefix
|
||||
cls.bulk_edit_data = {
|
||||
'vrf': vrfs[1].pk,
|
||||
'tenant': None,
|
||||
@@ -771,7 +763,6 @@ class IPAddressTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
),
|
||||
)
|
||||
FHRPGroup.objects.bulk_create(fhrp_groups)
|
||||
# TODO: Alter for prefix
|
||||
cls.form_data = {
|
||||
'vrf': vrfs[1].pk,
|
||||
'address': IPNetwork('192.0.2.99/24'),
|
||||
@@ -784,7 +775,6 @@ class IPAddressTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
'tags': [t.pk for t in tags],
|
||||
}
|
||||
|
||||
# TODO: Alter for prefix
|
||||
cls.csv_data = (
|
||||
"vrf,address,status,fhrp_group",
|
||||
"VRF 1,192.0.2.4/24,active,FHRP Group 1",
|
||||
@@ -792,7 +782,6 @@ class IPAddressTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
"VRF 1,192.0.2.6/24,active,FHRP Group 3",
|
||||
)
|
||||
|
||||
# TODO: Alter for prefix
|
||||
cls.csv_update_data = (
|
||||
"id,description,status",
|
||||
f"{ipaddresses[0].pk},New description 7,{IPAddressStatusChoices.STATUS_RESERVED}",
|
||||
@@ -800,7 +789,6 @@ class IPAddressTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
f"{ipaddresses[2].pk},New description 9,{IPAddressStatusChoices.STATUS_RESERVED}",
|
||||
)
|
||||
|
||||
# TODO: Alter for prefix
|
||||
cls.bulk_edit_data = {
|
||||
'vrf': vrfs[1].pk,
|
||||
'tenant': None,
|
||||
|
||||
@@ -1,220 +0,0 @@
|
||||
ipam_prefix_delete_adjust_prefix_parent = """
|
||||
-- Update Child Prefix's with Prefix's PARENT This is a safe assumption based on the fact that the parent would be the
|
||||
-- next direct parent for anything else that could contain this prefix
|
||||
UPDATE ipam_prefix SET parent_id=OLD.parent_id WHERE parent_id=OLD.id;
|
||||
RETURN OLD;
|
||||
"""
|
||||
|
||||
|
||||
ipam_prefix_insert_adjust_prefix_parent = """
|
||||
-- Update the prefix with the new parent if the parent is the most appropriate prefix
|
||||
UPDATE ipam_prefix
|
||||
SET parent_id=NEW.id
|
||||
WHERE
|
||||
prefix << NEW.prefix
|
||||
AND
|
||||
(
|
||||
(vrf_id = NEW.vrf_id OR (vrf_id IS NULL AND NEW.vrf_id IS NULL))
|
||||
OR
|
||||
(
|
||||
NEW.vrf_id IS NULL
|
||||
AND
|
||||
NEW.status = 'container'
|
||||
AND
|
||||
NOT EXISTS(
|
||||
SELECT 1 FROM ipam_prefix p WHERE p.prefix >> ipam_prefix.prefix AND p.vrf_id = ipam_prefix.vrf_id
|
||||
)
|
||||
)
|
||||
)
|
||||
AND id != NEW.id
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM ipam_prefix p
|
||||
WHERE
|
||||
p.prefix >> ipam_prefix.prefix
|
||||
AND p.prefix << NEW.prefix
|
||||
AND (
|
||||
(p.vrf_id = ipam_prefix.vrf_id OR (p.vrf_id IS NULL AND ipam_prefix.vrf_id IS NULL))
|
||||
OR
|
||||
(p.vrf_id IS NULL AND p.status = 'container')
|
||||
)
|
||||
AND p.id != NEW.id
|
||||
)
|
||||
;
|
||||
RETURN NEW;
|
||||
"""
|
||||
|
||||
|
||||
ipam_prefix_update_adjust_prefix_parent = """
|
||||
-- When a prefix changes, reassign any child prefixes that no longer
|
||||
-- fall within the new prefix range to the parent prefix (or set null if no parent exists)
|
||||
UPDATE ipam_prefix
|
||||
SET parent_id = OLD.parent_id
|
||||
WHERE
|
||||
parent_id = NEW.id
|
||||
-- IP address no longer contained within the updated prefix
|
||||
AND NOT (prefix << NEW.prefix);
|
||||
|
||||
-- When a prefix changes, reassign any ip addresses that no longer
|
||||
-- fall within the new prefix range to the parent prefix (or set null if no parent exists)
|
||||
UPDATE ipam_ipaddress
|
||||
SET prefix_id = OLD.parent_id
|
||||
WHERE
|
||||
prefix_id = NEW.id
|
||||
-- IP address no longer contained within the updated prefix
|
||||
AND
|
||||
NOT (address << NEW.prefix)
|
||||
;
|
||||
|
||||
-- When a prefix changes, reassign any ip ranges that no longer
|
||||
-- fall within the new prefix range to the parent prefix (or set null if no parent exists)
|
||||
UPDATE ipam_iprange
|
||||
SET prefix_id = OLD.parent_id
|
||||
WHERE
|
||||
prefix_id = NEW.id
|
||||
-- IP address no longer contained within the updated prefix
|
||||
AND
|
||||
NOT (start_address << NEW.prefix)
|
||||
AND
|
||||
NOT (end_address << NEW.prefix)
|
||||
;
|
||||
|
||||
-- When a prefix changes, reassign any ip addresses that are in-scope but
|
||||
-- no longer within the same VRF
|
||||
UPDATE ipam_ipaddress
|
||||
SET prefix_id = OLD.parent_id
|
||||
WHERE
|
||||
prefix_id = NEW.id
|
||||
AND
|
||||
address << OLD.prefix
|
||||
AND
|
||||
(
|
||||
NOT address << NEW.prefix
|
||||
OR
|
||||
(
|
||||
vrf_id is NULL
|
||||
AND
|
||||
NEW.vrf_id IS NOT NULL
|
||||
)
|
||||
OR
|
||||
(
|
||||
OLD.vrf_id IS NULL
|
||||
AND
|
||||
NEW.vrf_id IS NOT NULL
|
||||
AND
|
||||
NEW.vrf_id != vrf_id
|
||||
)
|
||||
)
|
||||
;
|
||||
|
||||
-- When a prefix changes, reassign any ip ranges that are in-scope but
|
||||
-- no longer within the same VRF
|
||||
UPDATE ipam_iprange
|
||||
SET prefix_id = OLD.parent_id
|
||||
WHERE
|
||||
prefix_id = NEW.id
|
||||
AND
|
||||
start_address << OLD.prefix
|
||||
AND
|
||||
end_address << OLD.prefix
|
||||
AND
|
||||
(
|
||||
NOT start_address << NEW.prefix
|
||||
OR
|
||||
NOT end_address << NEW.prefix
|
||||
OR
|
||||
(
|
||||
vrf_id is NULL
|
||||
AND
|
||||
NEW.vrf_id IS NOT NULL
|
||||
)
|
||||
OR
|
||||
(
|
||||
OLD.vrf_id IS NULL
|
||||
AND
|
||||
NEW.vrf_id IS NOT NULL
|
||||
AND
|
||||
NEW.vrf_id != vrf_id
|
||||
)
|
||||
)
|
||||
;
|
||||
|
||||
-- Update the prefix with the new parent if the parent is the most appropriate prefix
|
||||
UPDATE ipam_prefix
|
||||
SET parent_id=NEW.id
|
||||
WHERE
|
||||
prefix << NEW.prefix
|
||||
AND
|
||||
(
|
||||
(vrf_id = NEW.vrf_id OR (vrf_id IS NULL AND NEW.vrf_id IS NULL))
|
||||
OR
|
||||
(
|
||||
NEW.vrf_id IS NULL
|
||||
AND
|
||||
NEW.status = 'container'
|
||||
AND
|
||||
NOT EXISTS(
|
||||
SELECT 1 FROM ipam_prefix p WHERE p.prefix >> prefix AND p.vrf_id = vrf_id
|
||||
)
|
||||
)
|
||||
)
|
||||
AND id != NEW.id
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM ipam_prefix p
|
||||
WHERE
|
||||
p.prefix >> ipam_prefix.prefix
|
||||
AND p.prefix << NEW.prefix
|
||||
AND (
|
||||
(p.vrf_id = ipam_prefix.vrf_id OR (p.vrf_id IS NULL AND ipam_prefix.vrf_id IS NULL))
|
||||
OR
|
||||
(p.vrf_id IS NULL AND p.status = 'container')
|
||||
)
|
||||
AND p.id != NEW.id
|
||||
)
|
||||
;
|
||||
UPDATE ipam_ipaddress
|
||||
SET prefix_id = NEW.id
|
||||
WHERE
|
||||
prefix_id != NEW.id
|
||||
AND
|
||||
address << NEW.prefix
|
||||
AND (
|
||||
(vrf_id = NEW.vrf_id OR (vrf_id IS NULL AND NEW.vrf_id IS NULL))
|
||||
OR (
|
||||
NEW.vrf_id IS NULL
|
||||
AND
|
||||
NEW.status = 'container'
|
||||
AND
|
||||
NOT EXISTS(
|
||||
SELECT 1 FROM ipam_prefix p WHERE p.prefix >> address AND p.vrf_id = vrf_id
|
||||
)
|
||||
)
|
||||
)
|
||||
;
|
||||
UPDATE ipam_iprange
|
||||
SET prefix_id = NEW.id
|
||||
WHERE
|
||||
prefix_id != NEW.id
|
||||
AND
|
||||
start_address << NEW.prefix
|
||||
AND
|
||||
end_address << NEW.prefix
|
||||
AND (
|
||||
(vrf_id = NEW.vrf_id OR (vrf_id IS NULL AND NEW.vrf_id IS NULL))
|
||||
OR (
|
||||
NEW.vrf_id IS NULL
|
||||
AND
|
||||
NEW.status = 'container'
|
||||
AND
|
||||
NOT EXISTS(
|
||||
SELECT 1 FROM ipam_prefix p WHERE
|
||||
p.prefix >> start_address
|
||||
AND
|
||||
p.prefix >> end_address
|
||||
AND
|
||||
p.vrf_id = vrf_id
|
||||
)
|
||||
)
|
||||
)
|
||||
;
|
||||
RETURN NEW;
|
||||
"""
|
||||
@@ -687,13 +687,13 @@ class PrefixIPAddressesView(generic.ObjectChildrenView):
|
||||
template_name = 'ipam/prefix/ip_addresses.html'
|
||||
tab = ViewTab(
|
||||
label=_('IP Addresses'),
|
||||
badge=lambda x: x.ip_addresses.count(),
|
||||
badge=lambda x: x.get_child_ips().count(),
|
||||
permission='ipam.view_ipaddress',
|
||||
weight=700
|
||||
)
|
||||
|
||||
def get_children(self, request, parent):
|
||||
return parent.ip_addresses.restrict(request.user, 'view').prefetch_related('vrf', 'tenant', 'tenant__group')
|
||||
return parent.get_child_ips().restrict(request.user, 'view').prefetch_related('vrf', 'tenant', 'tenant__group')
|
||||
|
||||
def prep_table_data(self, request, queryset, parent):
|
||||
if not request.GET.get('q') and not get_table_ordering(request, self.table):
|
||||
|
||||
@@ -463,7 +463,6 @@ INSTALLED_APPS = [
|
||||
'sorl.thumbnail',
|
||||
'taggit',
|
||||
'timezone_field',
|
||||
'pgtrigger',
|
||||
'core',
|
||||
'account',
|
||||
'circuits',
|
||||
@@ -774,7 +773,7 @@ SPECTACULAR_SETTINGS = {
|
||||
'COMPONENT_SPLIT_REQUEST': True,
|
||||
'REDOC_DIST': 'SIDECAR',
|
||||
'SERVERS': [{
|
||||
'url': BASE_PATH,
|
||||
'url': '',
|
||||
'description': 'NetBox',
|
||||
}],
|
||||
'SWAGGER_UI_DIST': 'SIDECAR',
|
||||
|
||||
2
netbox/project-static/dist/netbox.css
vendored
2
netbox/project-static/dist/netbox.css
vendored
File diff suppressed because one or more lines are too long
2
netbox/project-static/dist/netbox.js
vendored
2
netbox/project-static/dist/netbox.js
vendored
File diff suppressed because one or more lines are too long
6
netbox/project-static/dist/netbox.js.map
vendored
6
netbox/project-static/dist/netbox.js.map
vendored
File diff suppressed because one or more lines are too long
@@ -150,20 +150,22 @@ function initSidebarAccordions(): void {
|
||||
*/
|
||||
function initImagePreview(): void {
|
||||
for (const element of getElements<HTMLAnchorElement>('a.image-preview')) {
|
||||
// Generate a max-width that's a quarter of the screen's width (note - the actual element
|
||||
// width will be slightly larger due to the popover body's padding).
|
||||
const maxWidth = `${Math.round(window.innerWidth / 4)}px`;
|
||||
// Prefer a thumbnail URL for the popover (so we don't preload full-size images),
|
||||
// but fall back to the link target if no thumbnail was provided.
|
||||
const previewUrl = element.dataset.previewUrl ?? element.href;
|
||||
const image = createElement('img', { src: previewUrl });
|
||||
|
||||
// Create an image element that uses the linked image as its `src`.
|
||||
const image = createElement('img', { src: element.href });
|
||||
image.style.maxWidth = maxWidth;
|
||||
// Ensure lazy loading and async decoding
|
||||
image.loading = 'lazy';
|
||||
image.decoding = 'async';
|
||||
|
||||
// Create a container for the image.
|
||||
const content = createElement('div', null, null, [image]);
|
||||
|
||||
// Initialize the Bootstrap Popper instance.
|
||||
new Popover(element, {
|
||||
// Attach this custom class to the popover so that it styling can be controlled via CSS.
|
||||
// Attach this custom class to the popover so that its styling
|
||||
// can be controlled via CSS.
|
||||
customClass: 'image-preview-popover',
|
||||
trigger: 'hover',
|
||||
html: true,
|
||||
|
||||
@@ -89,6 +89,29 @@ img.plugin-icon {
|
||||
}
|
||||
}
|
||||
|
||||
// Image preview popover (rendered for <a.image-preview> by initImagePreview())
|
||||
.image-preview-popover {
|
||||
--bs-popover-max-width: clamp(240px, 25vw, 640px);
|
||||
|
||||
.popover-header {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.popover-body {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
img {
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
max-height: clamp(160px, 33vh, 640px);
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
body[data-bs-theme=dark] {
|
||||
// Assuming icon is black/white line art, invert it and tone down brightness
|
||||
img.plugin-icon {
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
<div class="col col-md-12">
|
||||
<div class="card">
|
||||
<h2 class="card-header">{% trans "Configuration Data" %}</h2>
|
||||
{% include 'core/inc/config_data.html' with config=object.data %}
|
||||
{% include 'core/inc/config_data.html' %}
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
|
||||
@@ -95,7 +95,7 @@
|
||||
<tr>
|
||||
<th scope="row" class="ps-3">{% trans "Custom validators" %}</th>
|
||||
{% if config.CUSTOM_VALIDATORS %}
|
||||
<td><pre>{{ config.CUSTOM_VALIDATORS }}</pre></td>
|
||||
<td><pre class="p-0">{{ config.CUSTOM_VALIDATORS }}</pre></td>
|
||||
{% else %}
|
||||
<td>{{ ''|placeholder }}</td>
|
||||
{% endif %}
|
||||
@@ -103,7 +103,7 @@
|
||||
<tr>
|
||||
<th scope="row" class="border-0 ps-3">{% trans "Protection rules" %}</th>
|
||||
{% if config.PROTECTION_RULES %}
|
||||
<td class="border-0"><pre>{{ config.PROTECTION_RULES }}</pre></td>
|
||||
<td class="border-0"><pre class="p-0">{{ config.PROTECTION_RULES }}</pre></td>
|
||||
{% else %}
|
||||
<td class="border-0">{{ ''|placeholder }}</td>
|
||||
{% endif %}
|
||||
@@ -116,7 +116,7 @@
|
||||
<tr>
|
||||
<th scope="row" class="border-0 ps-3">{% trans "Default preferences" %}</th>
|
||||
{% if config.DEFAULT_USER_PREFERENCES %}
|
||||
<td class="border-0"><pre>{{ config.DEFAULT_USER_PREFERENCES|json }}</pre></td>
|
||||
<td class="border-0"><pre class="p-0">{{ config.DEFAULT_USER_PREFERENCES }}</pre></td>
|
||||
{% else %}
|
||||
<td class="border-0">{{ ''|placeholder }}</td>
|
||||
{% endif %}
|
||||
|
||||
@@ -14,10 +14,6 @@
|
||||
<th scope="row">{% trans "Family" %}</th>
|
||||
<td>IPv{{ object.family }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Prefix" %}</th>
|
||||
<td>{{ object.prefix|linkify|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "VRF" %}</th>
|
||||
<td>
|
||||
|
||||
@@ -14,7 +14,6 @@
|
||||
<div class="row">
|
||||
<h2 class="col-9 offset-3">{% trans "IP Addresses" %}</h2>
|
||||
</div>
|
||||
{% render_field model_form.prefix %}
|
||||
{% render_field form.pattern %}
|
||||
{% render_field model_form.status %}
|
||||
{% render_field model_form.role %}
|
||||
|
||||
@@ -13,10 +13,6 @@
|
||||
<th scope="row">{% trans "Family" %}</th>
|
||||
<td>IPv{{ object.family }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Prefix" %}</th>
|
||||
<td>{{ object.prefix|linkify|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Starting Address" %}</th>
|
||||
<td>{{ object.start_address }}</td>
|
||||
|
||||
@@ -109,7 +109,7 @@
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% with child_ip_count=object.ip_addresses.count %}
|
||||
{% with child_ip_count=object.get_child_ips.count %}
|
||||
<tr>
|
||||
<th scope="row">{% trans "Child IPs" %}</th>
|
||||
<td>
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
{% load helpers %}
|
||||
{% load i18n %}
|
||||
|
||||
<div class="card">
|
||||
<h2 class="card-header">{% trans "Resources" %}</h2>
|
||||
<table class="table table-hover attr-table">
|
||||
<tr>
|
||||
<th scope="row"><i class="mdi mdi-gauge"></i> {% trans "Virtual CPUs" %}</th>
|
||||
<td>{{ object.vcpus|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row"><i class="mdi mdi-chip"></i> {% trans "Memory" %}</th>
|
||||
<td>
|
||||
{% if object.memory %}
|
||||
<span title={{ object.memory }}>{{ object.memory|humanize_ram_megabytes }}</span>
|
||||
{% else %}
|
||||
{{ ''|placeholder }}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<i class="mdi mdi-harddisk"></i> {% trans "Disk Space" %}
|
||||
</th>
|
||||
<td>
|
||||
{% if object.disk %}
|
||||
{{ object.disk|humanize_disk_megabytes }}
|
||||
{% else %}
|
||||
{{ ''|placeholder }}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
@@ -1,199 +1 @@
|
||||
{% extends 'virtualization/virtualmachine/base.html' %}
|
||||
{% load buttons %}
|
||||
{% load static %}
|
||||
{% load helpers %}
|
||||
{% load plugins %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row my-3">
|
||||
<div class="col col-12 col-md-6">
|
||||
<div class="card">
|
||||
<h2 class="card-header">{% trans "Virtual Machine" %}</h2>
|
||||
<table class="table table-hover attr-table">
|
||||
<tr>
|
||||
<th scope="row">{% trans "Name" %}</th>
|
||||
<td>{{ object }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Status" %}</th>
|
||||
<td>{% badge object.get_status_display bg_color=object.get_status_color %}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Start on boot" %}</th>
|
||||
<td>{% badge object.get_start_on_boot_display bg_color=object.get_start_on_boot_color %}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Role" %}</th>
|
||||
<td>{{ object.role|linkify|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Platform" %}</th>
|
||||
<td>{{ object.platform|linkify|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Description" %}</th>
|
||||
<td>{{ object.description|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Serial Number" %}</th>
|
||||
<td>{{ object.serial|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Tenant" %}</th>
|
||||
<td>
|
||||
{% if object.tenant.group %}
|
||||
{{ object.tenant.group|linkify }} /
|
||||
{% endif %}
|
||||
{{ object.tenant|linkify|placeholder }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Config Template" %}</th>
|
||||
<td>{{ object.config_template|linkify|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Primary IPv4" %}</th>
|
||||
<td>
|
||||
{% if object.primary_ip4 %}
|
||||
<a href="{% url 'ipam:ipaddress' pk=object.primary_ip4.pk %}" id="primary_ip4">{{ object.primary_ip4.address.ip }}</a>
|
||||
{% if object.primary_ip4.nat_inside %}
|
||||
({% trans "NAT for" %} <a href="{{ object.primary_ip4.nat_inside.get_absolute_url }}">{{ object.primary_ip4.nat_inside.address.ip }}</a>)
|
||||
{% elif object.primary_ip4.nat_outside.exists %}
|
||||
({% trans "NAT" %}: {% for nat in object.primary_ip4.nat_outside.all %}<a href="{{ nat.get_absolute_url }}">{{ nat.address.ip }}</a>{% if not forloop.last %}, {% endif %}{% endfor %})
|
||||
{% endif %}
|
||||
{% copy_content "primary_ip4" %}
|
||||
{% else %}
|
||||
{{ ''|placeholder }}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Primary IPv6" %}</th>
|
||||
<td>
|
||||
{% if object.primary_ip6 %}
|
||||
<a href="{% url 'ipam:ipaddress' pk=object.primary_ip6.pk %}" id="primary_ip6">{{ object.primary_ip6.address.ip }}</a>
|
||||
{% if object.primary_ip6.nat_inside %}
|
||||
({% trans "NAT for" %} <a href="{{ object.primary_ip6.nat_inside.get_absolute_url }}">{{ object.primary_ip6.nat_inside.address.ip }}</a>)
|
||||
{% elif object.primary_ip6.nat_outside.exists %}
|
||||
({% trans "NAT" %}: {% for nat in object.primary_ip6.nat_outside.all %}<a href="{{ nat.get_absolute_url }}">{{ nat.address.ip }}</a>{% if not forloop.last %}, {% endif %}{% endfor %})
|
||||
{% endif %}
|
||||
{% copy_content "primary_ip6" %}
|
||||
{% else %}
|
||||
{{ ''|placeholder }}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
{% include 'inc/panels/custom_fields.html' %}
|
||||
{% include 'inc/panels/tags.html' %}
|
||||
{% include 'inc/panels/comments.html' %}
|
||||
{% plugin_left_page object %}
|
||||
</div>
|
||||
<div class="col col-12 col-md-6">
|
||||
<div class="card">
|
||||
<h2 class="card-header">{% trans "Cluster" %}</h2>
|
||||
<table class="table table-hover attr-table">
|
||||
<tr>
|
||||
<th scope="row">{% trans "Site" %}</th>
|
||||
<td>
|
||||
{{ object.site|linkify|placeholder }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Cluster" %}</th>
|
||||
<td>
|
||||
{% if object.cluster.group %}
|
||||
{{ object.cluster.group|linkify }} /
|
||||
{% endif %}
|
||||
{{ object.cluster|linkify|placeholder }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Cluster Type" %}</th>
|
||||
<td>
|
||||
{{ object.cluster.type|linkify|placeholder }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Device" %}</th>
|
||||
<td>
|
||||
{{ object.device|linkify|placeholder }}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h2 class="card-header">{% trans "Resources" %}</h2>
|
||||
<table class="table table-hover attr-table">
|
||||
<tr>
|
||||
<th scope="row"><i class="mdi mdi-gauge"></i> {% trans "Virtual CPUs" %}</th>
|
||||
<td>{{ object.vcpus|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row"><i class="mdi mdi-chip"></i> {% trans "Memory" %}</th>
|
||||
<td>
|
||||
{% if object.memory %}
|
||||
<span title={{ object.memory }}>{{ object.memory|humanize_ram_megabytes }}</span>
|
||||
{% else %}
|
||||
{{ ''|placeholder }}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<i class="mdi mdi-harddisk"></i> {% trans "Disk Space" %}
|
||||
</th>
|
||||
<td>
|
||||
{% if object.disk %}
|
||||
{{ object.disk|humanize_disk_megabytes }}
|
||||
{% else %}
|
||||
{{ ''|placeholder }}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h2 class="card-header">
|
||||
{% trans "Application Services" %}
|
||||
{% if perms.ipam.add_service %}
|
||||
<div class="card-actions">
|
||||
<a href="{% url 'ipam:service_add' %}?parent_object_type={{ object|content_type_id }}&parent={{ object.pk }}" class="btn btn-ghost-primary btn-sm">
|
||||
<span class="mdi mdi-plus-thick" aria-hidden="true"></span> {% trans "Add an application service" %}
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</h2>
|
||||
{% htmx_table 'ipam:service_list' virtual_machine_id=object.pk %}
|
||||
</div>
|
||||
{% include 'inc/panels/image_attachments.html' %}
|
||||
{% plugin_right_page object %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col col-md-12">
|
||||
<div class="card">
|
||||
<h2 class="card-header">
|
||||
{% trans "Virtual Disks" %}
|
||||
{% if perms.virtualization.add_virtualdisk %}
|
||||
<div class="card-actions">
|
||||
<a href="{% url 'virtualization:virtualdisk_add' %}?device={{ object.device.pk }}&virtual_machine={{ object.pk }}&return_url={{ object.get_absolute_url }}" class="btn btn-ghost-primary btn-sm">
|
||||
<span class="mdi mdi-plus-thick" aria-hidden="true"></span> {% trans "Add Virtual Disk" %}
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</h2>
|
||||
{% htmx_table 'virtualization:virtualdisk_list' virtual_machine_id=object.pk %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col col-md-12">
|
||||
{% plugin_full_width_page object %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
{% load i18n %}
|
||||
<a href="{{ value.get_absolute_url }}"{% if name %} id="attr_{{ name }}"{% endif %}>{{ value.address.ip }}</a>
|
||||
{% if value.nat_inside %}
|
||||
({% trans "NAT for" %} <a href="{{ value.nat_inside.get_absolute_url }}">{{ value.nat_inside.address.ip }}</a>)
|
||||
{% elif value.nat_outside.exists %}
|
||||
({% trans "NAT" %}: {% for nat in value.nat_outside.all %}<a href="{{ nat.get_absolute_url }}">{{ nat.address.ip }}</a>{% if not forloop.last %}, {% endif %}{% endfor %})
|
||||
{% endif %}
|
||||
<a class="btn btn-sm btn-primary copy-content" data-clipboard-target="#attr_{{ name }}" title="{% trans "Copy to clipboard" %}">
|
||||
<i class="mdi mdi-content-copy"></i>
|
||||
</a>
|
||||
@@ -78,8 +78,8 @@
|
||||
<tr>
|
||||
<th scope="row">{% trans "MAC Address" %}</th>
|
||||
<td>
|
||||
{% if object.mac_address %}
|
||||
<span class="font-monospace">{{ object.mac_address }}</span>
|
||||
{% if object.primary_mac_address %}
|
||||
<span class="font-monospace">{{ object.primary_mac_address|linkify }}</span>
|
||||
<span class="badge text-bg-primary">{% trans "Primary" %}</span>
|
||||
{% else %}
|
||||
{{ ''|placeholder }}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -280,26 +280,6 @@ def as_range(n):
|
||||
return range(n)
|
||||
|
||||
|
||||
@register.filter()
|
||||
def parent_depth(n, parent=None):
|
||||
"""
|
||||
Return the depth of a node based on the parent's depth
|
||||
"""
|
||||
parent_depth = 0
|
||||
if parent and hasattr(parent, 'depth_count'):
|
||||
parent_depth = parent.depth_count + 1
|
||||
elif parent and hasattr(parent, 'depth'):
|
||||
try:
|
||||
parent_depth = int(parent.depth) + 1
|
||||
except TypeError:
|
||||
pass
|
||||
try:
|
||||
depth = int(n) - int(parent_depth)
|
||||
except TypeError:
|
||||
return n
|
||||
return depth
|
||||
|
||||
|
||||
@register.filter()
|
||||
def meters_to_feet(n):
|
||||
"""
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import django_filters
|
||||
import netaddr
|
||||
from django.db.models import Q
|
||||
from django.utils.translation import gettext as _
|
||||
from netaddr.core import AddrFormatError
|
||||
|
||||
from dcim.base_filtersets import ScopedFilterSet
|
||||
from dcim.filtersets import CommonInterfaceFilterSet
|
||||
@@ -229,14 +231,22 @@ class VirtualMachineFilterSet(
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
return queryset
|
||||
return queryset.filter(
|
||||
qs_filter = queryset.filter(
|
||||
Q(name__icontains=value) |
|
||||
Q(description__icontains=value) |
|
||||
Q(comments__icontains=value) |
|
||||
Q(primary_ip4__address__startswith=value) |
|
||||
Q(primary_ip6__address__startswith=value) |
|
||||
Q(serial__icontains=value)
|
||||
)
|
||||
# If the given value looks like an IP address, look for primary IPv4/IPv6 assignments
|
||||
try:
|
||||
ipaddress = netaddr.IPNetwork(value)
|
||||
if ipaddress.version == 4:
|
||||
qs_filter |= Q(primary_ip4__address__host__inet=ipaddress.ip)
|
||||
elif ipaddress.version == 6:
|
||||
qs_filter |= Q(primary_ip6__address__host__inet=ipaddress.ip)
|
||||
except (AddrFormatError, ValueError):
|
||||
pass
|
||||
return qs_filter
|
||||
|
||||
def _has_primary_ip(self, queryset, name, value):
|
||||
params = Q(primary_ip4__isnull=False) | Q(primary_ip6__isnull=False)
|
||||
|
||||
0
netbox/virtualization/ui/__init__.py
Normal file
0
netbox/virtualization/ui/__init__.py
Normal file
34
netbox/virtualization/ui/panels.py
Normal file
34
netbox/virtualization/ui/panels.py
Normal file
@@ -0,0 +1,34 @@
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from netbox.ui import attrs, panels
|
||||
|
||||
|
||||
class VirtualMachinePanel(panels.ObjectAttributesPanel):
|
||||
name = attrs.TextAttr('name')
|
||||
status = attrs.ChoiceAttr('status')
|
||||
start_on_boot = attrs.ChoiceAttr('start_on_boot')
|
||||
role = attrs.RelatedObjectAttr('role', linkify=True)
|
||||
platform = attrs.NestedObjectAttr('platform', linkify=True, max_depth=3)
|
||||
description = attrs.TextAttr('description')
|
||||
serial = attrs.TextAttr('serial', label=_('Serial number'), style='font-monospace', copy_button=True)
|
||||
tenant = attrs.RelatedObjectAttr('tenant', linkify=True, grouped_by='group')
|
||||
config_template = attrs.RelatedObjectAttr('config_template', linkify=True)
|
||||
primary_ip4 = attrs.TemplatedAttr(
|
||||
'primary_ip4',
|
||||
label=_('Primary IPv4'),
|
||||
template_name='virtualization/virtualmachine/attrs/ipaddress.html',
|
||||
)
|
||||
primary_ip6 = attrs.TemplatedAttr(
|
||||
'primary_ip6',
|
||||
label=_('Primary IPv6'),
|
||||
template_name='virtualization/virtualmachine/attrs/ipaddress.html',
|
||||
)
|
||||
|
||||
|
||||
class VirtualMachineClusterPanel(panels.ObjectAttributesPanel):
|
||||
title = _('Cluster')
|
||||
|
||||
site = attrs.RelatedObjectAttr('site', linkify=True, grouped_by='group')
|
||||
cluster = attrs.RelatedObjectAttr('cluster', linkify=True)
|
||||
cluster_type = attrs.RelatedObjectAttr('cluster.type', linkify=True)
|
||||
device = attrs.RelatedObjectAttr('device', linkify=True)
|
||||
@@ -10,12 +10,15 @@ from dcim.filtersets import DeviceFilterSet
|
||||
from dcim.forms import DeviceFilterForm
|
||||
from dcim.models import Device
|
||||
from dcim.tables import DeviceTable
|
||||
from extras.ui.panels import CustomFieldsPanel, ImageAttachmentsPanel, TagsPanel
|
||||
from extras.views import ObjectConfigContextView, ObjectRenderConfigView
|
||||
from ipam.models import IPAddress, VLANGroup
|
||||
from ipam.tables import InterfaceVLANTable, VLANTranslationRuleTable
|
||||
from netbox.object_actions import (
|
||||
AddObject, BulkDelete, BulkEdit, BulkExport, BulkImport, BulkRename, DeleteObject, EditObject,
|
||||
)
|
||||
from netbox.ui import actions, layout
|
||||
from netbox.ui.panels import CommentsPanel, ObjectsTablePanel, TemplatePanel
|
||||
from netbox.views import generic
|
||||
from utilities.query import count_related
|
||||
from utilities.query_functions import CollateAsChar
|
||||
@@ -23,6 +26,7 @@ from utilities.views import GetRelatedModelsMixin, ViewTab, register_model_view
|
||||
from . import filtersets, forms, tables
|
||||
from .models import *
|
||||
from .object_actions import BulkAddComponents
|
||||
from .ui import panels
|
||||
|
||||
|
||||
#
|
||||
@@ -336,6 +340,7 @@ class ClusterAddDevicesView(generic.ObjectEditView):
|
||||
# Virtual machines
|
||||
#
|
||||
|
||||
|
||||
@register_model_view(VirtualMachine, 'list', path='', detail=False)
|
||||
class VirtualMachineListView(generic.ObjectListView):
|
||||
queryset = VirtualMachine.objects.prefetch_related('primary_ip4', 'primary_ip6')
|
||||
@@ -348,6 +353,44 @@ class VirtualMachineListView(generic.ObjectListView):
|
||||
@register_model_view(VirtualMachine)
|
||||
class VirtualMachineView(generic.ObjectView):
|
||||
queryset = VirtualMachine.objects.all()
|
||||
layout = layout.SimpleLayout(
|
||||
left_panels=[
|
||||
panels.VirtualMachinePanel(),
|
||||
CustomFieldsPanel(),
|
||||
TagsPanel(),
|
||||
CommentsPanel(),
|
||||
],
|
||||
right_panels=[
|
||||
panels.VirtualMachineClusterPanel(),
|
||||
TemplatePanel('virtualization/panels/virtual_machine_resources.html'),
|
||||
ObjectsTablePanel(
|
||||
model='ipam.Service',
|
||||
title=_('Application Services'),
|
||||
filters={'virtual_machine_id': lambda ctx: ctx['object'].pk},
|
||||
actions=[
|
||||
actions.AddObject(
|
||||
'ipam.Service',
|
||||
url_params={
|
||||
'parent_object_type': lambda ctx: ContentType.objects.get_for_model(ctx['object']).pk,
|
||||
'parent': lambda ctx: ctx['object'].pk,
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
ImageAttachmentsPanel(),
|
||||
],
|
||||
bottom_panels=[
|
||||
ObjectsTablePanel(
|
||||
model='virtualization.VirtualDisk',
|
||||
filters={'virtual_machine_id': lambda ctx: ctx['object'].pk},
|
||||
actions=[
|
||||
actions.AddObject(
|
||||
'virtualization.VirtualDisk', url_params={'virtual_machine': lambda ctx: ctx['object'].pk}
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
@register_model_view(VirtualMachine, 'interfaces')
|
||||
|
||||
@@ -7,7 +7,6 @@ django-graphiql-debug-toolbar==0.2.0
|
||||
django-htmx==1.27.0
|
||||
django-mptt==0.17.0
|
||||
django-pglocks==1.0.4
|
||||
django-pgtrigger==4.15.4
|
||||
django-prometheus==2.4.1
|
||||
django-redis==6.0.0
|
||||
django-rich==2.2.0
|
||||
|
||||
Reference in New Issue
Block a user