Merge remote-tracking branch 'digitalocean/develop' into develop

This commit is contained in:
Joel 2016-07-12 13:17:29 -07:00
commit 8309cd4246
25 changed files with 316 additions and 70 deletions

View File

@ -9,6 +9,7 @@ services:
POSTGRES_PASSWORD: J5brHrAXFLQSif0K POSTGRES_PASSWORD: J5brHrAXFLQSif0K
POSTGRES_DB: netbox POSTGRES_DB: netbox
netbox: netbox:
build: .
image: digitalocean/netbox image: digitalocean/netbox
links: links:
- postgres - postgres

View File

@ -9,7 +9,21 @@ NetBox is an open source web application designed to help manage and document co
* **Data circuits** - Long-haul communications circuits and providers * **Data circuits** - Long-haul communications circuits and providers
* **Secrets** - Encrypted storage of sensitive credentials * **Secrets** - Encrypted storage of sensitive credentials
It was designed with the following tenets foremost in mind. # What NetBox Isn't
While NetBox strives to cover many areas of network management, the scope of its feature set is necessarily limited. This ensures that development focuses on core functionality and that scope creep is reasonably contained. To that end, it might help to provide some examples of functionality that NetBox **does not** provide:
* Network monitoring
* DNS server
* RADIUS server
* Configuration management
* Facilities management
That said, NetBox _can_ be used to great effect in populating external tools with the data they need to perform these functions.
# Design Philosophy
NetBox was designed with the following tenets foremost in mind.
## Replicate the Real World ## Replicate the Real World

View File

@ -9,9 +9,10 @@ NetBox requires following system dependencies:
* libxslt1-dev * libxslt1-dev
* libffi-dev * libffi-dev
* graphviz * graphviz
* libpq-dev
``` ```
# sudo apt-get install -y python2.7 python-dev git python-pip libxml2-dev libxslt1-dev libffi-dev graphviz # sudo apt-get install -y python2.7 python-dev git python-pip libxml2-dev libxslt1-dev libffi-dev graphviz libpq-dev
``` ```
You may opt to install NetBox either from a numbered release or by cloning the master branch of its repository on GitHub. You may opt to install NetBox either from a numbered release or by cloning the master branch of its repository on GitHub.

View File

@ -89,7 +89,7 @@ class DeviceTypeAdmin(admin.ModelAdmin):
power_port_count=Count('power_port_templates', distinct=True), power_port_count=Count('power_port_templates', distinct=True),
power_outlet_count=Count('power_outlet_templates', distinct=True), power_outlet_count=Count('power_outlet_templates', distinct=True),
interface_count=Count('interface_templates', distinct=True), interface_count=Count('interface_templates', distinct=True),
devicebay_count=Count('devicebay_templates', distinct=True), devicebay_count=Count('device_bay_templates', distinct=True),
) )
def console_ports(self, instance): def console_ports(self, instance):
@ -180,4 +180,4 @@ class DeviceAdmin(admin.ModelAdmin):
def get_queryset(self, request): def get_queryset(self, request):
qs = super(DeviceAdmin, self).get_queryset(request) qs = super(DeviceAdmin, self).get_queryset(request)
return qs.select_related('device_type__manufacturer', 'device_role', 'primary_ip', 'rack') return qs.select_related('device_type__manufacturer', 'device_role', 'primary_ip4', 'primary_ip6', 'rack')

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

@ -263,7 +263,7 @@ class Rack(CreatedUpdatedModel):
@property @property
def display_name(self): def display_name(self):
if self.facility_id: if self.facility_id:
return "{} ({})".format(self.name, self.facility_id) return u"{} ({})".format(self.name, self.facility_id)
return self.name return self.name
def get_rack_units(self, face=RACK_FACE_FRONT, exclude=None, remove_redundant=False): def get_rack_units(self, face=RACK_FACE_FRONT, exclude=None, remove_redundant=False):
@ -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:
@ -696,9 +698,9 @@ class Device(CreatedUpdatedModel):
if self.name: if self.name:
return self.name return self.name
elif self.position: elif self.position:
return "{} ({} U{})".format(self.device_type, self.rack.name, self.position) return u"{} ({} U{})".format(self.device_type, self.rack.name, self.position)
else: else:
return "{} ({})".format(self.device_type, self.rack.name) return u"{} ({})".format(self.device_type, self.rack.name)
@property @property
def identifier(self): def identifier(self):
@ -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

