From e31d2c9857baeffc067fbeab0efcbcde4b9b34e7 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 20 Sep 2017 09:56:20 -0400 Subject: [PATCH 1/8] Fixes #1520: Redirect on GET request to bulk edit/delete views --- netbox/utilities/views.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/netbox/utilities/views.py b/netbox/utilities/views.py index 1ec21b628..ec91399df 100644 --- a/netbox/utilities/views.py +++ b/netbox/utilities/views.py @@ -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 From 4f9379a5e22783815cf7ed7c502f86ac90b26065 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 20 Sep 2017 14:44:51 -0400 Subject: [PATCH 2/8] Cleaned up device interfaces/IPs table --- netbox/project-static/css/base.css | 13 ++-- netbox/templates/dcim/inc/interface.html | 88 ++++++++++++++---------- 2 files changed, 58 insertions(+), 43 deletions(-) diff --git a/netbox/project-static/css/base.css b/netbox/project-static/css/base.css index 0f6b24077..b013aab97 100644 --- a/netbox/project-static/css/base.css +++ b/netbox/project-static/css/base.css @@ -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 */ diff --git a/netbox/templates/dcim/inc/interface.html b/netbox/templates/dcim/inc/interface.html index 75d0f027d..3845c8759 100644 --- a/netbox/templates/dcim/inc/interface.html +++ b/netbox/templates/dcim/inc/interface.html @@ -1,4 +1,4 @@ - + {% if selectable and perms.dcim.change_interface or perms.dcim.delete_interface %} @@ -113,41 +113,55 @@ {% endif %} -{% for ip in iface.ip_addresses.all %} - - {% if selectable and perms.dcim.change_interface or perms.dcim.delete_interface %} - - {% endif %} - - {{ ip }} - {% if ip.description %} - - {% endif %} - {% if device.primary_ip4 == ip or device.primary_ip6 == ip %} - Primary - {% endif %} - - - {% if ip.vrf %} - {{ ip.vrf }} +{% with iface.ip_addresses.all as ipaddresses %} + {% if ipaddresses %} + + {% if selectable and perms.dcim.change_interface or perms.dcim.delete_interface %} + + {% else %} - Global + {% endif %} - - - {{ ip.get_status_display }} - - - {% if perms.ipam.change_ipaddress %} - - - - {% endif %} - {% if perms.ipam.delete_ipaddress %} - - - - {% endif %} - - -{% endfor %} + + {% for ip in ipaddresses %} + + + + + + + + {% endfor %} +
+ {{ ip }} + {% if ip.description %} + + {% endif %} + + {% if device.primary_ip4 == ip or device.primary_ip6 == ip %} + Primary + {% endif %} + + {% if ip.vrf %} + {{ ip.vrf }} + {% else %} + Global table + {% endif %} + + {{ ip.get_status_display }} + + {% if perms.ipam.change_ipaddress %} + + + + {% endif %} + {% if perms.ipam.delete_ipaddress %} + + + + {% endif %} +
+ + + {% endif %} +{% endwith %} From a52a871a94383a1b13b2341a296189ca119322a8 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 20 Sep 2017 16:54:27 -0400 Subject: [PATCH 3/8] Fixes #1522: Removed object create/edit forms from the browsable API --- netbox/netbox/settings.py | 4 ++++ netbox/utilities/api.py | 13 +++++++++++++ 2 files changed, 17 insertions(+) diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py index e9d8c04f5..13ae4c48b 100644 --- a/netbox/netbox/settings.py +++ b/netbox/netbox/settings.py @@ -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, diff --git a/netbox/utilities/api.py b/netbox/utilities/api.py index 36728a660..3698bc47c 100644 --- a/netbox/utilities/api.py +++ b/netbox/utilities/api.py @@ -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 # From 466ab0b4042da616d33e403cbd0b49a49788b821 Mon Sep 17 00:00:00 2001 From: Lars Hartmann Date: Thu, 21 Sep 2017 16:36:25 +0200 Subject: [PATCH 4/8] mentioned support of nested groups in LDAP installation documentation. --- docs/installation/ldap.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/installation/ldap.md b/docs/installation/ldap.md index b43105b8b..bb1618e32 100644 --- a/docs/installation/ldap.md +++ b/docs/installation/ldap.md @@ -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 From 126a5e5e4e7a74c34f63872bc5500dc5dccadbd6 Mon Sep 17 00:00:00 2001 From: Joey Wilhelm Date: Thu, 21 Sep 2017 10:33:34 -0700 Subject: [PATCH 5/8] Fix order_naturally with unbalanced names and use RawSQL instead of extra --- netbox/dcim/models.py | 48 +++++++++++++++-------- netbox/dcim/tests/test_models.py | 66 ++++++++++++++++++++++++++++++++ 2 files changed, 98 insertions(+), 16 deletions(-) diff --git a/netbox/dcim/models.py b/netbox/dcim/models.py index a189d9f29..52527cc19 100644 --- a/netbox/dcim/models.py +++ b/netbox/dcim/models.py @@ -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,31 @@ 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', 'name'), + IFACE_ORDERING_NAME: ('_type', '_slot', '_subslot', '_position', '_subposition', '_channel', '_vc', '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) + + fields = { + '_type': RawSQL(r"SUBSTRING({} FROM '^([^0-9]+)')".format(sql_col), []), + '_slot': RawSQL(r"CAST(SUBSTRING({} FROM '^(?:[^0-9]+)([0-9]+)') AS integer)".format(sql_col), []), + '_subslot': RawSQL( + r"COALESCE(CAST(SUBSTRING({} FROM '^(?:[^0-9]+)(?:[0-9]+)\/([0-9]+)') AS integer), 0)".format( + sql_col), [] + ), + '_position': RawSQL( + r"COALESCE(CAST(SUBSTRING({} FROM '^(?:[^0-9]+)(?:[0-9]+\/){{2}}([0-9]+)') AS integer), 0)".format( + sql_col), [] + ), + '_subposition': RawSQL( + r"COALESCE(CAST(SUBSTRING({} FROM '^(?:[^0-9]+)(?:[0-9]+\/){{3}}([0-9]+)') AS integer), 0)".format( + sql_col), [] + ), + '_channel': RawSQL( + r"COALESCE(CAST(SUBSTRING({} FROM ':([0-9]+)(\.[0-9]+)?$') AS integer), 0)".format(sql_col), []), + '_vc': RawSQL(r"COALESCE(CAST(SUBSTRING({} FROM '\.([0-9]+)$') AS integer), 0)".format(sql_col), []), + } + + return self.annotate(**fields).order_by(*ordering) def connectable(self): """ diff --git a/netbox/dcim/tests/test_models.py b/netbox/dcim/tests/test_models.py index 340c58092..9acbe3901 100644 --- a/netbox/dcim/tests/test_models.py +++ b/netbox/dcim/tests/test_models.py @@ -98,3 +98,69 @@ 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_device_port_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' + ) + + self.assertEqual( + list(Interface.objects.all().order_naturally()), + [interface1, interface5, interface4, interface3, interface2] + ) From 89194c067b89287eb5b912a01772f1aedb308ca0 Mon Sep 17 00:00:00 2001 From: Joey Wilhelm Date: Thu, 21 Sep 2017 17:20:21 -0700 Subject: [PATCH 6/8] Another test case to ensure subinterface ordering --- netbox/dcim/tests/test_models.py | 41 +++++++++++++++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/netbox/dcim/tests/test_models.py b/netbox/dcim/tests/test_models.py index 9acbe3901..00a4de28c 100644 --- a/netbox/dcim/tests/test_models.py +++ b/netbox/dcim/tests/test_models.py @@ -129,7 +129,7 @@ class InterfaceTestCase(TestCase): slug='switch', ) - def test_device_port_order_natural(self): + def test_interface_order_natural(self): device1 = Device.objects.create( name='TestSwitch1', device_type=self.device_type, @@ -164,3 +164,42 @@ class InterfaceTestCase(TestCase): list(Interface.objects.all().order_naturally()), [interface1, interface5, interface4, interface3, interface2] ) + + 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()), + [interface6, interface4, interface3, interface5, interface2, interface1] + ) From a010f7439d4598df4d0edfe6db0118e801aed39c Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Fri, 22 Sep 2017 16:18:43 -0400 Subject: [PATCH 7/8] More work on #1523 (natural ordering for interfaces) --- netbox/dcim/models.py | 42 +++++++++++++++++--------------- netbox/dcim/tests/test_models.py | 8 ++++-- 2 files changed, 29 insertions(+), 21 deletions(-) diff --git a/netbox/dcim/models.py b/netbox/dcim/models.py index 52527cc19..311f02e99 100644 --- a/netbox/dcim/models.py +++ b/netbox/dcim/models.py @@ -661,28 +661,32 @@ class InterfaceQuerySet(models.QuerySet): """ sql_col = '{}.name'.format(self.model._meta.db_table) ordering = { - IFACE_ORDERING_POSITION: ('_slot', '_subslot', '_position', '_subposition', '_channel', '_vc', '_type', 'name'), - IFACE_ORDERING_NAME: ('_type', '_slot', '_subslot', '_position', '_subposition', '_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] + 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(r"SUBSTRING({} FROM '^([^0-9]+)')".format(sql_col), []), - '_slot': RawSQL(r"CAST(SUBSTRING({} FROM '^(?:[^0-9]+)([0-9]+)') AS integer)".format(sql_col), []), - '_subslot': RawSQL( - r"COALESCE(CAST(SUBSTRING({} FROM '^(?:[^0-9]+)(?:[0-9]+)\/([0-9]+)') AS integer), 0)".format( - sql_col), [] - ), - '_position': RawSQL( - r"COALESCE(CAST(SUBSTRING({} FROM '^(?:[^0-9]+)(?:[0-9]+\/){{2}}([0-9]+)') AS integer), 0)".format( - sql_col), [] - ), - '_subposition': RawSQL( - r"COALESCE(CAST(SUBSTRING({} FROM '^(?:[^0-9]+)(?:[0-9]+\/){{3}}([0-9]+)') AS integer), 0)".format( - sql_col), [] - ), - '_channel': RawSQL( - r"COALESCE(CAST(SUBSTRING({} FROM ':([0-9]+)(\.[0-9]+)?$') AS integer), 0)".format(sql_col), []), - '_vc': RawSQL(r"COALESCE(CAST(SUBSTRING({} FROM '\.([0-9]+)$') AS integer), 0)".format(sql_col), []), + '_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) diff --git a/netbox/dcim/tests/test_models.py b/netbox/dcim/tests/test_models.py index 00a4de28c..b4101cfd5 100644 --- a/netbox/dcim/tests/test_models.py +++ b/netbox/dcim/tests/test_models.py @@ -159,10 +159,14 @@ class InterfaceTestCase(TestCase): 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] + [interface1, interface5, interface4, interface3, interface2, interface6] ) def test_interface_order_natural_subinterfaces(self): @@ -201,5 +205,5 @@ class InterfaceTestCase(TestCase): ) self.assertEqual( list(Interface.objects.all().order_naturally()), - [interface6, interface4, interface3, interface5, interface2, interface1] + [interface4, interface3, interface5, interface2, interface1, interface6] ) From ce9f1eb20106ffbef060b896151264137012618c Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Fri, 22 Sep 2017 21:17:46 -0400 Subject: [PATCH 8/8] Closes #1499: Added utilization graph to child prefixes table --- netbox/ipam/tables.py | 10 +++++----- netbox/ipam/views.py | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/netbox/ipam/tables.py b/netbox/ipam/tables.py index 512b75649..24bb9b3c2 100644 --- a/netbox/ipam/tables.py +++ b/netbox/ipam/tables.py @@ -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') # diff --git a/netbox/ipam/views.py b/netbox/ipam/views.py index d0225e567..89eecd352 100644 --- a/netbox/ipam/views.py +++ b/netbox/ipam/views.py @@ -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