1
0
mirror of https://github.com/netbox-community/netbox.git synced 2024-05-10 07:54:54 +00:00

Initial work on #93: Primary IPv4/IPv6 support

This commit is contained in:
Jeremy Stretch
2016-07-11 16:24:46 -04:00
parent f617828712
commit 4e4bb01a55
16 changed files with 203 additions and 45 deletions

View File

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

View File

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

View File

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

View File

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

View File

@ -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'),
),
]

View File

@ -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),
]

View File

@ -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',
),
]

View File

@ -605,8 +605,10 @@ class Device(CreatedUpdatedModel):
help_text='Number of the lowest U position occupied by the device') 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') 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') 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, primary_ip4 = models.OneToOneField('ipam.IPAddress', related_name='primary_ip4_for', on_delete=models.SET_NULL,
blank=True, null=True, verbose_name='Primary IP') 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) comments = models.TextField(blank=True)
class Meta: class Meta:
@ -709,6 +711,15 @@ class Device(CreatedUpdatedModel):
return self.name return self.name
return '{{{}}}'.format(self.pk) 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): def get_children(self):
""" """
Return the set of child Devices installed in DeviceBays within this Device. Return the set of child Devices installed in DeviceBays within this Device.

View File

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

View File

@ -501,7 +501,8 @@ class PlatformBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
# #
class DeviceListView(ObjectListView): 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 = filters.DeviceFilter
filter_form = forms.DeviceFilterForm filter_form = forms.DeviceFilterForm
table = tables.DeviceTable table = tables.DeviceTable
@ -1634,7 +1635,10 @@ def ipaddress_assign(request, pk):
ipaddress.interface)) ipaddress.interface))
if form.cleaned_data['set_as_primary']: 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() device.save()
if '_addanother' in request.POST: if '_addanother' in request.POST:

View File

@ -329,7 +329,7 @@ class IPAddressForm(forms.ModelForm, BootstrapMixin):
class IPAddressFromCSVForm(forms.ModelForm): class IPAddressFromCSVForm(forms.ModelForm):
vrf = forms.ModelChoiceField(queryset=VRF.objects.all(), required=False, to_field_name='rd', 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', device = forms.ModelChoiceField(queryset=Device.objects.all(), required=False, to_field_name='name',
error_messages={'invalid_choice': 'Device not found.'}) error_messages={'invalid_choice': 'Device not found.'})
interface_name = forms.CharField(required=False) interface_name = forms.CharField(required=False)
@ -368,7 +368,10 @@ class IPAddressFromCSVForm(forms.ModelForm):
name=self.cleaned_data['interface_name']) name=self.cleaned_data['interface_name'])
# Set as primary for device # Set as primary for device
if self.cleaned_data['is_primary']: 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) return super(IPAddressFromCSVForm, self).save(commit=commit)

View File

@ -314,12 +314,20 @@ class IPAddress(CreatedUpdatedModel):
super(IPAddress, self).save(*args, **kwargs) super(IPAddress, self).save(*args, **kwargs)
def to_csv(self): 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([ return ','.join([
str(self.address), str(self.address),
self.vrf.rd if self.vrf else '', self.vrf.rd if self.vrf else '',
self.device.identifier if self.device else '', self.device.identifier if self.device else '',
self.interface.name if self.interface else '', self.interface.name if self.interface else '',
'True' if getattr(self, 'primary_for', False) else '', 'True' if is_primary else '',
self.description, self.description,
]) ])

View File

@ -364,7 +364,7 @@ def prefix_ipaddresses(request, pk):
# Find all IPAddresses belonging to this Prefix # Find all IPAddresses belonging to this Prefix
ipaddresses = IPAddress.objects.filter(address__net_contained_or_equal=str(prefix.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 = tables.IPAddressTable(ipaddresses)
ip_table.model = IPAddress ip_table.model = IPAddress
@ -383,7 +383,7 @@ def prefix_ipaddresses(request, pk):
# #
class IPAddressListView(ObjectListView): 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 = filters.IPAddressFilter
filter_form = forms.IPAddressFilterForm filter_form = forms.IPAddressFilterForm
table = tables.IPAddressTable table = tables.IPAddressTable
@ -443,8 +443,13 @@ class IPAddressBulkImportView(PermissionRequiredMixin, BulkImportView):
obj.save() obj.save()
# Update primary IP for device if needed # Update primary IP for device if needed
try: try:
device = obj.primary_for if obj.family == 4 and obj.primary_ip4_for:
device.primary_ip = obj 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() device.save()
except Device.DoesNotExist: except Device.DoesNotExist:
pass pass

View File

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

View File

@ -31,7 +31,10 @@
<div class="panel-body"> <div class="panel-body">
{% render_field form.platform %} {% render_field form.platform %}
{% render_field form.status %} {% 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> </div>
<div class="panel panel-default"> <div class="panel panel-default">

View File

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