Parcourir la source

Initial work on #93: Primary IPv4/IPv6 support

Jeremy Stretch il y a 8 ans
Parent
commit
4e4bb01a55

+ 3 - 1
netbox/dcim/api/serializers.py

@@ -221,12 +221,14 @@ class DeviceSerializer(serializers.ModelSerializer):
     platform = PlatformNestedSerializer()
     rack = RackNestedSerializer()
     primary_ip = DeviceIPAddressNestedSerializer()
+    primary_ip4 = DeviceIPAddressNestedSerializer()
+    primary_ip6 = DeviceIPAddressNestedSerializer()
     parent_device = serializers.SerializerMethodField()
 
     class Meta:
         model = Device
         fields = ['id', 'name', 'display_name', 'device_type', 'device_role', 'platform', 'serial', 'rack', 'position',
-                  'face', 'parent_device', 'status', 'primary_ip', 'comments']
+                  'face', 'parent_device', 'status', 'primary_ip', 'primary_ip4', 'primary_ip6', 'comments']
 
     def get_parent_device(self, obj):
         try:

+ 1 - 1
netbox/dcim/api/views.py

@@ -194,7 +194,7 @@ class DeviceListView(generics.ListAPIView):
     List devices (filterable)
     """
     queryset = Device.objects.select_related('device_type__manufacturer', 'device_role', 'platform', 'rack__site')\
-        .prefetch_related('primary_ip__nat_outside')
+        .prefetch_related('primary_ip4__nat_outside', 'primary_ip6__nat_outside')
     serializer_class = serializers.DeviceSerializer
     filter_class = filters.DeviceFilter
     renderer_classes = api_settings.DEFAULT_RENDERER_CLASSES + [BINDZoneRenderer, FlatJSONRenderer]

+ 22 - 11
netbox/dcim/fixtures/dcim.json

@@ -1919,7 +1919,8 @@
         "position": 1,
         "face": 0,
         "status": true,
-        "primary_ip": 1,
+        "primary_ip4": 1,
+        "primary_ip6": null,
         "comments": ""
     }
 },
@@ -1938,7 +1939,8 @@
         "position": 17,
         "face": 0,
         "status": true,
-        "primary_ip": 5,
+        "primary_ip4": 5,
+        "primary_ip6": null,
         "comments": ""
     }
 },
@@ -1957,7 +1959,8 @@
         "position": 33,
         "face": 0,
         "status": true,
-        "primary_ip": null,
+        "primary_ip4": null,
+        "primary_ip6": null,
         "comments": ""
     }
 },
@@ -1976,7 +1979,8 @@
         "position": 34,
         "face": 0,
         "status": true,
-        "primary_ip": null,
+        "primary_ip4": null,
+        "primary_ip6": null,
         "comments": ""
     }
 },
@@ -1995,7 +1999,8 @@
         "position": 34,
         "face": 0,
         "status": true,
-        "primary_ip": null,
+        "primary_ip4": null,
+        "primary_ip6": null,
         "comments": ""
     }
 },
@@ -2014,7 +2019,8 @@
         "position": 33,
         "face": 0,
         "status": true,
-        "primary_ip": null,
+        "primary_ip4": null,
+        "primary_ip6": null,
         "comments": ""
     }
 },
@@ -2033,7 +2039,8 @@
         "position": 1,
         "face": 0,
         "status": true,
-        "primary_ip": 3,
+        "primary_ip4": 3,
+        "primary_ip6": null,
         "comments": ""
     }
 },
@@ -2052,7 +2059,8 @@
         "position": 17,
         "face": 0,
         "status": true,
-        "primary_ip": 19,
+        "primary_ip4": 19,
+        "primary_ip6": null,
         "comments": ""
     }
 },
@@ -2071,7 +2079,8 @@
         "position": 42,
         "face": 0,
         "status": true,
-        "primary_ip": null,
+        "primary_ip4": null,
+        "primary_ip6": null,
         "comments": ""
     }
 },
@@ -2090,7 +2099,8 @@
         "position": null,
         "face": null,
         "status": true,
-        "primary_ip": null,
+        "primary_ip4": null,
+        "primary_ip6": null,
         "comments": ""
     }
 },
@@ -2109,7 +2119,8 @@
         "position": null,
         "face": null,
         "status": true,
-        "primary_ip": null,
+        "primary_ip4": null,
+        "primary_ip6": null,
         "comments": ""
     }
 },

+ 14 - 11
netbox/dcim/forms.py

@@ -349,7 +349,7 @@ class DeviceForm(forms.ModelForm, BootstrapMixin):
     class Meta:
         model = Device
         fields = ['name', 'device_role', 'device_type', 'serial', 'site', 'rack', 'position', 'face', 'status',
-                  'platform', 'primary_ip', 'comments']
+                  'platform', 'primary_ip4', 'primary_ip6', 'comments']
         help_texts = {
             'device_role': "The function this device serves",
             'serial': "Chassis serial number",
@@ -369,20 +369,23 @@ class DeviceForm(forms.ModelForm, BootstrapMixin):
             self.initial['site'] = self.instance.rack.site
             self.initial['manufacturer'] = self.instance.device_type.manufacturer
 
-            # Compile list of IPs assigned to this device
-            primary_ip_choices = []
-            interface_ips = IPAddress.objects.filter(interface__device=self.instance)
-            primary_ip_choices += [(ip.id, '{} ({})'.format(ip.address, ip.interface)) for ip in interface_ips]
-            nat_ips = IPAddress.objects.filter(nat_inside__interface__device=self.instance)\
-                .select_related('nat_inside__interface')
-            primary_ip_choices += [(ip.id, '{} ({} NAT)'.format(ip.address, ip.nat_inside.interface)) for ip in nat_ips]
-            self.fields['primary_ip'].choices = [(None, '---------')] + primary_ip_choices
+            # Compile list of choices for primary IPv4 and IPv6 addresses
+            for family in [4, 6]:
+                ip_choices = []
+                interface_ips = IPAddress.objects.filter(family=family, interface__device=self.instance)
+                ip_choices += [(ip.id, '{} ({})'.format(ip.address, ip.interface)) for ip in interface_ips]
+                nat_ips = IPAddress.objects.filter(family=family, nat_inside__interface__device=self.instance)\
+                    .select_related('nat_inside__interface')
+                ip_choices += [(ip.id, '{} ({} NAT)'.format(ip.address, ip.nat_inside.interface)) for ip in nat_ips]
+                self.fields['primary_ip{}'.format(family)].choices = [(None, '---------')] + ip_choices
 
         else:
 
             # An object that doesn't exist yet can't have any IPs assigned to it
-            self.fields['primary_ip'].choices = []
-            self.fields['primary_ip'].widget.attrs['readonly'] = True
+            self.fields['primary_ip4'].choices = []
+            self.fields['primary_ip4'].widget.attrs['readonly'] = True
+            self.fields['primary_ip6'].choices = []
+            self.fields['primary_ip6'].widget.attrs['readonly'] = True
 
         # Limit rack choices
         if self.is_bound:

+ 27 - 0
netbox/dcim/migrations/0006_add_device_primary_ip4_ip6.py

@@ -0,0 +1,27 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.9.7 on 2016-07-11 18:40
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('ipam', '0001_initial'),
+        ('dcim', '0005_auto_20160706_1722'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='device',
+            name='primary_ip4',
+            field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='primary_ip4_for', to='ipam.IPAddress', verbose_name=b'Primary IPv4'),
+        ),
+        migrations.AddField(
+            model_name='device',
+            name='primary_ip6',
+            field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='primary_ip6_for', to='ipam.IPAddress', verbose_name=b'Primary IPv6'),
+        ),
+    ]

+ 41 - 0
netbox/dcim/migrations/0007_device_copy_primary_ip.py

@@ -0,0 +1,41 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.9.7 on 2016-07-11 18:40
+from __future__ import unicode_literals
+
+from django.db import migrations
+
+
+def copy_primary_ip(apps, schema_editor):
+    Device = apps.get_model('dcim', 'Device')
+    for d in Device.objects.select_related('primary_ip'):
+        if not d.primary_ip:
+            continue
+        if d.primary_ip.family == 4:
+            d.primary_ip4 = d.primary_ip
+        elif d.primary_ip.family == 6:
+            d.primary_ip6 = d.primary_ip
+        d.save()
+
+
+def restore_primary_ip(apps, schema_editor):
+    Device = apps.get_model('dcim', 'Device')
+    for d in Device.objects.select_related('primary_ip4', 'primary_ip6'):
+        if d.primary_ip:
+            continue
+        # Prefer IPv6 over IPv4
+        if d.primary_ip6:
+            d.primary_ip = d.primary_ip6
+        elif d.primary_ip4:
+            d.primary_ip = d.primary_ip4
+        d.save()
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('dcim', '0006_add_device_primary_ip4_ip6'),
+    ]
+
+    operations = [
+        migrations.RunPython(copy_primary_ip, restore_primary_ip),
+    ]

+ 19 - 0
netbox/dcim/migrations/0008_device_remove_primary_ip.py

@@ -0,0 +1,19 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.9.7 on 2016-07-11 19:01
+from __future__ import unicode_literals
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('dcim', '0007_device_copy_primary_ip'),
+    ]
+
+    operations = [
+        migrations.RemoveField(
+            model_name='device',
+            name='primary_ip',
+        ),
+    ]

+ 13 - 2
netbox/dcim/models.py

@@ -605,8 +605,10 @@ class Device(CreatedUpdatedModel):
                                                 help_text='Number of the lowest U position occupied by the device')
     face = models.PositiveSmallIntegerField(blank=True, null=True, choices=RACK_FACE_CHOICES, verbose_name='Rack face')
     status = models.BooleanField(choices=STATUS_CHOICES, default=STATUS_ACTIVE, verbose_name='Status')
-    primary_ip = models.OneToOneField('ipam.IPAddress', related_name='primary_for', on_delete=models.SET_NULL,
-                                      blank=True, null=True, verbose_name='Primary IP')
+    primary_ip4 = models.OneToOneField('ipam.IPAddress', related_name='primary_ip4_for', on_delete=models.SET_NULL,
+                                       blank=True, null=True, verbose_name='Primary IPv4')
+    primary_ip6 = models.OneToOneField('ipam.IPAddress', related_name='primary_ip6_for', on_delete=models.SET_NULL,
+                                       blank=True, null=True, verbose_name='Primary IPv6')
     comments = models.TextField(blank=True)
 
     class Meta:
@@ -709,6 +711,15 @@ class Device(CreatedUpdatedModel):
             return self.name
         return '{{{}}}'.format(self.pk)
 
+    @property
+    def primary_ip(self):
+        if self.primary_ip6:
+            return self.primary_ip6
+        elif self.primary_ip4:
+            return self.primary_ip4
+        else:
+            return None
+
     def get_children(self):
         """
         Return the set of child Devices installed in DeviceBays within this Device.

