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

This commit is contained in:
johnhu 2017-09-25 04:07:05 +00:00
commit dbcabe4223
10 changed files with 233 additions and 67 deletions

View File

@ -78,6 +78,8 @@ AUTH_LDAP_USER_ATTR_MAP = {
``` ```
# User Groups for Permissions # User Groups for Permissions
!!! Info
When using Microsoft Active Directory, Support for nested Groups can be activated by using `GroupOfNamesType()` instead of `NestedGroupOfNamesType()` for AUTH_LDAP_GROUP_TYPE.
```python ```python
from django_auth_ldap.config import LDAPSearch, GroupOfNamesType from django_auth_ldap.config import LDAPSearch, GroupOfNamesType

View File

@ -13,6 +13,7 @@ from django.core.exceptions import ValidationError
from django.core.validators import MaxValueValidator, MinValueValidator from django.core.validators import MaxValueValidator, MinValueValidator
from django.db import models from django.db import models
from django.db.models import Count, Q, ObjectDoesNotExist from django.db.models import Count, Q, ObjectDoesNotExist
from django.db.models.expressions import RawSQL
from django.urls import reverse from django.urls import reverse
from django.utils.encoding import python_2_unicode_compatible from django.utils.encoding import python_2_unicode_compatible
@ -642,15 +643,16 @@ class InterfaceQuerySet(models.QuerySet):
To order interfaces naturally, the `name` field is split into six distinct components: leading text (type), To order interfaces naturally, the `name` field is split into six distinct components: leading text (type),
slot, subslot, position, channel, and virtual circuit: slot, subslot, position, channel, and virtual circuit:
{type}{slot}/{subslot}/{position}:{channel}.{vc} {type}{slot}/{subslot}/{position}/{subposition}:{channel}.{vc}
Components absent from the interface name are ignored. For example, an interface named GigabitEthernet0/1 would Components absent from the interface name are ignored. For example, an interface named GigabitEthernet1/2/3
be parsed as follows: would be parsed as follows:
name = 'GigabitEthernet' name = 'GigabitEthernet'
slot = None slot = 1
subslot = 0 subslot = 2
position = 1 position = 3
subposition = 0
channel = None channel = None
vc = 0 vc = 0
@ -659,17 +661,35 @@ class InterfaceQuerySet(models.QuerySet):
""" """
sql_col = '{}.name'.format(self.model._meta.db_table) sql_col = '{}.name'.format(self.model._meta.db_table)
ordering = { ordering = {
IFACE_ORDERING_POSITION: ('_slot', '_subslot', '_position', '_channel', '_vc', '_type', 'name'), IFACE_ORDERING_POSITION: (
IFACE_ORDERING_NAME: ('_type', '_slot', '_subslot', '_position', '_channel', '_vc', 'name'), '_slot', '_subslot', '_position', '_subposition', '_channel', '_vc', '_type', '_id', 'name',
),
IFACE_ORDERING_NAME: (
'_type', '_slot', '_subslot', '_position', '_subposition', '_channel', '_vc', '_id', 'name',
),
}[method] }[method]
return self.extra(select={
'_type': "SUBSTRING({} FROM '^([^0-9]+)')".format(sql_col), TYPE_RE = r"SUBSTRING({} FROM '^([^0-9]+)')"
'_slot': "CAST(SUBSTRING({} FROM '([0-9]+)\/[0-9]+\/[0-9]+(:[0-9]+)?(\.[0-9]+)?$') AS integer)".format(sql_col), ID_RE = r"CAST(SUBSTRING({} FROM '^(?:[^0-9]+)([0-9]+)$') AS integer)"
'_subslot': "CAST(SUBSTRING({} FROM '([0-9]+)\/[0-9]+(:[0-9]+)?(\.[0-9]+)?$') AS integer)".format(sql_col), SLOT_RE = r"CAST(SUBSTRING({} FROM '^(?:[^0-9]+)([0-9]+)\/') AS integer)"
'_position': "CAST(SUBSTRING({} FROM '([0-9]+)(:[0-9]+)?(\.[0-9]+)?$') AS integer)".format(sql_col), SUBSLOT_RE = r"CAST(SUBSTRING({} FROM '^(?:[^0-9]+)(?:[0-9]+\/)([0-9]+)') AS integer)"
'_channel': "COALESCE(CAST(SUBSTRING({} FROM ':([0-9]+)(\.[0-9]+)?$') AS integer), 0)".format(sql_col), POSITION_RE = r"CAST(SUBSTRING({} FROM '^(?:[^0-9]+)(?:[0-9]+\/){{2}}([0-9]+)') AS integer)"
'_vc': "COALESCE(CAST(SUBSTRING({} FROM '\.([0-9]+)$') AS integer), 0)".format(sql_col), SUBPOSITION_RE = r"CAST(SUBSTRING({} FROM '^(?:[^0-9]+)(?:[0-9]+\/){{3}}([0-9]+)') AS integer)"
}).order_by(*ordering) CHANNEL_RE = r"COALESCE(CAST(SUBSTRING({} FROM ':([0-9]+)(\.[0-9]+)?$') AS integer), 0)"
VC_RE = r"COALESCE(CAST(SUBSTRING({} FROM '\.([0-9]+)$') AS integer), 0)"
fields = {
'_type': RawSQL(TYPE_RE.format(sql_col), []),
'_id': RawSQL(ID_RE.format(sql_col), []),
'_slot': RawSQL(SLOT_RE.format(sql_col), []),
'_subslot': RawSQL(SUBSLOT_RE.format(sql_col), []),
'_position': RawSQL(POSITION_RE.format(sql_col), []),
'_subposition': RawSQL(SUBPOSITION_RE.format(sql_col), []),
'_channel': RawSQL(CHANNEL_RE.format(sql_col), []),
'_vc': RawSQL(VC_RE.format(sql_col), []),
}
return self.annotate(**fields).order_by(*ordering)
def connectable(self): def connectable(self):
""" """

View File

@ -98,3 +98,112 @@ class RackTestCase(TestCase):
face=None, face=None,
) )
self.assertTrue(pdu) self.assertTrue(pdu)
class InterfaceTestCase(TestCase):
def setUp(self):
self.site = Site.objects.create(
name='TestSite1',
slug='my-test-site'
)
self.rack = Rack.objects.create(
name='TestRack1',
facility_id='A101',
site=self.site,
u_height=42
)
self.manufacturer = Manufacturer.objects.create(
name='Acme',
slug='acme'
)
self.device_type = DeviceType.objects.create(
manufacturer=self.manufacturer,
model='FrameForwarder 2048',
slug='ff2048'
)
self.role = DeviceRole.objects.create(
name='Switch',
slug='switch',
)
def test_interface_order_natural(self):
device1 = Device.objects.create(
name='TestSwitch1',
device_type=self.device_type,
device_role=self.role,
site=self.site,
rack=self.rack,
position=10,
face=RACK_FACE_REAR,
)
interface1 = Interface.objects.create(
device=device1,
name='Ethernet1/3/1'
)
interface2 = Interface.objects.create(
device=device1,
name='Ethernet1/5/1'
)
interface3 = Interface.objects.create(
device=device1,
name='Ethernet1/4'
)
interface4 = Interface.objects.create(
device=device1,
name='Ethernet1/3/2/4'
)
interface5 = Interface.objects.create(
device=device1,
name='Ethernet1/3/2/1'
)
interface6 = Interface.objects.create(
device=device1,
name='Loopback1'
)
self.assertEqual(
list(Interface.objects.all().order_naturally()),
[interface1, interface5, interface4, interface3, interface2, interface6]
)
def test_interface_order_natural_subinterfaces(self):
device1 = Device.objects.create(
name='TestSwitch1',
device_type=self.device_type,
device_role=self.role,
site=self.site,
rack=self.rack,
position=10,
face=RACK_FACE_REAR,
)
interface1 = Interface.objects.create(
device=device1,
name='GigabitEthernet0/0/3'
)
interface2 = Interface.objects.create(
device=device1,
name='GigabitEthernet0/0/2.2'
)
interface3 = Interface.objects.create(
device=device1,
name='GigabitEthernet0/0/0.120'
)
interface4 = Interface.objects.create(
device=device1,
name='GigabitEthernet0/0/0'
)
interface5 = Interface.objects.create(
device=device1,
name='GigabitEthernet0/0/1.117'
)
interface6 = Interface.objects.create(
device=device1,
name='GigabitEthernet0'
)
self.assertEqual(
list(Interface.objects.all().order_naturally()),
[interface4, interface3, interface5, interface2, interface1, interface6]
)

View File

@ -34,7 +34,7 @@ RIR_ACTIONS = """
UTILIZATION_GRAPH = """ UTILIZATION_GRAPH = """
{% load helpers %} {% load helpers %}
{% if record.pk %}{% utilization_graph value %}{% else %}—{% endif %} {% if record.pk %}{% utilization_graph record.get_utilization %}{% else %}—{% endif %}
""" """
ROLE_ACTIONS = """ ROLE_ACTIONS = """
@ -210,10 +210,10 @@ class AggregateTable(BaseTable):
class AggregateDetailTable(AggregateTable): class AggregateDetailTable(AggregateTable):
child_count = tables.Column(verbose_name='Prefixes') child_count = tables.Column(verbose_name='Prefixes')
get_utilization = tables.TemplateColumn(UTILIZATION_GRAPH, orderable=False, verbose_name='Utilization') utilization = tables.TemplateColumn(UTILIZATION_GRAPH, orderable=False, verbose_name='Utilization')
class Meta(AggregateTable.Meta): class Meta(AggregateTable.Meta):
fields = ('pk', 'prefix', 'rir', 'child_count', 'get_utilization', 'date_added', 'description') fields = ('pk', 'prefix', 'rir', 'child_count', 'utilization', 'date_added', 'description')
# #
@ -256,10 +256,10 @@ class PrefixTable(BaseTable):
class PrefixDetailTable(PrefixTable): class PrefixDetailTable(PrefixTable):
get_utilization = tables.TemplateColumn(UTILIZATION_GRAPH, orderable=False, verbose_name='Utilization') utilization = tables.TemplateColumn(UTILIZATION_GRAPH, orderable=False)
class Meta(PrefixTable.Meta): class Meta(PrefixTable.Meta):
fields = ('pk', 'prefix', 'status', 'vrf', 'get_utilization', 'tenant', 'site', 'vlan', 'role', 'description') fields = ('pk', 'prefix', 'status', 'vrf', 'utilization', 'tenant', 'site', 'vlan', 'role', 'description')
# #

View File

@ -473,11 +473,11 @@ class PrefixView(View):
child_prefixes = Prefix.objects.filter( child_prefixes = Prefix.objects.filter(
vrf=prefix.vrf, prefix__net_contained=str(prefix.prefix) vrf=prefix.vrf, prefix__net_contained=str(prefix.prefix)
).select_related( ).select_related(
'site', 'role' 'site', 'vlan', 'role',
).annotate_depth(limit=0) ).annotate_depth(limit=0)
if child_prefixes: if child_prefixes:
child_prefixes = add_available_prefixes(prefix.prefix, child_prefixes) child_prefixes = add_available_prefixes(prefix.prefix, child_prefixes)
child_prefix_table = tables.PrefixTable(child_prefixes) child_prefix_table = tables.PrefixDetailTable(child_prefixes)
if request.user.has_perm('ipam.change_prefix') or request.user.has_perm('ipam.delete_prefix'): if request.user.has_perm('ipam.change_prefix') or request.user.has_perm('ipam.delete_prefix'):
child_prefix_table.base_columns['pk'].visible = True child_prefix_table.base_columns['pk'].visible = True

View File

@ -234,6 +234,10 @@ REST_FRAMEWORK = {
'DEFAULT_PERMISSION_CLASSES': ( 'DEFAULT_PERMISSION_CLASSES': (
'utilities.api.TokenPermissions', 'utilities.api.TokenPermissions',
), ),
'DEFAULT_RENDERER_CLASSES': (
'rest_framework.renderers.JSONRenderer',
'utilities.api.FormlessBrowsableAPIRenderer',
),
'DEFAULT_VERSION': REST_FRAMEWORK_VERSION, 'DEFAULT_VERSION': REST_FRAMEWORK_VERSION,
'DEFAULT_VERSIONING_CLASS': 'rest_framework.versioning.AcceptHeaderVersioning', 'DEFAULT_VERSIONING_CLASS': 'rest_framework.versioning.AcceptHeaderVersioning',
'PAGE_SIZE': PAGINATE_COUNT, 'PAGE_SIZE': PAGINATE_COUNT,

View File

@ -329,13 +329,14 @@ li.occupied + li.available {
} }
/* Devices */ /* Devices */
table.component-list tr.ipaddress td { table.component-list td.subtable {
background-color: #eeffff; padding: 0;
padding-bottom: 4px; padding-left: 16px;
padding-top: 4px;
} }
table.component-list tr.ipaddress:hover td { table.component-list td.subtable td {
background-color: #e6f7f7; border: none;
padding-bottom: 6px;
padding-top: 6px;
} }
/* AJAX loader */ /* AJAX loader */

View File

@ -1,4 +1,4 @@
<tr class="interface{% if not iface.enabled %} danger{% elif iface.connection and iface.connection.connection_status %} success{% elif iface.connection and not iface.connection.connection_status %} info{% endif %}"> <tr class="interface{% if not iface.enabled %} danger{% elif iface.connection and iface.connection.connection_status or iface.circuit_termination %} success{% elif iface.connection and not iface.connection.connection_status %} info{% elif iface.is_virtual %} warning{% 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 }}" />
@ -113,41 +113,55 @@
{% endif %} {% endif %}
</td> </td>
</tr> </tr>
{% for ip in iface.ip_addresses.all %} {% with iface.ip_addresses.all as ipaddresses %}
<tr class="ipaddress"> {% if ipaddresses %}
{% if selectable and perms.dcim.change_interface or perms.dcim.delete_interface %} <tr class="ipaddress">
<td></td> {% if selectable and perms.dcim.change_interface or perms.dcim.delete_interface %}
{% endif %} <td></td>
<td colspan="3"> <td colspan="6" class="subtable">
<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>
{% endif %}
</td>
<td class="text-right">
{% if ip.vrf %}
<a href="{% url 'ipam:vrf' pk=ip.vrf.pk %}">{{ ip.vrf }}</a>
{% else %} {% else %}
<span class="text-muted">Global</span> <td colspan="7" class="subtable">
{% endif %} {% endif %}
</td> <table class="table table-hover">
<td> {% for ip in ipaddresses %}
<span class="label label-{{ ip.get_status_class }}">{{ ip.get_status_display }}</span> <tr>
</td> <td>
<td class="text-right"> <a href="{% url 'ipam:ipaddress' pk=ip.pk %}">{{ ip }}</a>
{% if perms.ipam.change_ipaddress %} {% if ip.description %}
<a href="{% url 'ipam:ipaddress_edit' pk=ip.pk %}?return_url={{ device.get_absolute_url }}" class="btn btn-info btn-xs"> <i class="fa fa-fw fa-comment-o" title="{{ ip.description }}"></i>
<i class="glyphicon glyphicon-pencil" aria-hidden="true" title="Edit IP address"></i> {% endif %}
</a> </td>
{% endif %} <td>
{% if perms.ipam.delete_ipaddress %} {% if device.primary_ip4 == ip or device.primary_ip6 == ip %}
<a href="{% url 'ipam:ipaddress_delete' pk=ip.pk %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs"> <span class="label label-success">Primary</span>
<i class="glyphicon glyphicon-trash" aria-hidden="true" title="Delete IP address"></i> {% endif %}
</a> </td>
{% endif %} <td>
</td> {% if ip.vrf %}
</tr> <a href="{% url 'ipam:vrf' pk=ip.vrf.pk %}">{{ ip.vrf }}</a>
{% endfor %} {% else %}
<span class="text-muted">Global table</span>
{% endif %}
</td>
<td>
<span class="label label-{{ ip.get_status_class }}">{{ ip.get_status_display }}</span>
</td>
<td class="text-right">
{% if perms.ipam.change_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 %}
</table>
</td>
</tr>
{% endif %}
{% endwith %}

View File

@ -8,6 +8,7 @@ from rest_framework.compat import is_authenticated
from rest_framework.exceptions import APIException from rest_framework.exceptions import APIException
from rest_framework.pagination import LimitOffsetPagination from rest_framework.pagination import LimitOffsetPagination
from rest_framework.permissions import BasePermission, DjangoModelPermissions, SAFE_METHODS from rest_framework.permissions import BasePermission, DjangoModelPermissions, SAFE_METHODS
from rest_framework.renderers import BrowsableAPIRenderer
from rest_framework.serializers import Field, ModelSerializer, ValidationError from rest_framework.serializers import Field, ModelSerializer, ValidationError
from rest_framework.views import get_view_name as drf_get_view_name from rest_framework.views import get_view_name as drf_get_view_name
@ -206,6 +207,18 @@ class OptionalLimitOffsetPagination(LimitOffsetPagination):
return self.default_limit return self.default_limit
#
# Renderers
#
class FormlessBrowsableAPIRenderer(BrowsableAPIRenderer):
"""
Override the built-in BrowsableAPIRenderer to disable HTML forms.
"""
def show_form_for_method(self, *args, **kwargs):
return False
# #
# Miscellaneous # Miscellaneous
# #

View File

@ -478,7 +478,7 @@ class BulkEditView(View):
template_name = 'utilities/obj_bulk_edit.html' template_name = 'utilities/obj_bulk_edit.html'
default_return_url = 'home' default_return_url = 'home'
def get(self): def get(self, request):
return redirect(self.default_return_url) return redirect(self.default_return_url)
def post(self, request, **kwargs): def post(self, request, **kwargs):
@ -626,6 +626,9 @@ class BulkDeleteView(View):
template_name = 'utilities/obj_bulk_delete.html' template_name = 'utilities/obj_bulk_delete.html'
default_return_url = 'home' default_return_url = 'home'
def get(self, request):
return redirect(self.default_return_url)
def post(self, request, **kwargs): def post(self, request, **kwargs):
# Attempt to derive parent object if a parent class has been given # Attempt to derive parent object if a parent class has been given