diff --git a/docker-compose.yml b/docker-compose.yml index 860022707..d435066d6 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -9,6 +9,7 @@ services: POSTGRES_PASSWORD: J5brHrAXFLQSif0K POSTGRES_DB: netbox netbox: + build: . image: digitalocean/netbox links: - postgres diff --git a/docs/index.md b/docs/index.md index 1c99bab4b..e9f57253d 100644 --- a/docs/index.md +++ b/docs/index.md @@ -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 * **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 diff --git a/docs/installation/netbox.md b/docs/installation/netbox.md index 96692af18..fde5ab019 100644 --- a/docs/installation/netbox.md +++ b/docs/installation/netbox.md @@ -9,9 +9,10 @@ NetBox requires following system dependencies: * libxslt1-dev * libffi-dev * 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. diff --git a/netbox/dcim/admin.py b/netbox/dcim/admin.py index 4dae1d998..09e16c348 100644 --- a/netbox/dcim/admin.py +++ b/netbox/dcim/admin.py @@ -89,7 +89,7 @@ class DeviceTypeAdmin(admin.ModelAdmin): power_port_count=Count('power_port_templates', distinct=True), power_outlet_count=Count('power_outlet_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): @@ -180,4 +180,4 @@ class DeviceAdmin(admin.ModelAdmin): def get_queryset(self, 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') 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..df07fa748 100644 --- a/netbox/dcim/models.py +++ b/netbox/dcim/models.py @@ -263,7 +263,7 @@ class Rack(CreatedUpdatedModel): @property def display_name(self): if self.facility_id: - return "{} ({})".format(self.name, self.facility_id) + return u"{} ({})".format(self.name, self.facility_id) return self.name 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') 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: @@ -696,9 +698,9 @@ class Device(CreatedUpdatedModel): if self.name: return self.name 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: - return "{} ({})".format(self.device_type, self.rack.name) + return u"{} ({})".format(self.device_type, self.rack.name) @property def identifier(self): @@ -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 cb03a41d1..92c8c029c 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -1,4 +1,5 @@ import re +from natsort import natsorted from operator import attrgetter 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.views.generic import View -from natsort import natsorted - from ipam.models import Prefix, IPAddress, VLAN from circuits.models import Circuit from extras.models import TopologyMap @@ -262,13 +261,22 @@ def devicetype(request, pk): devicetype = get_object_or_404(DeviceType, pk=pk) # Component tables - consoleport_table = tables.ConsolePortTemplateTable(ConsolePortTemplate.objects.filter(device_type=devicetype)) - consoleserverport_table = tables.ConsoleServerPortTemplateTable(ConsoleServerPortTemplate.objects - .filter(device_type=devicetype)) - powerport_table = tables.PowerPortTemplateTable(PowerPortTemplate.objects.filter(device_type=devicetype)) - poweroutlet_table = tables.PowerOutletTemplateTable(PowerOutletTemplate.objects.filter(device_type=devicetype)) + consoleport_table = tables.ConsolePortTemplateTable( + natsorted(ConsolePortTemplate.objects.filter(device_type=devicetype), key=attrgetter('name')) + ) + consoleserverport_table = tables.ConsoleServerPortTemplateTable( + 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)) - 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'): consoleport_table.base_columns['pk'].visible = True consoleserverport_table.base_columns['pk'].visible = True @@ -504,7 +512,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 @@ -515,17 +524,25 @@ class DeviceListView(ObjectListView): def device(request, pk): device = get_object_or_404(Device, pk=pk) - console_ports = ConsolePort.objects.filter(device=device).select_related('cs_port__device') - cs_ports = ConsoleServerPort.objects.filter(device=device).select_related('connected_console') - power_ports = PowerPort.objects.filter(device=device).select_related('power_outlet__device') - power_outlets = PowerOutlet.objects.filter(device=device).select_related('connected_port') + console_ports = natsorted( + ConsolePort.objects.filter(device=device).select_related('cs_port__device'), key=attrgetter('name') + ) + 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)\ .select_related('connected_as_a', 'connected_as_b', 'circuit') mgmt_interfaces = Interface.objects.filter(device=device, mgmt_only=True)\ .select_related('connected_as_a', 'connected_as_b', 'circuit') device_bays = natsorted( 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 @@ -1640,7 +1657,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..402e02330 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, ]) @@ -367,7 +375,7 @@ class VLAN(CreatedUpdatedModel): @property def display_name(self): - return "{} ({})".format(self.vid, self.name) + return u"{} ({})".format(self.vid, self.name) def get_status_class(self): return STATUS_CHOICE_CLASSES[self.status] 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/netbox/configuration.example.py b/netbox/netbox/configuration.example.py index 96e605859..aba0eb3f5 100644 --- a/netbox/netbox/configuration.example.py +++ b/netbox/netbox/configuration.example.py @@ -73,3 +73,8 @@ TIME_FORMAT = 'g:i a' SHORT_TIME_FORMAT = 'H:i:s' DATETIME_FORMAT = 'N j, Y g:i a' 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 = '' diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py index e8b0b2289..917b2ee4d 100644 --- a/netbox/netbox/settings.py +++ b/netbox/netbox/settings.py @@ -12,7 +12,7 @@ except ImportError: "the documentation.") -VERSION = '1.1.1-dev' +VERSION = '1.2.1-dev' # Import local configuration 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') 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') +BANNER_TOP = getattr(configuration, 'BANNER_TOP', False) +BANNER_BOTTOM = getattr(configuration, 'BANNER_BOTTOM', False) CSRF_TRUSTED_ORIGINS = ALLOWED_HOSTS # Attempt to import LDAP configuration if it has been defined diff --git a/netbox/project-static/css/base.css b/netbox/project-static/css/base.css index 04bab2c63..b1a9e88c3 100644 --- a/netbox/project-static/css/base.css +++ b/netbox/project-static/css/base.css @@ -28,6 +28,42 @@ body { footer p { 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 */ label { @@ -259,6 +295,9 @@ ul.rack_near_face li.empty:hover a { .dark_gray:hover { background-color: #2c3e50; } /* Misc */ +.banner-bottom { + margin-bottom: 50px; +} .panel table { margin-bottom: 0; } diff --git a/netbox/secrets/api/views.py b/netbox/secrets/api/views.py index 21a71e22f..629a28300 100644 --- a/netbox/secrets/api/views.py +++ b/netbox/secrets/api/views.py @@ -42,7 +42,7 @@ class SecretListView(generics.GenericAPIView): """ 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') serializer_class = serializers.SecretSerializer 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. """ - 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') serializer_class = serializers.SecretSerializer renderer_classes = [FormlessBrowsableAPIRenderer, JSONRenderer, FreeRADIUSClientsRenderer] diff --git a/netbox/templates/_base.html b/netbox/templates/_base.html index 438ffdff5..a6ab34c26 100644 --- a/netbox/templates/_base.html +++ b/netbox/templates/_base.html @@ -224,6 +224,11 @@
+ {% if settings.BANNER_TOP %} + + {% endif %} {% if settings.MAINTENANCE_MODE %} {% endfor %} {% block content %}{% endblock %} -
+
+ {% if settings.BANNER_BOTTOM %} + + {% endif %}