@ -1,4 +1,5 @@
import re import re
from natsort import natsorted
from operator import attrgetter from operator import attrgetter
from django.contrib import messages from django.contrib import messages
@ -13,8 +14,6 @@ from django.shortcuts import get_object_or_404, redirect, render
from django.utils.http import urlencode from django.utils.http import urlencode
from django.views.generic import View from django.views.generic import View
from natsort import natsorted
from ipam.models import Prefix, IPAddress, VLAN from ipam.models import Prefix, IPAddress, VLAN
from circuits.models import Circuit from circuits.models import Circuit
from extras.models import TopologyMap from extras.models import TopologyMap
@ -262,13 +261,22 @@ def devicetype(request, pk):
devicetype = get_object_or_404(DeviceType, pk=pk) devicetype = get_object_or_404(DeviceType, pk=pk)
# Component tables # Component tables
consoleport_table = tables.ConsolePortTemplateTable(ConsolePortTemplate.objects.filter(device_type=devicetype)) consoleport_table = tables.ConsolePortTemplateTable(
consoleserverport_table = tables.ConsoleServerPortTemplateTable(ConsoleServerPortTemplate.objects natsorted(ConsolePortTemplate.objects.filter(device_type=devicetype), key=attrgetter('name'))
.filter(device_type=devicetype)) )
powerport_table = tables.PowerPortTemplateTable(PowerPortTemplate.objects.filter(device_type=devicetype)) consoleserverport_table = tables.ConsoleServerPortTemplateTable(
poweroutlet_table = tables.PowerOutletTemplateTable(PowerOutletTemplate.objects.filter(device_type=devicetype)) natsorted(ConsoleServerPortTemplate.objects.filter(device_type=devicetype), key=attrgetter('name'))
)
powerport_table = tables.PowerPortTemplateTable(
natsorted(PowerPortTemplate.objects.filter(device_type=devicetype), key=attrgetter('name'))
)
poweroutlet_table = tables.PowerOutletTemplateTable(
natsorted(PowerOutletTemplate.objects.filter(device_type=devicetype), key=attrgetter('name'))
)
interface_table = tables.InterfaceTemplateTable(InterfaceTemplate.objects.filter(device_type=devicetype)) interface_table = tables.InterfaceTemplateTable(InterfaceTemplate.objects.filter(device_type=devicetype))
devicebay_table = tables.DeviceBayTemplateTable(DeviceBayTemplate.objects.filter(device_type=devicetype)) devicebay_table = tables.DeviceBayTemplateTable(
natsorted(DeviceBayTemplate.objects.filter(device_type=devicetype), key=attrgetter('name'))
)
if request.user.has_perm('dcim.change_devicetype'): if request.user.has_perm('dcim.change_devicetype'):
consoleport_table.base_columns['pk'].visible = True consoleport_table.base_columns['pk'].visible = True
consoleserverport_table.base_columns['pk'].visible = True consoleserverport_table.base_columns['pk'].visible = True
@ -504,7 +512,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
@ -515,17 +524,25 @@ class DeviceListView(ObjectListView):
def device(request, pk): def device(request, pk):
device = get_object_or_404(Device, pk=pk) device = get_object_or_404(Device, pk=pk)
console_ports = ConsolePort.objects.filter(device=device).select_related('cs_port__device') console_ports = natsorted(
cs_ports = ConsoleServerPort.objects.filter(device=device).select_related('connected_console') ConsolePort.objects.filter(device=device).select_related('cs_port__device'), key=attrgetter('name')
power_ports = PowerPort.objects.filter(device=device).select_related('power_outlet__device') )
power_outlets = PowerOutlet.objects.filter(device=device).select_related('connected_port') cs_ports = natsorted(
ConsoleServerPort.objects.filter(device=device).select_related('connected_console'), key=attrgetter('name')
)
power_ports = natsorted(
PowerPort.objects.filter(device=device).select_related('power_outlet__device'), key=attrgetter('name')
)
power_outlets = natsorted(
PowerOutlet.objects.filter(device=device).select_related('connected_port'), key=attrgetter('name')
)
interfaces = Interface.objects.filter(device=device, mgmt_only=False)\ interfaces = Interface.objects.filter(device=device, mgmt_only=False)\
.select_related('connected_as_a', 'connected_as_b', 'circuit') .select_related('connected_as_a', 'connected_as_b', 'circuit')
mgmt_interfaces = Interface.objects.filter(device=device, mgmt_only=True)\ mgmt_interfaces = Interface.objects.filter(device=device, mgmt_only=True)\
.select_related('connected_as_a', 'connected_as_b', 'circuit') .select_related('connected_as_a', 'connected_as_b', 'circuit')
device_bays = natsorted( device_bays = natsorted(
DeviceBay.objects.filter(device=device).select_related('installed_device__device_type__manufacturer'), DeviceBay.objects.filter(device=device).select_related('installed_device__device_type__manufacturer'),
key=attrgetter("name") key=attrgetter('name')
) )
# Gather any secrets which belong to this device # Gather any secrets which belong to this device
@ -1640,7 +1657,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,
]) ])
@ -367,7 +375,7 @@ class VLAN(CreatedUpdatedModel):
@property @property
def display_name(self): def display_name(self):
return "{} ({})".format(self.vid, self.name) return u"{} ({})".format(self.vid, self.name)
def get_status_class(self): def get_status_class(self):
return STATUS_CHOICE_CLASSES[self.status] return STATUS_CHOICE_CLASSES[self.status]

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

