diff --git a/netbox/ipam/models/ip.py b/netbox/ipam/models/ip.py
index 439348152..6d629d89c 100644
--- a/netbox/ipam/models/ip.py
+++ b/netbox/ipam/models/ip.py
@@ -367,6 +367,16 @@ class Prefix(ContactsMixin, GetAvailablePrefixesMixin, CachedScopeMixin, Primary
def get_status_color(self):
return PrefixStatusChoices.colors.get(self.status)
+ @cached_property
+ def aggregate(self):
+ """
+ Return the containing Aggregate for this Prefix, if any.
+ """
+ try:
+ return Aggregate.objects.get(prefix__net_contains_or_equals=str(self.prefix))
+ except Aggregate.DoesNotExist:
+ return None
+
def get_parents(self, include_self=False):
"""
Return all containing Prefixes in the hierarchy.
diff --git a/netbox/ipam/ui/attrs.py b/netbox/ipam/ui/attrs.py
new file mode 100644
index 000000000..cd5cba0f2
--- /dev/null
+++ b/netbox/ipam/ui/attrs.py
@@ -0,0 +1,24 @@
+from django.template.loader import render_to_string
+
+from netbox.ui import attrs
+
+
+class VRFDisplayAttr(attrs.ObjectAttribute):
+ """
+ Renders a VRF reference, displaying 'Global' when no VRF is assigned. Optionally includes
+ the route distinguisher (RD).
+ """
+ template_name = 'ipam/attrs/vrf.html'
+
+ def __init__(self, *args, show_rd=False, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.show_rd = show_rd
+
+ def render(self, obj, context):
+ value = self.get_value(obj)
+ return render_to_string(self.template_name, {
+ **self.get_context(obj, context),
+ 'name': context['name'],
+ 'value': value,
+ 'show_rd': self.show_rd,
+ })
diff --git a/netbox/ipam/ui/panels.py b/netbox/ipam/ui/panels.py
index a2148057a..c47791cc5 100644
--- a/netbox/ipam/ui/panels.py
+++ b/netbox/ipam/ui/panels.py
@@ -2,14 +2,15 @@ from django.contrib.contenttypes.models import ContentType
from django.urls import reverse
from django.utils.translation import gettext_lazy as _
-from netbox.ui import actions, panels
+from netbox.ui import actions, attrs, panels
+
+from .attrs import VRFDisplayAttr
class FHRPGroupAssignmentsPanel(panels.ObjectPanel):
"""
A panel which lists all FHRP group assignments for a given object.
"""
-
template_name = 'ipam/panels/fhrp_groups.html'
title = _('FHRP Groups')
actions = [
@@ -35,3 +36,220 @@ class FHRPGroupAssignmentsPanel(panels.ObjectPanel):
label=_('Assign Group'),
),
]
+
+
+class VRFPanel(panels.ObjectAttributesPanel):
+ rd = attrs.TextAttr('rd', label=_('Route Distinguisher'), style='font-monospace')
+ tenant = attrs.RelatedObjectAttr('tenant', linkify=True, grouped_by='group')
+ enforce_unique = attrs.BooleanAttr('enforce_unique', label=_('Unique IP Space'))
+ description = attrs.TextAttr('description')
+
+
+class RouteTargetPanel(panels.ObjectAttributesPanel):
+ name = attrs.TextAttr('name', style='font-monospace')
+ tenant = attrs.RelatedObjectAttr('tenant', linkify=True, grouped_by='group')
+ description = attrs.TextAttr('description')
+
+
+class RIRPanel(panels.OrganizationalObjectPanel):
+ is_private = attrs.BooleanAttr('is_private', label=_('Private'))
+
+
+class ASNRangePanel(panels.ObjectAttributesPanel):
+ name = attrs.TextAttr('name')
+ rir = attrs.RelatedObjectAttr('rir', linkify=True, label=_('RIR'))
+ range = attrs.TextAttr('range_as_string_with_asdot', label=_('Range'))
+ tenant = attrs.RelatedObjectAttr('tenant', linkify=True, grouped_by='group')
+ description = attrs.TextAttr('description')
+
+
+class ASNPanel(panels.ObjectAttributesPanel):
+ asn = attrs.TextAttr('asn_with_asdot', label=_('AS Number'))
+ rir = attrs.RelatedObjectAttr('rir', linkify=True, label=_('RIR'))
+ tenant = attrs.RelatedObjectAttr('tenant', linkify=True, grouped_by='group')
+ description = attrs.TextAttr('description')
+
+
+class AggregatePanel(panels.ObjectAttributesPanel):
+ family = attrs.TextAttr('family', format_string='IPv{}', label=_('Family'))
+ rir = attrs.RelatedObjectAttr('rir', linkify=True, label=_('RIR'))
+ utilization = attrs.TemplatedAttr(
+ 'prefix',
+ template_name='ipam/aggregate/attrs/utilization.html',
+ label=_('Utilization'),
+ )
+ tenant = attrs.RelatedObjectAttr('tenant', linkify=True, grouped_by='group')
+ date_added = attrs.DateTimeAttr('date_added', spec='date', label=_('Date Added'))
+ description = attrs.TextAttr('description')
+
+
+class RolePanel(panels.OrganizationalObjectPanel):
+ weight = attrs.NumericAttr('weight')
+
+
+class IPRangePanel(panels.ObjectAttributesPanel):
+ family = attrs.TextAttr('family', format_string='IPv{}', label=_('Family'))
+ start_address = attrs.TextAttr('start_address', label=_('Starting Address'))
+ end_address = attrs.TextAttr('end_address', label=_('Ending Address'))
+ size = attrs.NumericAttr('size')
+ mark_populated = attrs.BooleanAttr('mark_populated', label=_('Marked Populated'))
+ mark_utilized = attrs.BooleanAttr('mark_utilized', label=_('Marked Utilized'))
+ utilization = attrs.TemplatedAttr(
+ 'utilization',
+ template_name='ipam/iprange/attrs/utilization.html',
+ label=_('Utilization'),
+ )
+ vrf = VRFDisplayAttr('vrf', label=_('VRF'), show_rd=True)
+ role = attrs.RelatedObjectAttr('role', linkify=True)
+ status = attrs.ChoiceAttr('status')
+ tenant = attrs.RelatedObjectAttr('tenant', linkify=True, grouped_by='group')
+ description = attrs.TextAttr('description')
+
+
+class IPAddressPanel(panels.ObjectAttributesPanel):
+ family = attrs.TextAttr('family', format_string='IPv{}', label=_('Family'))
+ vrf = VRFDisplayAttr('vrf', label=_('VRF'))
+ tenant = attrs.RelatedObjectAttr('tenant', linkify=True, grouped_by='group')
+ status = attrs.ChoiceAttr('status')
+ role = attrs.ChoiceAttr('role')
+ dns_name = attrs.TextAttr('dns_name', label=_('DNS Name'))
+ description = attrs.TextAttr('description')
+ assigned_object = attrs.RelatedObjectAttr(
+ 'assigned_object',
+ linkify=True,
+ grouped_by='parent_object',
+ label=_('Assignment'),
+ )
+ nat_inside = attrs.TemplatedAttr(
+ 'nat_inside',
+ template_name='ipam/ipaddress/attrs/nat_inside.html',
+ label=_('NAT (inside)'),
+ )
+ nat_outside = attrs.TemplatedAttr(
+ 'nat_outside',
+ template_name='ipam/ipaddress/attrs/nat_outside.html',
+ label=_('NAT (outside)'),
+ )
+ is_primary_ip = attrs.BooleanAttr('is_primary_ip', label=_('Primary IP'))
+ is_oob_ip = attrs.BooleanAttr('is_oob_ip', label=_('OOB IP'))
+
+
+class PrefixPanel(panels.ObjectAttributesPanel):
+ family = attrs.TextAttr('family', format_string='IPv{}', label=_('Family'))
+ vrf = VRFDisplayAttr('vrf', label=_('VRF'))
+ tenant = attrs.RelatedObjectAttr('tenant', linkify=True, grouped_by='group')
+ aggregate = attrs.TemplatedAttr(
+ 'aggregate',
+ template_name='ipam/prefix/attrs/aggregate.html',
+ label=_('Aggregate'),
+ )
+ scope = attrs.GenericForeignKeyAttr('scope', linkify=True)
+ vlan = attrs.RelatedObjectAttr('vlan', linkify=True, label=_('VLAN'), grouped_by='group')
+ status = attrs.ChoiceAttr('status')
+ role = attrs.RelatedObjectAttr('role', linkify=True)
+ description = attrs.TextAttr('description')
+ is_pool = attrs.BooleanAttr('is_pool', label=_('Is a pool'))
+
+
+class VLANGroupPanel(panels.ObjectAttributesPanel):
+ name = attrs.TextAttr('name')
+ description = attrs.TextAttr('description')
+ scope = attrs.GenericForeignKeyAttr('scope', linkify=True)
+ vid_ranges = attrs.TemplatedAttr(
+ 'vid_ranges_items',
+ template_name='ipam/vlangroup/attrs/vid_ranges.html',
+ label=_('VLAN IDs'),
+ )
+ utilization = attrs.UtilizationAttr('utilization')
+ tenant = attrs.RelatedObjectAttr('tenant', linkify=True, grouped_by='group')
+
+
+class VLANTranslationPolicyPanel(panels.ObjectAttributesPanel):
+ name = attrs.TextAttr('name')
+ description = attrs.TextAttr('description')
+
+
+class VLANTranslationRulePanel(panels.ObjectAttributesPanel):
+ policy = attrs.RelatedObjectAttr('policy', linkify=True)
+ local_vid = attrs.NumericAttr('local_vid', label=_('Local VID'))
+ remote_vid = attrs.NumericAttr('remote_vid', label=_('Remote VID'))
+ description = attrs.TextAttr('description')
+
+
+class FHRPGroupPanel(panels.ObjectAttributesPanel):
+ protocol = attrs.ChoiceAttr('protocol')
+ group_id = attrs.NumericAttr('group_id', label=_('Group ID'))
+ name = attrs.TextAttr('name')
+ description = attrs.TextAttr('description')
+ member_count = attrs.NumericAttr('member_count', label=_('Members'))
+
+
+class FHRPGroupAuthPanel(panels.ObjectAttributesPanel):
+ title = _('Authentication')
+
+ auth_type = attrs.ChoiceAttr('auth_type', label=_('Authentication Type'))
+ auth_key = attrs.TextAttr('auth_key', label=_('Authentication Key'))
+
+
+class VLANPanel(panels.ObjectAttributesPanel):
+ region = attrs.NestedObjectAttr('site.region', linkify=True, label=_('Region'))
+ site = attrs.RelatedObjectAttr('site', linkify=True)
+ group = attrs.RelatedObjectAttr('group', linkify=True)
+ vid = attrs.NumericAttr('vid', label=_('VLAN ID'))
+ name = attrs.TextAttr('name')
+ tenant = attrs.RelatedObjectAttr('tenant', linkify=True, grouped_by='group')
+ status = attrs.ChoiceAttr('status')
+ role = attrs.RelatedObjectAttr('role', linkify=True)
+ description = attrs.TextAttr('description')
+ qinq_role = attrs.ChoiceAttr('qinq_role', label=_('Q-in-Q Role'))
+ qinq_svlan = attrs.RelatedObjectAttr('qinq_svlan', linkify=True, label=_('Q-in-Q SVLAN'))
+ l2vpn = attrs.RelatedObjectAttr('l2vpn_termination.l2vpn', linkify=True, label=_('L2VPN'))
+
+
+class VLANCustomerVLANsPanel(panels.ObjectsTablePanel):
+ """
+ A panel listing customer VLANs (C-VLANs) for an S-VLAN. Only renders when the VLAN has Q-in-Q
+ role 'svlan'.
+ """
+ def __init__(self):
+ super().__init__(
+ 'ipam.vlan',
+ filters={'qinq_svlan_id': lambda ctx: ctx['object'].pk},
+ title=_('Customer VLANs'),
+ actions=[
+ actions.AddObject(
+ 'ipam.vlan',
+ url_params={
+ 'qinq_role': 'cvlan',
+ 'qinq_svlan': lambda ctx: ctx['object'].pk,
+ },
+ label=_('Add a VLAN'),
+ ),
+ ],
+ )
+
+ def render(self, context):
+ obj = context.get('object')
+ if not obj or obj.qinq_role != 'svlan':
+ return ''
+ return super().render(context)
+
+
+class ServiceTemplatePanel(panels.ObjectAttributesPanel):
+ name = attrs.TextAttr('name')
+ protocol = attrs.ChoiceAttr('protocol')
+ ports = attrs.TextAttr('port_list', label=_('Ports'))
+ description = attrs.TextAttr('description')
+
+
+class ServicePanel(panels.ObjectAttributesPanel):
+ name = attrs.TextAttr('name')
+ parent = attrs.RelatedObjectAttr('parent', linkify=True)
+ protocol = attrs.ChoiceAttr('protocol')
+ ports = attrs.TextAttr('port_list', label=_('Ports'))
+ ip_addresses = attrs.TemplatedAttr(
+ 'ipaddresses',
+ template_name='ipam/service/attrs/ip_addresses.html',
+ label=_('IP Addresses'),
+ )
+ description = attrs.TextAttr('description')
diff --git a/netbox/ipam/views.py b/netbox/ipam/views.py
index cbea4f318..44cba22b2 100644
--- a/netbox/ipam/views.py
+++ b/netbox/ipam/views.py
@@ -9,8 +9,16 @@ from circuits.models import Provider
from dcim.filtersets import InterfaceFilterSet
from dcim.forms import InterfaceFilterForm
from dcim.models import Device, Interface, Site
-from ipam.tables import VLANTranslationRuleTable
+from extras.ui.panels import CustomFieldsPanel, TagsPanel
from netbox.object_actions import AddObject, BulkDelete, BulkEdit, BulkExport, BulkImport
+from netbox.ui import actions, layout
+from netbox.ui.panels import (
+ CommentsPanel,
+ ContextTablePanel,
+ ObjectsTablePanel,
+ RelatedObjectsPanel,
+ TemplatePanel,
+)
from netbox.views import generic
from utilities.query import count_related
from utilities.tables import get_table_ordering
@@ -23,6 +31,7 @@ from . import filtersets, forms, tables
from .choices import PrefixStatusChoices
from .constants import *
from .models import *
+from .ui import panels
from .utils import add_available_vlans, add_requested_prefixes, annotate_ip_space
#
@@ -41,6 +50,27 @@ class VRFListView(generic.ObjectListView):
@register_model_view(VRF)
class VRFView(GetRelatedModelsMixin, generic.ObjectView):
queryset = VRF.objects.all()
+ layout = layout.Layout(
+ layout.Row(
+ layout.Column(
+ panels.VRFPanel(),
+ TagsPanel(),
+ ),
+ layout.Column(
+ RelatedObjectsPanel(),
+ CustomFieldsPanel(),
+ CommentsPanel(),
+ ),
+ ),
+ layout.Row(
+ layout.Column(
+ ContextTablePanel('import_targets_table', title=_('Import route targets')),
+ ),
+ layout.Column(
+ ContextTablePanel('export_targets_table', title=_('Export route targets')),
+ ),
+ ),
+ )
def get_extra_context(self, request, instance):
import_targets_table = tables.RouteTargetTable(
@@ -134,6 +164,50 @@ class RouteTargetListView(generic.ObjectListView):
@register_model_view(RouteTarget)
class RouteTargetView(generic.ObjectView):
queryset = RouteTarget.objects.all()
+ layout = layout.Layout(
+ layout.Row(
+ layout.Column(
+ panels.RouteTargetPanel(),
+ TagsPanel(),
+ ),
+ layout.Column(
+ CustomFieldsPanel(),
+ CommentsPanel(),
+ ),
+ ),
+ layout.Row(
+ layout.Column(
+ ObjectsTablePanel(
+ 'ipam.vrf',
+ filters={'import_target_id': lambda ctx: ctx['object'].pk},
+ title=_('Importing VRFs'),
+ ),
+ ),
+ layout.Column(
+ ObjectsTablePanel(
+ 'ipam.vrf',
+ filters={'export_target_id': lambda ctx: ctx['object'].pk},
+ title=_('Exporting VRFs'),
+ ),
+ ),
+ ),
+ layout.Row(
+ layout.Column(
+ ObjectsTablePanel(
+ 'vpn.l2vpn',
+ filters={'import_target_id': lambda ctx: ctx['object'].pk},
+ title=_('Importing L2VPNs'),
+ ),
+ ),
+ layout.Column(
+ ObjectsTablePanel(
+ 'vpn.l2vpn',
+ filters={'export_target_id': lambda ctx: ctx['object'].pk},
+ title=_('Exporting L2VPNs'),
+ ),
+ ),
+ ),
+ )
@register_model_view(RouteTarget, 'add', detail=False)
@@ -192,6 +266,17 @@ class RIRListView(generic.ObjectListView):
@register_model_view(RIR)
class RIRView(GetRelatedModelsMixin, generic.ObjectView):
queryset = RIR.objects.all()
+ layout = layout.SimpleLayout(
+ left_panels=[
+ panels.RIRPanel(),
+ TagsPanel(),
+ ],
+ right_panels=[
+ RelatedObjectsPanel(),
+ CommentsPanel(),
+ CustomFieldsPanel(),
+ ],
+ )
def get_extra_context(self, request, instance):
return {
@@ -257,6 +342,16 @@ class ASNRangeListView(generic.ObjectListView):
@register_model_view(ASNRange)
class ASNRangeView(generic.ObjectView):
queryset = ASNRange.objects.all()
+ layout = layout.SimpleLayout(
+ left_panels=[
+ panels.ASNRangePanel(),
+ TagsPanel(),
+ ],
+ right_panels=[
+ CommentsPanel(),
+ CustomFieldsPanel(),
+ ],
+ )
@register_model_view(ASNRange, 'asns')
@@ -337,6 +432,17 @@ class ASNListView(generic.ObjectListView):
@register_model_view(ASN)
class ASNView(GetRelatedModelsMixin, generic.ObjectView):
queryset = ASN.objects.all()
+ layout = layout.SimpleLayout(
+ left_panels=[
+ panels.ASNPanel(),
+ TagsPanel(),
+ ],
+ right_panels=[
+ RelatedObjectsPanel(),
+ CustomFieldsPanel(),
+ CommentsPanel(),
+ ],
+ )
def get_extra_context(self, request, instance):
return {
@@ -412,6 +518,16 @@ class AggregateListView(generic.ObjectListView):
@register_model_view(Aggregate)
class AggregateView(generic.ObjectView):
queryset = Aggregate.objects.all()
+ layout = layout.SimpleLayout(
+ left_panels=[
+ panels.AggregatePanel(),
+ ],
+ right_panels=[
+ CustomFieldsPanel(),
+ TagsPanel(),
+ CommentsPanel(),
+ ],
+ )
@register_model_view(Aggregate, 'prefixes')
@@ -506,6 +622,17 @@ class RoleListView(generic.ObjectListView):
@register_model_view(Role)
class RoleView(GetRelatedModelsMixin, generic.ObjectView):
queryset = Role.objects.all()
+ layout = layout.SimpleLayout(
+ left_panels=[
+ panels.RolePanel(),
+ TagsPanel(),
+ ],
+ right_panels=[
+ RelatedObjectsPanel(),
+ CommentsPanel(),
+ CustomFieldsPanel(),
+ ],
+ )
def get_extra_context(self, request, instance):
return {
@@ -569,15 +696,23 @@ class PrefixListView(generic.ObjectListView):
@register_model_view(Prefix)
class PrefixView(generic.ObjectView):
queryset = Prefix.objects.all()
+ layout = layout.SimpleLayout(
+ left_panels=[
+ panels.PrefixPanel(),
+ ],
+ right_panels=[
+ TemplatePanel('ipam/panels/prefix_addressing.html'),
+ CustomFieldsPanel(),
+ TagsPanel(),
+ CommentsPanel(),
+ ],
+ bottom_panels=[
+ ContextTablePanel('duplicate_prefix_table', title=_('Duplicate prefixes')),
+ ContextTablePanel('parent_prefix_table', title=_('Parent prefixes')),
+ ],
+ )
def get_extra_context(self, request, instance):
- try:
- aggregate = Aggregate.objects.restrict(request.user, 'view').get(
- prefix__net_contains_or_equals=str(instance.prefix)
- )
- except Aggregate.DoesNotExist:
- aggregate = None
-
# Parent prefixes table
parent_prefixes = Prefix.objects.restrict(request.user, 'view').filter(
Q(vrf=instance.vrf) | Q(vrf__isnull=True, status=PrefixStatusChoices.STATUS_CONTAINER)
@@ -608,11 +743,12 @@ class PrefixView(generic.ObjectView):
)
duplicate_prefix_table.configure(request)
- return {
- 'aggregate': aggregate,
+ context = {
'parent_prefix_table': parent_prefix_table,
- 'duplicate_prefix_table': duplicate_prefix_table,
}
+ if duplicate_prefixes.exists():
+ context['duplicate_prefix_table'] = duplicate_prefix_table
+ return context
@register_model_view(Prefix, 'prefixes')
@@ -756,6 +892,19 @@ class IPRangeListView(generic.ObjectListView):
@register_model_view(IPRange)
class IPRangeView(generic.ObjectView):
queryset = IPRange.objects.all()
+ layout = layout.SimpleLayout(
+ left_panels=[
+ panels.IPRangePanel(),
+ ],
+ right_panels=[
+ TagsPanel(),
+ CustomFieldsPanel(),
+ CommentsPanel(),
+ ],
+ bottom_panels=[
+ ContextTablePanel('parent_prefixes_table', title=_('Parent prefixes')),
+ ],
+ )
def get_extra_context(self, request, instance):
@@ -853,6 +1002,23 @@ class IPAddressListView(generic.ObjectListView):
@register_model_view(IPAddress)
class IPAddressView(generic.ObjectView):
queryset = IPAddress.objects.prefetch_related('vrf__tenant', 'tenant')
+ layout = layout.SimpleLayout(
+ left_panels=[
+ panels.IPAddressPanel(),
+ TagsPanel(),
+ CustomFieldsPanel(),
+ CommentsPanel(),
+ ],
+ right_panels=[
+ ContextTablePanel('parent_prefixes_table', title=_('Parent prefixes')),
+ ContextTablePanel('duplicate_ips_table', title=_('Duplicate IPs')),
+ ObjectsTablePanel(
+ 'ipam.service',
+ filters={'ip_address_id': lambda ctx: ctx['object'].pk},
+ title=_('Application services'),
+ ),
+ ],
+ )
def get_extra_context(self, request, instance):
# Parent prefixes table
@@ -885,10 +1051,12 @@ class IPAddressView(generic.ObjectView):
duplicate_ips_table = tables.IPAddressTable(duplicate_ips[:10], orderable=False)
duplicate_ips_table.configure(request)
- return {
+ context = {
'parent_prefixes_table': parent_prefixes_table,
- 'duplicate_ips_table': duplicate_ips_table,
}
+ if duplicate_ips.exists():
+ context['duplicate_ips_table'] = duplicate_ips_table
+ return context
@register_model_view(IPAddress, 'add', detail=False)
@@ -1038,6 +1206,17 @@ class VLANGroupListView(generic.ObjectListView):
@register_model_view(VLANGroup)
class VLANGroupView(GetRelatedModelsMixin, generic.ObjectView):
queryset = VLANGroup.objects.annotate_utilization()
+ layout = layout.SimpleLayout(
+ left_panels=[
+ panels.VLANGroupPanel(),
+ TagsPanel(),
+ ],
+ right_panels=[
+ RelatedObjectsPanel(),
+ CommentsPanel(),
+ CustomFieldsPanel(),
+ ],
+ )
def get_extra_context(self, request, instance):
return {
@@ -1125,19 +1304,32 @@ class VLANTranslationPolicyListView(generic.ObjectListView):
@register_model_view(VLANTranslationPolicy)
-class VLANTranslationPolicyView(GetRelatedModelsMixin, generic.ObjectView):
+class VLANTranslationPolicyView(generic.ObjectView):
queryset = VLANTranslationPolicy.objects.all()
-
- def get_extra_context(self, request, instance):
- vlan_translation_table = VLANTranslationRuleTable(
- data=instance.rules.all(),
- orderable=False
- )
- vlan_translation_table.configure(request)
-
- return {
- 'vlan_translation_table': vlan_translation_table,
- }
+ layout = layout.SimpleLayout(
+ left_panels=[
+ panels.VLANTranslationPolicyPanel(),
+ ],
+ right_panels=[
+ TagsPanel(),
+ CustomFieldsPanel(),
+ CommentsPanel(),
+ ],
+ bottom_panels=[
+ ObjectsTablePanel(
+ 'ipam.vlantranslationrule',
+ filters={'policy_id': lambda ctx: ctx['object'].pk},
+ title=_('VLAN translation rules'),
+ actions=[
+ actions.AddObject(
+ 'ipam.vlantranslationrule',
+ url_params={'policy': lambda ctx: ctx['object'].pk},
+ label=_('Add Rule'),
+ ),
+ ],
+ ),
+ ],
+ )
@register_model_view(VLANTranslationPolicy, 'add', detail=False)
@@ -1193,13 +1385,17 @@ class VLANTranslationRuleListView(generic.ObjectListView):
@register_model_view(VLANTranslationRule)
-class VLANTranslationRuleView(GetRelatedModelsMixin, generic.ObjectView):
+class VLANTranslationRuleView(generic.ObjectView):
queryset = VLANTranslationRule.objects.all()
-
- def get_extra_context(self, request, instance):
- return {
- 'related_models': self.get_related_models(request, instance),
- }
+ layout = layout.SimpleLayout(
+ left_panels=[
+ panels.VLANTranslationRulePanel(),
+ ],
+ right_panels=[
+ TagsPanel(),
+ CustomFieldsPanel(),
+ ],
+ )
@register_model_view(VLANTranslationRule, 'add', detail=False)
@@ -1251,7 +1447,36 @@ class FHRPGroupListView(generic.ObjectListView):
@register_model_view(FHRPGroup)
class FHRPGroupView(GetRelatedModelsMixin, generic.ObjectView):
- queryset = FHRPGroup.objects.all()
+ queryset = FHRPGroup.objects.annotate(
+ member_count=count_related(FHRPGroupAssignment, 'group')
+ )
+ layout = layout.SimpleLayout(
+ left_panels=[
+ panels.FHRPGroupPanel(),
+ TagsPanel(),
+ CommentsPanel(),
+ ],
+ right_panels=[
+ panels.FHRPGroupAuthPanel(),
+ RelatedObjectsPanel(),
+ CustomFieldsPanel(),
+ ],
+ bottom_panels=[
+ ObjectsTablePanel(
+ 'ipam.ipaddress',
+ filters={'fhrpgroup_id': lambda ctx: ctx['object'].pk},
+ title=_('Virtual IP addresses'),
+ actions=[
+ actions.AddObject(
+ 'ipam.ipaddress',
+ url_params={'fhrpgroup': lambda ctx: ctx['object'].pk},
+ label=_('Add IP Address'),
+ ),
+ ],
+ ),
+ ContextTablePanel('members_table', title=_('Members')),
+ ],
+ )
def get_extra_context(self, request, instance):
# Get assigned interfaces
@@ -1276,7 +1501,6 @@ class FHRPGroupView(GetRelatedModelsMixin, generic.ObjectView):
),
),
'members_table': members_table,
- 'member_count': FHRPGroupAssignment.objects.filter(group=instance).count(),
}
@@ -1379,17 +1603,35 @@ class VLANListView(generic.ObjectListView):
@register_model_view(VLAN)
class VLANView(generic.ObjectView):
queryset = VLAN.objects.all()
-
- def get_extra_context(self, request, instance):
- prefixes = Prefix.objects.restrict(request.user, 'view').filter(vlan=instance).prefetch_related(
- 'vrf', 'scope', 'role', 'tenant'
- )
- prefix_table = tables.PrefixTable(list(prefixes), exclude=('vlan', 'utilization'), orderable=False)
- prefix_table.configure(request)
-
- return {
- 'prefix_table': prefix_table,
- }
+ layout = layout.SimpleLayout(
+ left_panels=[
+ panels.VLANPanel(),
+ ],
+ right_panels=[
+ CustomFieldsPanel(),
+ TagsPanel(),
+ CommentsPanel(),
+ ],
+ bottom_panels=[
+ ObjectsTablePanel(
+ 'ipam.prefix',
+ filters={'vlan_id': lambda ctx: ctx['object'].pk},
+ title=_('Prefixes'),
+ actions=[
+ actions.AddObject(
+ 'ipam.prefix',
+ url_params={
+ 'tenant': lambda ctx: ctx['object'].tenant.pk if ctx['object'].tenant else None,
+ 'site': lambda ctx: ctx['object'].site.pk if ctx['object'].site else None,
+ 'vlan': lambda ctx: ctx['object'].pk,
+ },
+ label=_('Add a Prefix'),
+ ),
+ ],
+ ),
+ panels.VLANCustomerVLANsPanel(),
+ ],
+ )
@register_model_view(VLAN, 'interfaces')
@@ -1483,6 +1725,16 @@ class ServiceTemplateListView(generic.ObjectListView):
@register_model_view(ServiceTemplate)
class ServiceTemplateView(generic.ObjectView):
queryset = ServiceTemplate.objects.all()
+ layout = layout.SimpleLayout(
+ left_panels=[
+ panels.ServiceTemplatePanel(),
+ ],
+ right_panels=[
+ CustomFieldsPanel(),
+ TagsPanel(),
+ CommentsPanel(),
+ ],
+ )
@register_model_view(ServiceTemplate, 'add', detail=False)
@@ -1539,6 +1791,16 @@ class ServiceListView(generic.ObjectListView):
@register_model_view(Service)
class ServiceView(generic.ObjectView):
queryset = Service.objects.all()
+ layout = layout.SimpleLayout(
+ left_panels=[
+ panels.ServicePanel(),
+ ],
+ right_panels=[
+ CustomFieldsPanel(),
+ TagsPanel(),
+ CommentsPanel(),
+ ],
+ )
def get_extra_context(self, request, instance):
context = {}
diff --git a/netbox/templates/ipam/aggregate.html b/netbox/templates/ipam/aggregate.html
index 54452f1f3..c02c5f1a3 100644
--- a/netbox/templates/ipam/aggregate.html
+++ b/netbox/templates/ipam/aggregate.html
@@ -1,62 +1 @@
{% extends 'ipam/aggregate/base.html' %}
-{% load buttons %}
-{% load helpers %}
-{% load plugins %}
-{% load i18n %}
-
-{% block content %}
-
-
-
-
-
-
- | {% trans "Family" %} |
- IPv{{ object.family }} |
-
-
- | {% trans "RIR" %} |
-
- {{ object.rir }}
- |
-
-
- | {% trans "Utilization" %} |
-
- {% utilization_graph object.get_utilization %}
- |
-
-
- | {% trans "Tenant" %} |
-
- {% if object.tenant.group %}
- {{ object.tenant.group|linkify }} /
- {% endif %}
- {{ object.tenant|linkify|placeholder }}
- |
-
-
- | {% trans "Date Added" %} |
- {{ object.date_added|isodate|placeholder }} |
-
-
- | {% trans "Description" %} |
- {{ object.description|placeholder }} |
-
-
-
- {% plugin_left_page object %}
-
-
- {% include 'inc/panels/custom_fields.html' %}
- {% include 'inc/panels/tags.html' %}
- {% include 'inc/panels/comments.html' %}
- {% plugin_right_page object %}
-
-
-
-
- {% plugin_full_width_page object %}
-
-
-{% endblock %}
diff --git a/netbox/templates/ipam/aggregate/attrs/utilization.html b/netbox/templates/ipam/aggregate/attrs/utilization.html
new file mode 100644
index 000000000..f105ba59c
--- /dev/null
+++ b/netbox/templates/ipam/aggregate/attrs/utilization.html
@@ -0,0 +1,2 @@
+{% load helpers %}
+{% utilization_graph object.get_utilization %}
diff --git a/netbox/templates/ipam/asn.html b/netbox/templates/ipam/asn.html
index 3a54e453b..f10cf5243 100644
--- a/netbox/templates/ipam/asn.html
+++ b/netbox/templates/ipam/asn.html
@@ -1,8 +1,4 @@
{% extends 'generic/object.html' %}
-{% load buttons %}
-{% load helpers %}
-{% load plugins %}
-{% load render_table from django_tables2 %}
{% load i18n %}
{% block breadcrumbs %}
@@ -12,51 +8,3 @@
{{ object.range }}
{% endif %}
{% endblock breadcrumbs %}
-
-{% block content %}
-
-
-
-
-
-
- | {% trans "AS Number" %} |
- {{ object.asn_with_asdot }} |
-
-
- | {% trans "RIR" %} |
-
- {{ object.rir }}
- |
-
-
- | {% trans "Tenant" %} |
-
- {% if object.tenant.group %}
- {{ object.tenant.group|linkify }} /
- {% endif %}
- {{ object.tenant|linkify|placeholder }}
- |
-
-
- | {% trans "Description" %} |
- {{ object.description|placeholder }} |
-
-
-
- {% plugin_left_page object %}
- {% include 'inc/panels/tags.html' %}
-
-
- {% include 'inc/panels/related_objects.html' %}
- {% include 'inc/panels/custom_fields.html' %}
- {% include 'inc/panels/comments.html' %}
- {% plugin_right_page object %}
-
-
-
-
- {% plugin_full_width_page object %}
-
-
-{% endblock content %}
diff --git a/netbox/templates/ipam/asnrange.html b/netbox/templates/ipam/asnrange.html
index eed3630ef..5ad59ca0c 100644
--- a/netbox/templates/ipam/asnrange.html
+++ b/netbox/templates/ipam/asnrange.html
@@ -1,57 +1 @@
{% extends 'ipam/asnrange/base.html' %}
-{% load buttons %}
-{% load helpers %}
-{% load plugins %}
-{% load render_table from django_tables2 %}
-{% load i18n %}
-
-{% block content %}
-
-
-
-
-
-
- | {% trans "Name" %} |
- {{ object.name }} |
-
-
- | {% trans "RIR" %} |
-
- {{ object.rir }}
- |
-
-
- | {% trans "Range" %} |
- {{ object.range_as_string_with_asdot }} |
-
-
- | {% trans "Tenant" %} |
-
- {% if object.tenant.group %}
- {{ object.tenant.group|linkify }} /
- {% endif %}
- {{ object.tenant|linkify|placeholder }}
- |
-
-
- | {% trans "Description" %} |
- {{ object.description|placeholder }} |
-
-
-
- {% plugin_left_page object %}
- {% include 'inc/panels/tags.html' %}
-
-
- {% include 'inc/panels/comments.html' %}
- {% include 'inc/panels/custom_fields.html' %}
- {% plugin_right_page object %}
-
-
-
-
- {% plugin_full_width_page object %}
-
-
-{% endblock content %}
diff --git a/netbox/templates/ipam/attrs/vrf.html b/netbox/templates/ipam/attrs/vrf.html
new file mode 100644
index 000000000..10280ed8a
--- /dev/null
+++ b/netbox/templates/ipam/attrs/vrf.html
@@ -0,0 +1,2 @@
+{% load helpers i18n %}
+{% if value %}{{ value|linkify }}{% if show_rd %} ({{ value.rd }}){% endif %}{% else %}{% trans "Global" %}{% endif %}
diff --git a/netbox/templates/ipam/fhrpgroup.html b/netbox/templates/ipam/fhrpgroup.html
index 9c414398e..835f26881 100644
--- a/netbox/templates/ipam/fhrpgroup.html
+++ b/netbox/templates/ipam/fhrpgroup.html
@@ -1,7 +1,4 @@
{% extends 'generic/object.html' %}
-{% load helpers %}
-{% load plugins %}
-{% load render_table from django_tables2 %}
{% load i18n %}
{# Omit assigned IP addresses from object representation #}
@@ -11,75 +8,3 @@
{{ block.super }}
{{ object.get_protocol_display }}
{% endblock breadcrumbs %}
-
-{% block content %}
-
-
-
-
-
-
- | {% trans "Protocol" %} |
- {{ object.get_protocol_display }} |
-
-
- | {% trans "Group ID" %} |
- {{ object.group_id }} |
-
-
- | {% trans "Name" %} |
- {{ object.name|placeholder }} |
-
-
- | {% trans "Description" %} |
- {{ object.description|placeholder }} |
-
-
- | {% trans "Members" %} |
- {{ member_count }} |
-
-
-
- {% include 'inc/panels/tags.html' %}
- {% include 'inc/panels/comments.html' %}
- {% plugin_left_page object %}
-
-
-
-
-
-
- | {% trans "Authentication Type" %} |
- {{ object.get_auth_type_display|placeholder }} |
-
-
- | {% trans "Authentication Key" %} |
- {{ object.auth_key|placeholder }} |
-
-
-
- {% include 'inc/panels/related_objects.html' %}
- {% include 'inc/panels/custom_fields.html' %}
- {% plugin_right_page object %}
-
-
-
-
-
-
- {% htmx_table 'ipam:ipaddress_list' fhrpgroup_id=object.pk %}
-
- {% include 'inc/panel_table.html' with table=members_table heading='Members' %}
- {% plugin_full_width_page object %}
-
-
-{% endblock %}
diff --git a/netbox/templates/ipam/ipaddress.html b/netbox/templates/ipam/ipaddress.html
index 8b4473192..18045590d 100644
--- a/netbox/templates/ipam/ipaddress.html
+++ b/netbox/templates/ipam/ipaddress.html
@@ -1,129 +1 @@
-{% extends 'generic/object.html' %}
-{% load helpers %}
-{% load plugins %}
-{% load render_table from django_tables2 %}
-{% load i18n %}
-
-{% block content %}
-
-
-
-
-
-
- | {% trans "Family" %} |
- IPv{{ object.family }} |
-
-
- | {% trans "VRF" %} |
-
- {% if object.vrf %}
- {{ object.vrf }}
- {% else %}
- {% trans "Global" %}
- {% endif %}
- |
-
-
- | {% trans "Tenant" %} |
-
- {% if object.tenant.group %}
- {{ object.tenant.group|linkify }} /
- {% endif %}
- {{ object.tenant|linkify|placeholder }}
- |
-
-
- | {% trans "Status" %} |
- {% badge object.get_status_display bg_color=object.get_status_color %} |
-
-
- | {% trans "Role" %} |
-
- {% if object.role %}
- {{ object.get_role_display }}
- {% else %}
- {{ ''|placeholder }}
- {% endif %}
- |
-
-
- | {% trans "DNS Name" %} |
- {{ object.dns_name|placeholder }} |
-
-
- | {% trans "Description" %} |
- {{ object.description|placeholder }} |
-
-
- | {% trans "Assignment" %} |
-
- {% if object.assigned_object %}
- {% if object.assigned_object.parent_object %}
- {{ object.assigned_object.parent_object|linkify }} /
- {% endif %}
- {{ object.assigned_object|linkify }}
- {% else %}
- {{ ''|placeholder }}
- {% endif %}
- |
-
-
- | {% trans "NAT (inside)" %} |
-
- {% if object.nat_inside %}
- {{ object.nat_inside|linkify }}
- {% if object.nat_inside.assigned_object %}
- ({{ object.nat_inside.assigned_object.parent_object|linkify }})
- {% endif %}
- {% else %}
- {{ ''|placeholder }}
- {% endif %}
- |
-
-
- | {% trans "NAT (outside)" %} |
-
- {% for ip in object.nat_outside.all %}
- {{ ip|linkify }}
- {% if ip.assigned_object %}
- ({{ ip.assigned_object.parent_object|linkify }})
- {% endif %}
- {% empty %}
- {{ ''|placeholder }}
- {% endfor %}
- |
-
-
- | Primary IP |
- {% checkmark object.is_primary_ip %} |
-
-
- | OOB IP |
- {% checkmark object.is_oob_ip %} |
-
-
-
- {% include 'inc/panels/tags.html' %}
- {% include 'inc/panels/custom_fields.html' %}
- {% include 'inc/panels/comments.html' %}
- {% plugin_left_page object %}
-
-
- {% include 'inc/panel_table.html' with table=parent_prefixes_table heading='Parent Prefixes' %}
- {% if duplicate_ips_table.rows %}
- {% include 'inc/panel_table.html' with table=duplicate_ips_table heading='Duplicate IPs' panel_class='danger' %}
- {% endif %}
-
-
- {% htmx_table 'ipam:service_list' ip_address_id=object.pk %}
-
- {% plugin_right_page object %}
-
-
-
-
- {% plugin_full_width_page object %}
-
-
-{% endblock %}
+{% extends 'ipam/ipaddress/base.html' %}
diff --git a/netbox/templates/ipam/ipaddress/attrs/assigned_object.html b/netbox/templates/ipam/ipaddress/attrs/assigned_object.html
new file mode 100644
index 000000000..7e585a627
--- /dev/null
+++ b/netbox/templates/ipam/ipaddress/attrs/assigned_object.html
@@ -0,0 +1,2 @@
+{% load helpers %}
+{% if value.parent_object %}{{ value.parent_object|linkify }} / {% endif %}{{ value|linkify }}
diff --git a/netbox/templates/ipam/ipaddress/attrs/nat_inside.html b/netbox/templates/ipam/ipaddress/attrs/nat_inside.html
new file mode 100644
index 000000000..17dd6f8ea
--- /dev/null
+++ b/netbox/templates/ipam/ipaddress/attrs/nat_inside.html
@@ -0,0 +1,2 @@
+{% load helpers %}
+{{ value|linkify }}{% if value.assigned_object %} ({{ value.assigned_object.parent_object|linkify }}){% endif %}
diff --git a/netbox/templates/ipam/ipaddress/attrs/nat_outside.html b/netbox/templates/ipam/ipaddress/attrs/nat_outside.html
new file mode 100644
index 000000000..73dd72035
--- /dev/null
+++ b/netbox/templates/ipam/ipaddress/attrs/nat_outside.html
@@ -0,0 +1,2 @@
+{% load helpers %}
+{% for ip in value.all %}{{ ip|linkify }}{% if ip.assigned_object %} ({{ ip.assigned_object.parent_object|linkify }}){% endif %}
{% empty %}—{% endfor %}
diff --git a/netbox/templates/ipam/iprange.html b/netbox/templates/ipam/iprange.html
index c4026848b..7fcb860c9 100644
--- a/netbox/templates/ipam/iprange.html
+++ b/netbox/templates/ipam/iprange.html
@@ -1,98 +1 @@
{% extends 'ipam/iprange/base.html' %}
-{% load helpers %}
-{% load plugins %}
-{% load i18n %}
-
-{% block content %}
-
-
-
-
-
-
- | {% trans "Family" %} |
- IPv{{ object.family }} |
-
-
- | {% trans "Starting Address" %} |
- {{ object.start_address }} |
-
-
- | {% trans "Ending Address" %} |
- {{ object.end_address }} |
-
-
- | {% trans "Size" %} |
- {{ object.size }} |
-
-
- | {% trans "Marked Populated" %} |
- {% checkmark object.mark_populated %} |
-
-
- | {% trans "Marked Utilized" %} |
- {% checkmark object.mark_utilized %} |
-
-
- | {% trans "Utilization" %} |
-
- {% if object.mark_utilized %}
- {% utilization_graph 100 warning_threshold=0 danger_threshold=0 %}
- {% else %}
- {% utilization_graph object.utilization %}
- {% endif %}
- |
-
-
- | {% trans "VRF" %} |
-
- {% if object.vrf %}
- {{ object.vrf|linkify }} ({{ object.vrf.rd }})
- {% else %}
- {% trans "Global" %}
- {% endif %}
- |
-
-
- | {% trans "Role" %} |
- {{ object.role|linkify|placeholder }} |
-
-
- | {% trans "Status" %} |
- {% badge object.get_status_display bg_color=object.get_status_color %} |
-
-
- | {% trans "Tenant" %} |
-
- {% if object.tenant.group %}
- {{ object.tenant.group|linkify }} /
- {% endif %}
- {{ object.tenant|linkify|placeholder }}
- |
-
-
- | {% trans "Description" %} |
- {{ object.description|placeholder }} |
-
-
-
- {% plugin_left_page object %}
-
-
- {% include 'inc/panels/tags.html' %}
- {% include 'inc/panels/custom_fields.html' %}
- {% include 'inc/panels/comments.html' %}
- {% plugin_right_page object %}
-
-
-
-
- {% include 'inc/panel_table.html' with table=parent_prefixes_table heading='Parent Prefixes' %}
-
-
-
-
- {% plugin_full_width_page object %}
-
-
-{% endblock %}
diff --git a/netbox/templates/ipam/iprange/attrs/utilization.html b/netbox/templates/ipam/iprange/attrs/utilization.html
new file mode 100644
index 000000000..ed9579ed6
--- /dev/null
+++ b/netbox/templates/ipam/iprange/attrs/utilization.html
@@ -0,0 +1,6 @@
+{% load helpers %}
+{% if object.mark_utilized %}
+ {% utilization_graph 100 warning_threshold=0 danger_threshold=0 %}
+{% else %}
+ {% utilization_graph value %}
+{% endif %}
diff --git a/netbox/templates/ipam/panels/prefix_addressing.html b/netbox/templates/ipam/panels/prefix_addressing.html
new file mode 100644
index 000000000..ff74d6c0a
--- /dev/null
+++ b/netbox/templates/ipam/panels/prefix_addressing.html
@@ -0,0 +1,62 @@
+{% load humanize helpers i18n %}
+
+
+
+
+ | {% trans "Utilization" %} |
+
+ {% if object.mark_utilized %}
+ {% utilization_graph 100 warning_threshold=0 danger_threshold=0 %}
+ ({% trans "Marked fully utilized" %})
+ {% else %}
+ {% utilization_graph object.get_utilization %}
+ {% endif %}
+ |
+
+ {% with child_ip_count=object.get_child_ips.count %}
+
+ | {% trans "Child IPs" %} |
+
+ {{ child_ip_count }}
+ |
+
+ {% endwith %}
+ {% with available_count=object.get_available_ips.size %}
+
+ | {% trans "Available IPs" %} |
+
+ {% if available_count > 1000000 %}
+ {{ available_count|intword }}
+ {% else %}
+ {{ available_count|intcomma }}
+ {% endif %}
+ |
+
+ {% endwith %}
+
+ | {% trans "First available IP" %} |
+
+ {% with first_available_ip=object.get_first_available_ip %}
+ {% if first_available_ip %}
+ {% if perms.ipam.add_ipaddress %}
+ {{ first_available_ip }}
+ {% else %}
+ {{ first_available_ip }}
+ {% endif %}
+ {% else %}
+ {{ ''|placeholder }}
+ {% endif %}
+ {% endwith %}
+ |
+
+
+
diff --git a/netbox/templates/ipam/prefix.html b/netbox/templates/ipam/prefix.html
index a582561bd..dbee0253d 100644
--- a/netbox/templates/ipam/prefix.html
+++ b/netbox/templates/ipam/prefix.html
@@ -1,202 +1 @@
{% extends 'ipam/prefix/base.html' %}
-{% load humanize %}
-{% load helpers %}
-{% load plugins %}
-{% load i18n %}
-{% load mptt %}
-
-{% block content %}
-
-
-
-
-
-
- | {% trans "Family" %} |
- IPv{{ object.family }} |
-
-
- | {% trans "VRF" %} |
-
- {% if object.vrf %}
- {{ object.vrf }}
- {% else %}
- {% trans "Global" %}
- {% endif %}
- |
-
-
- | {% trans "Tenant" %} |
-
- {% if object.tenant.group %}
- {{ object.tenant.group|linkify }} /
- {% endif %}
- {{ object.tenant|linkify|placeholder }}
- |
-
-
- | {% trans "Aggregate" %} |
-
- {% if aggregate %}
- {{ aggregate.prefix }} ({{ aggregate.rir }})
- {% else %}
- {{ ''|placeholder }}
- {% endif %}
- |
-
-
- | {% trans "Scope" %} |
- {% if object.scope %}
- {{ object.scope|linkify }} ({% trans object.scope_type.name %}) |
- {% else %}
- {{ ''|placeholder }} |
- {% endif %}
-
-
- | {% trans "VLAN" %} |
-
- {% if object.vlan %}
- {% if object.vlan.group %}
- {{ object.vlan.group|linkify }} /
- {% endif %}
- {{ object.vlan|linkify }}
- {% else %}
- {{ ''|placeholder }}
- {% endif %}
- |
-
-
- | {% trans "Status" %} |
- {% badge object.get_status_display bg_color=object.get_status_color %} |
-
-
- | {% trans "Role" %} |
- {{ object.role|linkify|placeholder }} |
-
-
- | {% trans "Description" %} |
- {{ object.description|placeholder }} |
-
-
- | {% trans "Is a pool" %} |
- {% checkmark object.is_pool %} |
-
-
-
- {% plugin_left_page object %}
-
-
-
-
-
-
- | {% trans "Utilization" %} |
-
- {% if object.mark_utilized %}
- {% utilization_graph 100 warning_threshold=0 danger_threshold=0 %}
- ({% trans "Marked fully utilized" %})
- {% else %}
- {% utilization_graph object.get_utilization %}
- {% endif %}
- |
-
- {% with child_ip_count=object.get_child_ips.count %}
-
- | {% trans "Child IPs" %} |
-
- {{ child_ip_count }}
- |
-
- {% endwith %}
- {% with available_count=object.get_available_ips.size %}
-
- | {% trans "Available IPs" %} |
-
- {# Use human-friendly words for counts greater than one million #}
- {% if available_count > 1000000 %}
- {{ available_count|intword }}
- {% else %}
- {{ available_count|intcomma }}
- {% endif %}
- |
-
- {% endwith %}
-
- | {% trans "First available IP" %} |
-
- {% with first_available_ip=object.get_first_available_ip %}
- {% if first_available_ip %}
- {% if perms.ipam.add_ipaddress %}
- {{ first_available_ip }}
- {% else %}
- {{ first_available_ip }}
- {% endif %}
- {% else %}
- {{ ''|placeholder }}
- {% endif %}
- {% endwith %}
- |
-
-
-
- {% include 'inc/panels/custom_fields.html' %}
- {% include 'inc/panels/tags.html' %}
- {% include 'inc/panels/comments.html' %}
- {% plugin_right_page object %}
-
-
-
-
- {% if duplicate_prefix_table.rows %}
- {% include 'inc/panel_table.html' with table=duplicate_prefix_table heading='Duplicate Prefixes' %}
- {% endif %}
- {% include 'inc/panel_table.html' with table=parent_prefix_table heading='Parent Prefixes' %}
- {% plugin_full_width_page object %}
-
-
-{% endblock %}
-
-{% block modals %}
- {{ block.super }}
- {% if object.prefix.version == 4 %}
-
-
-
-
-
-
-
- | {% trans "Network Address" %} |
- {{ object.prefix.network }} |
-
-
- | {% trans "Network Mask" %} |
- {{ object.prefix.netmask }} |
-
-
- | {% trans "Wildcard Mask" %} |
- {{ object.prefix.hostmask }} |
-
-
- | {% trans "Broadcast Address" %} |
- {{ object.prefix.broadcast }} |
-
-
-
-
-
-
- {% endif %}
-{% endblock %}
diff --git a/netbox/templates/ipam/prefix/attrs/aggregate.html b/netbox/templates/ipam/prefix/attrs/aggregate.html
new file mode 100644
index 000000000..924993f4e
--- /dev/null
+++ b/netbox/templates/ipam/prefix/attrs/aggregate.html
@@ -0,0 +1,2 @@
+{% load helpers %}
+{{ value|linkify }} ({{ value.rir }})
diff --git a/netbox/templates/ipam/prefix/base.html b/netbox/templates/ipam/prefix/base.html
index 7ac307014..9d7f60cb7 100644
--- a/netbox/templates/ipam/prefix/base.html
+++ b/netbox/templates/ipam/prefix/base.html
@@ -1,6 +1,7 @@
{% extends 'generic/object.html' %}
{% load buttons %}
{% load helpers %}
+{% load i18n %}
{% block breadcrumbs %}
{{ block.super }}
@@ -8,3 +9,39 @@
{{ object.vrf }}
{% endif %}
{% endblock %}
+
+{% block modals %}
+ {{ block.super }}
+ {% if object.prefix.version == 4 %}
+
+
+
+
+
+
+
+ | {% trans "Network Address" %} |
+ {{ object.prefix.network }} |
+
+
+ | {% trans "Network Mask" %} |
+ {{ object.prefix.netmask }} |
+
+
+ | {% trans "Wildcard Mask" %} |
+ {{ object.prefix.hostmask }} |
+
+
+ | {% trans "Broadcast Address" %} |
+ {{ object.prefix.broadcast }} |
+
+
+
+
+
+
+ {% endif %}
+{% endblock modals %}
diff --git a/netbox/templates/ipam/rir.html b/netbox/templates/ipam/rir.html
index a9a1b7849..41f31de93 100644
--- a/netbox/templates/ipam/rir.html
+++ b/netbox/templates/ipam/rir.html
@@ -1,7 +1,4 @@
{% extends 'generic/object.html' %}
-{% load helpers %}
-{% load plugins %}
-{% load render_table from django_tables2 %}
{% load i18n %}
{% block extra_controls %}
@@ -11,40 +8,3 @@
{% endif %}
{% endblock extra_controls %}
-
-{% block content %}
-
-
-
-
-
-
- | {% trans "Name" %} |
- {{ object.name }} |
-
-
- | {% trans "Description" %} |
- {{ object.description|placeholder }} |
-
-
- | {% trans "Private" %} |
- {% checkmark object.is_private %} |
-
-
-
- {% include 'inc/panels/tags.html' %}
- {% plugin_left_page object %}
-
-
- {% include 'inc/panels/related_objects.html' %}
- {% include 'inc/panels/comments.html' %}
- {% include 'inc/panels/custom_fields.html' %}
- {% plugin_right_page object %}
-
-
-
-
- {% plugin_full_width_page object %}
-
-
-{% endblock %}
diff --git a/netbox/templates/ipam/role.html b/netbox/templates/ipam/role.html
index 8b780d992..b7763170a 100644
--- a/netbox/templates/ipam/role.html
+++ b/netbox/templates/ipam/role.html
@@ -1,7 +1,4 @@
{% extends 'generic/object.html' %}
-{% load helpers %}
-{% load plugins %}
-{% load render_table from django_tables2 %}
{% load i18n %}
{% block extra_controls %}
@@ -11,40 +8,3 @@
{% endif %}
{% endblock extra_controls %}
-
-{% block content %}
-
-
-
-
-
-
- | {% trans "Name" %} |
- {{ object.name }} |
-
-
- | {% trans "Description" %} |
- {{ object.description|placeholder }} |
-
-
- | {% trans "Weight" %} |
- {{ object.weight }} |
-
-
-
- {% include 'inc/panels/tags.html' %}
- {% plugin_left_page object %}
-
-
- {% include 'inc/panels/related_objects.html' %}
- {% include 'inc/panels/comments.html' %}
- {% include 'inc/panels/custom_fields.html' %}
- {% plugin_right_page object %}
-
-
-
-
- {% plugin_full_width_page object %}
-
-
-{% endblock %}
diff --git a/netbox/templates/ipam/routetarget.html b/netbox/templates/ipam/routetarget.html
index 2f7d8e806..f15e1d050 100644
--- a/netbox/templates/ipam/routetarget.html
+++ b/netbox/templates/ipam/routetarget.html
@@ -1,68 +1 @@
{% extends 'generic/object.html' %}
-{% load helpers %}
-{% load plugins %}
-{% load i18n %}
-
-{% block content %}
-
-
-
-
-
-
- | {% trans "Name" %} |
- {{ object.name }} |
-
-
- | {% trans "Tenant" %} |
- {{ object.tenant|linkify|placeholder }} |
-
-
- | {% trans "Description" %} |
- {{ object.description|placeholder }} |
-
-
-
- {% include 'inc/panels/tags.html' %}
- {% plugin_left_page object %}
-
-
- {% include 'inc/panels/custom_fields.html' %}
- {% include 'inc/panels/comments.html' %}
- {% plugin_right_page object %}
-
-
-
-
-
-
- {% htmx_table 'ipam:vrf_list' import_target_id=object.pk %}
-
-
-
-
-
- {% htmx_table 'ipam:vrf_list' export_target_id=object.pk %}
-
-
-
-
-
-
-
- {% htmx_table 'vpn:l2vpn_list' import_target_id=object.pk %}
-
-
-
-
-
- {% htmx_table 'vpn:l2vpn_list' export_target_id=object.pk %}
-
-
-
-
-
- {% plugin_full_width_page object %}
-
-
-{% endblock %}
diff --git a/netbox/templates/ipam/service.html b/netbox/templates/ipam/service.html
index 2a504485b..18aac8b08 100644
--- a/netbox/templates/ipam/service.html
+++ b/netbox/templates/ipam/service.html
@@ -1,8 +1,4 @@
{% extends 'generic/object.html' %}
-{% load buttons %}
-{% load helpers %}
-{% load perms %}
-{% load plugins %}
{% load i18n %}
{% block breadcrumbs %}
@@ -14,58 +10,4 @@
{% endif %}
-{% endblock %}
-
-{% block content %}
-
-
-
-
-
-
- | {% trans "Name" %} |
- {{ object.name }} |
-
-
- | {% trans "Parent" %} |
- {{ object.parent|linkify }} |
-
-
- | {% trans "Protocol" %} |
- {{ object.get_protocol_display }} |
-
-
- | {% trans "Ports" %} |
- {{ object.port_list }} |
-
-
- | {% trans "IP Addresses" %} |
-
- {% for ipaddress in object.ipaddresses.all %}
- {{ ipaddress|linkify }}
- {% empty %}
- {{ ''|placeholder }}
- {% endfor %}
- |
-
-
- | {% trans "Description" %} |
- {{ object.description|placeholder }} |
-
-
-
- {% plugin_left_page object %}
-
-
- {% include 'inc/panels/custom_fields.html' %}
- {% include 'inc/panels/tags.html' %}
- {% include 'inc/panels/comments.html' %}
- {% plugin_right_page object %}
-
-
-
-
- {% plugin_full_width_page object %}
-
-
-{% endblock %}
+{% endblock breadcrumbs %}
diff --git a/netbox/templates/ipam/service/attrs/ip_addresses.html b/netbox/templates/ipam/service/attrs/ip_addresses.html
new file mode 100644
index 000000000..ce0516c69
--- /dev/null
+++ b/netbox/templates/ipam/service/attrs/ip_addresses.html
@@ -0,0 +1,2 @@
+{% load helpers %}
+{% for ipaddress in value.all %}{{ ipaddress|linkify }}
{% empty %}—{% endfor %}
diff --git a/netbox/templates/ipam/servicetemplate.html b/netbox/templates/ipam/servicetemplate.html
index 5c3e53621..f15e1d050 100644
--- a/netbox/templates/ipam/servicetemplate.html
+++ b/netbox/templates/ipam/servicetemplate.html
@@ -1,46 +1 @@
{% extends 'generic/object.html' %}
-{% load buttons %}
-{% load helpers %}
-{% load perms %}
-{% load plugins %}
-{% load i18n %}
-
-{% block content %}
-
-
-
-
-
-
- | {% trans "Name" %} |
- {{ object.name }} |
-
-
- | {% trans "Protocol" %} |
- {{ object.get_protocol_display }} |
-
-
- | {% trans "Ports" %} |
- {{ object.port_list }} |
-
-
- | {% trans "Description" %} |
- {{ object.description|placeholder }} |
-
-
-
- {% plugin_left_page object %}
-
-
- {% include 'inc/panels/custom_fields.html' %}
- {% include 'inc/panels/tags.html' %}
- {% include 'inc/panels/comments.html' %}
- {% plugin_right_page object %}
-
-
-
-
- {% plugin_full_width_page object %}
-
-
-{% endblock %}
diff --git a/netbox/templates/ipam/vlan.html b/netbox/templates/ipam/vlan.html
index 72f5058aa..f158586cd 100644
--- a/netbox/templates/ipam/vlan.html
+++ b/netbox/templates/ipam/vlan.html
@@ -1,129 +1 @@
{% extends 'ipam/vlan/base.html' %}
-{% load helpers %}
-{% load render_table from django_tables2 %}
-{% load plugins %}
-{% load i18n %}
-{% load mptt %}
-
-{% block content %}
-
-
-
-
-
- {% if object.site.region %}
-
- | {% trans "Region" %} |
-
- {% nested_tree object.site.region %}
- |
-
- {% endif %}
-
- | {% trans "Site" %} |
- {{ object.site|linkify|placeholder }} |
-
-
- | {% trans "Group" %} |
- {{ object.group|linkify|placeholder }} |
-
-
- | {% trans "VLAN ID" %} |
- {{ object.vid }} |
-
-
- | {% trans "Name" %} |
- {{ object.name }} |
-
-
- | {% trans "Tenant" %} |
-
- {% if object.tenant.group %}
- {{ object.tenant.group|linkify }} /
- {% endif %}
- {{ object.tenant|linkify|placeholder }}
- |
-
-
- | {% trans "Status" %} |
- {% badge object.get_status_display bg_color=object.get_status_color %} |
-
-
- | {% trans "Role" %} |
-
- {% if object.role %}
- {{ object.role }}
- {% else %}
- {{ ''|placeholder }}
- {% endif %}
- |
-
-
- | {% trans "Description" %} |
- {{ object.description|placeholder }} |
-
-
- | {% trans "Q-in-Q Role" %} |
-
- {% if object.qinq_role %}
- {% badge object.get_qinq_role_display bg_color=object.get_qinq_role_color %}
- {% else %}
- {{ ''|placeholder }}
- {% endif %}
- |
-
- {% if object.qinq_role == 'cvlan' %}
-
- | {% trans "Q-in-Q SVLAN" %} |
- {{ object.qinq_svlan|linkify|placeholder }} |
-
- {% endif %}
-
- | {% trans "L2VPN" %} |
- {{ object.l2vpn_termination.l2vpn|linkify|placeholder }} |
-
-
-
- {% plugin_left_page object %}
-
-
- {% include 'inc/panels/custom_fields.html' %}
- {% include 'inc/panels/tags.html' %}
- {% include 'inc/panels/comments.html' %}
- {% plugin_right_page object %}
-
-
-
-
-
-
- {% htmx_table 'ipam:prefix_list' vlan_id=object.pk %}
-
- {% if object.qinq_role == 'svlan' %}
-
-
- {% htmx_table 'ipam:vlan_list' qinq_svlan_id=object.pk %}
-
- {% endif %}
- {% plugin_full_width_page object %}
-
-
-{% endblock %}
diff --git a/netbox/templates/ipam/vlangroup.html b/netbox/templates/ipam/vlangroup.html
index 86a12dfe0..43908db2c 100644
--- a/netbox/templates/ipam/vlangroup.html
+++ b/netbox/templates/ipam/vlangroup.html
@@ -1,7 +1,5 @@
{% extends 'generic/object.html' %}
{% load helpers %}
-{% load plugins %}
-{% load render_table from django_tables2 %}
{% load i18n %}
{% block breadcrumbs %}
@@ -18,53 +16,4 @@
{% trans "Add VLAN" %}
{% endif %}
-{% endblock %}
-
-{% block content %}
-
-
-
-
-
-
- | {% trans "Name" %} |
- {{ object.name }} |
-
-
- | {% trans "Description" %} |
- {{ object.description|placeholder }} |
-
-
- | {% trans "Scope" %} |
- {{ object.scope|linkify|placeholder }} |
-
-
- | {% trans "VLAN IDs" %} |
- {{ object.vid_ranges_items|join:", " }} |
-
-
- | Utilization |
- {% utilization_graph object.utilization %} |
-
-
- | {% trans "Tenant" %} |
-
- {% if object.tenant.group %}
- {{ object.tenant.group|linkify }} /
- {% endif %}
- {{ object.tenant|linkify|placeholder }}
- |
-
-
-
- {% include 'inc/panels/tags.html' %}
- {% plugin_left_page object %}
-
-
- {% include 'inc/panels/related_objects.html' %}
- {% include 'inc/panels/comments.html' %}
- {% include 'inc/panels/custom_fields.html' %}
- {% plugin_right_page object %}
-
-
-{% endblock %}
+{% endblock extra_controls %}
diff --git a/netbox/templates/ipam/vlangroup/attrs/vid_ranges.html b/netbox/templates/ipam/vlangroup/attrs/vid_ranges.html
new file mode 100644
index 000000000..9ceaf63b1
--- /dev/null
+++ b/netbox/templates/ipam/vlangroup/attrs/vid_ranges.html
@@ -0,0 +1 @@
+{{ value|join:", " }}
diff --git a/netbox/templates/ipam/vlantranslationpolicy.html b/netbox/templates/ipam/vlantranslationpolicy.html
index 399054881..f15e1d050 100644
--- a/netbox/templates/ipam/vlantranslationpolicy.html
+++ b/netbox/templates/ipam/vlantranslationpolicy.html
@@ -1,65 +1 @@
{% extends 'generic/object.html' %}
-{% load helpers %}
-{% load plugins %}
-{% load render_table from django_tables2 %}
-{% load i18n %}
-
-{% block content %}
-
-
-
-
-
-
- | {% trans "Name" %} |
- {{ object.name|placeholder }} |
-
-
- | {% trans "Description" %} |
- {{ object.description|placeholder }} |
-
-
- | {% trans "Rules" %} |
-
- {% if object.rules.count %}
- {{ object.rules.count }}
- {% else %}
- 0
- {% endif %}
- |
-
-
-
- {% plugin_left_page object %}
-
-
- {% include 'inc/panels/tags.html' %}
- {% include 'inc/panels/custom_fields.html' %}
- {% include 'inc/panels/comments.html' %}
- {% plugin_right_page object %}
-
-
-
-
-
-
- {% htmx_table 'ipam:vlantranslationrule_list' policy_id=object.pk %}
-
-
-
-
-
- {% plugin_full_width_page object %}
-
-
-{% endblock %}
diff --git a/netbox/templates/ipam/vlantranslationrule.html b/netbox/templates/ipam/vlantranslationrule.html
index 042ade7fc..f15e1d050 100644
--- a/netbox/templates/ipam/vlantranslationrule.html
+++ b/netbox/templates/ipam/vlantranslationrule.html
@@ -1,45 +1 @@
{% extends 'generic/object.html' %}
-{% load helpers %}
-{% load plugins %}
-{% load render_table from django_tables2 %}
-{% load i18n %}
-
-{% block content %}
-
-
-
-
-
-
- | {% trans "Policy" %} |
- {{ object.policy|linkify }} |
-
-
- | {% trans "Local VID" %} |
- {{ object.local_vid }} |
-
-
- | {% trans "Remote VID" %} |
- {{ object.remote_vid }} |
-
-
- | {% trans "Description" %} |
- {{ object.description }} |
-
-
-
- {% plugin_left_page object %}
-
-
- {% include 'inc/panels/tags.html' %}
- {% include 'inc/panels/custom_fields.html' %}
- {% include 'inc/panels/comments.html' %}
- {% plugin_right_page object %}
-
-
-
-
- {% plugin_full_width_page object %}
-
-
-{% endblock %}
diff --git a/netbox/templates/ipam/vrf.html b/netbox/templates/ipam/vrf.html
index e632e827d..f15e1d050 100644
--- a/netbox/templates/ipam/vrf.html
+++ b/netbox/templates/ipam/vrf.html
@@ -1,61 +1 @@
{% extends 'generic/object.html' %}
-{% load buttons %}
-{% load helpers %}
-{% load plugins %}
-{% load i18n %}
-
-{% block title %}{% trans "VRF" %} {{ object }}{% endblock %}
-
-{% block content %}
-
-
-
-
-
-
- | {% trans "Route Distinguisher" %} |
- {{ object.rd|placeholder }} |
-
-
- | {% trans "Tenant" %} |
-
- {% if object.tenant.group %}
- {{ object.tenant.group|linkify }} /
- {% endif %}
- {{ object.tenant|linkify|placeholder }}
- |
-
-
- | {% trans "Unique IP Space" %} |
- {% checkmark object.enforce_unique %} |
-
-
- | {% trans "Description" %} |
- {{ object.description|placeholder }} |
-
-
-
- {% include 'inc/panels/tags.html' %}
- {% plugin_left_page object %}
-
-
- {% include 'inc/panels/related_objects.html' %}
- {% include 'inc/panels/custom_fields.html' %}
- {% include 'inc/panels/comments.html' %}
- {% plugin_right_page object %}
-
-
-
-
- {% include 'inc/panel_table.html' with table=import_targets_table heading="Import Route Targets" %}
-
-
- {% include 'inc/panel_table.html' with table=export_targets_table heading="Export Route Targets" %}
-
-
-
-
- {% plugin_full_width_page object %}
-
-
-{% endblock %}