diff --git a/netbox/ipam/models.py b/netbox/ipam/models.py index 9aea44229..29702ff5c 100644 --- a/netbox/ipam/models.py +++ b/netbox/ipam/models.py @@ -528,7 +528,7 @@ class VLANGroup(models.Model): return self.name def get_absolute_url(self): - return "{}?group_id={}".format(reverse('ipam:vlan_list'), self.pk) + return reverse('ipam:vlangroup_vlans', args=[self.pk]) def to_csv(self): return ( diff --git a/netbox/ipam/tables.py b/netbox/ipam/tables.py index 382eeae20..b2610eef1 100644 --- a/netbox/ipam/tables.py +++ b/netbox/ipam/tables.py @@ -110,6 +110,16 @@ STATUS_LABEL = """ {% endif %} """ +VLAN_LINK = """ +{% if record.pk %} + {{ record.vid }} +{% elif perms.ipam.add_vlan %} + {{ record.available }} VLAN{{ record.available|pluralize }} available +{% else %} + {{ record.available }} VLAN{{ record.available|pluralize }} available +{% endif %} +""" + VLAN_PREFIXES = """ {% for prefix in record.prefixes.all %} {{ prefix }}{% if not forloop.last %}{% endif %} @@ -375,9 +385,9 @@ class VLANGroupTable(BaseTable): class VLANTable(BaseTable): pk = ToggleColumn() - vid = tables.LinkColumn('ipam:vlan', args=[Accessor('pk')], verbose_name='ID') + vid = tables.TemplateColumn(VLAN_LINK, verbose_name='ID') site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')]) - group = tables.Column(accessor=Accessor('group.name'), verbose_name='Group') + group = tables.LinkColumn('ipam:vlangroup_vlans', args=[Accessor('group.pk')], verbose_name='Group') tenant = tables.TemplateColumn(template_code=COL_TENANT) status = tables.TemplateColumn(STATUS_LABEL) role = tables.TemplateColumn(VLAN_ROLE_LINK) @@ -385,6 +395,9 @@ class VLANTable(BaseTable): class Meta(BaseTable.Meta): model = VLAN fields = ('pk', 'vid', 'site', 'group', 'name', 'tenant', 'status', 'role', 'description') + row_attrs = { + 'class': lambda record: 'success' if not isinstance(record, VLAN) else '', + } class VLANDetailTable(VLANTable): diff --git a/netbox/ipam/urls.py b/netbox/ipam/urls.py index aa7c17a5c..20bdf8e31 100644 --- a/netbox/ipam/urls.py +++ b/netbox/ipam/urls.py @@ -72,6 +72,7 @@ urlpatterns = [ url(r'^vlan-groups/import/$', views.VLANGroupBulkImportView.as_view(), name='vlangroup_import'), url(r'^vlan-groups/delete/$', views.VLANGroupBulkDeleteView.as_view(), name='vlangroup_bulk_delete'), url(r'^vlan-groups/(?P\d+)/edit/$', views.VLANGroupEditView.as_view(), name='vlangroup_edit'), + url(r'^vlan-groups/(?P\d+)/vlans/$', views.VLANGroupVLANsView.as_view(), name='vlangroup_vlans'), # VLANs url(r'^vlans/$', views.VLANListView.as_view(), name='vlan_list'), diff --git a/netbox/ipam/views.py b/netbox/ipam/views.py index 752ab97c2..e5c6670c0 100644 --- a/netbox/ipam/views.py +++ b/netbox/ipam/views.py @@ -84,6 +84,34 @@ def add_available_ipaddresses(prefix, ipaddress_list, is_pool=False): return output +def add_available_vlans(vlan_group, vlans): + """ + Create fake records for all gaps between used VLANs + """ + MIN_VLAN = 1 + MAX_VLAN = 4094 + + if not vlans: + return [{'vid': MIN_VLAN, 'available': MAX_VLAN - MIN_VLAN + 1}] + + prev_vid = MAX_VLAN + new_vlans = [] + for vlan in vlans: + if vlan.vid - prev_vid > 1: + new_vlans.append({'vid': prev_vid + 1, 'available': vlan.vid - prev_vid - 1}) + prev_vid = vlan.vid + + if vlans[0].vid > MIN_VLAN: + new_vlans.append({'vid': MIN_VLAN, 'available': vlans[0].vid - MIN_VLAN}) + if prev_vid < MAX_VLAN: + new_vlans.append({'vid': prev_vid + 1, 'available': MAX_VLAN - prev_vid}) + + vlans = list(vlans) + new_vlans + vlans.sort(key=lambda v: v.vid if type(v) == VLAN else v['vid']) + + return vlans + + # # VRFs # @@ -814,6 +842,41 @@ class VLANGroupBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): default_return_url = 'ipam:vlangroup_list' +class VLANGroupVLANsView(View): + def get(self, request, pk): + + vlan_group = get_object_or_404(VLANGroup.objects.all(), pk=pk) + + vlans = VLAN.objects.filter(group_id=pk) + vlans = add_available_vlans(vlan_group, vlans) + + vlan_table = tables.VLANDetailTable(vlans) + if request.user.has_perm('ipam.change_vlan') or request.user.has_perm('ipam.delete_vlan'): + vlan_table.columns.show('pk') + vlan_table.columns.hide('site') + vlan_table.columns.hide('group') + + paginate = { + 'klass': EnhancedPaginator, + 'per_page': request.GET.get('per_page', settings.PAGINATE_COUNT) + } + RequestConfig(request, paginate).configure(vlan_table) + + # Compile permissions list for rendering the object table + permissions = { + 'add': request.user.has_perm('ipam.add_vlan'), + 'change': request.user.has_perm('ipam.change_vlan'), + 'delete': request.user.has_perm('ipam.delete_vlan'), + } + + return render(request, 'ipam/vlangroup_vlans.html', { + 'vlan_group': vlan_group, + 'first_available_vlan': vlan_group.get_next_available_vid(), + 'vlan_table': vlan_table, + 'permissions': permissions, + }) + + # # VLANs # diff --git a/netbox/templates/ipam/inc/vlangroup_header.html b/netbox/templates/ipam/inc/vlangroup_header.html new file mode 100644 index 000000000..221f41994 --- /dev/null +++ b/netbox/templates/ipam/inc/vlangroup_header.html @@ -0,0 +1,14 @@ + + {% if perms.ipam.add_vlan and first_available_vlan %} + + Add a VLAN + + {% endif %} + {% if perms.ipam.change_vlangroup %} + + + Edit this VLAN Group + + {% endif %} + +{{ vlan_group }} diff --git a/netbox/templates/ipam/vlangroup_vlans.html b/netbox/templates/ipam/vlangroup_vlans.html new file mode 100644 index 000000000..49532fb95 --- /dev/null +++ b/netbox/templates/ipam/vlangroup_vlans.html @@ -0,0 +1,24 @@ +{% extends '_base.html' %} + +{% block title %}{{ vlan_group }} - VLANs{% endblock %} + +{% block content %} + + + + VLAN Groups + {% if vlan_group.site %} + {{ vlan_group.site }} + {% endif %} + {{ vlan_group }} + + + + {% include 'ipam/inc/vlangroup_header.html' %} + + + {% include 'utilities/obj_table.html' with table=vlan_table table_template='panel_table.html' heading='VLANs' bulk_edit_url='ipam:vlan_bulk_edit' bulk_delete_url='ipam:vlan_bulk_delete' %} + + +{% endblock %} +