Parcourir la source

Additional work on #904

Jeremy Stretch il y a 8 ans
Parent
commit
b0f9035e2d

+ 4 - 2
docs/data-model/ipam.md

@@ -83,9 +83,11 @@ One IP address can be designated as the network address translation (NAT) IP add
 
 # VLANs
 
-A VLAN represents an isolated layer two domain, identified by a name and a numeric ID (1-4094). Note that while it is good practice, neither VLAN names nor IDs must be unique within a site. This is to accommodate the fact that many real-world network use less-than-optimal VLAN allocations and may have overlapping VLAN ID assignments in practice.
+A VLAN represents an isolated layer two domain, identified by a name and a numeric ID (1-4094) as defined in [IEEE 802.1Q](https://en.wikipedia.org/wiki/IEEE_802.1Q). VLANs may be assigned to a site and/or VLAN group. Like prefixes, each VLAN is assigned an operational status and (optionally) a functional role.
 
-Like prefixes, each VLAN is assigned an operational status and (optionally) a functional role.
+### VLAN Groups
+
+VLAN groups can be employed for administrative organization within NetBox. Each VLAN within a group must have a unique ID and name. VLANs which are not assigned to a group may have overlapping names and IDs, including within a site.
 
 ---
 

+ 2 - 18
netbox/ipam/filters.py

@@ -266,25 +266,17 @@ class VLANGroupFilter(django_filters.FilterSet):
         name='site',
         queryset=Site.objects.all(),
         label='Site (ID)',
-        method='site_search',
     )
     site = NullableModelMultipleChoiceFilter(
-        name='site__slug',
+        name='site',
         queryset=Site.objects.all(),
         to_field_name='slug',
         label='Site (slug)',
-        method='site_search',
     )
 
     class Meta:
         model = VLANGroup
 
-    def site_search(self, queryset, name, value):
-        q = Q(**{name: None})
-        for v in value:
-            q |= Q(**{name: v})
-        return queryset.filter(q)
-
 
 class VLANFilter(CustomFieldFilterSet, django_filters.FilterSet):
     q = django_filters.MethodFilter(
@@ -295,14 +287,12 @@ class VLANFilter(CustomFieldFilterSet, django_filters.FilterSet):
         name='site',
         queryset=Site.objects.all(),
         label='Site (ID)',
-        method='site_search',
     )
     site = NullableModelMultipleChoiceFilter(
-        name='site__slug',
+        name='site',
         queryset=Site.objects.all(),
         to_field_name='slug',
         label='Site (slug)',
-        method='site_search',
     )
     group_id = NullableModelMultipleChoiceFilter(
         name='group',
@@ -359,12 +349,6 @@ class VLANFilter(CustomFieldFilterSet, django_filters.FilterSet):
             pass
         return queryset.filter(qs_filter)
 
-    def site_search(self, queryset, name, value):
-        q = Q(**{name: None})
-        for v in value:
-            q |= Q(**{name: v})
-        return queryset.filter(q)
-
 
 class ServiceFilter(django_filters.FilterSet):
     device_id = django_filters.ModelMultipleChoiceFilter(

+ 8 - 6
netbox/ipam/forms.py

@@ -153,8 +153,7 @@ class RoleForm(BootstrapMixin, forms.ModelForm):
 
 class PrefixForm(BootstrapMixin, CustomFieldForm):
     site = forms.ModelChoiceField(queryset=Site.objects.all(), required=False, label='Site',
-                                  widget=forms.Select(attrs={'filter-for': 'vlan',
-                                                             'default_value': '0'}))
+                                  widget=forms.Select(attrs={'filter-for': 'vlan', 'nullable': 'true'}))
     vlan = forms.ModelChoiceField(queryset=VLAN.objects.all(), required=False, label='VLAN',
                                   widget=APISelect(api_url='/api/ipam/vlans/?site_id={{site}}',
                                                    display_field='display_name'))
@@ -509,8 +508,11 @@ class VLANGroupForm(BootstrapMixin, forms.ModelForm):
 
 
 class VLANGroupFilterForm(BootstrapMixin, forms.Form):
-    site = FilterChoiceField(queryset=Site.objects.annotate(filter_count=Count('vlan_groups')), to_field_name='slug',
-                             null_option=(0, 'Global'))
+    site = FilterChoiceField(
+        queryset=Site.objects.annotate(filter_count=Count('vlan_groups')),
+        to_field_name='slug',
+        null_option=(0, 'Global')
+    )
 
 
 #
@@ -526,7 +528,7 @@ class VLANForm(BootstrapMixin, CustomFieldForm):
         model = VLAN
         fields = ['site', 'group', 'vid', 'name', 'tenant', 'status', 'role', 'description']
         help_texts = {
-            'site': "The site at which this VLAN exists",
+            'site': "Leave blank if this VLAN spans multiple sites",
             'group': "VLAN group (optional)",
             'vid': "Configured VLAN ID",
             'name': "Configured VLAN name",
@@ -534,7 +536,7 @@ class VLANForm(BootstrapMixin, CustomFieldForm):
             'role': "The primary function of this VLAN",
         }
         widgets = {
-            'site': forms.Select(attrs={'filter-for': 'group', 'default_value': '0'}),
+            'site': forms.Select(attrs={'filter-for': 'group', 'nullable': 'true'}),
         }
 
     def __init__(self, *args, **kwargs):

+ 3 - 3
netbox/ipam/migrations/0015_auto_20170219_0726.py

@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-# Generated by Django 1.10.5 on 2017-02-19 07:26
+# Generated by Django 1.10.4 on 2017-02-21 18:45
 from __future__ import unicode_literals
 
 from django.db import migrations, models
@@ -16,11 +16,11 @@ class Migration(migrations.Migration):
         migrations.AlterField(
             model_name='vlan',
             name='site',
-            field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='vlans', to='dcim.Site'),
+            field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='vlans', to='dcim.Site'),
         ),
         migrations.AlterField(
             model_name='vlangroup',
             name='site',
-            field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='vlan_groups', to='dcim.Site'),
+            field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='vlan_groups', to='dcim.Site'),
         ),
     ]