+ 6 - 0
netbox/dcim/tests/test_apis.py

@@ -318,6 +318,8 @@ class DeviceTest(APITestCase):
         'parent_device',
         'status',
         'primary_ip',
+        'primary_ip4',
+        'primary_ip6',
         'comments',
     ]
 
@@ -375,6 +377,10 @@ class DeviceTest(APITestCase):
             'primary_ip_address',
             'primary_ip_family',
             'primary_ip_id',
+            'primary_ip4_address',
+            'primary_ip4_family',
+            'primary_ip4_id',
+            'primary_ip6',
             'rack_display_name',
             'rack_facility_id',
             'rack_id',

+ 6 - 2
netbox/dcim/views.py

@@ -501,7 +501,8 @@ class PlatformBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
 #
 
 class DeviceListView(ObjectListView):
-    queryset = Device.objects.select_related('device_type__manufacturer', 'device_role', 'rack__site', 'primary_ip')
+    queryset = Device.objects.select_related('device_type__manufacturer', 'device_role', 'rack__site', 'primary_ip4',
+                                             'primary_ip6')
     filter = filters.DeviceFilter
     filter_form = forms.DeviceFilterForm
     table = tables.DeviceTable
@@ -1634,7 +1635,10 @@ def ipaddress_assign(request, pk):
                                                                                          ipaddress.interface))
 
             if form.cleaned_data['set_as_primary']:
