jeremystretch 3 лет назад
Родитель
Сommit
1fc8de85a3

+ 0 - 4
netbox/dcim/urls.py

@@ -233,10 +233,6 @@ urlpatterns = [
     path('devices/<int:pk>/', views.DeviceView.as_view(), name='device'),
     path('devices/<int:pk>/edit/', views.DeviceEditView.as_view(), name='device_edit'),
     path('devices/<int:pk>/delete/', views.DeviceDeleteView.as_view(), name='device_delete'),
-    path('devices/<int:pk>/inventory/', views.DeviceInventoryView.as_view(), name='device_inventory'),
-    path('devices/<int:pk>/status/', views.DeviceStatusView.as_view(), name='device_status'),
-    path('devices/<int:pk>/lldp-neighbors/', views.DeviceLLDPNeighborsView.as_view(), name='device_lldp_neighbors'),
-    path('devices/<int:pk>/config/', views.DeviceConfigView.as_view(), name='device_config'),
     path('devices/<int:pk>/', include(get_model_urls('dcim', 'device'))),
 
     # Modules

+ 89 - 95
netbox/dcim/views.py

@@ -860,8 +860,7 @@ class DeviceTypeConsolePortsView(DeviceTypeComponentsView):
     tab = ViewTab(
         label=_('Console Ports'),
         badge=lambda obj: obj.consoleporttemplates.count(),
-        permission='dcim.view_consoleporttemplate',
-        always_display=False
+        permission='dcim.view_consoleporttemplate'
     )
 
 
@@ -874,8 +873,7 @@ class DeviceTypeConsoleServerPortsView(DeviceTypeComponentsView):
     tab = ViewTab(
         label=_('Console Server Ports'),
         badge=lambda obj: obj.consoleserverporttemplates.count(),
-        permission='dcim.view_consoleserverporttemplate',
-        always_display=False
+        permission='dcim.view_consoleserverporttemplate'
     )
 
 
@@ -888,8 +886,7 @@ class DeviceTypePowerPortsView(DeviceTypeComponentsView):
     tab = ViewTab(
         label=_('Power Ports'),
         badge=lambda obj: obj.powerporttemplates.count(),
-        permission='dcim.view_powerporttemplate',
-        always_display=False
+        permission='dcim.view_powerporttemplate'
     )
 
 
@@ -902,8 +899,7 @@ class DeviceTypePowerOutletsView(DeviceTypeComponentsView):
     tab = ViewTab(
         label=_('Power Outlets'),
         badge=lambda obj: obj.poweroutlettemplates.count(),
-        permission='dcim.view_poweroutlettemplate',
-        always_display=False
+        permission='dcim.view_poweroutlettemplate'
     )
 
 
@@ -916,8 +912,7 @@ class DeviceTypeInterfacesView(DeviceTypeComponentsView):
     tab = ViewTab(
         label=_('Interfaces'),
         badge=lambda obj: obj.interfacetemplates.count(),
-        permission='dcim.view_interfacetemplate',
-        always_display=False
+        permission='dcim.view_interfacetemplate'
     )
 
 
@@ -930,8 +925,7 @@ class DeviceTypeFrontPortsView(DeviceTypeComponentsView):
     tab = ViewTab(
         label=_('Front Ports'),
         badge=lambda obj: obj.frontporttemplates.count(),
-        permission='dcim.view_frontporttemplate',
-        always_display=False
+        permission='dcim.view_frontporttemplate'
     )
 
 
@@ -944,8 +938,7 @@ class DeviceTypeRearPortsView(DeviceTypeComponentsView):
     tab = ViewTab(
         label=_('Rear Ports'),
         badge=lambda obj: obj.rearporttemplates.count(),
-        permission='dcim.view_rearporttemplate',
-        always_display=False
+        permission='dcim.view_rearporttemplate'
     )
 
 
@@ -958,8 +951,7 @@ class DeviceTypeModuleBaysView(DeviceTypeComponentsView):
     tab = ViewTab(
         label=_('Module Bays'),
         badge=lambda obj: obj.modulebaytemplates.count(),
-        permission='dcim.view_modulebaytemplate',
-        always_display=False
+        permission='dcim.view_modulebaytemplate'
     )
 
 
@@ -972,8 +964,7 @@ class DeviceTypeDeviceBaysView(DeviceTypeComponentsView):
     tab = ViewTab(
         label=_('Device Bays'),
         badge=lambda obj: obj.devicebaytemplates.count(),
-        permission='dcim.view_devicebaytemplate',
-        always_display=False
+        permission='dcim.view_devicebaytemplate'
     )
 
 
@@ -986,8 +977,7 @@ class DeviceTypeInventoryItemsView(DeviceTypeComponentsView):
     tab = ViewTab(
         label=_('Inventory Items'),
         badge=lambda obj: obj.inventoryitemtemplates.count(),
-        permission='dcim.view_invenotryitemtemplate',
-        always_display=False
+        permission='dcim.view_invenotryitemtemplate'
     )
 
 
@@ -1084,8 +1074,7 @@ class ModuleTypeConsolePortsView(ModuleTypeComponentsView):
     tab = ViewTab(
         label=_('Console Ports'),
         badge=lambda obj: obj.consoleporttemplates.count(),
-        permission='dcim.view_consoleporttemplate',
-        always_display=False
+        permission='dcim.view_consoleporttemplate'
     )
 
 
@@ -1098,8 +1087,7 @@ class ModuleTypeConsoleServerPortsView(ModuleTypeComponentsView):
     tab = ViewTab(
         label=_('Console Server Ports'),
         badge=lambda obj: obj.consoleserverporttemplates.count(),
-        permission='dcim.view_consoleserverporttemplate',
-        always_display=False
+        permission='dcim.view_consoleserverporttemplate'
     )
 
 
@@ -1112,8 +1100,7 @@ class ModuleTypePowerPortsView(ModuleTypeComponentsView):
     tab = ViewTab(
         label=_('Power Ports'),
         badge=lambda obj: obj.powerporttemplates.count(),
-        permission='dcim.view_powerporttemplate',
-        always_display=False
+        permission='dcim.view_powerporttemplate'
     )
 
 
@@ -1126,8 +1113,7 @@ class ModuleTypePowerOutletsView(ModuleTypeComponentsView):
     tab = ViewTab(
         label=_('Power Outlets'),
         badge=lambda obj: obj.poweroutlettemplates.count(),
-        permission='dcim.view_poweroutlettemplate',
-        always_display=False
+        permission='dcim.view_poweroutlettemplate'
     )
 
 
@@ -1140,8 +1126,7 @@ class ModuleTypeInterfacesView(ModuleTypeComponentsView):
     tab = ViewTab(
         label=_('Interfaces'),
         badge=lambda obj: obj.interfacetemplates.count(),
-        permission='dcim.view_interfacetemplate',
-        always_display=False
+        permission='dcim.view_interfacetemplate'
     )
 
 
@@ -1154,8 +1139,7 @@ class ModuleTypeFrontPortsView(ModuleTypeComponentsView):
     tab = ViewTab(
         label=_('Front Ports'),
         badge=lambda obj: obj.frontporttemplates.count(),
-        permission='dcim.view_frontporttemplate',
-        always_display=False
+        permission='dcim.view_frontporttemplate'
     )
 
 
@@ -1168,8 +1152,7 @@ class ModuleTypeRearPortsView(ModuleTypeComponentsView):
     tab = ViewTab(
         label=_('Rear Ports'),
         badge=lambda obj: obj.rearporttemplates.count(),
-        permission='dcim.view_rearporttemplate',
-        always_display=False
+        permission='dcim.view_rearporttemplate'
     )
 
 
@@ -1742,8 +1725,7 @@ class DeviceConsolePortsView(DeviceComponentsView):
     tab = ViewTab(
         label=_('Console Ports'),
         badge=lambda obj: obj.consoleports.count(),
-        permission='dcim.view_consoleport',
-        always_display=False
+        permission='dcim.view_consoleport'
     )
 
 
@@ -1756,8 +1738,7 @@ class DeviceConsoleServerPortsView(DeviceComponentsView):
     tab = ViewTab(
         label=_('Console Server Ports'),
         badge=lambda obj: obj.consoleserverports.count(),
-        permission='dcim.view_consoleserverport',
-        always_display=False
+        permission='dcim.view_consoleserverport'
     )
 
 
@@ -1770,8 +1751,7 @@ class DevicePowerPortsView(DeviceComponentsView):
     tab = ViewTab(
         label=_('Power Ports'),
         badge=lambda obj: obj.powerports.count(),
-        permission='dcim.view_powerport',
-        always_display=False
+        permission='dcim.view_powerport'
     )
 
 
@@ -1784,8 +1764,7 @@ class DevicePowerOutletsView(DeviceComponentsView):
     tab = ViewTab(
         label=_('Power Outlets'),
         badge=lambda obj: obj.poweroutlets.count(),
-        permission='dcim.view_poweroutlet',
-        always_display=False
+        permission='dcim.view_poweroutlet'
     )
 
 
@@ -1798,8 +1777,7 @@ class DeviceInterfacesView(DeviceComponentsView):
     tab = ViewTab(
         label=_('Interfaces'),
         badge=lambda obj: obj.interfaces.count(),
-        permission='dcim.view_interface',
-        always_display=False
+        permission='dcim.view_interface'
     )
 
     def get_children(self, request, parent):
@@ -1818,8 +1796,7 @@ class DeviceFrontPortsView(DeviceComponentsView):
     tab = ViewTab(
         label=_('Front Ports'),
         badge=lambda obj: obj.frontports.count(),
-        permission='dcim.view_frontport',
-        always_display=False
+        permission='dcim.view_frontport'
     )
 
 
@@ -1832,8 +1809,7 @@ class DeviceRearPortsView(DeviceComponentsView):
     tab = ViewTab(
         label=_('Rear Ports'),
         badge=lambda obj: obj.rearports.count(),
-        permission='dcim.view_rearport',
-        always_display=False
+        permission='dcim.view_rearport'
     )
 
 
@@ -1846,8 +1822,7 @@ class DeviceModuleBaysView(DeviceComponentsView):
     tab = ViewTab(
         label=_('Module Bays'),
         badge=lambda obj: obj.modulebays.count(),
-        permission='dcim.view_modulebay',
-        always_display=False
+        permission='dcim.view_modulebay'
     )
 
 
@@ -1860,8 +1835,7 @@ class DeviceDeviceBaysView(DeviceComponentsView):
     tab = ViewTab(
         label=_('Device Bays'),
         badge=lambda obj: obj.devicebays.count(),
-        permission='dcim.view_devicebay',
-        always_display=False
+        permission='dcim.view_devicebay'
     )
 
 
@@ -1874,51 +1848,10 @@ class DeviceInventoryView(DeviceComponentsView):
     tab = ViewTab(
         label=_('Inventory Items'),
         badge=lambda obj: obj.inventoryitems.count(),
-        permission='dcim.view_inventoryitem',
-        always_display=False
+        permission='dcim.view_inventoryitem'
     )
 
 
-class DeviceStatusView(generic.ObjectView):
-    additional_permissions = ['dcim.napalm_read_device']
-    queryset = Device.objects.all()
-    template_name = 'dcim/device/status.html'
-
-    def get_extra_context(self, request, instance):
-        return {
-            'active_tab': 'status',
-        }
-
-
-class DeviceLLDPNeighborsView(generic.ObjectView):
-    additional_permissions = ['dcim.napalm_read_device']
-    queryset = Device.objects.all()
-    template_name = 'dcim/device/lldp_neighbors.html'
-
-    def get_extra_context(self, request, instance):
-        interfaces = instance.vc_interfaces().restrict(request.user, 'view').prefetch_related(
-            '_path'
-        ).exclude(
-            type__in=NONCONNECTABLE_IFACE_TYPES
-        )
-
-        return {
-            'interfaces': interfaces,
-            'active_tab': 'lldp-neighbors',
-        }
-
-
-class DeviceConfigView(generic.ObjectView):
-    additional_permissions = ['dcim.napalm_read_device']
-    queryset = Device.objects.all()
-    template_name = 'dcim/device/config.html'
-
-    def get_extra_context(self, request, instance):
-        return {
-            'active_tab': 'config',
-        }
-
-
 @register_model_view(Device, 'configcontext', path='config-context')
 class DeviceConfigContextView(ObjectConfigContextView):
     queryset = Device.objects.annotate_config_context_data()
@@ -1984,7 +1917,68 @@ class DeviceBulkRenameView(generic.BulkRenameView):
 
 
 #
-# Devices
+# Device NAPALM views
+#
+
+class NAPALMViewTab(ViewTab):
+
+    def render(self, instance):
+        # Display NAPALM tabs only for devices which meet certain requirements
+        if not (
+            instance.status == 'active' and
+            instance.primary_ip and
+            instance.platform.napalm_driver
+        ):
+            return None
+        return super().render(instance)
+
+
+@register_model_view(Device, 'status')
+class DeviceStatusView(generic.ObjectView):
+    additional_permissions = ['dcim.napalm_read_device']
+    queryset = Device.objects.all()
+    template_name = 'dcim/device/status.html'
+    tab = NAPALMViewTab(
+        label=_('Status'),
+        permission='dcim.napalm_read_device',
+    )
+
+
+@register_model_view(Device, 'lldp_neighbors', path='lldp-neighbors')
+class DeviceLLDPNeighborsView(generic.ObjectView):
+    additional_permissions = ['dcim.napalm_read_device']
+    queryset = Device.objects.all()
+    template_name = 'dcim/device/lldp_neighbors.html'
+    tab = NAPALMViewTab(
+        label=_('LLDP Neighbors'),
+        permission='dcim.napalm_read_device',
+    )
+
+    def get_extra_context(self, request, instance):
+        interfaces = instance.vc_interfaces().restrict(request.user, 'view').prefetch_related(
+            '_path'
+        ).exclude(
+            type__in=NONCONNECTABLE_IFACE_TYPES
+        )
+
+        return {
+            'interfaces': interfaces,
+        }
+
+
+@register_model_view(Device, 'config')
+class DeviceConfigView(generic.ObjectView):
+    additional_permissions = ['dcim.napalm_read_device']
+    queryset = Device.objects.all()
+    template_name = 'dcim/device/config.html'
+    tab = NAPALMViewTab(
+        label=_('Config'),
+        permission='dcim.napalm_read_device',
+    )
+
+
+#
+# Modules
 #
 
 class ModuleListView(generic.ObjectListView):

+ 0 - 21
netbox/templates/dcim/device/base.html

@@ -54,24 +54,3 @@
         </div>
     {% endif %}
 {% endblock %}
-
-{% block extra_tabs %}
-    {% if perms.dcim.napalm_read_device and object.status == 'active' and object.primary_ip and object.platform.napalm_driver %}
-        {# NAPALM-enabled tabs #}
-        <li role="presentation" class="nav-item">
-            <a class="nav-link{% if active_tab == 'status' %} active{% endif %}" href="{% url 'dcim:device_status' pk=object.pk %}">
-                Status
-            </a>
-        </li>
-        <li role="presentation" class="nav-item">
-            <a class="nav-link{% if active_tab == 'lldp-neighbors' %} active{% endif %}" href="{% url 'dcim:device_lldp_neighbors' pk=object.pk %}">
-                LLDP Neighbors
-            </a>
-        </li>
-        <li role="presentation" class="nav-item">
-            <a class="nav-link{% if active_tab == 'config' %} active{% endif %}" href="{% url 'dcim:device_config' pk=object.pk %}">
-                Configuration
-            </a>
-        </li>
-    {% endif %}
-{% endblock %}

+ 1 - 2
netbox/utilities/templates/tabs/model_view_tabs.html

@@ -1,8 +1,7 @@
 {% for tab in tabs %}
   <li role="presentation" class="nav-item">
     <a href="{{ tab.url }}" class="nav-link{% if tab.is_active %} active{% endif %}">
-      {{ tab.label }}
-      {% if tab.badge_value %}{% badge tab.badge_value %}{% endif %}
+      {{ tab.label }} {% badge tab.badge %}
     </a>
   </li>
 {% endfor %}

+ 10 - 18
netbox/utilities/templatetags/tabs.py

@@ -32,24 +32,16 @@ def model_view_tabs(context, instance):
             if tab.permission and not user.has_perm(tab.permission):
                 continue
 
-            # Determine the value of the tab's badge (if any)
-            if tab.badge and callable(tab.badge):
-                badge_value = tab.badge(instance)
-            else:
-                badge_value = tab.badge
-
-            if not tab.always_display and not badge_value:
-                continue
-
-            viewname = f"{app_label}:{model_name}_{config['name']}"
-            active_tab = context.get('tab')
-            tabs.append({
-                'name': config['name'],
-                'url': reverse(viewname, args=[instance.pk]),
-                'label': tab.label,
-                'badge_value': badge_value,
-                'is_active': active_tab and active_tab == tab,
-            })
+            if attrs := tab.render(instance):
+                viewname = f"{app_label}:{model_name}_{config['name']}"
+                active_tab = context.get('tab')
+                tabs.append({
+                    'name': config['name'],
+                    'url': reverse(viewname, args=[instance.pk]),
+                    'label': attrs['label'],
+                    'badge': attrs['badge'],
+                    'is_active': active_tab and active_tab == tab,
+                })
 
     return {
         'tabs': tabs,

+ 22 - 3
netbox/utilities/views.py

@@ -134,12 +134,31 @@ class GetReturnURLMixin:
 
 
 class ViewTab:
-
-    def __init__(self, label, badge=None, permission=None, always_display=True):
+    """
+    ViewTabs are used for navigation among multiple object-specific views, such as the changelog or journal for
+    a particular object.
+    """
+    def __init__(self, label, badge=None, permission=None):
         self.label = label
         self.badge = badge
         self.permission = permission
-        self.always_display = always_display
+
+    def render(self, instance):
+        """Return the attributes needed to render a tab in HTML."""
+        badge_value = self._get_badge_value(instance)
+        if self.badge and not badge_value:
+            return None
+        return {
+            'label': self.label,
+            'badge': badge_value,
+        }
+
+    def _get_badge_value(self, instance):
+        if not self.badge:
+            return None
+        if callable(self.badge):
+            return self.badge(instance)
+        return self.badge
 
 
 def register_model_view(model, name, path=None, kwargs=None):