+ 4 - 3
netbox/ipam/models.py

@@ -485,7 +485,7 @@ class VLANGroup(models.Model):
     """
     name = models.CharField(max_length=50)
     slug = models.SlugField()
-    site = models.ForeignKey('dcim.Site', related_name='vlan_groups', on_delete=models.SET_NULL, blank=True, null=True)
+    site = models.ForeignKey('dcim.Site', related_name='vlan_groups', on_delete=models.PROTECT, blank=True, null=True)
 
     class Meta:
         ordering = ['site', 'name']
@@ -497,8 +497,9 @@ class VLANGroup(models.Model):
         verbose_name_plural = 'VLAN groups'
 
     def __str__(self):
-        site_name = self.site.name if self.site else '__global'
-        return u'{} - {}'.format(site_name, self.name)
+        if self.site is None:
+            return self.name
+        return u'{} - {}'.format(self.site.name, self.name)
 
     def get_absolute_url(self):
         return "{}?group_id={}".format(reverse('ipam:vlan_list'), self.pk)

+ 1 - 3
netbox/templates/ipam/vlan.html

@@ -10,11 +10,9 @@
             <li><a href="{% url 'ipam:vlan_list' %}">VLANs</a></li>
             {% if vlan.site %}
                 <li><a href="{% url 'ipam:vlan_list' %}?site={{ vlan.site.slug }}">{{ vlan.site }}</a></li>
-            {% else %}
-                <li><a href="{% url 'ipam:vlan_list' %}?site_id=0">Global</a></li>
             {% endif %}
             {% if vlan.group %}
-                <li><a href="{% url 'ipam:vlan_list' %}?site={{ vlan.site.slug }}&group={{ vlan.group.slug }}">{{ vlan.group.name }}</a></li>
+                <li><a href="{% url 'ipam:vlan_list' %}?group={{ vlan.group.slug }}">{{ vlan.group.name }}</a></li>
             {% endif %}
             <li>{{ vlan.name }} ({{ vlan.vid }})</li>
         </ol>