-                device.primary_ip = ipaddress
+                if ipaddress.family == 4:
+                    device.primary_ip4 = ipaddress
+                elif ipaddress.family == 6:
+                    device.primary_ip6 = ipaddress
                 device.save()
 
             if '_addanother' in request.POST:

+ 5 - 2
netbox/ipam/forms.py

@@ -329,7 +329,7 @@ class IPAddressForm(forms.ModelForm, BootstrapMixin):
 
 class IPAddressFromCSVForm(forms.ModelForm):
     vrf = forms.ModelChoiceField(queryset=VRF.objects.all(), required=False, to_field_name='rd',
-                                 error_messages={'invalid_choice': 'Site not found.'})
+                                 error_messages={'invalid_choice': 'VRF not found.'})
     device = forms.ModelChoiceField(queryset=Device.objects.all(), required=False, to_field_name='name',
                                     error_messages={'invalid_choice': 'Device not found.'})
     interface_name = forms.CharField(required=False)
@@ -368,7 +368,10 @@ class IPAddressFromCSVForm(forms.ModelForm):
                                                             name=self.cleaned_data['interface_name'])
         # Set as primary for device
         if self.cleaned_data['is_primary']:
-            self.instance.primary_for = self.cleaned_data['device']
+            if self.instance.family == 4:
+                self.instance.primary_ip4_for = self.cleaned_data['device']
+            elif self.instance.family == 6:
+                self.instance.primary_ip6_for = self.cleaned_data['device']
 
         return super(IPAddressFromCSVForm, self).save(commit=commit)
 