@ -73,3 +73,8 @@ TIME_FORMAT = 'g:i a'
SHORT_TIME_FORMAT = 'H:i:s' SHORT_TIME_FORMAT = 'H:i:s'
DATETIME_FORMAT = 'N j, Y g:i a' DATETIME_FORMAT = 'N j, Y g:i a'
SHORT_DATETIME_FORMAT = 'Y-m-d H:i' SHORT_DATETIME_FORMAT = 'Y-m-d H:i'
# Optionally display a persistent banner at the top and/or bottom of every page. To display the same content in both
# banners, define BANNER_TOP and set BANNER_BOTTOM = BANNER_TOP.
BANNER_TOP = ''
BANNER_BOTTOM = ''

View File

@ -12,7 +12,7 @@ except ImportError:
"the documentation.") "the documentation.")
VERSION = '1.1.1-dev' VERSION = '1.2.1-dev'
# Import local configuration # Import local configuration
for setting in ['ALLOWED_HOSTS', 'DATABASE', 'SECRET_KEY']: for setting in ['ALLOWED_HOSTS', 'DATABASE', 'SECRET_KEY']:
@ -38,6 +38,8 @@ TIME_FORMAT = getattr(configuration, 'TIME_FORMAT', 'g:i a')
SHORT_TIME_FORMAT = getattr(configuration, 'SHORT_TIME_FORMAT', 'H:i:s') SHORT_TIME_FORMAT = getattr(configuration, 'SHORT_TIME_FORMAT', 'H:i:s')
DATETIME_FORMAT = getattr(configuration, 'DATETIME_FORMAT', 'N j, Y g:i a') DATETIME_FORMAT = getattr(configuration, 'DATETIME_FORMAT', 'N j, Y g:i a')
SHORT_DATETIME_FORMAT = getattr(configuration, 'SHORT_DATETIME_FORMAT', 'Y-m-d H:i') SHORT_DATETIME_FORMAT = getattr(configuration, 'SHORT_DATETIME_FORMAT', 'Y-m-d H:i')
BANNER_TOP = getattr(configuration, 'BANNER_TOP', False)
BANNER_BOTTOM = getattr(configuration, 'BANNER_BOTTOM', False)
CSRF_TRUSTED_ORIGINS = ALLOWED_HOSTS CSRF_TRUSTED_ORIGINS = ALLOWED_HOSTS
# Attempt to import LDAP configuration if it has been defined # Attempt to import LDAP configuration if it has been defined

