Explorar o código

Closes #5075: Include a VLAN membership view for VM interfaces

Jeremy Stretch %!s(int64=5) %!d(string=hai) anos
pai
achega
1f0a4cc548

+ 1 - 0
docs/release-notes/version-2.9.md

@@ -13,6 +13,7 @@
 ### Bug Fixes
 ### Bug Fixes
 
 
 * [#5050](https://github.com/netbox-community/netbox/issues/5050) - Fix potential failure on `0016_replicate_interfaces` schema migration from old release
 * [#5050](https://github.com/netbox-community/netbox/issues/5050) - Fix potential failure on `0016_replicate_interfaces` schema migration from old release
+* [#5075](https://github.com/netbox-community/netbox/issues/5075) - Include a VLAN membership view for VM interfaces
 * [#5105](https://github.com/netbox-community/netbox/issues/5105) - Validation should fail when reassigning a primary IP from device to VM
 * [#5105](https://github.com/netbox-community/netbox/issues/5105) - Validation should fail when reassigning a primary IP from device to VM
 * [#5109](https://github.com/netbox-community/netbox/issues/5109) - Fix representation of custom choice field values for webhook data
 * [#5109](https://github.com/netbox-community/netbox/issues/5109) - Fix representation of custom choice field values for webhook data
 * [#5108](https://github.com/netbox-community/netbox/issues/5108) - Fix execution of reports via CLI
 * [#5108](https://github.com/netbox-community/netbox/issues/5108) - Fix execution of reports via CLI

+ 9 - 2
netbox/ipam/models.py

@@ -985,13 +985,20 @@ class VLAN(ChangeLoggedModel, CustomFieldModel):
     def get_status_class(self):
     def get_status_class(self):
         return self.STATUS_CLASS_MAP[self.status]
         return self.STATUS_CLASS_MAP[self.status]
 
 
-    def get_members(self):
-        # Return all interfaces assigned to this VLAN
+    def get_interfaces(self):
+        # Return all device interfaces assigned to this VLAN
         return Interface.objects.filter(
         return Interface.objects.filter(
             Q(untagged_vlan_id=self.pk) |
             Q(untagged_vlan_id=self.pk) |
             Q(tagged_vlans=self.pk)
             Q(tagged_vlans=self.pk)
         ).distinct()
         ).distinct()
 
 
+    def get_vminterfaces(self):
+        # Return all VM interfaces assigned to this VLAN
+        return VMInterface.objects.filter(
+            Q(untagged_vlan_id=self.pk) |
+            Q(tagged_vlans=self.pk)
+        ).distinct()
+
 
 
 @extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
 @extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
 class Service(ChangeLoggedModel, CustomFieldModel):
 class Service(ChangeLoggedModel, CustomFieldModel):

+ 24 - 9
netbox/ipam/tables.py

@@ -4,6 +4,7 @@ from django_tables2.utils import Accessor
 from dcim.models import Interface
 from dcim.models import Interface
 from tenancy.tables import COL_TENANT
 from tenancy.tables import COL_TENANT
 from utilities.tables import BaseTable, BooleanColumn, ButtonsColumn, TagColumn, ToggleColumn
 from utilities.tables import BaseTable, BooleanColumn, ButtonsColumn, TagColumn, ToggleColumn
+from virtualization.models import VMInterface
 from .models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF
 from .models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF
 
 
 RIR_UTILIZATION = """
 RIR_UTILIZATION = """
@@ -124,9 +125,11 @@ VLANGROUP_ADD_VLAN = """
 {% endwith %}
 {% endwith %}
 """
 """
 
 
-VLAN_MEMBER_UNTAGGED = """
+VLAN_MEMBER_TAGGED = """
 {% if record.untagged_vlan_id == vlan.pk %}
 {% if record.untagged_vlan_id == vlan.pk %}
-    <i class="glyphicon glyphicon-ok">
+    <span class="text-danger"><i class="fa fa-close"></i></span>
+{% else %}
+    <span class="text-success"><i class="fa fa-check"></i></span>
 {% endif %}
 {% endif %}
 """
 """
 
 
@@ -553,15 +556,15 @@ class VLANDetailTable(VLANTable):
         default_columns = ('pk', 'vid', 'site', 'group', 'name', 'prefixes', 'tenant', 'status', 'role', 'description')
         default_columns = ('pk', 'vid', 'site', 'group', 'name', 'prefixes', 'tenant', 'status', 'role', 'description')
 
 
 
 
-class VLANMemberTable(BaseTable):
-    parent = tables.LinkColumn(
-        order_by=['device', 'virtual_machine']
-    )
+class VLANMembersTable(BaseTable):
+    """
+    Base table for Interface and VMInterface assignments
+    """
     name = tables.LinkColumn(
     name = tables.LinkColumn(
         verbose_name='Interface'
         verbose_name='Interface'
     )
     )
-    untagged = tables.TemplateColumn(
-        template_code=VLAN_MEMBER_UNTAGGED,
+    tagged = tables.TemplateColumn(
+        template_code=VLAN_MEMBER_TAGGED,
         orderable=False
         orderable=False
     )
     )
     actions = tables.TemplateColumn(
     actions = tables.TemplateColumn(
@@ -570,9 +573,21 @@ class VLANMemberTable(BaseTable):
         verbose_name=''
         verbose_name=''
     )
     )
 
 
+
+class VLANDevicesTable(VLANMembersTable):
+    device = tables.LinkColumn()
+
     class Meta(BaseTable.Meta):
     class Meta(BaseTable.Meta):
         model = Interface
         model = Interface
-        fields = ('parent', 'name', 'untagged', 'actions')
+        fields = ('device', 'name', 'tagged', 'actions')
+
+
+class VLANVirtualMachinesTable(VLANMembersTable):
+    virtual_machine = tables.LinkColumn()
+
+    class Meta(BaseTable.Meta):
+        model = VMInterface
+        fields = ('virtual_machine', 'name', 'tagged', 'actions')
 
 
 
 
 class InterfaceVLANTable(BaseTable):
 class InterfaceVLANTable(BaseTable):

+ 2 - 1
netbox/ipam/urls.py

@@ -90,7 +90,8 @@ urlpatterns = [
     path('vlans/edit/', views.VLANBulkEditView.as_view(), name='vlan_bulk_edit'),
     path('vlans/edit/', views.VLANBulkEditView.as_view(), name='vlan_bulk_edit'),
     path('vlans/delete/', views.VLANBulkDeleteView.as_view(), name='vlan_bulk_delete'),
     path('vlans/delete/', views.VLANBulkDeleteView.as_view(), name='vlan_bulk_delete'),
     path('vlans/<int:pk>/', views.VLANView.as_view(), name='vlan'),
     path('vlans/<int:pk>/', views.VLANView.as_view(), name='vlan'),
-    path('vlans/<int:pk>/members/', views.VLANMembersView.as_view(), name='vlan_members'),
+    path('vlans/<int:pk>/interfaces/', views.VLANInterfacesView.as_view(), name='vlan_interfaces'),
+    path('vlans/<int:pk>/vm-interfaces/', views.VLANVMInterfacesView.as_view(), name='vlan_vminterfaces'),
     path('vlans/<int:pk>/edit/', views.VLANEditView.as_view(), name='vlan_edit'),
     path('vlans/<int:pk>/edit/', views.VLANEditView.as_view(), name='vlan_edit'),
     path('vlans/<int:pk>/delete/', views.VLANDeleteView.as_view(), name='vlan_delete'),
     path('vlans/<int:pk>/delete/', views.VLANDeleteView.as_view(), name='vlan_delete'),
     path('vlans/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='vlan_changelog', kwargs={'model': VLAN}),
     path('vlans/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='vlan_changelog', kwargs={'model': VLAN}),

+ 25 - 6
netbox/ipam/views.py

@@ -749,15 +749,34 @@ class VLANView(ObjectView):
         })
         })
 
 
 
 
-class VLANMembersView(ObjectView):
+class VLANInterfacesView(ObjectView):
     queryset = VLAN.objects.all()
     queryset = VLAN.objects.all()
 
 
     def get(self, request, pk):
     def get(self, request, pk):
-
         vlan = get_object_or_404(self.queryset, pk=pk)
         vlan = get_object_or_404(self.queryset, pk=pk)
-        members = vlan.get_members().restrict(request.user, 'view').prefetch_related('device', 'virtual_machine')
+        interfaces = vlan.get_interfaces().prefetch_related('device')
+        members_table = tables.VLANDevicesTable(interfaces)
+
+        paginate = {
+            'paginator_class': EnhancedPaginator,
+            'per_page': request.GET.get('per_page', settings.PAGINATE_COUNT)
+        }
+        RequestConfig(request, paginate).configure(members_table)
+
+        return render(request, 'ipam/vlan_interfaces.html', {
+            'vlan': vlan,
+            'members_table': members_table,
+            'active_tab': 'interfaces',
+        })
+
 
 
-        members_table = tables.VLANMemberTable(members)
+class VLANVMInterfacesView(ObjectView):
+    queryset = VLAN.objects.all()
+
+    def get(self, request, pk):
+        vlan = get_object_or_404(self.queryset, pk=pk)
+        interfaces = vlan.get_vminterfaces().prefetch_related('virtual_machine')
+        members_table = tables.VLANVirtualMachinesTable(interfaces)
 
 
         paginate = {
         paginate = {
             'paginator_class': EnhancedPaginator,
             'paginator_class': EnhancedPaginator,
@@ -765,10 +784,10 @@ class VLANMembersView(ObjectView):
         }
         }
         RequestConfig(request, paginate).configure(members_table)
         RequestConfig(request, paginate).configure(members_table)
 
 
-        return render(request, 'ipam/vlan_members.html', {
+        return render(request, 'ipam/vlan_vminterfaces.html', {
             'vlan': vlan,
             'vlan': vlan,
             'members_table': members_table,
             'members_table': members_table,
-            'active_tab': 'members',
+            'active_tab': 'vminterfaces',
         })
         })
 
 
 
 

+ 5 - 2
netbox/templates/ipam/vlan.html

@@ -52,8 +52,11 @@
         <li role="presentation"{% if not active_tab %} class="active"{% endif %}>
         <li role="presentation"{% if not active_tab %} class="active"{% endif %}>
             <a href="{% url 'ipam:vlan' pk=vlan.pk %}">VLAN</a>
             <a href="{% url 'ipam:vlan' pk=vlan.pk %}">VLAN</a>
         </li>
         </li>
-        <li role="presentation"{% if active_tab == 'members' %} class="active"{% endif %}>
-            <a href="{% url 'ipam:vlan_members' pk=vlan.pk %}">Members <span class="badge">{{ vlan.get_members.count }}</span></a>
+        <li role="presentation"{% if active_tab == 'interfaces' %} class="active"{% endif %}>
+            <a href="{% url 'ipam:vlan_interfaces' pk=vlan.pk %}">Device Interfaces <span class="badge">{{ vlan.get_interfaces.count }}</span></a>
+        </li>
+        <li role="presentation"{% if active_tab == 'vminterfaces' %} class="active"{% endif %}>
+            <a href="{% url 'ipam:vlan_vminterfaces' pk=vlan.pk %}">VM Interfaces <span class="badge">{{ vlan.get_vminterfaces.count }}</span></a>
         </li>
         </li>
         {% if perms.extras.view_objectchange %}
         {% if perms.extras.view_objectchange %}
             <li role="presentation"{% if active_tab == 'changelog' %} class="active"{% endif %}>
             <li role="presentation"{% if active_tab == 'changelog' %} class="active"{% endif %}>

+ 1 - 3
netbox/templates/ipam/vlan_members.html → netbox/templates/ipam/vlan_interfaces.html

@@ -1,11 +1,9 @@
 {% extends 'ipam/vlan.html' %}
 {% extends 'ipam/vlan.html' %}
 
 
-{% block title %}{{ block.super }} - Members{% endblock %}
-
 {% block content %}
 {% block content %}
     <div class="row">
     <div class="row">
         <div class="col-md-12">
         <div class="col-md-12">
-            {% include 'utilities/obj_table.html' with table=members_table table_template='panel_table.html' heading='VLAN Members' parent=vlan %}
+            {% include 'utilities/obj_table.html' with table=members_table table_template='panel_table.html' heading='Device Interfaces' parent=vlan %}
         </div>
         </div>
     </div>
     </div>
 {% endblock %}
 {% endblock %}

+ 9 - 0
netbox/templates/ipam/vlan_vminterfaces.html

@@ -0,0 +1,9 @@
+{% extends 'ipam/vlan.html' %}
+
+{% block content %}
+    <div class="row">
+        <div class="col-md-12">
+            {% include 'utilities/obj_table.html' with table=members_table table_template='panel_table.html' heading='Virtual Machine Interfaces' parent=vlan %}
+        </div>
+    </div>
+{% endblock %}