Parcourir la source

Fix #235: Enable global vlan (#904)

* Fix #235: Enable global vlan

Decouple site/vlan, make site optional for vlan/vlangroup
Change html generation code to check site existence before
dereference
Create site search function, if site is None for a VLAN, view it as
global VLAN

* commit1

* commit2

* commit3

* Add migration file for VLAN&VLAN group

* Revert unintentional commits
Shawn Peng il y a 8 ans
Parent
commit
aba9748ffd

+ 20 - 4
netbox/ipam/filters.py

@@ -262,37 +262,47 @@ class IPAddressFilter(CustomFieldFilterSet, django_filters.FilterSet):
 
 
 class VLANGroupFilter(django_filters.FilterSet):
-    site_id = django_filters.ModelMultipleChoiceFilter(
+    site_id = NullableModelMultipleChoiceFilter(
         name='site',
         queryset=Site.objects.all(),
         label='Site (ID)',
+        method='site_search',
     )
-    site = django_filters.ModelMultipleChoiceFilter(
+    site = NullableModelMultipleChoiceFilter(
         name='site__slug',
         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(
         action='search',
         label='Search',
     )
-    site_id = django_filters.ModelMultipleChoiceFilter(
+    site_id = NullableModelMultipleChoiceFilter(
         name='site',
         queryset=Site.objects.all(),
         label='Site (ID)',
+        method='site_search',
     )
-    site = django_filters.ModelMultipleChoiceFilter(
+    site = NullableModelMultipleChoiceFilter(
         name='site__slug',
         queryset=Site.objects.all(),
         to_field_name='slug',
         label='Site (slug)',
+        method='site_search',
     )
     group_id = NullableModelMultipleChoiceFilter(
         name='group',
@@ -349,6 +359,12 @@ 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(

+ 10 - 7
netbox/ipam/forms.py

@@ -153,7 +153,8 @@ 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'}))
+                                  widget=forms.Select(attrs={'filter-for': 'vlan',
+                                                             'default_value': '0'}))
     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'))
@@ -173,7 +174,7 @@ class PrefixForm(BootstrapMixin, CustomFieldForm):
         elif self.initial.get('site'):
             self.fields['vlan'].queryset = VLAN.objects.filter(site=self.initial['site'])
         else:
-            self.fields['vlan'].choices = []
+            self.fields['vlan'].queryset = VLAN.objects.filter(site=None)
 
 
 class PrefixFromCSVForm(forms.ModelForm):
@@ -508,7 +509,8 @@ 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')
+    site = FilterChoiceField(queryset=Site.objects.annotate(filter_count=Count('vlan_groups')), to_field_name='slug',
+                             null_option=(0, 'Global'))
 
 
 #
@@ -532,7 +534,7 @@ class VLANForm(BootstrapMixin, CustomFieldForm):
             'role': "The primary function of this VLAN",
         }
         widgets = {
-            'site': forms.Select(attrs={'filter-for': 'group'}),
+            'site': forms.Select(attrs={'filter-for': 'group', 'default_value': '0'}),
         }
 
     def __init__(self, *args, **kwargs):
@@ -545,11 +547,11 @@ class VLANForm(BootstrapMixin, CustomFieldForm):
         elif self.initial.get('site'):
             self.fields['group'].queryset = VLANGroup.objects.filter(site=self.initial['site'])
         else:
-            self.fields['group'].choices = []
+            self.fields['group'].queryset = VLANGroup.objects.filter(site=None)
 
 
 class VLANFromCSVForm(forms.ModelForm):
-    site = forms.ModelChoiceField(queryset=Site.objects.all(), to_field_name='name',
+    site = forms.ModelChoiceField(queryset=Site.objects.all(), required=False, to_field_name='name',
                                   error_messages={'invalid_choice': 'Site not found.'})
     group = forms.ModelChoiceField(queryset=VLANGroup.objects.all(), required=False, to_field_name='name',
                                    error_messages={'invalid_choice': 'VLAN group not found.'})
@@ -599,7 +601,8 @@ def vlan_status_choices():
 class VLANFilterForm(BootstrapMixin, CustomFieldFilterForm):
     model = VLAN
     q = forms.CharField(required=False, label='Search')
-    site = FilterChoiceField(queryset=Site.objects.annotate(filter_count=Count('vlans')), to_field_name='slug')
+    site = FilterChoiceField(queryset=Site.objects.annotate(filter_count=Count('vlans')), to_field_name='slug',
+                             null_option=(0, 'Global'))
     group_id = FilterChoiceField(queryset=VLANGroup.objects.annotate(filter_count=Count('vlans')), label='VLAN group',
                                  null_option=(0, 'None'))
     tenant = FilterChoiceField(queryset=Tenant.objects.annotate(filter_count=Count('vlans')), to_field_name='slug',

+ 26 - 0
netbox/ipam/migrations/0015_auto_20170219_0726.py

@@ -0,0 +1,26 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.10.5 on 2017-02-19 07:26
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('ipam', '0014_ipaddress_status_add_deprecated'),
+    ]
+
+    operations = [
+        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'),
+        ),
+        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'),
+        ),
+    ]

+ 5 - 4
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')
+    site = models.ForeignKey('dcim.Site', related_name='vlan_groups', on_delete=models.SET_NULL, blank=True, null=True)
 
     class Meta:
         ordering = ['site', 'name']
@@ -497,7 +497,8 @@ class VLANGroup(models.Model):
         verbose_name_plural = 'VLAN groups'
 
     def __str__(self):
-        return u'{} - {}'.format(self.site.name, self.name)
+        site_name = self.site.name if self.site else '__global'
+        return u'{} - {}'.format(site_name, self.name)
 
     def get_absolute_url(self):
         return "{}?group_id={}".format(reverse('ipam:vlan_list'), self.pk)
@@ -513,7 +514,7 @@ class VLAN(CreatedUpdatedModel, CustomFieldModel):
     Like Prefixes, each VLAN is assigned an operational status and optionally a user-defined Role. A VLAN can have zero
     or more Prefixes assigned to it.
     """
-    site = models.ForeignKey('dcim.Site', related_name='vlans', on_delete=models.PROTECT)
+    site = models.ForeignKey('dcim.Site', related_name='vlans', on_delete=models.PROTECT, blank=True, null=True)
     group = models.ForeignKey('VLANGroup', related_name='vlans', blank=True, null=True, on_delete=models.PROTECT)
     vid = models.PositiveSmallIntegerField(verbose_name='ID', validators=[
         MinValueValidator(1),
@@ -551,7 +552,7 @@ class VLAN(CreatedUpdatedModel, CustomFieldModel):
 
     def to_csv(self):
         return csv_format([
-            self.site.name,
+            self.site.name if self.site else None,
             self.group.name if self.group else None,
             self.vid,
             self.name,

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

@@ -8,7 +8,11 @@
     <div class="col-sm-8 col-md-9">
         <ol class="breadcrumb">
             <li><a href="{% url 'ipam:vlan_list' %}">VLANs</a></li>
-            <li><a href="{% url 'ipam:vlan_list' %}?site={{ vlan.site.slug }}">{{ vlan.site }}</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>
             {% endif %}
@@ -53,7 +57,13 @@
             <table class="table table-hover panel-body attr-table">
                 <tr>
                     <td>Site</td>
-                    <td><a href="{% url 'dcim:site' slug=vlan.site.slug %}">{{ vlan.site }}</a></td>
+                    <td>
+                        {% if vlan.site %}
+                            <a href="{% url 'dcim:site' slug=vlan.site.slug %}">{{ vlan.site }}</a>
+                        {% else %}
+                            <span class="text-muted">None</span>
+                        {% endif %}
+                    </td>
                 </tr>
                 <tr>
                     <td>Group</td>