Merge branch 'develop' of https://github.com/digitalocean/netbox into develop

# Conflicts:
#	netbox/dcim/views.py
#	netbox/templates/dcim/inc/interface.html
#	netbox/templates/dcim/inc/ipaddress.html
This commit is contained in:
Stephen Maunder 2017-04-13 08:43:35 +01:00
commit 776fd301ce
9 changed files with 135 additions and 110 deletions

View File

@ -20,7 +20,8 @@ Python 3:
```no-highlight ```no-highlight
# yum install -y epel-release # 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: Python 2:

View File

@ -1422,9 +1422,16 @@ class InterfaceBulkEditForm(BootstrapMixin, BulkEditForm):
super(InterfaceBulkEditForm, self).__init__(*args, **kwargs) super(InterfaceBulkEditForm, self).__init__(*args, **kwargs)
# Limit LAG choices to interfaces which belong to the parent device. # Limit LAG choices to interfaces which belong to the parent device.
device = None
if self.initial.get('device'): if self.initial.get('device'):
self.fields['lag'].queryset = Interface.objects.filter( try:
device=self.initial['device'], form_factor=IFACE_FF_LAG 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: else:
self.fields['lag'].choices = [] self.fields['lag'].choices = []
@ -1706,7 +1713,7 @@ class IPAddressForm(BootstrapMixin, CustomFieldForm):
self.fields['interface'].required = True self.fields['interface'].required = True
# If this device has only one interface, select it by default. # 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] 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. # If this device does not have any IP addresses assigned, default to setting the first IP as its primary.

View File

@ -13,7 +13,7 @@ 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 ipam.models import Prefix, IPAddress, Service, VLAN from ipam.models import Prefix, Service, VLAN
from circuits.models import Circuit from circuits.models import Circuit
from extras.models import Graph, TopologyMap, GRAPH_TYPE_INTERFACE, GRAPH_TYPE_SITE from extras.models import Graph, TopologyMap, GRAPH_TYPE_INTERFACE, GRAPH_TYPE_SITE
from utilities.forms import ConfirmationForm from utilities.forms import ConfirmationForm
@ -700,19 +700,15 @@ def device(request, pk):
interfaces = Interface.objects.order_naturally(device.device_type.interface_ordering)\ interfaces = Interface.objects.order_naturally(device.device_type.interface_ordering)\
.filter(device=device, mgmt_only=False)\ .filter(device=device, mgmt_only=False)\
.select_related('connected_as_a__interface_b__device', 'connected_as_b__interface_a__device', .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)\ mgmt_interfaces = Interface.objects.order_naturally(device.device_type.interface_ordering)\
.filter(device=device, mgmt_only=True)\ .filter(device=device, mgmt_only=True)\
.select_related('connected_as_a__interface_b__device', 'connected_as_b__interface_a__device', .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( 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 relevant device objects
ip_addresses = IPAddress.objects.filter(interface__device=device).select_related('interface', 'vrf')\
.order_by('address')
services = Service.objects.filter(device=device) services = Service.objects.filter(device=device)
secrets = device.secrets.all() secrets = device.secrets.all()
@ -743,7 +739,6 @@ def device(request, pk):
'interfaces': interfaces, 'interfaces': interfaces,
'mgmt_interfaces': mgmt_interfaces, 'mgmt_interfaces': mgmt_interfaces,
'device_bays': device_bays, 'device_bays': device_bays,
'ip_addresses': ip_addresses,
'services': services, 'services': services,
'secrets': secrets, 'secrets': secrets,
'related_devices': related_devices, 'related_devices': related_devices,
@ -1599,9 +1594,7 @@ def ipaddress_assign(request, pk):
return redirect('dcim:device', pk=device.pk) return redirect('dcim:device', pk=device.pk)
else: else:
form = forms.IPAddressForm(device, initial={ form = forms.IPAddressForm(device, initial=request.GET)
'interface': request.GET.get('interface', None)
})
return render(request, 'dcim/ipaddress_assign.html', { return render(request, 'dcim/ipaddress_assign.html', {
'device': device, 'device': device,

View File

@ -210,28 +210,33 @@ class PrefixFromCSVForm(forms.ModelForm):
site = self.cleaned_data.get('site') site = self.cleaned_data.get('site')
vlan_group_name = self.cleaned_data.get('vlan_group_name') vlan_group_name = self.cleaned_data.get('vlan_group_name')
vlan_vid = self.cleaned_data.get('vlan_vid') vlan_vid = self.cleaned_data.get('vlan_vid')
# Validate VLAN
vlan_group = None vlan_group = None
vlan = None
# Validate VLAN group
if vlan_group_name: if vlan_group_name:
try: try:
vlan_group = VLANGroup.objects.get(site=site, name=vlan_group_name) vlan_group = VLANGroup.objects.get(site=site, name=vlan_group_name)
except VLANGroup.DoesNotExist: except VLANGroup.DoesNotExist:
if site:
self.add_error('vlan_group_name', "Invalid VLAN group ({} - {}).".format(site, vlan_group_name)) self.add_error('vlan_group_name', "Invalid VLAN group ({} - {}).".format(site, vlan_group_name))
if vlan_vid and vlan_group: else:
self.add_error('vlan_group_name', "Invalid global VLAN group ({}).".format(vlan_group_name))
# Validate VLAN
if vlan_vid:
try: 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: except VLAN.DoesNotExist:
if site:
self.add_error('vlan_vid', "Invalid VLAN ID ({}) for site {}.".format(vlan_vid, 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: except VLAN.MultipleObjectsReturned:
self.add_error('vlan_vid', "Multiple VLANs found ({} - VID {})".format(site, vlan_vid)) self.add_error('vlan_vid', "Multiple VLANs found ({} - VID {})".format(site, vlan_vid))
elif vlan_vid: self.instance.vlan = vlan
self.add_error('vlan_vid', "Must specify site and/or VLAN group when assigning a VLAN.")
def save(self, *args, **kwargs): def save(self, *args, **kwargs):

View File

@ -313,6 +313,16 @@ li.occupied + li.available {
border-top: 1px solid #474747; 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 */ /* Misc */
.banner-bottom { .banner-bottom {
margin-bottom: 50px; margin-bottom: 50px;

View File

@ -273,7 +273,7 @@
<button type="button" class="close" data-dismiss="alert" aria-label="Close"> <button type="button" class="close" data-dismiss="alert" aria-label="Close">
<span aria-hidden="true">&times;</span> <span aria-hidden="true">&times;</span>
</button> </button>
{{ message|safe }} {{ message }}
</div> </div>
{% endfor %} {% endfor %}
{% block content %}{% endblock %} {% block content %}{% endblock %}

View File

@ -194,35 +194,6 @@
{% endif %} {% endif %}
</div> </div>
{% endif %} {% endif %}
<div class="panel panel-default">
<div class="panel-heading">
<strong>IP Addresses</strong>
</div>
{% if ip_addresses %}
<table class="table table-hover panel-body">
{% for ip in ip_addresses %}
{% include 'dcim/inc/ipaddress.html' %}
{% endfor %}
</table>
{% elif interfaces or mgmt_interfaces %}
<div class="panel-body text-muted">
None assigned
</div>
{% else %}
<div class="panel-body">
<a href="{% url 'dcim:interface_add' pk=device.pk %}">Create an interface</a> to assign an IP.
</div>
{% endif %}
{% if perms.ipam.add_ipaddress %}
{% if interfaces or mgmt_interfaces %}
<div class="panel-footer text-right">
<a href="{% url 'dcim:ipaddress_assign' pk=device.pk %}" class="btn btn-xs btn-primary">
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> Assign IP address
</a>
</div>
{% endif %}
{% endif %}
</div>
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading"> <div class="panel-heading">
<strong>Services</strong> <strong>Services</strong>
@ -250,7 +221,7 @@
<div class="panel-heading"> <div class="panel-heading">
<strong>Critical Connections</strong> <strong>Critical Connections</strong>
</div> </div>
<table class="table table-hover panel-body"> <table class="table table-hover panel-body component-list">
{% for iface in mgmt_interfaces %} {% for iface in mgmt_interfaces %}
{% include 'dcim/inc/interface.html' with icon='wrench' %} {% include 'dcim/inc/interface.html' with icon='wrench' %}
{% empty %} {% empty %}
@ -375,7 +346,7 @@
{% endif %} {% endif %}
</div> </div>
</div> </div>
<table class="table table-hover panel-body"> <table class="table table-hover panel-body component-list">
{% for devicebay in device_bays %} {% for devicebay in device_bays %}
{% include 'dcim/inc/devicebay.html' with selectable=True %} {% include 'dcim/inc/devicebay.html' with selectable=True %}
{% empty %} {% empty %}
@ -416,6 +387,9 @@
<div class="panel-heading"> <div class="panel-heading">
<strong>Interfaces</strong> <strong>Interfaces</strong>
<div class="pull-right"> <div class="pull-right">
<button class="btn btn-default btn-xs toggle-ips" selected="selected">
<span class="glyphicon glyphicon-check" aria-hidden="true"></span> Show IPs
</button>
{% if perms.dcim.change_interface and interfaces|length > 1 %} {% if perms.dcim.change_interface and interfaces|length > 1 %}
<button class="btn btn-default btn-xs toggle"> <button class="btn btn-default btn-xs toggle">
<span class="glyphicon glyphicon-unchecked" aria-hidden="true"></span> Select all <span class="glyphicon glyphicon-unchecked" aria-hidden="true"></span> Select all
@ -428,7 +402,7 @@
{% endif %} {% endif %}
</div> </div>
</div> </div>
<table class="table table-hover panel-body"> <table class="table table-hover panel-body component-list">
{% for iface in interfaces %} {% for iface in interfaces %}
{% include 'dcim/inc/interface.html' with selectable=True %} {% include 'dcim/inc/interface.html' with selectable=True %}
{% empty %} {% empty %}
@ -485,7 +459,7 @@
{% endif %} {% endif %}
</div> </div>
</div> </div>
<table class="table table-hover panel-body"> <table class="table table-hover panel-body component-list">
{% for csp in cs_ports %} {% for csp in cs_ports %}
{% include 'dcim/inc/consoleserverport.html' with selectable=True %} {% include 'dcim/inc/consoleserverport.html' with selectable=True %}
{% empty %} {% empty %}
@ -537,7 +511,7 @@
{% endif %} {% endif %}
</div> </div>
</div> </div>
<table class="table table-hover panel-body"> <table class="table table-hover panel-body component-list">
{% for po in power_outlets %} {% for po in power_outlets %}
{% include 'dcim/inc/poweroutlet.html' with selectable=True %} {% include 'dcim/inc/poweroutlet.html' with selectable=True %}
{% empty %} {% empty %}
@ -628,6 +602,18 @@ $(".powerport-toggle").click(function() {
$(".interface-toggle").click(function() { $(".interface-toggle").click(function() {
return toggleConnection($(this), "dcim/interface-connections/"); return toggleConnection($(this), "dcim/interface-connections/");
}); });
// Toggle the display of IP addresses under interfaces
$('button.toggle-ips').click(function() {
var selected = $(this).attr('selected');
if (selected) {
$('table.component-list tr.ipaddress').hide();
} else {
$('table.component-list tr.ipaddress').show();
}
$(this).attr('selected', !selected);
$(this).children('span').toggleClass('glyphicon-check glyphicon-unchecked');
return false;
});
</script> </script>
<script src="{% static 'js/graphs.js' %}"></script> <script src="{% static 'js/graphs.js' %}"></script>
<script src="{% static 'js/secrets.js' %}"></script> <script src="{% static 'js/secrets.js' %}"></script>

View File

@ -1,4 +1,4 @@
<tr{% if iface.connection and not iface.connection.connection_status %} class="info"{% endif %}> <tr class="interface{% if iface.connection and not iface.connection.connection_status %} info{% endif %}">
{% if selectable and perms.dcim.change_interface or perms.dcim.delete_interface %} {% if selectable and perms.dcim.change_interface or perms.dcim.delete_interface %}
<td class="pk"> <td class="pk">
<input name="pk" type="checkbox" value="{{ iface.pk }}" /> <input name="pk" type="checkbox" value="{{ iface.pk }}" />
@ -16,10 +16,9 @@
<br /><small class="text-muted">{{ iface.member_interfaces.all|join:", "|default:"No members" }}</small> <br /><small class="text-muted">{{ iface.member_interfaces.all|join:", "|default:"No members" }}</small>
{% endif %} {% endif %}
</td> </td>
<td> {% if iface.is_lag %}
<small>{{ iface.mac_address|default:'' }}</small> <td colspan="2" class="text-muted">LAG interface</td>
</td> {% elif iface.is_virtual %}
{% if iface.is_virtual %}
<td colspan="2" class="text-muted">Virtual interface</td> <td colspan="2" class="text-muted">Virtual interface</td>
{% elif iface.connection %} {% elif iface.connection %}
{% with iface.connected_interface as connected_iface %} {% with iface.connected_interface as connected_iface %}
@ -51,7 +50,7 @@
<span class="text-muted">Not connected</span> <span class="text-muted">Not connected</span>
</td> </td>
{% endif %} {% endif %}
<td class="text-right"> <td colspan="2" class="text-right">
{% if show_graphs %} {% if show_graphs %}
{% if iface.circuit_termination or iface.connection %} {% if iface.circuit_termination or iface.connection %}
<button type="button" class="btn btn-primary btn-xs" data-toggle="modal" data-target="#graphs_modal" data-obj="{{ device.name }} - {{ iface.name }}" data-url="{% url 'dcim-api:interface_graphs' pk=iface.pk %}" title="Show graphs"> <button type="button" class="btn btn-primary btn-xs" data-toggle="modal" data-target="#graphs_modal" data-obj="{{ device.name }} - {{ iface.name }}" data-url="{% url 'dcim-api:interface_graphs' pk=iface.pk %}" title="Show graphs">
@ -60,8 +59,8 @@
{% endif %} {% endif %}
{% endif %} {% endif %}
{% if perms.ipam.add_ipaddress %} {% if perms.ipam.add_ipaddress %}
<a href="{% url 'dcim:ipaddress_assign' pk=device.pk %}?interface={{ iface.pk }}" class="btn btn-xs btn-primary"> <a href="{% url 'dcim:ipaddress_assign' pk=device.pk %}?interface={{ iface.pk }}" class="btn btn-xs btn-success" title="Assign IP address">
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span> <i class="glyphicon glyphicon-plus" aria-hidden="true"></i>
</a> </a>
{% endif %} {% endif %}
{% if perms.dcim.change_interface %} {% if perms.dcim.change_interface %}
@ -76,7 +75,7 @@
<i class="fa fa-plug" aria-hidden="true"></i> <i class="fa fa-plug" aria-hidden="true"></i>
</a> </a>
{% endif %} {% endif %}
<a href="{% url 'dcim:interfaceconnection_delete' pk=iface.connection.pk %}?device={{ device.pk }}" class="btn btn-danger btn-xs" title="Delete connection"> <a href="{% url 'dcim:interfaceconnection_delete' pk=iface.connection.pk %}?device={{ device.pk }}" class="btn btn-danger btn-xs" title="Disconnect">
<i class="glyphicon glyphicon-resize-full" aria-hidden="true"></i> <i class="glyphicon glyphicon-resize-full" aria-hidden="true"></i>
</a> </a>
{% elif iface.circuit_termination and perms.circuits.change_circuittermination %} {% elif iface.circuit_termination and perms.circuits.change_circuittermination %}
@ -84,7 +83,7 @@
<i class="glyphicon glyphicon-ban-circle" aria-hidden="true"></i> <i class="glyphicon glyphicon-ban-circle" aria-hidden="true"></i>
</button> </button>
<a href="{% url 'circuits:circuittermination_edit' pk=iface.circuit_termination.pk %}" class="btn btn-danger btn-xs" title="Edit circuit termination"> <a href="{% url 'circuits:circuittermination_edit' pk=iface.circuit_termination.pk %}" class="btn btn-danger btn-xs" title="Edit circuit termination">
<i class="glyphicon glyphicon-remove" aria-hidden="true"></i> <i class="glyphicon glyphicon-resize-full" aria-hidden="true"></i>
</a> </a>
{% else %} {% else %}
<a href="{% url 'dcim:interfaceconnection_add' pk=device.pk %}?interface_a={{ iface.pk }}" class="btn btn-success btn-xs" title="Connect"> <a href="{% url 'dcim:interfaceconnection_add' pk=device.pk %}?interface_a={{ iface.pk }}" class="btn btn-success btn-xs" title="Connect">
@ -109,19 +108,41 @@
{% endif %} {% endif %}
</td> </td>
</tr> </tr>
{% if ip_addresses %} {% for ip in iface.ip_addresses.all %}
{% for ip in ip_addresses %} <tr class="ipaddress">
{% if ip.interface_id == iface.id %} {% if selectable and perms.dcim.change_interface or perms.dcim.delete_interface %}
<tr style="background: #eff">
<td></td> <td></td>
<td><a href="{% url 'ipam:ipaddress' pk=ip.pk %}">{{ ip }}</a></td> {% endif %}
<td>{{ ip.vrf|default:"Global" }}</td> <td colspan="2">
<td>{% if device.primary_ip4 == ip or device.primary_ip6 == ip %} <a href="{% url 'ipam:ipaddress' pk=ip.pk %}">{{ ip }}</a>
{% if ip.description %}
<i class="fa fa-fw fa-comment-o" title="{{ ip.description }}"></i>
{% endif %}
{% 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>
<td colspan="2">{{ ip.description }}</td> <td class="text-right">
</tr> {% if ip.vrf %}
<a href="{% url 'ipam:vrf' pk=ip.vrf.pk %}">{{ ip.vrf }}</a>
{% else %}
<span class="text-muted">Global</span>
{% endif %} {% endif %}
{% endfor %} </td>
{% endif %} <td>
<span class="label label-{{ ip.get_status_class }}">{{ ip.get_status_display }}</span>
</td>
<td class="text-right">
{% if perms.ipam.edit_ipaddress %}
<a href="{% url 'ipam:ipaddress_edit' pk=ip.pk %}?return_url={{ device.get_absolute_url }}" class="btn btn-info btn-xs">
<i class="glyphicon glyphicon-pencil" aria-hidden="true" title="Edit IP address"></i>
</a>
{% endif %}
{% if perms.ipam.delete_ipaddress %}
<a href="{% url 'ipam:ipaddress_delete' pk=ip.pk %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs">
<i class="glyphicon glyphicon-trash" aria-hidden="true" title="Delete IP address"></i>
</a>
{% endif %}
</td>
</tr>
{% endfor %}

View File

@ -12,7 +12,9 @@ from django.forms import CharField, ModelMultipleChoiceField, MultipleHiddenInpu
from django.http import HttpResponse from django.http import HttpResponse
from django.shortcuts import get_object_or_404, redirect, render from django.shortcuts import get_object_or_404, redirect, render
from django.template import TemplateSyntaxError from django.template import TemplateSyntaxError
from django.utils.html import escape
from django.utils.http import is_safe_url from django.utils.http import is_safe_url
from django.utils.safestring import mark_safe
from django.views.generic import View from django.views.generic import View
from extras.forms import CustomFieldForm from extras.forms import CustomFieldForm
@ -194,10 +196,10 @@ class ObjectEditView(View):
msg = u'Created ' if obj_created else u'Modified ' msg = u'Created ' if obj_created else u'Modified '
msg += self.model._meta.verbose_name msg += self.model._meta.verbose_name
if hasattr(obj, 'get_absolute_url'): if hasattr(obj, 'get_absolute_url'):
msg = u'{} <a href="{}">{}</a>'.format(msg, obj.get_absolute_url(), obj) msg = u'{} <a href="{}">{}</a>'.format(msg, obj.get_absolute_url(), escape(obj))
else: else:
msg = u'{} {}'.format(msg, obj) msg = u'{} {}'.format(msg, escape(obj))
messages.success(request, msg) messages.success(request, mark_safe(msg))
if obj_created: if obj_created:
UserAction.objects.log_create(request.user, obj, msg) UserAction.objects.log_create(request.user, obj, msg)
else: else: