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
!!! 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
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.db import models
from django.db.models import Count, Q, ObjectDoesNotExist
from django.db.models.expressions import RawSQL
from django.urls import reverse
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),
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
be parsed as follows:
Components absent from the interface name are ignored. For example, an interface named GigabitEthernet1/2/3
would be parsed as follows:
name = 'GigabitEthernet'
slot = None
subslot = 0
position = 1
slot = 1
subslot = 2
position = 3
subposition = 0
channel = None
vc = 0
@ -659,17 +661,35 @@ class InterfaceQuerySet(models.QuerySet):
"""
sql_col = '{}.name'.format(self.model._meta.db_table)
ordering = {
IFACE_ORDERING_POSITION: ('_slot', '_subslot', '_position', '_channel', '_vc', '_type', 'name'),
IFACE_ORDERING_NAME: ('_type', '_slot', '_subslot', '_position', '_channel', '_vc', 'name'),
IFACE_ORDERING_POSITION: (
'_slot', '_subslot', '_position', '_subposition', '_channel', '_vc', '_type', '_id', 'name',
),
IFACE_ORDERING_NAME: (
'_type', '_slot', '_subslot', '_position', '_subposition', '_channel', '_vc', '_id', 'name',
),
}[method]
return self.extra(select={
'_type': "SUBSTRING({} FROM '^([^0-9]+)')".format(sql_col),
'_slot': "CAST(SUBSTRING({} FROM '([0-9]+)\/[0-9]+\/[0-9]+(:[0-9]+)?(\.[0-9]+)?$') AS integer)".format(sql_col),
'_subslot': "CAST(SUBSTRING({} FROM '([0-9]+)\/[0-9]+(:[0-9]+)?(\.[0-9]+)?$') AS integer)".format(sql_col),
'_position': "CAST(SUBSTRING({} FROM '([0-9]+)(:[0-9]+)?(\.[0-9]+)?$') AS integer)".format(sql_col),
'_channel': "COALESCE(CAST(SUBSTRING({} FROM ':([0-9]+)(\.[0-9]+)?$') AS integer), 0)".format(sql_col),
'_vc': "COALESCE(CAST(SUBSTRING({} FROM '\.([0-9]+)$') AS integer), 0)".format(sql_col),
}).order_by(*ordering)
TYPE_RE = r"SUBSTRING({} FROM '^([^0-9]+)')"
ID_RE = r"CAST(SUBSTRING({} FROM '^(?:[^0-9]+)([0-9]+)$') AS integer)"
SLOT_RE = r"CAST(SUBSTRING({} FROM '^(?:[^0-9]+)([0-9]+)\/') AS integer)"
SUBSLOT_RE = r"CAST(SUBSTRING({} FROM '^(?:[^0-9]+)(?:[0-9]+\/)([0-9]+)') AS integer)"
POSITION_RE = r"CAST(SUBSTRING({} FROM '^(?:[^0-9]+)(?:[0-9]+\/){{2}}([0-9]+)') AS integer)"
SUBPOSITION_RE = r"CAST(SUBSTRING({} FROM '^(?:[^0-9]+)(?:[0-9]+\/){{3}}([0-9]+)') AS integer)"
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):
"""

View File

@ -98,3 +98,112 @@ class RackTestCase(TestCase):
face=None,
)
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 = """
{% load helpers %}
{% if record.pk %}{% utilization_graph value %}{% else %}—{% endif %}
{% if record.pk %}{% utilization_graph record.get_utilization %}{% else %}—{% endif %}
"""
ROLE_ACTIONS = """
@ -210,10 +210,10 @@ class AggregateTable(BaseTable):
class AggregateDetailTable(AggregateTable):
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):
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):
get_utilization = tables.TemplateColumn(UTILIZATION_GRAPH, orderable=False, verbose_name='Utilization')
utilization = tables.TemplateColumn(UTILIZATION_GRAPH, orderable=False)
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(
vrf=prefix.vrf, prefix__net_contained=str(prefix.prefix)
).select_related(
'site', 'role'
'site', 'vlan', 'role',
).annotate_depth(limit=0)
if 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'):
child_prefix_table.base_columns['pk'].visible = True

View File

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

View File

@ -329,13 +329,14 @@ li.occupied + li.available {
}
/* Devices */
table.component-list tr.ipaddress td {
background-color: #eeffff;
padding-bottom: 4px;
padding-top: 4px;
table.component-list td.subtable {
padding: 0;
padding-left: 16px;
}
table.component-list tr.ipaddress:hover td {
background-color: #e6f7f7;
table.component-list td.subtable td {
border: none;
padding-bottom: 6px;
padding-top: 6px;
}
/* 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 %}
<td class="pk">
<input name="pk" type="checkbox" value="{{ iface.pk }}" />
@ -113,25 +113,34 @@
{% endif %}
</td>
</tr>
{% for ip in iface.ip_addresses.all %}
{% with iface.ip_addresses.all as ipaddresses %}
{% if ipaddresses %}
<tr class="ipaddress">
{% if selectable and perms.dcim.change_interface or perms.dcim.delete_interface %}
<td></td>
<td colspan="6" class="subtable">
{% else %}
<td colspan="7" class="subtable">
{% endif %}
<td colspan="3">
<table class="table table-hover">
{% for ip in ipaddresses %}
<tr>
<td>
<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 %}
</td>
<td>
{% if device.primary_ip4 == ip or device.primary_ip6 == ip %}
<span class="label label-success">Primary</span>
{% endif %}
</td>
<td class="text-right">
<td>
{% if ip.vrf %}
<a href="{% url 'ipam:vrf' pk=ip.vrf.pk %}">{{ ip.vrf }}</a>
{% else %}
<span class="text-muted">Global</span>
<span class="text-muted">Global table</span>
{% endif %}
</td>
<td>
@ -151,3 +160,8 @@
</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.pagination import LimitOffsetPagination
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.views import get_view_name as drf_get_view_name
@ -206,6 +207,18 @@ class OptionalLimitOffsetPagination(LimitOffsetPagination):
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
#

View File

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