+ 9 - 1
netbox/ipam/models.py

@@ -314,12 +314,20 @@ class IPAddress(CreatedUpdatedModel):
         super(IPAddress, self).save(*args, **kwargs)
 
     def to_csv(self):
+
+        # Determine if this IP is primary for a Device
+        is_primary = False
+        if self.family == 4 and getattr(self, 'primary_ip4_for', False):
+            is_primary = True
+        elif self.family == 6 and getattr(self, 'primary_ip6_for', False):
+            is_primary = True
+
         return ','.join([
             str(self.address),
             self.vrf.rd if self.vrf else '',
             self.device.identifier if self.device else '',
             self.interface.name if self.interface else '',
-            'True' if getattr(self, 'primary_for', False) else '',
+            'True' if is_primary else '',
             self.description,
         ])
 

+ 10 - 5
netbox/ipam/views.py

@@ -364,7 +364,7 @@ def prefix_ipaddresses(request, pk):
 
     # Find all IPAddresses belonging to this Prefix
     ipaddresses = IPAddress.objects.filter(address__net_contained_or_equal=str(prefix.prefix))\
-        .select_related('vrf', 'interface__device', 'primary_for')
+        .select_related('vrf', 'interface__device', 'primary_ip4_for', 'primary_ip6_for')
 
     ip_table = tables.IPAddressTable(ipaddresses)
     ip_table.model = IPAddress
@@ -383,7 +383,7 @@ def prefix_ipaddresses(request, pk):
 #
 
 class IPAddressListView(ObjectListView):
