mirror of
https://github.com/netbox-community/netbox.git
synced 2025-08-25 08:46:10 -06:00
Merge branch 'develop' into ip_functional_relationship
This commit is contained in:
commit
69ffbb42df
2
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
2
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
@ -14,7 +14,7 @@ body:
|
||||
attributes:
|
||||
label: NetBox version
|
||||
description: What version of NetBox are you currently running?
|
||||
placeholder: v3.5.4
|
||||
placeholder: v3.5.5
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
|
2
.github/ISSUE_TEMPLATE/feature_request.yaml
vendored
2
.github/ISSUE_TEMPLATE/feature_request.yaml
vendored
@ -14,7 +14,7 @@ body:
|
||||
attributes:
|
||||
label: NetBox version
|
||||
description: What version of NetBox are you currently running?
|
||||
placeholder: v3.5.4
|
||||
placeholder: v3.5.5
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
|
@ -1,15 +1,29 @@
|
||||
# NetBox v3.5
|
||||
|
||||
## v3.5.5 (FUTURE)
|
||||
## v3.5.6 (FUTURE)
|
||||
|
||||
---
|
||||
|
||||
## v3.5.5 (2023-07-06)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [#11738](https://github.com/netbox-community/netbox/issues/11738) - Annotate VLAN group utilization
|
||||
* [#12499](https://github.com/netbox-community/netbox/issues/12499) - Add "copy to clipboard" buttons in UI for IP addresses
|
||||
* [#12945](https://github.com/netbox-community/netbox/issues/12945) - Add 100GE QSFP-DD interface type
|
||||
* [#12955](https://github.com/netbox-community/netbox/issues/12955) - Include additional contact details on contact assignments table
|
||||
* [#13065](https://github.com/netbox-community/netbox/issues/13065) - Associate contact assignments with their objects in the change log
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* [#11335](https://github.com/netbox-community/netbox/issues/11335) - Exclude stale content types when retrieving changelog records
|
||||
* [#12533](https://github.com/netbox-community/netbox/issues/12533) - Fix REST API validation of null values for several interface attributes
|
||||
* [#12579](https://github.com/netbox-community/netbox/issues/12579) - Fix exception when clicking "create and add another" to add a cable
|
||||
* [#12617](https://github.com/netbox-community/netbox/issues/12617) - Populate prechange snapshot on parent object when assigning/removing primary IP address
|
||||
* [#12760](https://github.com/netbox-community/netbox/issues/12760) - Avoid rendering partial HTMX responses when restoring browser tabs
|
||||
* [#12842](https://github.com/netbox-community/netbox/issues/12842) - Improve handling of exceptions when loading reports
|
||||
* [#12849](https://github.com/netbox-community/netbox/issues/12849) - Fix LDAP group permissions assignment for API clients
|
||||
* [#12951](https://github.com/netbox-community/netbox/issues/12951) - Display consistent parent information for each termination under cable view
|
||||
* [#12953](https://github.com/netbox-community/netbox/issues/12953) - Fix designation of primary IP addresses during interface assignment
|
||||
* [#12960](https://github.com/netbox-community/netbox/issues/12960) - Fix OpenAPI schema for various choice fields
|
||||
* [#12961](https://github.com/netbox-community/netbox/issues/12961) - Set correct return URL for object contacts tabs
|
||||
@ -19,6 +33,10 @@
|
||||
* [#12983](https://github.com/netbox-community/netbox/issues/12983) - Avoid erroneously clearing many-to-many assignments during bulk edit
|
||||
* [#12989](https://github.com/netbox-community/netbox/issues/12989) - Fix bulk import of tags for device & module types
|
||||
* [#13011](https://github.com/netbox-community/netbox/issues/13011) - Do not escape commas when rendering custom links
|
||||
* [#13047](https://github.com/netbox-community/netbox/issues/13047) - Correct ASN count under ASN ranges list
|
||||
* [#13056](https://github.com/netbox-community/netbox/issues/13056) - Add `config_template` field to device API serializer
|
||||
* [#13092](https://github.com/netbox-community/netbox/issues/13092) - Allow nullifying power port max & allocated draw values during bulk edit
|
||||
* [#13100](https://github.com/netbox-community/netbox/issues/13100) - Fix ValueError exception when searching for virtual device context for non-numeric values
|
||||
|
||||
---
|
||||
|
||||
|
@ -708,8 +708,8 @@ class DeviceWithConfigContextSerializer(DeviceSerializer):
|
||||
'id', 'url', 'display', 'name', 'device_type', 'device_role', 'tenant', 'platform', 'serial', 'asset_tag',
|
||||
'site', 'location', 'rack', 'position', 'face', 'parent_device', 'status', 'airflow', 'primary_ip',
|
||||
'primary_ip4', 'primary_ip6', 'cluster', 'virtual_chassis', 'vc_position', 'vc_priority', 'description',
|
||||
'comments', 'local_context_data', 'tags', 'custom_fields', 'config_context', 'created', 'last_updated',
|
||||
'ipaddressfunctions',
|
||||
'comments', 'local_context_data', 'tags', 'custom_fields', 'config_context', 'config_template',
|
||||
'ipaddressfunctions', 'created', 'last_updated',
|
||||
]
|
||||
|
||||
@extend_schema_field(serializers.JSONField(allow_null=True))
|
||||
|
@ -1077,10 +1077,13 @@ class VirtualDeviceContextFilterSet(NetBoxModelFilterSet, TenancyFilterSet):
|
||||
def search(self, queryset, name, value):
|
||||
if not value.strip():
|
||||
return queryset
|
||||
return queryset.filter(
|
||||
Q(name__icontains=value) |
|
||||
Q(identifier=value.strip())
|
||||
).distinct()
|
||||
|
||||
qs_filter = Q(name__icontains=value)
|
||||
try:
|
||||
qs_filter |= Q(identifier=int(value))
|
||||
except ValueError:
|
||||
pass
|
||||
return queryset.filter(qs_filter).distinct()
|
||||
|
||||
def _has_primary_ip(self, queryset, name, value):
|
||||
params = Q(primary_ip4__isnull=False) | Q(primary_ip6__isnull=False)
|
||||
|
@ -1106,7 +1106,7 @@ class PowerPortBulkEditForm(
|
||||
(None, ('module', 'type', 'label', 'description', 'mark_connected')),
|
||||
('Power', ('maximum_draw', 'allocated_draw')),
|
||||
)
|
||||
nullable_fields = ('module', 'label', 'description')
|
||||
nullable_fields = ('module', 'label', 'description', 'maximum_draw', 'allocated_draw')
|
||||
|
||||
|
||||
class PowerOutletBulkEditForm(
|
||||
|
@ -3131,6 +3131,19 @@ class CableEditView(generic.ObjectEditView):
|
||||
|
||||
return obj
|
||||
|
||||
def get_extra_addanother_params(self, request):
|
||||
|
||||
params = {
|
||||
'a_terminations_type': request.GET.get('a_terminations_type'),
|
||||
'b_terminations_type': request.GET.get('b_terminations_type')
|
||||
}
|
||||
|
||||
for key in request.POST:
|
||||
if 'device' in key or 'power_panel' in key or 'circuit' in key:
|
||||
params.update({key: request.POST.get(key)})
|
||||
|
||||
return params
|
||||
|
||||
|
||||
@register_model_view(Cable, 'delete')
|
||||
class CableDeleteView(generic.ObjectDeleteView):
|
||||
|
@ -368,7 +368,7 @@ class ObjectChangeViewSet(ReadOnlyModelViewSet):
|
||||
Retrieve a list of recent changes.
|
||||
"""
|
||||
metadata_class = ContentTypeMetadata
|
||||
queryset = ObjectChange.objects.prefetch_related('user')
|
||||
queryset = ObjectChange.objects.valid_models().prefetch_related('user')
|
||||
serializer_class = serializers.ObjectChangeSerializer
|
||||
filterset_class = filtersets.ObjectChangeFilterSet
|
||||
|
||||
|
@ -5,7 +5,7 @@ from django.db import models
|
||||
from django.urls import reverse
|
||||
|
||||
from extras.choices import *
|
||||
from utilities.querysets import RestrictedQuerySet
|
||||
from ..querysets import ObjectChangeQuerySet
|
||||
|
||||
__all__ = (
|
||||
'ObjectChange',
|
||||
@ -82,7 +82,7 @@ class ObjectChange(models.Model):
|
||||
null=True
|
||||
)
|
||||
|
||||
objects = RestrictedQuerySet.as_manager()
|
||||
objects = ObjectChangeQuerySet.as_manager()
|
||||
|
||||
class Meta:
|
||||
ordering = ['-time']
|
||||
|
@ -1,5 +1,8 @@
|
||||
from django.apps import apps
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.contrib.postgres.aggregates import JSONBAgg
|
||||
from django.db.models import OuterRef, Subquery, Q
|
||||
from django.db.utils import ProgrammingError
|
||||
|
||||
from extras.models.tags import TaggedItem
|
||||
from utilities.query_functions import EmptyGroupByJSONBAgg
|
||||
@ -151,3 +154,20 @@ class ConfigContextModelQuerySet(RestrictedQuerySet):
|
||||
)
|
||||
|
||||
return base_query
|
||||
|
||||
|
||||
class ObjectChangeQuerySet(RestrictedQuerySet):
|
||||
|
||||
def valid_models(self):
|
||||
# Exclude any change records which refer to an instance of a model that's no longer installed. This
|
||||
# can happen when a plugin is removed but its data remains in the database, for example.
|
||||
try:
|
||||
content_types = ContentType.objects.get_for_models(*apps.get_models()).values()
|
||||
except ProgrammingError:
|
||||
# Handle the case where the database schema has not yet been initialized
|
||||
content_types = ContentType.objects.none()
|
||||
|
||||
content_type_ids = set(
|
||||
ct.pk for ct in content_types
|
||||
)
|
||||
return self.filter(changed_object_type_id__in=content_type_ids)
|
||||
|
@ -8,7 +8,6 @@ from rest_framework import status
|
||||
|
||||
from core.choices import ManagedFileRootPathChoices
|
||||
from dcim.models import Device, DeviceRole, DeviceType, Manufacturer, Rack, Location, RackRole, Site
|
||||
from extras.api.views import ReportViewSet, ScriptViewSet
|
||||
from extras.models import *
|
||||
from extras.reports import Report
|
||||
from extras.scripts import BooleanVar, IntegerVar, Script, StringVar
|
||||
@ -579,6 +578,7 @@ class ReportTest(APITestCase):
|
||||
super().setUp()
|
||||
|
||||
# Monkey-patch the API viewset's _get_report() method to return our test Report above
|
||||
from extras.api.views import ReportViewSet
|
||||
ReportViewSet._get_report = self.get_test_report
|
||||
|
||||
def test_get_report(self):
|
||||
@ -621,6 +621,7 @@ class ScriptTest(APITestCase):
|
||||
super().setUp()
|
||||
|
||||
# Monkey-patch the API viewset's _get_script() method to return our test Script above
|
||||
from extras.api.views import ScriptViewSet
|
||||
ScriptViewSet._get_script = self.get_test_script
|
||||
|
||||
def test_get_script(self):
|
||||
|
@ -511,7 +511,7 @@ class ConfigTemplateBulkSyncDataView(generic.BulkSyncDataView):
|
||||
#
|
||||
|
||||
class ObjectChangeListView(generic.ObjectListView):
|
||||
queryset = ObjectChange.objects.all()
|
||||
queryset = ObjectChange.objects.valid_models()
|
||||
filterset = filtersets.ObjectChangeFilterSet
|
||||
filterset_form = forms.ObjectChangeFilterForm
|
||||
table = tables.ObjectChangeTable
|
||||
@ -521,10 +521,10 @@ class ObjectChangeListView(generic.ObjectListView):
|
||||
|
||||
@register_model_view(ObjectChange)
|
||||
class ObjectChangeView(generic.ObjectView):
|
||||
queryset = ObjectChange.objects.all()
|
||||
queryset = ObjectChange.objects.valid_models()
|
||||
|
||||
def get_extra_context(self, request, instance):
|
||||
related_changes = ObjectChange.objects.restrict(request.user, 'view').filter(
|
||||
related_changes = ObjectChange.objects.valid_models().restrict(request.user, 'view').filter(
|
||||
request_id=instance.request_id
|
||||
).exclude(
|
||||
pk=instance.pk
|
||||
@ -534,7 +534,7 @@ class ObjectChangeView(generic.ObjectView):
|
||||
orderable=False
|
||||
)
|
||||
|
||||
objectchanges = ObjectChange.objects.restrict(request.user, 'view').filter(
|
||||
objectchanges = ObjectChange.objects.valid_models().restrict(request.user, 'view').filter(
|
||||
changed_object_type=instance.changed_object_type,
|
||||
changed_object_id=instance.changed_object_id,
|
||||
)
|
||||
|
@ -218,12 +218,13 @@ class VLANGroupSerializer(NetBoxModelSerializer):
|
||||
scope_id = serializers.IntegerField(allow_null=True, required=False, default=None)
|
||||
scope = serializers.SerializerMethodField(read_only=True)
|
||||
vlan_count = serializers.IntegerField(read_only=True)
|
||||
utilization = serializers.CharField(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = VLANGroup
|
||||
fields = [
|
||||
'id', 'url', 'display', 'name', 'slug', 'scope_type', 'scope_id', 'scope', 'min_vid', 'max_vid',
|
||||
'description', 'tags', 'custom_fields', 'created', 'last_updated', 'vlan_count',
|
||||
'description', 'tags', 'custom_fields', 'created', 'last_updated', 'vlan_count', 'utilization'
|
||||
]
|
||||
validators = []
|
||||
|
||||
|
@ -1,5 +1,7 @@
|
||||
from django.core.exceptions import ObjectDoesNotExist, PermissionDenied
|
||||
from django.db import transaction
|
||||
from django.db.models import F
|
||||
from django.db.models.functions import Round
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django_pglocks import advisory_lock
|
||||
from drf_spectacular.utils import extend_schema
|
||||
@ -151,9 +153,7 @@ class FHRPGroupAssignmentViewSet(NetBoxModelViewSet):
|
||||
|
||||
|
||||
class VLANGroupViewSet(NetBoxModelViewSet):
|
||||
queryset = VLANGroup.objects.annotate(
|
||||
vlan_count=count_related(VLAN, 'group')
|
||||
).prefetch_related('tags')
|
||||
queryset = VLANGroup.objects.annotate_utilization().prefetch_related('tags')
|
||||
serializer_class = serializers.VLANGroupSerializer
|
||||
filterset_class = filtersets.VLANGroupFilterSet
|
||||
|
||||
|
@ -379,6 +379,7 @@ class IPAddressForm(TenancyForm, NetBoxModelForm):
|
||||
interface = self.instance.assigned_object
|
||||
if type(interface) in (Interface, VMInterface):
|
||||
parent = interface.parent_object
|
||||
parent.snapshot()
|
||||
if self.cleaned_data['primary_for_parent']:
|
||||
if ipaddress.address.version == 4:
|
||||
parent.primary_ip4 = ipaddress
|
||||
|
@ -4,6 +4,7 @@ from django.urls import reverse
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
from ipam.fields import ASNField
|
||||
from ipam.querysets import ASNRangeQuerySet
|
||||
from netbox.models import OrganizationalModel, PrimaryModel
|
||||
|
||||
__all__ = (
|
||||
@ -37,6 +38,8 @@ class ASNRange(OrganizationalModel):
|
||||
null=True
|
||||
)
|
||||
|
||||
objects = ASNRangeQuerySet.as_manager()
|
||||
|
||||
class Meta:
|
||||
ordering = ('name',)
|
||||
verbose_name = 'ASN range'
|
||||
|
@ -9,7 +9,7 @@ from django.utils.translation import gettext as _
|
||||
from dcim.models import Interface
|
||||
from ipam.choices import *
|
||||
from ipam.constants import *
|
||||
from ipam.querysets import VLANQuerySet
|
||||
from ipam.querysets import VLANQuerySet, VLANGroupQuerySet
|
||||
from netbox.models import OrganizationalModel, PrimaryModel
|
||||
from virtualization.models import VMInterface
|
||||
|
||||
@ -63,6 +63,8 @@ class VLANGroup(OrganizationalModel):
|
||||
help_text=_('Highest permissible ID of a child VLAN')
|
||||
)
|
||||
|
||||
objects = VLANGroupQuerySet.as_manager()
|
||||
|
||||
class Meta:
|
||||
ordering = ('name', 'pk') # Name may be non-unique
|
||||
constraints = (
|
||||
|
@ -1,8 +1,34 @@
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.db.models import Q
|
||||
from django.db.models import Count, F, OuterRef, Q, Subquery, Value
|
||||
from django.db.models.expressions import RawSQL
|
||||
from django.db.models.functions import Round
|
||||
|
||||
from utilities.querysets import RestrictedQuerySet
|
||||
from utilities.utils import count_related
|
||||
|
||||
__all__ = (
|
||||
'ASNRangeQuerySet',
|
||||
'PrefixQuerySet',
|
||||
'VLANQuerySet',
|
||||
)
|
||||
|
||||
|
||||
class ASNRangeQuerySet(RestrictedQuerySet):
|
||||
|
||||
def annotate_asn_counts(self):
|
||||
"""
|
||||
Annotate the number of ASNs which appear within each range.
|
||||
"""
|
||||
from .models import ASN
|
||||
|
||||
# Because ASN does not have a foreign key to ASNRange, we create a fake column "_" with a consistent value
|
||||
# that we can use to count ASNs and return a single value per ASNRange.
|
||||
asns = ASN.objects.filter(
|
||||
asn__gte=OuterRef('start'),
|
||||
asn__lte=OuterRef('end')
|
||||
).order_by().annotate(_=Value(1)).values('_').annotate(c=Count('*')).values('c')
|
||||
|
||||
return self.annotate(asn_count=Subquery(asns))
|
||||
|
||||
|
||||
class PrefixQuerySet(RestrictedQuerySet):
|
||||
@ -30,6 +56,17 @@ class PrefixQuerySet(RestrictedQuerySet):
|
||||
)
|
||||
|
||||
|
||||
class VLANGroupQuerySet(RestrictedQuerySet):
|
||||
|
||||
def annotate_utilization(self):
|
||||
from .models import VLAN
|
||||
|
||||
return self.annotate(
|
||||
vlan_count=count_related(VLAN, 'group'),
|
||||
utilization=Round(F('vlan_count') / (F('max_vid') - F('min_vid') + 1.0) * 100, 2)
|
||||
)
|
||||
|
||||
|
||||
class VLANQuerySet(RestrictedQuerySet):
|
||||
|
||||
def get_for_device(self, device):
|
||||
|
@ -21,10 +21,8 @@ class ASNRangeTable(TenancyColumnsMixin, NetBoxTable):
|
||||
tags = columns.TagColumn(
|
||||
url_name='ipam:asnrange_list'
|
||||
)
|
||||
asn_count = columns.LinkedCountColumn(
|
||||
viewname='ipam:asn_list',
|
||||
url_params={'asn_id': 'pk'},
|
||||
verbose_name=_('ASN Count')
|
||||
asn_count = tables.Column(
|
||||
verbose_name=_('ASNs')
|
||||
)
|
||||
|
||||
class Meta(NetBoxTable.Meta):
|
||||
@ -59,7 +57,8 @@ class ASNTable(TenancyColumnsMixin, NetBoxTable):
|
||||
verbose_name=_('Provider Count')
|
||||
)
|
||||
sites = columns.ManyToManyColumn(
|
||||
linkify_item=True
|
||||
linkify_item=True,
|
||||
verbose_name=_('Sites')
|
||||
)
|
||||
comments = columns.MarkdownColumn()
|
||||
tags = columns.TagColumn(
|
||||
|
@ -19,14 +19,22 @@ __all__ = (
|
||||
|
||||
AVAILABLE_LABEL = mark_safe('<span class="badge bg-success">Available</span>')
|
||||
|
||||
AGGREGATE_COPY_BUTTON = """
|
||||
{% copy_content record.pk prefix="aggregate_" %}
|
||||
"""
|
||||
|
||||
PREFIX_LINK = """
|
||||
{% if record.pk %}
|
||||
<a href="{{ record.get_absolute_url }}">{{ record.prefix }}</a>
|
||||
<a href="{{ record.get_absolute_url }}" id="prefix_{{ record.pk }}">{{ record.prefix }}</a>
|
||||
{% else %}
|
||||
<a href="{% url 'ipam:prefix_add' %}?prefix={{ record }}{% if object.vrf %}&vrf={{ object.vrf.pk }}{% endif %}{% if object.site %}&site={{ object.site.pk }}{% endif %}{% if object.tenant %}&tenant_group={{ object.tenant.group.pk }}&tenant={{ object.tenant.pk }}{% endif %}">{{ record.prefix }}</a>
|
||||
{% endif %}
|
||||
"""
|
||||
|
||||
PREFIX_COPY_BUTTON = """
|
||||
{% copy_content record.pk prefix="prefix_" %}
|
||||
"""
|
||||
|
||||
PREFIX_LINK_WITH_DEPTH = """
|
||||
{% load helpers %}
|
||||
{% if record.depth %}
|
||||
@ -40,7 +48,7 @@ PREFIX_LINK_WITH_DEPTH = """
|
||||
|
||||
IPADDRESS_LINK = """
|
||||
{% if record.pk %}
|
||||
<a href="{{ record.get_absolute_url }}">{{ record.address }}</a>
|
||||
<a href="{{ record.get_absolute_url }}" id="ipaddress_{{ record.pk }}">{{ record.address }}</a>
|
||||
{% elif perms.ipam.add_ipaddress %}
|
||||
<a href="{% url 'ipam:ipaddress_add' %}?address={{ record.1 }}{% if object.vrf %}&vrf={{ object.vrf.pk }}{% endif %}{% if object.tenant %}&tenant={{ object.tenant.pk }}{% endif %}" class="btn btn-sm btn-success">{% if record.0 <= 65536 %}{{ record.0 }}{% else %}Many{% endif %} IP{{ record.0|pluralize }} available</a>
|
||||
{% else %}
|
||||
@ -48,6 +56,10 @@ IPADDRESS_LINK = """
|
||||
{% endif %}
|
||||
"""
|
||||
|
||||
IPADDRESS_COPY_BUTTON = """
|
||||
{% copy_content record.pk prefix="ipaddress_" %}
|
||||
"""
|
||||
|
||||
IPADDRESS_ASSIGN_LINK = """
|
||||
<a href="{% url 'ipam:ipaddress_edit' pk=record.pk %}?{% if request.GET.interface %}interface={{ request.GET.interface }}{% elif request.GET.vminterface %}vminterface={{ request.GET.vminterface }}{% endif %}&return_url={{ request.GET.return_url }}">{{ record }}</a>
|
||||
"""
|
||||
@ -99,7 +111,11 @@ class RIRTable(NetBoxTable):
|
||||
class AggregateTable(TenancyColumnsMixin, NetBoxTable):
|
||||
prefix = tables.Column(
|
||||
linkify=True,
|
||||
verbose_name='Aggregate'
|
||||
verbose_name='Aggregate',
|
||||
attrs={
|
||||
# Allow the aggregate to be copied to the clipboard
|
||||
'a': {'id': lambda record: f"aggregate_{record.pk}"}
|
||||
}
|
||||
)
|
||||
date_added = tables.DateColumn(
|
||||
format="Y-m-d",
|
||||
@ -116,6 +132,9 @@ class AggregateTable(TenancyColumnsMixin, NetBoxTable):
|
||||
tags = columns.TagColumn(
|
||||
url_name='ipam:aggregate_list'
|
||||
)
|
||||
actions = columns.ActionsColumn(
|
||||
extra_buttons=AGGREGATE_COPY_BUTTON
|
||||
)
|
||||
|
||||
class Meta(NetBoxTable.Meta):
|
||||
model = Aggregate
|
||||
@ -242,6 +261,9 @@ class PrefixTable(TenancyColumnsMixin, NetBoxTable):
|
||||
tags = columns.TagColumn(
|
||||
url_name='ipam:prefix_list'
|
||||
)
|
||||
actions = columns.ActionsColumn(
|
||||
extra_buttons=PREFIX_COPY_BUTTON
|
||||
)
|
||||
|
||||
class Meta(NetBoxTable.Meta):
|
||||
model = Prefix
|
||||
@ -348,6 +370,9 @@ class IPAddressTable(TenancyColumnsMixin, NetBoxTable):
|
||||
tags = columns.TagColumn(
|
||||
url_name='ipam:ipaddress_list'
|
||||
)
|
||||
actions = columns.ActionsColumn(
|
||||
extra_buttons=IPADDRESS_COPY_BUTTON
|
||||
)
|
||||
|
||||
class Meta(NetBoxTable.Meta):
|
||||
model = IPAddress
|
||||
|
@ -70,6 +70,10 @@ class VLANGroupTable(NetBoxTable):
|
||||
url_params={'group_id': 'pk'},
|
||||
verbose_name='VLANs'
|
||||
)
|
||||
utilization = columns.UtilizationColumn(
|
||||
orderable=False,
|
||||
verbose_name='Utilization'
|
||||
)
|
||||
tags = columns.TagColumn(
|
||||
url_name='ipam:vlangroup_list'
|
||||
)
|
||||
@ -81,9 +85,9 @@ class VLANGroupTable(NetBoxTable):
|
||||
model = VLANGroup
|
||||
fields = (
|
||||
'pk', 'id', 'name', 'scope_type', 'scope', 'min_vid', 'max_vid', 'vlan_count', 'slug', 'description',
|
||||
'tags', 'created', 'last_updated', 'actions',
|
||||
'tags', 'created', 'last_updated', 'actions', 'utilization',
|
||||
)
|
||||
default_columns = ('pk', 'name', 'scope_type', 'scope', 'vlan_count', 'description')
|
||||
default_columns = ('pk', 'name', 'scope_type', 'scope', 'vlan_count', 'utilization', 'description')
|
||||
|
||||
|
||||
#
|
||||
|
@ -1,6 +1,7 @@
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.db.models import Prefetch
|
||||
from django.db.models import F, Prefetch
|
||||
from django.db.models.expressions import RawSQL
|
||||
from django.db.models.functions import Round
|
||||
from django.shortcuts import get_object_or_404, redirect, render
|
||||
from django.urls import reverse
|
||||
from django.utils.translation import gettext as _
|
||||
@ -198,7 +199,7 @@ class RIRBulkDeleteView(generic.BulkDeleteView):
|
||||
#
|
||||
|
||||
class ASNRangeListView(generic.ObjectListView):
|
||||
queryset = ASNRange.objects.all()
|
||||
queryset = ASNRange.objects.annotate_asn_counts()
|
||||
filterset = filtersets.ASNRangeFilterSet
|
||||
filterset_form = forms.ASNRangeFilterForm
|
||||
table = tables.ASNRangeTable
|
||||
@ -247,18 +248,14 @@ class ASNRangeBulkImportView(generic.BulkImportView):
|
||||
|
||||
|
||||
class ASNRangeBulkEditView(generic.BulkEditView):
|
||||
queryset = ASNRange.objects.annotate(
|
||||
site_count=count_related(Site, 'asns')
|
||||
)
|
||||
queryset = ASNRange.objects.annotate_asn_counts()
|
||||
filterset = filtersets.ASNRangeFilterSet
|
||||
table = tables.ASNRangeTable
|
||||
form = forms.ASNRangeBulkEditForm
|
||||
|
||||
|
||||
class ASNRangeBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = ASNRange.objects.annotate(
|
||||
site_count=count_related(Site, 'asns')
|
||||
)
|
||||
queryset = ASNRange.objects.annotate_asn_counts()
|
||||
filterset = filtersets.ASNRangeFilterSet
|
||||
table = tables.ASNRangeTable
|
||||
|
||||
@ -886,9 +883,7 @@ class IPAddressRelatedIPsView(generic.ObjectChildrenView):
|
||||
#
|
||||
|
||||
class VLANGroupListView(generic.ObjectListView):
|
||||
queryset = VLANGroup.objects.annotate(
|
||||
vlan_count=count_related(VLAN, 'group')
|
||||
)
|
||||
queryset = VLANGroup.objects.annotate_utilization().prefetch_related('tags')
|
||||
filterset = filtersets.VLANGroupFilterSet
|
||||
filterset_form = forms.VLANGroupFilterForm
|
||||
table = tables.VLANGroupTable
|
||||
@ -896,7 +891,7 @@ class VLANGroupListView(generic.ObjectListView):
|
||||
|
||||
@register_model_view(VLANGroup)
|
||||
class VLANGroupView(generic.ObjectView):
|
||||
queryset = VLANGroup.objects.all()
|
||||
queryset = VLANGroup.objects.annotate_utilization().prefetch_related('tags')
|
||||
|
||||
def get_extra_context(self, request, instance):
|
||||
related_models = (
|
||||
@ -938,18 +933,14 @@ class VLANGroupBulkImportView(generic.BulkImportView):
|
||||
|
||||
|
||||
class VLANGroupBulkEditView(generic.BulkEditView):
|
||||
queryset = VLANGroup.objects.annotate(
|
||||
vlan_count=count_related(VLAN, 'group')
|
||||
)
|
||||
queryset = VLANGroup.objects.annotate_utilization().prefetch_related('tags')
|
||||
filterset = filtersets.VLANGroupFilterSet
|
||||
table = tables.VLANGroupTable
|
||||
form = forms.VLANGroupBulkEditForm
|
||||
|
||||
|
||||
class VLANGroupBulkDeleteView(generic.BulkDeleteView):
|
||||
queryset = VLANGroup.objects.annotate(
|
||||
vlan_count=count_related(VLAN, 'group')
|
||||
)
|
||||
queryset = VLANGroup.objects.annotate_utilization().prefetch_related('tags')
|
||||
filterset = filtersets.VLANGroupFilterSet
|
||||
table = tables.VLANGroupTable
|
||||
|
||||
|
@ -25,7 +25,7 @@ from netbox.constants import RQ_QUEUE_DEFAULT, RQ_QUEUE_HIGH, RQ_QUEUE_LOW
|
||||
# Environment setup
|
||||
#
|
||||
|
||||
VERSION = '3.5.5-dev'
|
||||
VERSION = '3.5.6-dev'
|
||||
|
||||
# Hostname
|
||||
HOSTNAME = platform.node()
|
||||
|
BIN
netbox/project-static/dist/netbox.js
vendored
BIN
netbox/project-static/dist/netbox.js
vendored
Binary file not shown.
BIN
netbox/project-static/dist/netbox.js.map
vendored
BIN
netbox/project-static/dist/netbox.js.map
vendored
Binary file not shown.
@ -2,7 +2,7 @@ import Clipboard from 'clipboard';
|
||||
import { getElements } from './util';
|
||||
|
||||
export function initClipboard(): void {
|
||||
for (const element of getElements('a.copy-token', 'button.copy-secret')) {
|
||||
for (const element of getElements('a.copy-content')) {
|
||||
new Clipboard(element);
|
||||
}
|
||||
}
|
||||
|
@ -39,9 +39,7 @@
|
||||
<th scope="row">Path</th>
|
||||
<td>
|
||||
<span class="font-monospace" id="datafile_path">{{ object.path }}</span>
|
||||
<a class="btn btn-sm btn-primary copy-token" data-clipboard-target="#datafile_path" title="Copy to clipboard">
|
||||
<i class="mdi mdi-content-copy"></i>
|
||||
</a>
|
||||
{% copy_content "datafile_path" %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
@ -56,9 +54,7 @@
|
||||
<th scope="row">SHA256 Hash</th>
|
||||
<td>
|
||||
<span class="font-monospace" id="datafile_hash">{{ object.hash }}</span>
|
||||
<a class="btn btn-sm btn-primary copy-token" data-clipboard-target="#datafile_hash" title="Copy to clipboard">
|
||||
<i class="mdi mdi-content-copy"></i>
|
||||
</a>
|
||||
{% copy_content "datafile_hash" %}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
@ -194,12 +194,13 @@
|
||||
<th scope="row">Primary IPv4</th>
|
||||
<td>
|
||||
{% if object.primary_ip4 %}
|
||||
<a href="{{ object.primary_ip4.get_absolute_url }}">{{ object.primary_ip4.address.ip }}</a>
|
||||
<a href="{{ object.primary_ip4.get_absolute_url }}" id="primary_ip4">{{ object.primary_ip4.address.ip }}</a>
|
||||
{% if object.primary_ip4.nat_inside %}
|
||||
(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 %}
|
||||
(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 %}
|
||||
@ -209,12 +210,13 @@
|
||||
<th scope="row">Primary IPv6</th>
|
||||
<td>
|
||||
{% if object.primary_ip6 %}
|
||||
<a href="{{ object.primary_ip6.get_absolute_url }}">{{ object.primary_ip6.address.ip }}</a>
|
||||
<a href="{{ object.primary_ip6.get_absolute_url }}" id="primary_ip6">{{ object.primary_ip6.address.ip }}</a>
|
||||
{% if object.primary_ip6.nat_inside %}
|
||||
(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 %}
|
||||
(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 %}
|
||||
|
@ -31,13 +31,23 @@
|
||||
<tr>
|
||||
<th scope="row">Primary IPv4</th>
|
||||
<td>
|
||||
{{ object.primary_ip4|linkify|placeholder }}
|
||||
{% if object.primary_ip4 %}
|
||||
<a href="{{ object.primary_ip4.get_absolute_url }}" id="primary_ip4">{{ object.primary_ip4 }}</a>
|
||||
{% copy_content "primary_ip4" %}
|
||||
{% else %}
|
||||
<span class="text-muted">—</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Primary IPv6</th>
|
||||
<td>
|
||||
{{ object.primary_ip6|linkify|placeholder }}
|
||||
{% if object.primary_ip6 %}
|
||||
<a href="{{ object.primary_ip6.get_absolute_url }}" id="primary_ip6">{{ object.primary_ip6 }}</a>
|
||||
{% copy_content "primary_ip6" %}
|
||||
{% else %}
|
||||
<span class="text-muted">—</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
|
@ -42,6 +42,10 @@
|
||||
<th scope="row">Permitted VIDs</th>
|
||||
<td>{{ object.min_vid }} - {{ object.max_vid }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Utilization</th>
|
||||
<td>{% utilization_graph object.utilization %}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -8,7 +8,7 @@
|
||||
<div class="col col-md-12">
|
||||
{% if not settings.ALLOW_TOKEN_RETRIEVAL %}
|
||||
<div class="alert alert-danger" role="alert">
|
||||
<i class="mdi mdi-alert"></i> Tokens cannot be retrieved at a later time. You must <a href="#" class="copy-token" data-clipboard-target="#token_id" title="Copy to clipboard">copy the token value</a> below and store it securely.
|
||||
<i class="mdi mdi-alert"></i> Tokens cannot be retrieved at a later time. You must <a href="#" class="copy-content" data-clipboard-target="#token_id" title="Copy to clipboard">copy the token value</a> below and store it securely.
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="card">
|
||||
@ -19,9 +19,7 @@
|
||||
<th scope="row">Key</th>
|
||||
<td>
|
||||
<div class="float-end">
|
||||
<a class="btn btn-sm btn-success copy-token" data-clipboard-target="#token_id" title="Copy to clipboard">
|
||||
<i class="mdi mdi-content-copy"></i>
|
||||
</a>
|
||||
{% copy_content "token_id" %}
|
||||
</div>
|
||||
<div id="token_id">{{ key }}</div>
|
||||
</td>
|
||||
|
@ -46,12 +46,13 @@
|
||||
<th scope="row">Primary IPv4</th>
|
||||
<td>
|
||||
{% if object.primary_ip4 %}
|
||||
<a href="{% url 'ipam:ipaddress' pk=object.primary_ip4.pk %}">{{ object.primary_ip4.address.ip }}</a>
|
||||
<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 %}
|
||||
(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 %}
|
||||
(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 %}
|
||||
@ -61,12 +62,13 @@
|
||||
<th scope="row">Primary IPv6</th>
|
||||
<td>
|
||||
{% if object.primary_ip6 %}
|
||||
<a href="{% url 'ipam:ipaddress' pk=object.primary_ip6.pk %}">{{ object.primary_ip6.address.ip }}</a>
|
||||
<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 %}
|
||||
(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 %}
|
||||
(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 %}
|
||||
|
@ -136,3 +136,8 @@ class ContactAssignment(ChangeLoggedModel):
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('tenancy:contact', args=[self.contact.pk])
|
||||
|
||||
def to_objectchange(self, action):
|
||||
objectchange = super().to_objectchange(action)
|
||||
objectchange.related_object = self.object
|
||||
return objectchange
|
||||
|
@ -12,9 +12,7 @@ ALLOWED_IPS = """{{ value|join:", " }}"""
|
||||
|
||||
COPY_BUTTON = """
|
||||
{% if settings.ALLOW_TOKEN_RETRIEVAL %}
|
||||
<a class="btn btn-sm btn-success copy-token" data-clipboard-target="#token_{{ record.pk }}" title="Copy to clipboard">
|
||||
<i class="mdi mdi-content-copy"></i>
|
||||
</a>
|
||||
{% copy_content record.pk prefix="token_" color="success" %}
|
||||
{% endif %}
|
||||
"""
|
||||
|
||||
|
@ -159,7 +159,9 @@ class ProfileView(LoginRequiredMixin, View):
|
||||
def get(self, request):
|
||||
|
||||
# Compile changelog table
|
||||
changelog = ObjectChange.objects.restrict(request.user, 'view').filter(user=request.user).prefetch_related(
|
||||
changelog = ObjectChange.objects.valid_models().restrict(request.user, 'view').filter(
|
||||
user=request.user
|
||||
).prefetch_related(
|
||||
'changed_object_type'
|
||||
)[:20]
|
||||
changelog_table = ObjectChangeTable(changelog)
|
||||
|
3
netbox/utilities/templates/builtins/copy_content.html
Normal file
3
netbox/utilities/templates/builtins/copy_content.html
Normal file
@ -0,0 +1,3 @@
|
||||
<a class="btn btn-sm {{ color }} copy-content" data-clipboard-target="{{ target }}" title="Copy to clipboard">
|
||||
<i class="mdi mdi-content-copy"></i>
|
||||
</a>
|
@ -6,6 +6,7 @@ from utilities.utils import dict_to_querydict
|
||||
__all__ = (
|
||||
'badge',
|
||||
'checkmark',
|
||||
'copy_content',
|
||||
'customfield_value',
|
||||
'tag',
|
||||
)
|
||||
@ -79,6 +80,17 @@ def checkmark(value, show_false=True, true='Yes', false='No'):
|
||||
}
|
||||
|
||||
|
||||
@register.inclusion_tag('builtins/copy_content.html')
|
||||
def copy_content(target, prefix=None, color='primary'):
|
||||
"""
|
||||
Display a copy button to copy the content of a field.
|
||||
"""
|
||||
return {
|
||||
'target': f'#{prefix or ""}{target}',
|
||||
'color': f'btn-{color}'
|
||||
}
|
||||
|
||||
|
||||
@register.inclusion_tag('builtins/htmx_table.html', takes_context=True)
|
||||
def htmx_table(context, viewname, return_url=None, **kwargs):
|
||||
"""
|
||||
|
@ -1,6 +1,6 @@
|
||||
bleach==6.0.0
|
||||
boto3==1.26.156
|
||||
Django==4.1.9
|
||||
boto3==1.27.1
|
||||
Django==4.1.10
|
||||
django-cors-headers==4.1.0
|
||||
django-debug-toolbar==4.1.0
|
||||
django-filter==23.2
|
||||
@ -11,25 +11,25 @@ django-prometheus==2.3.1
|
||||
django-redis==5.3.0
|
||||
django-rich==1.6.0
|
||||
django-rq==2.8.1
|
||||
django-tables2==2.5.3
|
||||
django-tables2==2.6.0
|
||||
django-taggit==4.0.0
|
||||
django-timezone-field==5.1
|
||||
djangorestframework==3.14.0
|
||||
drf-spectacular==0.26.2
|
||||
drf-spectacular-sidecar==2023.6.1
|
||||
drf-spectacular==0.26.3
|
||||
drf-spectacular-sidecar==2023.7.1
|
||||
dulwich==0.21.5
|
||||
feedparser==6.0.10
|
||||
graphene-django==3.0.0
|
||||
gunicorn==20.1.0
|
||||
Jinja2==3.1.2
|
||||
Markdown==3.3.7
|
||||
mkdocs-material==9.1.16
|
||||
mkdocs-material==9.1.18
|
||||
mkdocstrings[python-legacy]==0.22.0
|
||||
netaddr==0.8.0
|
||||
Pillow==9.5.0
|
||||
Pillow==10.0.0
|
||||
psycopg2-binary==2.9.6
|
||||
PyYAML==6.0
|
||||
sentry-sdk==1.25.1
|
||||
sentry-sdk==1.27.1
|
||||
social-auth-app-django==5.2.0
|
||||
social-auth-core[openidconnect]==4.4.2
|
||||
svgwrite==1.4.3
|
||||
|
Loading…
Reference in New Issue
Block a user