diff --git a/docs/installation/netbox.md b/docs/installation/netbox.md index e47e92133..0e116a29f 100644 --- a/docs/installation/netbox.md +++ b/docs/installation/netbox.md @@ -20,7 +20,8 @@ Python 3: ```no-highlight # yum install -y epel-release -# yum install -y gcc python3 python3-devel python3-pip libxml2-devel libxslt-devel libffi-devel graphviz openssl-devel +# yum install -y gcc python34 python34-devel python34-setuptools libxml2-devel libxslt-devel libffi-devel graphviz openssl-devel +# easy_install-3.4 pip ``` Python 2: diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index 32b8850f6..80326b137 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -1422,9 +1422,16 @@ class InterfaceBulkEditForm(BootstrapMixin, BulkEditForm): super(InterfaceBulkEditForm, self).__init__(*args, **kwargs) # Limit LAG choices to interfaces which belong to the parent device. + device = None if self.initial.get('device'): - self.fields['lag'].queryset = Interface.objects.filter( - device=self.initial['device'], form_factor=IFACE_FF_LAG + try: + device = Device.objects.get(pk=self.initial.get('device')) + except Device.DoesNotExist: + pass + if device is not None: + interface_ordering = device.device_type.interface_ordering + self.fields['lag'].queryset = Interface.objects.order_naturally(method=interface_ordering).filter( + device=device, form_factor=IFACE_FF_LAG ) else: self.fields['lag'].choices = [] @@ -1706,7 +1713,7 @@ class IPAddressForm(BootstrapMixin, CustomFieldForm): self.fields['interface'].required = True # If this device has only one interface, select it by default. - if len(interfaces) == 1: + if 'interface' not in self.initial and len(interfaces) == 1: self.fields['interface'].initial = interfaces[0] # If this device does not have any IP addresses assigned, default to setting the first IP as its primary. diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index cd1e0cd75..edaa56d22 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -13,7 +13,7 @@ from django.shortcuts import get_object_or_404, redirect, render from django.utils.http import urlencode from django.views.generic import View -from ipam.models import Prefix, IPAddress, Service, VLAN +from ipam.models import Prefix, Service, VLAN from circuits.models import Circuit from extras.models import Graph, TopologyMap, GRAPH_TYPE_INTERFACE, GRAPH_TYPE_SITE from utilities.forms import ConfirmationForm @@ -700,19 +700,15 @@ def device(request, pk): interfaces = Interface.objects.order_naturally(device.device_type.interface_ordering)\ .filter(device=device, mgmt_only=False)\ .select_related('connected_as_a__interface_b__device', 'connected_as_b__interface_a__device', - 'circuit_termination__circuit') + 'circuit_termination__circuit').prefetch_related('ip_addresses') mgmt_interfaces = Interface.objects.order_naturally(device.device_type.interface_ordering)\ .filter(device=device, mgmt_only=True)\ .select_related('connected_as_a__interface_b__device', 'connected_as_b__interface_a__device', - 'circuit_termination__circuit') + 'circuit_termination__circuit').prefetch_related('ip_addresses') device_bays = natsorted( DeviceBay.objects.filter(device=device).select_related('installed_device__device_type__manufacturer'), key=attrgetter('name') ) - - # Gather relevant device objects - ip_addresses = IPAddress.objects.filter(interface__device=device).select_related('interface', 'vrf')\ - .order_by('address') services = Service.objects.filter(device=device) secrets = device.secrets.all() @@ -743,7 +739,6 @@ def device(request, pk): 'interfaces': interfaces, 'mgmt_interfaces': mgmt_interfaces, 'device_bays': device_bays, - 'ip_addresses': ip_addresses, 'services': services, 'secrets': secrets, 'related_devices': related_devices, @@ -1599,9 +1594,7 @@ def ipaddress_assign(request, pk): return redirect('dcim:device', pk=device.pk) else: - form = forms.IPAddressForm(device, initial={ - 'interface': request.GET.get('interface', None) - }) + form = forms.IPAddressForm(device, initial=request.GET) return render(request, 'dcim/ipaddress_assign.html', { 'device': device, diff --git a/netbox/ipam/forms.py b/netbox/ipam/forms.py index 25bc82b91..d31c1aa41 100644 --- a/netbox/ipam/forms.py +++ b/netbox/ipam/forms.py @@ -210,28 +210,33 @@ class PrefixFromCSVForm(forms.ModelForm): site = self.cleaned_data.get('site') vlan_group_name = self.cleaned_data.get('vlan_group_name') vlan_vid = self.cleaned_data.get('vlan_vid') - - # Validate VLAN vlan_group = None + vlan = None + + # Validate VLAN group if vlan_group_name: try: vlan_group = VLANGroup.objects.get(site=site, name=vlan_group_name) except VLANGroup.DoesNotExist: - self.add_error('vlan_group_name', "Invalid VLAN group ({} - {}).".format(site, vlan_group_name)) - if vlan_vid and vlan_group: + if site: + self.add_error('vlan_group_name', "Invalid VLAN group ({} - {}).".format(site, vlan_group_name)) + else: + self.add_error('vlan_group_name', "Invalid global VLAN group ({}).".format(vlan_group_name)) + + # Validate VLAN + if vlan_vid: try: - self.instance.vlan = VLAN.objects.get(group=vlan_group, vid=vlan_vid) + self.instance.vlan = VLAN.objects.get(site=site, group=vlan_group, vid=vlan_vid) except VLAN.DoesNotExist: - self.add_error('vlan_vid', "Invalid VLAN ID ({} - {}).".format(vlan_group, vlan_vid)) - elif vlan_vid and site: - try: - self.instance.vlan = VLAN.objects.get(site=site, vid=vlan_vid) - except VLAN.DoesNotExist: - self.add_error('vlan_vid', "Invalid VLAN ID ({}) for site {}.".format(vlan_vid, site)) + if site: + self.add_error('vlan_vid', "Invalid VLAN ID ({}) for site {}.".format(vlan_vid, site)) + elif vlan_group: + self.add_error('vlan_vid', "Invalid VLAN ID ({}) for group {}.".format(vlan_vid, vlan_group_name)) + elif not vlan_group_name: + self.add_error('vlan_vid', "Invalid global VLAN ID ({}).".format(vlan_vid)) except VLAN.MultipleObjectsReturned: self.add_error('vlan_vid', "Multiple VLANs found ({} - VID {})".format(site, vlan_vid)) - elif vlan_vid: - self.add_error('vlan_vid', "Must specify site and/or VLAN group when assigning a VLAN.") + self.instance.vlan = vlan def save(self, *args, **kwargs): diff --git a/netbox/project-static/css/base.css b/netbox/project-static/css/base.css index 11ea04b72..0b740159a 100644 --- a/netbox/project-static/css/base.css +++ b/netbox/project-static/css/base.css @@ -313,6 +313,16 @@ li.occupied + li.available { border-top: 1px solid #474747; } +/* Devices */ +table.component-list tr.ipaddress td { + background-color: #eeffff; + padding-bottom: 4px; + padding-top: 4px; +} +table.component-list tr.ipaddress:hover td { + background-color: #e6f7f7; +} + /* Misc */ .banner-bottom { margin-bottom: 50px; diff --git a/netbox/templates/_base.html b/netbox/templates/_base.html index 800739b74..7dcb83d73 100644 --- a/netbox/templates/_base.html +++ b/netbox/templates/_base.html @@ -3,11 +3,11 @@
-NetBox is currently in maintenance mode. Functionality may be limited.
@@ -16,10 +16,9 @@
{{ iface.member_interfaces.all|join:", "|default:"No members" }} {% endif %} |
- - {{ iface.mac_address|default:'' }} - | - {% if iface.is_virtual %} + {% if iface.is_lag %} +LAG interface | + {% elif iface.is_virtual %}Virtual interface | {% elif iface.connection %} {% with iface.connected_interface as connected_iface %} @@ -51,7 +50,7 @@ Not connected {% endif %} -+ | {% if show_graphs %} {% if iface.circuit_termination or iface.connection %} - + {% else %} @@ -109,19 +108,41 @@ {% endif %} | |||
- | {{ ip }} | -{{ ip.vrf|default:"Global" }} | -{% if device.primary_ip4 == ip or device.primary_ip6 == ip %} - Primary - {% endif %} - | -{{ ip.description }} | -+ {{ ip }} + {% if ip.description %} + + {% endif %} + {% if device.primary_ip4 == ip or device.primary_ip6 == ip %} + Primary + {% endif %} + | ++ {% if ip.vrf %} + {{ ip.vrf }} + {% else %} + Global + {% endif %} + | ++ {{ ip.get_status_display }} + | ++ {% if perms.ipam.edit_ipaddress %} + + + + {% endif %} + {% if perms.ipam.delete_ipaddress %} + + + + {% endif %} + | + +{% endfor %} diff --git a/netbox/utilities/views.py b/netbox/utilities/views.py index 9a8b5ba1d..4edf87ce8 100644 --- a/netbox/utilities/views.py +++ b/netbox/utilities/views.py @@ -12,7 +12,9 @@ from django.forms import CharField, ModelMultipleChoiceField, MultipleHiddenInpu from django.http import HttpResponse from django.shortcuts import get_object_or_404, redirect, render from django.template import TemplateSyntaxError +from django.utils.html import escape from django.utils.http import is_safe_url +from django.utils.safestring import mark_safe from django.views.generic import View from extras.forms import CustomFieldForm @@ -194,10 +196,10 @@ class ObjectEditView(View): msg = u'Created ' if obj_created else u'Modified ' msg += self.model._meta.verbose_name if hasattr(obj, 'get_absolute_url'): - msg = u'{} {}'.format(msg, obj.get_absolute_url(), obj) + msg = u'{} {}'.format(msg, obj.get_absolute_url(), escape(obj)) else: - msg = u'{} {}'.format(msg, obj) - messages.success(request, msg) + msg = u'{} {}'.format(msg, escape(obj)) + messages.success(request, mark_safe(msg)) if obj_created: UserAction.objects.log_create(request.user, obj, msg) else: