diff --git a/netbox/dcim/api/serializers.py b/netbox/dcim/api/serializers.py index dd3de9baf..a17301823 100644 --- a/netbox/dcim/api/serializers.py +++ b/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: diff --git a/netbox/dcim/api/views.py b/netbox/dcim/api/views.py index 243d92e36..d573cddde 100644 --- a/netbox/dcim/api/views.py +++ b/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] diff --git a/netbox/dcim/fixtures/dcim.json b/netbox/dcim/fixtures/dcim.json index 89c889b65..7c011eb89 100644 --- a/netbox/dcim/fixtures/dcim.json +++ b/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": "" } }, diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index bc3c8d8e8..89292717b 100644 --- a/netbox/dcim/forms.py +++ b/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: diff --git a/netbox/dcim/migrations/0006_add_device_primary_ip4_ip6.py b/netbox/dcim/migrations/0006_add_device_primary_ip4_ip6.py new file mode 100644 index 000000000..670a174f9 --- /dev/null +++ b/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'), + ), + ] diff --git a/netbox/dcim/migrations/0007_device_copy_primary_ip.py b/netbox/dcim/migrations/0007_device_copy_primary_ip.py new file mode 100644 index 000000000..055eac7d0 --- /dev/null +++ b/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), + ] diff --git a/netbox/dcim/migrations/0008_device_remove_primary_ip.py b/netbox/dcim/migrations/0008_device_remove_primary_ip.py new file mode 100644 index 000000000..91465e878 --- /dev/null +++ b/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', + ), + ] diff --git a/netbox/dcim/models.py b/netbox/dcim/models.py index 2f1a62d36..68c485b40 100644 --- a/netbox/dcim/models.py +++ b/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. diff --git a/netbox/dcim/tests/test_apis.py b/netbox/dcim/tests/test_apis.py index 1f31fac14..b68764482 100644 --- a/netbox/dcim/tests/test_apis.py +++ b/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', diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index 4f0d7ec29..e2b1d5ac9 100644 --- a/netbox/dcim/views.py +++ b/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: diff --git a/netbox/ipam/forms.py b/netbox/ipam/forms.py index 0c7a411cd..eb7d39e1d 100644 --- a/netbox/ipam/forms.py +++ b/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) diff --git a/netbox/ipam/models.py b/netbox/ipam/models.py index 4dbfa801a..79873f549 100644 --- a/netbox/ipam/models.py +++ b/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, ]) diff --git a/netbox/ipam/views.py b/netbox/ipam/views.py index b230783fc..1db9a6255 100644 --- a/netbox/ipam/views.py +++ b/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 diff --git a/netbox/templates/dcim/device.html b/netbox/templates/dcim/device.html index 3f03fcee9..0f405b68e 100644 --- a/netbox/templates/dcim/device.html +++ b/netbox/templates/dcim/device.html @@ -101,14 +101,29 @@ - Primary IP + Primary IPv4 - {% if device.primary_ip %} - {{ device.primary_ip.address.ip }} - {% if device.primary_ip.nat_inside %} - (NAT for {{ device.primary_ip.nat_inside.address.ip }}) - {% elif device.primary_ip.nat_outside %} - (NAT: {{ device.primary_ip.nat_outside.address.ip }}) + {% if device.primary_ip4 %} + {{ device.primary_ip4.address.ip }} + {% if device.primary_ip4.nat_inside %} + (NAT for {{ device.primary_ip4.nat_inside.address.ip }}) + {% elif device.primary_ip4.nat_outside %} + (NAT: {{ device.primary_ip4.nat_outside.address.ip }}) + {% endif %} + {% else %} + Not defined + {% endif %} + + + + Primary IPv6 + + {% if device.primary_ip6 %} + {{ device.primary_ip6.address.ip }} + {% if device.primary_ip6.nat_inside %} + (NAT for {{ device.primary_ip6.nat_inside.address.ip }}) + {% elif device.primary_ip6.nat_outside %} + (NAT: {{ device.primary_ip6.nat_outside.address.ip }}) {% endif %} {% else %} Not defined diff --git a/netbox/templates/dcim/device_edit.html b/netbox/templates/dcim/device_edit.html index e2f2d95a2..2694bad7a 100644 --- a/netbox/templates/dcim/device_edit.html +++ b/netbox/templates/dcim/device_edit.html @@ -31,7 +31,10 @@
{% 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 %}
diff --git a/netbox/templates/dcim/inc/_ipaddress.html b/netbox/templates/dcim/inc/_ipaddress.html index 4b73f6721..3f805b611 100644 --- a/netbox/templates/dcim/inc/_ipaddress.html +++ b/netbox/templates/dcim/inc/_ipaddress.html @@ -4,7 +4,7 @@ {{ ip.interface }} - {% if device.primary_ip == ip %} + {% if device.primary_ip4 == ip or device.primary_ip6 == ip %} Primary {% endif %}