View File

@ -28,6 +28,42 @@ body {
footer p { footer p {
margin: 20px 0; margin: 20px 0;
} }
@media (max-width: 1120px) {
.navbar-header {
float: none;
}
.navbar-left,.navbar-right {
float: none !important;
}
.navbar-toggle {
display: block;
}
.navbar-collapse {
border-top: 1px solid transparent;
box-shadow: inset 0 1px 0 rgba(255,255,255,0.1);
}
.navbar-fixed-top {
top: 0;
border-width: 0 0 1px;
}
.navbar-collapse.collapse {
display: none!important;
}
.navbar-nav {
float: none!important;
margin-top: 7.5px;
}
.navbar-nav>li {
float: none;
}
.navbar-nav>li>a {
padding-top: 10px;
padding-bottom: 10px;
}
.collapse.in {
display:block !important;
}
}
/* Forms */ /* Forms */
label { label {
@ -259,6 +295,9 @@ ul.rack_near_face li.empty:hover a {
.dark_gray:hover { background-color: #2c3e50; } .dark_gray:hover { background-color: #2c3e50; }
/* Misc */ /* Misc */
.banner-bottom {
margin-bottom: 50px;
}
.panel table { .panel table {
margin-bottom: 0; margin-bottom: 0;
} }

View File

@ -42,7 +42,7 @@ class SecretListView(generics.GenericAPIView):
""" """
List secrets (filterable). If a private key is POSTed, attempt to decrypt each Secret. List secrets (filterable). If a private key is POSTed, attempt to decrypt each Secret.
""" """
queryset = Secret.objects.select_related('device__primary_ip', 'role')\ queryset = Secret.objects.select_related('device__primary_ip4', 'device__primary_ip6', 'role')\
.prefetch_related('role__users', 'role__groups') .prefetch_related('role__users', 'role__groups')
serializer_class = serializers.SecretSerializer serializer_class = serializers.SecretSerializer
filter_class = SecretFilter filter_class = SecretFilter
@ -87,7 +87,7 @@ class SecretDetailView(generics.GenericAPIView):
""" """
Retrieve a single Secret. If a private key is POSTed, attempt to decrypt the Secret. Retrieve a single Secret. If a private key is POSTed, attempt to decrypt the Secret.
""" """
queryset = Secret.objects.select_related('device__primary_ip', 'role')\ queryset = Secret.objects.select_related('device__primary_ip4', 'device__primary_ip6', 'role')\
.prefetch_related('role__users', 'role__groups') .prefetch_related('role__users', 'role__groups')
serializer_class = serializers.SecretSerializer serializer_class = serializers.SecretSerializer
renderer_classes = [FormlessBrowsableAPIRenderer, JSONRenderer, FreeRADIUSClientsRenderer] renderer_classes = [FormlessBrowsableAPIRenderer, JSONRenderer, FreeRADIUSClientsRenderer]

View File

@ -224,6 +224,11 @@
</div> </div>
</nav> </nav>
<div class="container wrapper"> <div class="container wrapper">
{% if settings.BANNER_TOP %}
<div class="alert alert-info text-center" role="alert">
{{ settings.BANNER_TOP|safe }}
</div>
{% endif %}
{% if settings.MAINTENANCE_MODE %} {% if settings.MAINTENANCE_MODE %}
<div class="alert alert-warning text-center" role="alert"> <div class="alert alert-warning text-center" role="alert">
<h4><i class="fa fa-exclamation-triangle"></i> Maintenance Mode</h4> <h4><i class="fa fa-exclamation-triangle"></i> Maintenance Mode</h4>
@ -240,6 +245,11 @@
{% endfor %} {% endfor %}
{% block content %}{% endblock %} {% block content %}{% endblock %}
<div class="push"></div> <div class="push"></div>
{% if settings.BANNER_BOTTOM %}
<div class="alert alert-info text-center banner-bottom" role="alert">
{{ settings.BANNER_BOTTOM|safe }}
</div>
{% endif %}
</div> </div>
<footer class="footer"> <footer class="footer">
<div class="container"> <div class="container">

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>