-    queryset = IPAddress.objects.select_related('vrf', 'interface__device', 'primary_for')
+    queryset = IPAddress.objects.select_related('vrf', 'interface__device', 'primary_ip4_for', 'primary_ip6_for')
     filter = filters.IPAddressFilter
     filter_form = forms.IPAddressFilterForm
     table = tables.IPAddressTable
@@ -443,9 +443,14 @@ class IPAddressBulkImportView(PermissionRequiredMixin, BulkImportView):
         obj.save()
         # Update primary IP for device if needed
         try:
-            device = obj.primary_for
-            device.primary_ip = obj
-            device.save()
+            if obj.family == 4 and obj.primary_ip4_for:
+                device = obj.primary_ip4_for
+                device.primary_ip4 = obj
+                device.save()
+            elif obj.family == 6 and obj.primary_ip6_for:
+                device = obj.primary_ip6_for
+                device.primary_ip6 = obj
+                device.save()
         except Device.DoesNotExist:
             pass
 

+ 22 - 7
netbox/templates/dcim/device.html

@@ -101,14 +101,29 @@
                     </td>
                 </tr>
                 <tr>
-                    <td>Primary IP</td>
+                    <td>Primary IPv4</td>
                     <td>
-                        {% if device.primary_ip %}
-                            <a href="{% url 'ipam:ipaddress' pk=device.primary_ip.pk %}">{{ device.primary_ip.address.ip }}</a>
-                            {% if device.primary_ip.nat_inside %}
-                                <span>(NAT for {{ device.primary_ip.nat_inside.address.ip }})</span>
-                            {% elif device.primary_ip.nat_outside %}
-                                <span>(NAT: {{ device.primary_ip.nat_outside.address.ip }})</span>
+                        {% if device.primary_ip4 %}
+                            <a href="{% url 'ipam:ipaddress' pk=device.primary_ip4.pk %}">{{ device.primary_ip4.address.ip }}</a>
+                            {% if device.primary_ip4.nat_inside %}
+                                <span>(NAT for {{ device.primary_ip4.nat_inside.address.ip }})</span>
+                            {% elif device.primary_ip4.nat_outside %}
+                                <span>(NAT: {{ device.primary_ip4.nat_outside.address.ip }})</span>
+                            {% endif %}
+                        {% else %}
+                            <span class="text-muted">Not defined</span>
+                        {% endif %}
+                    </td>
+                </tr>
+                <tr>
+                    <td>Primary IPv6</td>
+                    <td>
+                        {% if device.primary_ip6 %}
+                            <a href="{% url 'ipam:ipaddress' pk=device.primary_ip6.pk %}">{{ device.primary_ip6.address.ip }}</a>
+                            {% if device.primary_ip6.nat_inside %}
+                                <span>(NAT for {{ device.primary_ip6.nat_inside.address.ip }})</span>
+                            {% elif device.primary_ip6.nat_outside %}
+                                <span>(NAT: {{ device.primary_ip6.nat_outside.address.ip }})</span>
                             {% endif %}
                         {% else %}
                             <span class="text-muted">Not defined</span>

+ 4 - 1
netbox/templates/dcim/device_edit.html

@@ -31,7 +31,10 @@
         <div class="panel-body">
             {% render_field form.platform %}
             {% render_field form.status %}
-            {% if obj %}{% render_field form.primary_ip %}{% endif %}
+            {% if obj %}
+                {% render_field form.primary_ip4 %}
+                {% render_field form.primary_ip6 %}
+            {% endif %}
         </div>
     </div>
     <div class="panel panel-default">

+ 1 - 1
netbox/templates/dcim/inc/_ipaddress.html

@@ -4,7 +4,7 @@
     </td>
     <td>{{ ip.interface }}</td>
     <td>
-        {% if device.primary_ip == ip %}
+        {% if device.primary_ip4 == ip or device.primary_ip6 == ip %}
             <span class="label label-success">Primary</span>
         {% endif %}
     </td>