From b44ec35ade2ddf6f697ccabfcf4484c91d47eb2d Mon Sep 17 00:00:00 2001 From: Oliver <932481+tb-killa@users.noreply.github.com> Date: Fri, 27 Aug 2021 20:27:26 +0200 Subject: [PATCH 01/43] Close #7032 Add URM connector to the list of available types for front and rear ports. There are URM-P2, URM-P4 and URM-P8 connectors available. --- netbox/dcim/choices.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/netbox/dcim/choices.py b/netbox/dcim/choices.py index 63054c2ce..5874a8e3e 100644 --- a/netbox/dcim/choices.py +++ b/netbox/dcim/choices.py @@ -958,6 +958,9 @@ class PortTypeChoices(ChoiceSet): TYPE_SPLICE = 'splice' TYPE_CS = 'cs' TYPE_SN = 'sn' + TYPE_URM_P2 = 'urm-p2' + TYPE_URM_P4 = 'urm-p4' + TYPE_URM_P8 = 'urm-p8' CHOICES = ( ( @@ -998,6 +1001,9 @@ class PortTypeChoices(ChoiceSet): (TYPE_ST, 'ST'), (TYPE_CS, 'CS'), (TYPE_SN, 'SN'), + (TYPE_URM_P2, 'URM-P2'), + (TYPE_URM_P4, 'URM-P4'), + (TYPE_URM_P8, 'URM-P8'), (TYPE_SPLICE, 'Splice'), ) ) From d5e5cdda23ca184c9e40cdaa0da611269dd72d65 Mon Sep 17 00:00:00 2001 From: Max Rink Date: Mon, 8 Feb 2021 14:55:56 +0100 Subject: [PATCH 02/43] Add Remote Group Support to the RemoteUserAuth Backend and Middleware fix incorrect assumption about when to run the group sync Add documentation for new Settings format to autopep8 compliance add first set of basic testcases format test to comply with pep8 rename SEPERATOR to SEPARATOR remove accidentally carried over parameter --- docs/configuration/optional-settings.md | 56 ++++++++ netbox/netbox/authentication.py | 160 +++++++++++++++++---- netbox/netbox/middleware.py | 64 ++++++++- netbox/netbox/settings.py | 7 + netbox/netbox/tests/test_authentication.py | 142 +++++++++++++++--- 5 files changed, 383 insertions(+), 46 deletions(-) diff --git a/docs/configuration/optional-settings.md b/docs/configuration/optional-settings.md index e324f3d46..88dd80918 100644 --- a/docs/configuration/optional-settings.md +++ b/docs/configuration/optional-settings.md @@ -490,6 +490,14 @@ NetBox can be configured to support remote user authentication by inferring user --- +## REMOTE_AUTH_GROUP_SYNC_ENABLED + +Default: `False` + +NetBox can be configured to sync remote user groups by inferring user authentication from an HTTP header set by the HTTP reverse proxy (e.g. nginx or Apache). Set this to `True` to enable this functionality. (Local authentication will still take effect as a fallback.) (Requires `REMOTE_AUTH_ENABLED`.) + +--- + ## REMOTE_AUTH_HEADER Default: `'HTTP_REMOTE_USER'` @@ -498,6 +506,54 @@ When remote user authentication is in use, this is the name of the HTTP header w --- +## REMOTE_AUTH_GROUP_HEADER + +Default: `'HTTP_REMOTE_USER_GROUP'` + +When remote user authentication is in use, this is the name of the HTTP header which informs NetBox of the currently authenticated user. For example, to use the request header `X-Remote-User-Groups` it needs to be set to `HTTP_X_REMOTE_USER_GROUPS`. (Requires `REMOTE_AUTH_ENABLED` and `REMOTE_AUTH_GROUP_SYNC_ENABLED` ) + +--- + +## REMOTE_AUTH_SUPERUSER_GROUPS + +Default: `[]` (Empty list) + +The list of groups that promote an remote User to Superuser on Login. If group isn't present on next Login, the Role gets revoked. (Requires `REMOTE_AUTH_ENABLED` and `REMOTE_AUTH_GROUP_SYNC_ENABLED` ) + +--- + +## REMOTE_AUTH_SUPERUSERS + +Default: `[]` (Empty list) + +The list of users that get promoted to Superuser on Login. If user isn't present in list on next Login, the Role gets revoked. (Requires `REMOTE_AUTH_ENABLED` and `REMOTE_AUTH_GROUP_SYNC_ENABLED` ) + +--- + +## REMOTE_AUTH_STAFF_GROUPS + +Default: `[]` (Empty list) + +The list of groups that promote an remote User to Staff on Login. If group isn't present on next Login, the Role gets revoked. (Requires `REMOTE_AUTH_ENABLED` and `REMOTE_AUTH_GROUP_SYNC_ENABLED` ) + +--- + +## REMOTE_AUTH_STAFF_USERS + +Default: `[]` (Empty list) + +The list of users that get promoted to Staff on Login. If user isn't present in list on next Login, the Role gets revoked. (Requires `REMOTE_AUTH_ENABLED` and `REMOTE_AUTH_GROUP_SYNC_ENABLED` ) + +--- + +## REMOTE_AUTH_GROUP_SEPARATOR + +Default: `|` (Pipe) + +The Seperator upon which `REMOTE_AUTH_GROUP_HEADER` gets split into individual Groups. This needs to be coordinated with your authentication Proxy. (Requires `REMOTE_AUTH_ENABLED` and `REMOTE_AUTH_GROUP_SYNC_ENABLED` ) + +--- + ## RELEASE_CHECK_URL Default: None (disabled) diff --git a/netbox/netbox/authentication.py b/netbox/netbox/authentication.py index 2c843f076..653fad3b0 100644 --- a/netbox/netbox/authentication.py +++ b/netbox/netbox/authentication.py @@ -2,14 +2,17 @@ import logging from collections import defaultdict from django.conf import settings +from django.contrib.auth import get_user_model from django.contrib.auth.backends import ModelBackend, RemoteUserBackend as _RemoteUserBackend -from django.contrib.auth.models import Group +from django.contrib.auth.models import Group, AnonymousUser from django.core.exceptions import ImproperlyConfigured from django.db.models import Q from users.models import ObjectPermission from utilities.permissions import permission_is_exempt, resolve_permission, resolve_permission_ct +UserModel = get_user_model() + class ObjectPermissionMixin(): @@ -101,38 +104,145 @@ class RemoteUserBackend(_RemoteUserBackend): def create_unknown_user(self): return settings.REMOTE_AUTH_AUTO_CREATE_USER - def configure_user(self, request, user): + def configure_groups(self, user, remote_groups): logger = logging.getLogger('netbox.authentication.RemoteUserBackend') # Assign default groups to the user group_list = [] - for name in settings.REMOTE_AUTH_DEFAULT_GROUPS: + for name in remote_groups: try: group_list.append(Group.objects.get(name=name)) except Group.DoesNotExist: - logging.error(f"Could not assign group {name} to remotely-authenticated user {user}: Group not found") - if group_list: - user.groups.add(*group_list) - logger.debug(f"Assigned groups to remotely-authenticated user {user}: {group_list}") - - # Assign default object permissions to the user - permissions_list = [] - for permission_name, constraints in settings.REMOTE_AUTH_DEFAULT_PERMISSIONS.items(): - try: - object_type, action = resolve_permission_ct(permission_name) - # TODO: Merge multiple actions into a single ObjectPermission per content type - obj_perm = ObjectPermission(actions=[action], constraints=constraints) - obj_perm.save() - obj_perm.users.add(user) - obj_perm.object_types.add(object_type) - permissions_list.append(permission_name) - except ValueError: logging.error( - f"Invalid permission name: '{permission_name}'. Permissions must be in the form " - "._. (Example: dcim.add_site)" - ) - if permissions_list: - logger.debug(f"Assigned permissions to remotely-authenticated user {user}: {permissions_list}") + f"Could not assign group {name} to remotely-authenticated user {user}: Group not found") + if group_list: + user.groups.set(group_list) + logger.debug( + f"Assigned groups to remotely-authenticated user {user}: {group_list}") + else: + user.groups.clear() + logger.debug(f"Stripping user {user} from Groups") + user.is_superuser = self._is_superuser(user) + logger.debug(f"User {user} is Superuser: {user.is_superuser}") + logger.debug( + f"User {user} should be Superuser: {self._is_superuser(user)}") + + user.is_staff = self._is_staff(user) + logger.debug(f"User {user} is Staff: {user.is_staff}") + logger.debug(f"User {user} should be Staff: {self._is_staff(user)}") + user.save() + return user + + def authenticate(self, request, remote_user, remote_groups=None): + """ + The username passed as ``remote_user`` is considered trusted. Return + the ``User`` object with the given username. Create a new ``User`` + object if ``create_unknown_user`` is ``True``. + Return None if ``create_unknown_user`` is ``False`` and a ``User`` + object with the given username is not found in the database. + """ + logger = logging.getLogger('netbox.authentication.RemoteUserBackend') + logger.debug( + f"trying to authenticate {remote_user} with groups {remote_groups}") + if not remote_user: + return + user = None + username = self.clean_username(remote_user) + + # Note that this could be accomplished in one try-except clause, but + # instead we use get_or_create when creating unknown users since it has + # built-in safeguards for multiple threads. + if self.create_unknown_user: + user, created = UserModel._default_manager.get_or_create(**{ + UserModel.USERNAME_FIELD: username + }) + if created: + user = self.configure_user(request, user) + else: + try: + user = UserModel._default_manager.get_by_natural_key(username) + except UserModel.DoesNotExist: + pass + if self.user_can_authenticate(user): + if settings.REMOTE_AUTH_GROUP_SYNC_ENABLED: + if user is not None and not isinstance(user, AnonymousUser): + return self.configure_groups(user, remote_groups) + else: + return user + else: + return None + + def _is_superuser(self, user): + logger = logging.getLogger('netbox.authentication.RemoteUserBackend') + superuser_groups = settings.REMOTE_AUTH_SUPERUSER_GROUPS + logger.debug(f"Superuser Groups: {superuser_groups}") + superusers = settings.REMOTE_AUTH_SUPERUSERS + logger.debug(f"Superuser Users: {superusers}") + user_groups = set() + for g in user.groups.all(): + user_groups.add(g.name) + logger.debug(f"User {user.username} is in Groups:{user_groups}") + + result = user.username in superusers or ( + set(user_groups) & set(superuser_groups)) + logger.debug(f"User {user.username} in Superuser Users :{result}") + return bool(result) + + def _is_staff(self, user): + logger = logging.getLogger('netbox.authentication.RemoteUserBackend') + staff_groups = settings.REMOTE_AUTH_STAFF_GROUPS + logger.debug(f"Superuser Groups: {staff_groups}") + staff_users = settings.REMOTE_AUTH_STAFF_USERS + logger.debug(f"Staff Users :{staff_users}") + user_groups = set() + for g in user.groups.all(): + user_groups.add(g.name) + logger.debug(f"User {user.username} is in Groups:{user_groups}") + result = user.username in staff_users or ( + set(user_groups) & set(staff_groups)) + logger.debug(f"User {user.username} in Staff Users :{result}") + return bool(result) + + def configure_user(self, request, user): + logger = logging.getLogger('netbox.authentication.RemoteUserBackend') + if not settings.REMOTE_AUTH_GROUP_SYNC_ENABLED: + # Assign default groups to the user + group_list = [] + for name in settings.REMOTE_AUTH_DEFAULT_GROUPS: + try: + group_list.append(Group.objects.get(name=name)) + except Group.DoesNotExist: + logging.error( + f"Could not assign group {name} to remotely-authenticated user {user}: Group not found") + if group_list: + user.groups.add(*group_list) + logger.debug( + f"Assigned groups to remotely-authenticated user {user}: {group_list}") + + # Assign default object permissions to the user + permissions_list = [] + for permission_name, constraints in settings.REMOTE_AUTH_DEFAULT_PERMISSIONS.items(): + try: + object_type, action = resolve_permission_ct( + permission_name) + # TODO: Merge multiple actions into a single ObjectPermission per content type + obj_perm = ObjectPermission( + actions=[action], constraints=constraints) + obj_perm.save() + obj_perm.users.add(user) + obj_perm.object_types.add(object_type) + permissions_list.append(permission_name) + except ValueError: + logging.error( + f"Invalid permission name: '{permission_name}'. Permissions must be in the form " + "._. (Example: dcim.add_site)" + ) + if permissions_list: + logger.debug( + f"Assigned permissions to remotely-authenticated user {user}: {permissions_list}") + else: + logger.debug( + f"Skipped initial assignment of permissions and groups to remotely-authenticated user {user} as Group sync is enabled") return user diff --git a/netbox/netbox/middleware.py b/netbox/netbox/middleware.py index ef50edc4a..e3072470e 100644 --- a/netbox/netbox/middleware.py +++ b/netbox/netbox/middleware.py @@ -1,8 +1,11 @@ import uuid from urllib import parse +import logging from django.conf import settings from django.contrib.auth.middleware import RemoteUserMiddleware as RemoteUserMiddleware_ +from django.contrib import auth +from django.core.exceptions import ImproperlyConfigured from django.db import ProgrammingError from django.http import Http404, HttpResponseRedirect from django.urls import reverse @@ -16,6 +19,7 @@ class LoginRequiredMiddleware(object): """ If LOGIN_REQUIRED is True, redirect all non-authenticated users to the login page. """ + def __init__(self, get_response): self.get_response = get_response @@ -49,12 +53,65 @@ class RemoteUserMiddleware(RemoteUserMiddleware_): return settings.REMOTE_AUTH_HEADER def process_request(self, request): - + logger = logging.getLogger( + 'netbox.authentication.RemoteUserMiddleware') # Bypass middleware if remote authentication is not enabled if not settings.REMOTE_AUTH_ENABLED: return + # AuthenticationMiddleware is required so that request.user exists. + if not hasattr(request, 'user'): + raise ImproperlyConfigured( + "The Django remote user auth middleware requires the" + " authentication middleware to be installed. Edit your" + " MIDDLEWARE setting to insert" + " 'django.contrib.auth.middleware.AuthenticationMiddleware'" + " before the RemoteUserMiddleware class.") + try: + username = request.META[self.header] + except KeyError: + # If specified header doesn't exist then remove any existing + # authenticated remote-user, or return (leaving request.user set to + # AnonymousUser by the AuthenticationMiddleware). + if self.force_logout_if_no_header and request.user.is_authenticated: + self._remove_invalid_user(request) + return + # If the user is already authenticated and that user is the user we are + # getting passed in the headers, then the correct user is already + # persisted in the session and we don't need to continue. + if request.user.is_authenticated: + if request.user.get_username() == self.clean_username(username, request): + return + else: + # An authenticated user is associated with the request, but + # it does not match the authorized user in the header. + self._remove_invalid_user(request) - return super().process_request(request) + # We are seeing this user for the first time in this session, attempt + # to authenticate the user. + if settings.REMOTE_AUTH_GROUP_SYNC_ENABLED: + logger.debug("Trying to sync Groups") + user = auth.authenticate( + request, remote_user=username, remote_groups=self._get_groups(request)) + else: + user = auth.authenticate(request, remote_user=username) + if user: + # User is valid. Set request.user and persist user in the session + # by logging the user in. + request.user = user + auth.login(request, user) + + def _get_groups(self, request): + logger = logging.getLogger( + 'netbox.authentication.RemoteUserMiddleware') + + groups_string = request.META.get( + settings.REMOTE_AUTH_GROUP_HEADER, None) + if groups_string: + groups = groups_string.split(settings.REMOTE_AUTH_GROUP_SEPARATOR) + else: + groups = [] + logger.debug(f"Groups are {groups}") + return groups class ObjectChangeMiddleware(object): @@ -71,6 +128,7 @@ class ObjectChangeMiddleware(object): have been created. Conversely, deletions are acted upon immediately, so that the serialized representation of the object is recorded before it (and any related objects) are actually deleted from the database. """ + def __init__(self, get_response): self.get_response = get_response @@ -90,6 +148,7 @@ class APIVersionMiddleware(object): """ If the request is for an API endpoint, include the API version as a response header. """ + def __init__(self, get_response): self.get_response = get_response @@ -105,6 +164,7 @@ class ExceptionHandlingMiddleware(object): Intercept certain exceptions which are likely indicative of installation issues and provide helpful instructions to the user. """ + def __init__(self, get_response): self.get_response = get_response diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py index 4f0e8956e..51a717c5d 100644 --- a/netbox/netbox/settings.py +++ b/netbox/netbox/settings.py @@ -120,6 +120,13 @@ REMOTE_AUTH_DEFAULT_GROUPS = getattr(configuration, 'REMOTE_AUTH_DEFAULT_GROUPS' REMOTE_AUTH_DEFAULT_PERMISSIONS = getattr(configuration, 'REMOTE_AUTH_DEFAULT_PERMISSIONS', {}) REMOTE_AUTH_ENABLED = getattr(configuration, 'REMOTE_AUTH_ENABLED', False) REMOTE_AUTH_HEADER = getattr(configuration, 'REMOTE_AUTH_HEADER', 'HTTP_REMOTE_USER') +REMOTE_AUTH_GROUP_HEADER = getattr(configuration, 'REMOTE_AUTH_GROUP_HEADER', 'HTTP_REMOTE_USER_GROUP') +REMOTE_AUTH_GROUP_SYNC_ENABLED = getattr(configuration, 'REMOTE_AUTH_GROUP_SYNC_ENABLED', False) +REMOTE_AUTH_SUPERUSER_GROUPS = getattr(configuration, 'REMOTE_AUTH_SUPERUSER_GROUPS', []) +REMOTE_AUTH_SUPERUSERS = getattr(configuration, 'REMOTE_AUTH_SUPERUSERS', []) +REMOTE_AUTH_STAFF_GROUPS = getattr(configuration, 'REMOTE_AUTH_STAFF_GROUPS', []) +REMOTE_AUTH_STAFF_USERS = getattr(configuration, 'REMOTE_AUTH_STAFF_USERS', []) +REMOTE_AUTH_GROUP_SEPARATOR = getattr(configuration, 'REMOTE_AUTH_GROUP_SEPARATOR', '|') RELEASE_CHECK_URL = getattr(configuration, 'RELEASE_CHECK_URL', None) REPORTS_ROOT = getattr(configuration, 'REPORTS_ROOT', os.path.join(BASE_DIR, 'reports')).rstrip('/') RQ_DEFAULT_TIMEOUT = getattr(configuration, 'RQ_DEFAULT_TIMEOUT', 300) diff --git a/netbox/netbox/tests/test_authentication.py b/netbox/netbox/tests/test_authentication.py index 6b30d044d..7fc12b4fd 100644 --- a/netbox/netbox/tests/test_authentication.py +++ b/netbox/netbox/tests/test_authentication.py @@ -58,7 +58,8 @@ class ExternalAuthenticationTestCase(TestCase): response = self.client.get(reverse('home'), follow=True, **headers) self.assertEqual(response.status_code, 200) - self.assertEqual(int(self.client.session.get('_auth_user_id')), self.user.pk, msg='Authentication failed') + self.assertEqual(int(self.client.session.get( + '_auth_user_id')), self.user.pk, msg='Authentication failed') @override_settings( REMOTE_AUTH_ENABLED=True, @@ -78,7 +79,8 @@ class ExternalAuthenticationTestCase(TestCase): response = self.client.get(reverse('home'), follow=True, **headers) self.assertEqual(response.status_code, 200) - self.assertEqual(int(self.client.session.get('_auth_user_id')), self.user.pk, msg='Authentication failed') + self.assertEqual(int(self.client.session.get( + '_auth_user_id')), self.user.pk, msg='Authentication failed') @override_settings( REMOTE_AUTH_ENABLED=True, @@ -102,7 +104,8 @@ class ExternalAuthenticationTestCase(TestCase): # Local user should have been automatically created new_user = User.objects.get(username='remoteuser2') - self.assertEqual(int(self.client.session.get('_auth_user_id')), new_user.pk, msg='Authentication failed') + self.assertEqual(int(self.client.session.get( + '_auth_user_id')), new_user.pk, msg='Authentication failed') @override_settings( REMOTE_AUTH_ENABLED=True, @@ -121,7 +124,8 @@ class ExternalAuthenticationTestCase(TestCase): self.assertTrue(settings.REMOTE_AUTH_ENABLED) self.assertTrue(settings.REMOTE_AUTH_AUTO_CREATE_USER) self.assertEqual(settings.REMOTE_AUTH_HEADER, 'HTTP_REMOTE_USER') - self.assertEqual(settings.REMOTE_AUTH_DEFAULT_GROUPS, ['Group 1', 'Group 2']) + self.assertEqual(settings.REMOTE_AUTH_DEFAULT_GROUPS, + ['Group 1', 'Group 2']) # Create required groups groups = ( @@ -135,7 +139,8 @@ class ExternalAuthenticationTestCase(TestCase): self.assertEqual(response.status_code, 200) new_user = User.objects.get(username='remoteuser2') - self.assertEqual(int(self.client.session.get('_auth_user_id')), new_user.pk, msg='Authentication failed') + self.assertEqual(int(self.client.session.get( + '_auth_user_id')), new_user.pk, msg='Authentication failed') self.assertListEqual( [groups[0], groups[1]], list(new_user.groups.all()) @@ -144,7 +149,8 @@ class ExternalAuthenticationTestCase(TestCase): @override_settings( REMOTE_AUTH_ENABLED=True, REMOTE_AUTH_AUTO_CREATE_USER=True, - REMOTE_AUTH_DEFAULT_PERMISSIONS={'dcim.add_site': None, 'dcim.change_site': None}, + REMOTE_AUTH_DEFAULT_PERMISSIONS={ + 'dcim.add_site': None, 'dcim.change_site': None}, LOGIN_REQUIRED=True ) def test_remote_auth_default_permissions(self): @@ -158,14 +164,102 @@ class ExternalAuthenticationTestCase(TestCase): self.assertTrue(settings.REMOTE_AUTH_ENABLED) self.assertTrue(settings.REMOTE_AUTH_AUTO_CREATE_USER) self.assertEqual(settings.REMOTE_AUTH_HEADER, 'HTTP_REMOTE_USER') - self.assertEqual(settings.REMOTE_AUTH_DEFAULT_PERMISSIONS, {'dcim.add_site': None, 'dcim.change_site': None}) + self.assertEqual(settings.REMOTE_AUTH_DEFAULT_PERMISSIONS, { + 'dcim.add_site': None, 'dcim.change_site': None}) response = self.client.get(reverse('home'), follow=True, **headers) self.assertEqual(response.status_code, 200) new_user = User.objects.get(username='remoteuser2') - self.assertEqual(int(self.client.session.get('_auth_user_id')), new_user.pk, msg='Authentication failed') - self.assertTrue(new_user.has_perms(['dcim.add_site', 'dcim.change_site'])) + self.assertEqual(int(self.client.session.get( + '_auth_user_id')), new_user.pk, msg='Authentication failed') + self.assertTrue(new_user.has_perms( + ['dcim.add_site', 'dcim.change_site'])) + + @override_settings( + REMOTE_AUTH_ENABLED=True, + REMOTE_AUTH_AUTO_CREATE_USER=True, + REMOTE_AUTH_GROUP_SYNC_ENABLED=True, + LOGIN_REQUIRED=True + ) + def test_remote_auth_remote_groups_default(self): + """ + Test enabling remote authentication with group sync enabled with the default configuration. + """ + headers = { + 'HTTP_REMOTE_USER': 'remoteuser2', + 'HTTP_REMOTE_USER_GROUP': 'Group 1|Group 2', + } + + self.assertTrue(settings.REMOTE_AUTH_ENABLED) + self.assertTrue(settings.REMOTE_AUTH_AUTO_CREATE_USER) + self.assertTrue(settings.REMOTE_AUTH_GROUP_SYNC_ENABLED) + self.assertEqual(settings.REMOTE_AUTH_HEADER, 'HTTP_REMOTE_USER') + self.assertEqual(settings.REMOTE_AUTH_GROUP_HEADER, + 'HTTP_REMOTE_USER_GROUP') + self.assertEqual(settings.REMOTE_AUTH_GROUP_SEPARATOR, '|') + + # Create required groups + groups = ( + Group(name='Group 1'), + Group(name='Group 2'), + Group(name='Group 3'), + ) + Group.objects.bulk_create(groups) + + response = self.client.get(reverse('home'), follow=True, **headers) + self.assertEqual(response.status_code, 200) + + new_user = User.objects.get(username='remoteuser2') + self.assertEqual(int(self.client.session.get( + '_auth_user_id')), new_user.pk, msg='Authentication failed') + self.assertListEqual( + [groups[0], groups[1]], + list(new_user.groups.all()) + ) + + @override_settings( + REMOTE_AUTH_ENABLED=True, + REMOTE_AUTH_AUTO_CREATE_USER=True, + REMOTE_AUTH_GROUP_SYNC_ENABLED=True, + REMOTE_AUTH_HEADER='HTTP_FOO', + REMOTE_AUTH_GROUP_HEADER='HTTP_BAR', + LOGIN_REQUIRED=True + ) + def test_remote_auth_remote_groups_custom_header(self): + """ + Test enabling remote authentication with group sync enabled with the default configuration. + """ + headers = { + 'HTTP_FOO': 'remoteuser2', + 'HTTP_BAR': 'Group 1|Group 2', + } + + self.assertTrue(settings.REMOTE_AUTH_ENABLED) + self.assertTrue(settings.REMOTE_AUTH_AUTO_CREATE_USER) + self.assertTrue(settings.REMOTE_AUTH_GROUP_SYNC_ENABLED) + self.assertEqual(settings.REMOTE_AUTH_HEADER, 'HTTP_FOO') + self.assertEqual(settings.REMOTE_AUTH_GROUP_HEADER, 'HTTP_BAR') + self.assertEqual(settings.REMOTE_AUTH_GROUP_SEPARATOR, '|') + + # Create required groups + groups = ( + Group(name='Group 1'), + Group(name='Group 2'), + Group(name='Group 3'), + ) + Group.objects.bulk_create(groups) + + response = self.client.get(reverse('home'), follow=True, **headers) + self.assertEqual(response.status_code, 200) + + new_user = User.objects.get(username='remoteuser2') + self.assertEqual(int(self.client.session.get( + '_auth_user_id')), new_user.pk, msg='Authentication failed') + self.assertListEqual( + [groups[0], groups[1]], + list(new_user.groups.all()) + ) class ObjectPermissionAPIViewTestCase(TestCase): @@ -206,7 +300,8 @@ class ObjectPermissionAPIViewTestCase(TestCase): def test_get_object(self): # Attempt to retrieve object without permission - url = reverse('ipam-api:prefix-detail', kwargs={'pk': self.prefixes[0].pk}) + url = reverse('ipam-api:prefix-detail', + kwargs={'pk': self.prefixes[0].pk}) response = self.client.get(url, **self.header) self.assertEqual(response.status_code, 403) @@ -221,12 +316,14 @@ class ObjectPermissionAPIViewTestCase(TestCase): obj_perm.object_types.add(ContentType.objects.get_for_model(Prefix)) # Retrieve permitted object - url = reverse('ipam-api:prefix-detail', kwargs={'pk': self.prefixes[0].pk}) + url = reverse('ipam-api:prefix-detail', + kwargs={'pk': self.prefixes[0].pk}) response = self.client.get(url, **self.header) self.assertEqual(response.status_code, 200) # Attempt to retrieve non-permitted object - url = reverse('ipam-api:prefix-detail', kwargs={'pk': self.prefixes[3].pk}) + url = reverse('ipam-api:prefix-detail', + kwargs={'pk': self.prefixes[3].pk}) response = self.client.get(url, **self.header) self.assertEqual(response.status_code, 404) @@ -292,7 +389,8 @@ class ObjectPermissionAPIViewTestCase(TestCase): # Attempt to edit an object without permission data = {'site': self.sites[0].pk} - url = reverse('ipam-api:prefix-detail', kwargs={'pk': self.prefixes[0].pk}) + url = reverse('ipam-api:prefix-detail', + kwargs={'pk': self.prefixes[0].pk}) response = self.client.patch(url, data, format='json', **self.header) self.assertEqual(response.status_code, 403) @@ -308,19 +406,22 @@ class ObjectPermissionAPIViewTestCase(TestCase): # Attempt to edit a non-permitted object data = {'site': self.sites[0].pk} - url = reverse('ipam-api:prefix-detail', kwargs={'pk': self.prefixes[3].pk}) + url = reverse('ipam-api:prefix-detail', + kwargs={'pk': self.prefixes[3].pk}) response = self.client.patch(url, data, format='json', **self.header) self.assertEqual(response.status_code, 404) # Edit a permitted object data['status'] = 'reserved' - url = reverse('ipam-api:prefix-detail', kwargs={'pk': self.prefixes[0].pk}) + url = reverse('ipam-api:prefix-detail', + kwargs={'pk': self.prefixes[0].pk}) response = self.client.patch(url, data, format='json', **self.header) self.assertEqual(response.status_code, 200) # Attempt to modify a permitted object to a non-permitted object data['site'] = self.sites[1].pk - url = reverse('ipam-api:prefix-detail', kwargs={'pk': self.prefixes[0].pk}) + url = reverse('ipam-api:prefix-detail', + kwargs={'pk': self.prefixes[0].pk}) response = self.client.patch(url, data, format='json', **self.header) self.assertEqual(response.status_code, 403) @@ -328,7 +429,8 @@ class ObjectPermissionAPIViewTestCase(TestCase): def test_delete_object(self): # Attempt to delete an object without permission - url = reverse('ipam-api:prefix-detail', kwargs={'pk': self.prefixes[0].pk}) + url = reverse('ipam-api:prefix-detail', + kwargs={'pk': self.prefixes[0].pk}) response = self.client.delete(url, format='json', **self.header) self.assertEqual(response.status_code, 403) @@ -343,11 +445,13 @@ class ObjectPermissionAPIViewTestCase(TestCase): obj_perm.object_types.add(ContentType.objects.get_for_model(Prefix)) # Attempt to delete a non-permitted object - url = reverse('ipam-api:prefix-detail', kwargs={'pk': self.prefixes[3].pk}) + url = reverse('ipam-api:prefix-detail', + kwargs={'pk': self.prefixes[3].pk}) response = self.client.delete(url, format='json', **self.header) self.assertEqual(response.status_code, 404) # Delete a permitted object - url = reverse('ipam-api:prefix-detail', kwargs={'pk': self.prefixes[0].pk}) + url = reverse('ipam-api:prefix-detail', + kwargs={'pk': self.prefixes[0].pk}) response = self.client.delete(url, format='json', **self.header) self.assertEqual(response.status_code, 204) From a226f06b1beb575011d783b202d76cb74d3b1f79 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Wed, 8 Sep 2021 16:47:04 -0400 Subject: [PATCH 03/43] PRVB --- netbox/netbox/settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py index 416536654..4c71b9549 100644 --- a/netbox/netbox/settings.py +++ b/netbox/netbox/settings.py @@ -16,7 +16,7 @@ from django.core.validators import URLValidator # Environment setup # -VERSION = '3.0.2' +VERSION = '3.0.3-dev' # Hostname HOSTNAME = platform.node() From ad7b8a9ac899f7515f97a2f4e33fc8b7929e3af4 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Thu, 9 Sep 2021 09:06:45 -0400 Subject: [PATCH 04/43] Fixes #7226: Exempt GraphQL API requests from CSRF inspection --- docs/release-notes/version-3.0.md | 8 ++++++++ netbox/netbox/urls.py | 3 ++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/docs/release-notes/version-3.0.md b/docs/release-notes/version-3.0.md index 379a6877e..a60c5b0f6 100644 --- a/docs/release-notes/version-3.0.md +++ b/docs/release-notes/version-3.0.md @@ -1,5 +1,13 @@ # NetBox v3.0 +## v3.0.3 (FUTURE) + +### Bug Fixes + +* [#7226](https://github.com/netbox-community/netbox/issues/7226) - Exempt GraphQL API requests from CSRF inspection + +--- + ## v3.0.2 (2021-09-08) ### Bug Fixes diff --git a/netbox/netbox/urls.py b/netbox/netbox/urls.py index 06e1eee06..53e20351c 100644 --- a/netbox/netbox/urls.py +++ b/netbox/netbox/urls.py @@ -1,6 +1,7 @@ from django.conf import settings from django.conf.urls import include from django.urls import path, re_path +from django.views.decorators.csrf import csrf_exempt from django.views.static import serve from drf_yasg import openapi from drf_yasg.views import get_schema_view @@ -63,7 +64,7 @@ _patterns = [ re_path(r'^api/swagger(?P.json|.yaml)$', schema_view.without_ui(), name='schema_swagger'), # GraphQL - path('graphql/', GraphQLView.as_view(graphiql=True, schema=schema), name='graphql'), + path('graphql/', csrf_exempt(GraphQLView.as_view(graphiql=True, schema=schema)), name='graphql'), # Serving static media in Django to pipe it through LoginRequiredMiddleware path('media/', serve, {'document_root': settings.MEDIA_ROOT}), From 7a813349f3614813a760faad9113b757942747c2 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Thu, 9 Sep 2021 09:07:28 -0400 Subject: [PATCH 05/43] Correct example GraphQL queries --- docs/graphql-api/overview.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/graphql-api/overview.md b/docs/graphql-api/overview.md index f024306b0..f4cdc5fba 100644 --- a/docs/graphql-api/overview.md +++ b/docs/graphql-api/overview.md @@ -11,7 +11,7 @@ curl -H "Authorization: Token $TOKEN" \ -H "Content-Type: application/json" \ -H "Accept: application/json" \ http://netbox/graphql/ \ ---data '{"query": "query {circuits(status:\"active\" {cid provider {name}}}"}' +--data '{"query": "query {circuit_list(status:\"active\") {cid provider {name}}}"}' ``` The response will include the requested data formatted as JSON: @@ -54,7 +54,7 @@ For more detail on constructing GraphQL queries, see the [Graphene documentation The GraphQL API employs the same filtering logic as the UI and REST API. Filters can be specified as key-value pairs within parentheses immediately following the query name. For example, the following will return only sites within the North Carolina region with a status of active: ``` -{"query": "query {sites(region:\"north-carolina\", status:\"active\") {name}}"} +{"query": "query {site_list(region:\"north-carolina\", status:\"active\") {name}}"} ``` ## Authentication From 4493c31216a6505cb4f7b4263f7b4a21f3aebc20 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Thu, 9 Sep 2021 09:10:02 -0400 Subject: [PATCH 06/43] Fixes #7227: Correct placeholder value for webhook CA file path --- netbox/templates/extras/webhook.html | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/netbox/templates/extras/webhook.html b/netbox/templates/extras/webhook.html index cd9eefbe0..afd67f0dc 100644 --- a/netbox/templates/extras/webhook.html +++ b/netbox/templates/extras/webhook.html @@ -110,13 +110,7 @@ CA File Path - - {% if object.ca_file_path %} - {{ object.ca_file_path }} - {% else %} - &mdash - {% endif %} - + {{ object.ca_file_path|placeholder }} From 908e6a7a383356a1830b3dfe4788276ec30e2eee Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Thu, 9 Sep 2021 14:46:31 -0400 Subject: [PATCH 07/43] Update NetBox installation video --- docs/installation/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/installation/index.md b/docs/installation/index.md index db1e48620..893b1f639 100644 --- a/docs/installation/index.md +++ b/docs/installation/index.md @@ -13,7 +13,7 @@ The following sections detail how to set up a new instance of NetBox: The video below demonstrates the installation of NetBox v2.10.3 on Ubuntu 20.04 for your reference. - + ## Requirements From bd957612c6afa5ddf605287c697218048f9a49e6 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Thu, 9 Sep 2021 16:05:46 -0400 Subject: [PATCH 08/43] Fixes #7167: Ensure consistent font size when using monospace formatting --- docs/release-notes/version-3.0.md | 1 + netbox/templates/dcim/device.html | 4 ++-- netbox/templates/dcim/rack.html | 4 ++-- netbox/templates/extras/report_result.html | 2 +- netbox/templates/extras/webhook.html | 2 +- netbox/templates/ipam/routetarget.html | 2 +- 6 files changed, 8 insertions(+), 7 deletions(-) diff --git a/docs/release-notes/version-3.0.md b/docs/release-notes/version-3.0.md index a60c5b0f6..d3fcdc140 100644 --- a/docs/release-notes/version-3.0.md +++ b/docs/release-notes/version-3.0.md @@ -4,6 +4,7 @@ ### Bug Fixes +* [#7167](https://github.com/netbox-community/netbox/issues/7167) - Ensure consistent font size when using monospace formatting * [#7226](https://github.com/netbox-community/netbox/issues/7226) - Exempt GraphQL API requests from CSRF inspection --- diff --git a/netbox/templates/dcim/device.html b/netbox/templates/dcim/device.html index d3bff464d..aa3f834d6 100644 --- a/netbox/templates/dcim/device.html +++ b/netbox/templates/dcim/device.html @@ -95,11 +95,11 @@ Serial Number - {{ object.serial|placeholder }} + {{ object.serial|placeholder }} Asset Tag - {{ object.asset_tag|placeholder }} + {{ object.asset_tag|placeholder }} diff --git a/netbox/templates/dcim/rack.html b/netbox/templates/dcim/rack.html index e7bad2bad..289735200 100644 --- a/netbox/templates/dcim/rack.html +++ b/netbox/templates/dcim/rack.html @@ -96,11 +96,11 @@ Serial Number - {{ object.serial|placeholder }} + {{ object.serial|placeholder }} Asset Tag - {{ object.asset_tag|placeholder }} + {{ object.asset_tag|placeholder }} Devices diff --git a/netbox/templates/extras/report_result.html b/netbox/templates/extras/report_result.html index f8da50e8e..a5ec2a04b 100644 --- a/netbox/templates/extras/report_result.html +++ b/netbox/templates/extras/report_result.html @@ -32,7 +32,7 @@ {% for method, data in result.data.items %} - + - + diff --git a/netbox/templates/ipam/routetarget.html b/netbox/templates/ipam/routetarget.html index cde1274db..94eec6a15 100644 --- a/netbox/templates/ipam/routetarget.html +++ b/netbox/templates/ipam/routetarget.html @@ -13,7 +13,7 @@
{{ method }}{{ method }} {{ data.success }} {{ data.info }} diff --git a/netbox/templates/extras/webhook.html b/netbox/templates/extras/webhook.html index afd67f0dc..f1cf876c1 100644 --- a/netbox/templates/extras/webhook.html +++ b/netbox/templates/extras/webhook.html @@ -79,7 +79,7 @@
Payload URL{{ object.payload_url }}{{ object.payload_url }}
HTTP Content Type
- + From 2a95e1bf71675a0bed5e3f8cb273d5cb4622f037 Mon Sep 17 00:00:00 2001 From: Daniel Vaccaro-Senna Date: Fri, 10 Sep 2021 16:03:46 +0100 Subject: [PATCH 09/43] Fixes #7195 update base template in plugin dev doc --- docs/plugins/development.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/plugins/development.md b/docs/plugins/development.md index 007d8434f..8d17b66c3 100644 --- a/docs/plugins/development.md +++ b/docs/plugins/development.md @@ -218,7 +218,7 @@ NetBox provides a base template to ensure a consistent user experience, which pl For more information on how template blocks work, consult the [Django documentation](https://docs.djangoproject.com/en/stable/ref/templates/builtins/#block). ```jinja2 -{% extends 'base.html' %} +{% extends 'base/layout.html' %} {% block content %} {% with config=settings.PLUGINS_CONFIG.netbox_animal_sounds %} From ab0a2abc549c3b5717045502e335d098bf998067 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Mon, 13 Sep 2021 09:21:57 -0400 Subject: [PATCH 10/43] Fixes #7248: Fix global search results section links --- docs/release-notes/version-3.0.md | 1 + netbox/templates/search.html | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/release-notes/version-3.0.md b/docs/release-notes/version-3.0.md index d3fcdc140..37a1947c2 100644 --- a/docs/release-notes/version-3.0.md +++ b/docs/release-notes/version-3.0.md @@ -6,6 +6,7 @@ * [#7167](https://github.com/netbox-community/netbox/issues/7167) - Ensure consistent font size when using monospace formatting * [#7226](https://github.com/netbox-community/netbox/issues/7226) - Exempt GraphQL API requests from CSRF inspection +* [#7248](https://github.com/netbox-community/netbox/issues/7248) - Fix global search results section links --- diff --git a/netbox/templates/search.html b/netbox/templates/search.html index 7e92ea43a..45b93016f 100644 --- a/netbox/templates/search.html +++ b/netbox/templates/search.html @@ -12,7 +12,7 @@
{% for obj_type in results %}
-
{{ obj_type.name|bettertitle }}
+
{{ obj_type.name|bettertitle }}
{% render_table obj_type.table 'inc/table.html' %}
From 147a4cbfb053b8bbc5f0eb66c08c9c0220165bb6 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Mon, 13 Sep 2021 09:33:08 -0400 Subject: [PATCH 11/43] Closes #7239: Redirect global search to filtered object list when an object type is selected --- docs/release-notes/version-3.0.md | 4 ++++ netbox/netbox/views/__init__.py | 19 +++++-------------- netbox/templates/search.html | 2 +- 3 files changed, 10 insertions(+), 15 deletions(-) diff --git a/docs/release-notes/version-3.0.md b/docs/release-notes/version-3.0.md index 37a1947c2..1a20a9f84 100644 --- a/docs/release-notes/version-3.0.md +++ b/docs/release-notes/version-3.0.md @@ -2,6 +2,10 @@ ## v3.0.3 (FUTURE) +### Enhancements + +* [#7239](https://github.com/netbox-community/netbox/issues/7239) - Redirect global search to filtered object list when an object type is selected + ### Bug Fixes * [#7167](https://github.com/netbox-community/netbox/issues/7167) - Ensure consistent font size when using monospace formatting diff --git a/netbox/netbox/views/__init__.py b/netbox/netbox/views/__init__.py index bb24cff5a..3568204fe 100644 --- a/netbox/netbox/views/__init__.py +++ b/netbox/netbox/views/__init__.py @@ -26,7 +26,6 @@ from ipam.models import Aggregate, IPAddress, IPRange, Prefix, VLAN, VRF from netbox.constants import SEARCH_MAX_RESULTS, SEARCH_TYPES from netbox.forms import SearchForm from tenancy.models import Tenant -from utilities.tables import paginate_table from virtualization.models import Cluster, VirtualMachine @@ -154,26 +153,18 @@ class HomeView(View): class SearchView(View): def get(self, request): - - # No query - if 'q' not in request.GET: - return render(request, 'search.html', { - 'form': SearchForm(), - }) - form = SearchForm(request.GET) results = [] if form.is_valid(): + # If an object type has been specified, redirect to the dedicated view for it if form.cleaned_data['obj_type']: - # Searching for a single type of object - obj_types = [form.cleaned_data['obj_type']] - else: - # Searching all object types - obj_types = SEARCH_TYPES.keys() + object_type = form.cleaned_data['obj_type'] + url = reverse(SEARCH_TYPES[object_type]['url']) + return redirect(f"{url}?q={form.cleaned_data['q']}") - for obj_type in obj_types: + for obj_type in SEARCH_TYPES.keys(): queryset = SEARCH_TYPES[obj_type]['queryset'].restrict(request.user, 'view') filterset = SEARCH_TYPES[obj_type]['filterset'] diff --git a/netbox/templates/search.html b/netbox/templates/search.html index 45b93016f..187b76c59 100644 --- a/netbox/templates/search.html +++ b/netbox/templates/search.html @@ -54,7 +54,7 @@ {% endif %} {% else %}
-
+
From 1f4263aa6d7c4f054f1ccd6fd2249f8314dfa3c7 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Mon, 13 Sep 2021 09:36:01 -0400 Subject: [PATCH 12/43] Fixes #7253: Remove obsolete reference to queryset cache invalidation --- docs/administration/replicating-netbox.md | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/docs/administration/replicating-netbox.md b/docs/administration/replicating-netbox.md index ee956edf5..531f9c027 100644 --- a/docs/administration/replicating-netbox.md +++ b/docs/administration/replicating-netbox.md @@ -71,14 +71,3 @@ To extract the saved archive into a new installation, run the following from the ```no-highlight tar -xf netbox_media.tar.gz ``` - ---- - -## Cache Invalidation - -If you are migrating your instance of NetBox to a different machine, be sure to first invalidate the cache on the original instance by issuing the `invalidate all` management command (within the Python virtual environment): - -```no-highlight -# source /opt/netbox/venv/bin/activate -(venv) # python3 manage.py invalidate all -``` From a6e79a1d618139b61d631af179451369f36f3958 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Mon, 13 Sep 2021 10:58:03 -0400 Subject: [PATCH 13/43] Add virtualenv instructions to plugin development docs --- docs/plugins/development.md | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/docs/plugins/development.md b/docs/plugins/development.md index 8d17b66c3..cde659a45 100644 --- a/docs/plugins/development.md +++ b/docs/plugins/development.md @@ -17,12 +17,12 @@ However, keep in mind that each piece of functionality is entirely optional. For ## Initial Setup -## Plugin Structure +### Plugin Structure Although the specific structure of a plugin is largely left to the discretion of its authors, a typical NetBox plugin looks something like this: ```no-highlight -plugin_name/ +project-name/ - plugin_name/ - templates/ - plugin_name/ @@ -38,13 +38,13 @@ plugin_name/ - setup.py ``` -The top level is the project root. Immediately within the root should exist several items: +The top level is the project root, which can have any name that you like. Immediately within the root should exist several items: * `setup.py` - This is a standard installation script used to install the plugin package within the Python environment. * `README` - A brief introduction to your plugin, how to install and configure it, where to find help, and any other pertinent information. It is recommended to write README files using a markup language such as Markdown. -* The plugin source directory, with the same name as your plugin. +* The plugin source directory, with the same name as your plugin. This must be a valid Python package name (e.g. no spaces or hyphens). -The plugin source directory contains all of the actual Python code and other resources used by your plugin. Its structure is left to the author's discretion, however it is recommended to follow best practices as outlined in the [Django documentation](https://docs.djangoproject.com/en/stable/intro/reusable-apps/). At a minimum, this directory **must** contain an `__init__.py` file containing an instance of NetBox's `PluginConfig` class. +The plugin source directory contains all the actual Python code and other resources used by your plugin. Its structure is left to the author's discretion, however it is recommended to follow best practices as outlined in the [Django documentation](https://docs.djangoproject.com/en/stable/intro/reusable-apps/). At a minimum, this directory **must** contain an `__init__.py` file containing an instance of NetBox's `PluginConfig` class. ### Create setup.py @@ -118,6 +118,21 @@ NetBox looks for the `config` variable within a plugin's `__init__.py` to load i All required settings must be configured by the user. If a configuration parameter is listed in both `required_settings` and `default_settings`, the default setting will be ignored. +### Create a Virtual Environment + +It is strongly recommended to create a Python [virtual environment](https://docs.python.org/3/tutorial/venv.html) specific to your plugin. This will afford you complete control over the installed versions of all dependencies and avoid conflicting with any system packages. This environment can live wherever you'd like, however it should be excluded from revision control. (A popular convention is to keep all virtual environments in the user's home directory, e.g. `~/.virtualenvs/`.) + +```shell +python3 -m venv /path/to/my/venv +``` + +You can make NetBox available within this environment by creating a path file pointing to its location. This will add NetBox to the Python path upon activation. (Be sure to adjust the command below to specify your actual virtual environment path, Python version, and NetBox installation.) + +```shell +cd $VENV/lib/python3.7/site-packages/ +echo /opt/netbox/netbox > netbox.pth +``` + ### Install the Plugin for Development To ease development, it is recommended to go ahead and install the plugin at this point using setuptools' `develop` mode. This will create symbolic links within your Python environment to the plugin development directory. Call `setup.py` from the plugin's root directory with the `develop` argument (instead of `install`): From 2ba6a6fc45cb9d632fed9f517fdb2f52df7e2646 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Tue, 14 Sep 2021 16:56:47 -0400 Subject: [PATCH 14/43] Closes #6387: Add xDSL interface type --- docs/release-notes/version-3.0.md | 1 + netbox/dcim/choices.py | 9 +++++++++ 2 files changed, 10 insertions(+) diff --git a/docs/release-notes/version-3.0.md b/docs/release-notes/version-3.0.md index 1a20a9f84..8864cf806 100644 --- a/docs/release-notes/version-3.0.md +++ b/docs/release-notes/version-3.0.md @@ -4,6 +4,7 @@ ### Enhancements +* [#6387](https://github.com/netbox-community/netbox/issues/6387) - Add xDSL interface type * [#7239](https://github.com/netbox-community/netbox/issues/7239) - Redirect global search to filtered object list when an object type is selected ### Bug Fixes diff --git a/netbox/dcim/choices.py b/netbox/dcim/choices.py index d20db1aa9..d95064c46 100644 --- a/netbox/dcim/choices.py +++ b/netbox/dcim/choices.py @@ -761,6 +761,9 @@ class InterfaceTypeChoices(ChoiceSet): TYPE_T3 = 't3' TYPE_E3 = 'e3' + # ATM/DSL + TYPE_XDSL = 'xdsl' + # Stacking TYPE_STACKWISE = 'cisco-stackwise' TYPE_STACKWISE_PLUS = 'cisco-stackwise-plus' @@ -885,6 +888,12 @@ class InterfaceTypeChoices(ChoiceSet): (TYPE_E3, 'E3 (34 Mbps)'), ) ), + ( + 'ATM', + ( + (TYPE_XDSL, 'xDSL'), + ) + ), ( 'Stacking', ( From 13d8957cf1c4a8bd22c105ca55a4a0f0d4380289 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Tue, 14 Sep 2021 17:05:07 -0400 Subject: [PATCH 15/43] Changelog for #7032 --- docs/release-notes/version-3.0.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes/version-3.0.md b/docs/release-notes/version-3.0.md index 8864cf806..d99580d05 100644 --- a/docs/release-notes/version-3.0.md +++ b/docs/release-notes/version-3.0.md @@ -5,6 +5,7 @@ ### Enhancements * [#6387](https://github.com/netbox-community/netbox/issues/6387) - Add xDSL interface type +* [#7032](https://github.com/netbox-community/netbox/issues/7032) - Add URM port types * [#7239](https://github.com/netbox-community/netbox/issues/7239) - Redirect global search to filtered object list when an object type is selected ### Bug Fixes From ca72b07947de5ec76c8c0e5200118d5157254297 Mon Sep 17 00:00:00 2001 From: cimnine Date: Wed, 15 Sep 2021 11:00:51 +0200 Subject: [PATCH 16/43] Remove secrets from the feature list --- docs/index.md | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/index.md b/docs/index.md index ad28a708c..c25c5be16 100644 --- a/docs/index.md +++ b/docs/index.md @@ -10,7 +10,6 @@ NetBox is an infrastructure resource modeling (IRM) application designed to empo * **Connections** - Network, console, and power connections among devices * **Virtualization** - Virtual machines and clusters * **Data circuits** - Long-haul communications circuits and providers -* **Secrets** - Encrypted storage of sensitive credentials ## What NetBox Is Not From e813dda2759734aea31d134d4ca7b48ac2c7ed49 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Wed, 15 Sep 2021 11:18:47 -0400 Subject: [PATCH 17/43] Remove default_app_config (deprecated in Django 3.2) --- netbox/circuits/__init__.py | 1 - netbox/dcim/__init__.py | 1 - netbox/extras/__init__.py | 1 - netbox/ipam/__init__.py | 1 - netbox/virtualization/__init__.py | 1 - 5 files changed, 5 deletions(-) diff --git a/netbox/circuits/__init__.py b/netbox/circuits/__init__.py index e5400337f..e69de29bb 100644 --- a/netbox/circuits/__init__.py +++ b/netbox/circuits/__init__.py @@ -1 +0,0 @@ -default_app_config = 'circuits.apps.CircuitsConfig' diff --git a/netbox/dcim/__init__.py b/netbox/dcim/__init__.py index 1f3214979..e69de29bb 100644 --- a/netbox/dcim/__init__.py +++ b/netbox/dcim/__init__.py @@ -1 +0,0 @@ -default_app_config = 'dcim.apps.DCIMConfig' diff --git a/netbox/extras/__init__.py b/netbox/extras/__init__.py index 3db5f9c25..e69de29bb 100644 --- a/netbox/extras/__init__.py +++ b/netbox/extras/__init__.py @@ -1 +0,0 @@ -default_app_config = 'extras.apps.ExtrasConfig' diff --git a/netbox/ipam/__init__.py b/netbox/ipam/__init__.py index 63c45c90b..e69de29bb 100644 --- a/netbox/ipam/__init__.py +++ b/netbox/ipam/__init__.py @@ -1 +0,0 @@ -default_app_config = 'ipam.apps.IPAMConfig' diff --git a/netbox/virtualization/__init__.py b/netbox/virtualization/__init__.py index 3f12ae450..e69de29bb 100644 --- a/netbox/virtualization/__init__.py +++ b/netbox/virtualization/__init__.py @@ -1 +0,0 @@ -default_app_config = 'virtualization.apps.VirtualizationConfig' From baf045aed656b20e2fb473b2642adb9193076d0b Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Wed, 15 Sep 2021 11:46:58 -0400 Subject: [PATCH 18/43] Clean up documentation build warnings --- docs/administration/permissions.md | 2 +- docs/core-functionality/circuits.md | 10 ++++---- docs/core-functionality/device-types.md | 20 ++++++++-------- docs/core-functionality/devices.md | 28 +++++++++++----------- docs/core-functionality/ipam.md | 16 ++++++------- docs/core-functionality/power.md | 6 ++--- docs/core-functionality/services.md | 2 +- docs/core-functionality/sites-and-racks.md | 14 +++++------ docs/core-functionality/tenancy.md | 4 ++-- docs/core-functionality/virtualization.md | 10 ++++---- docs/core-functionality/vlans.md | 4 ++-- docs/customization/custom-scripts.md | 2 +- docs/customization/reports.md | 2 +- docs/models/dcim/cable.md | 2 +- docs/rest-api/authentication.md | 2 +- mkdocs.yml | 6 ++--- 16 files changed, 64 insertions(+), 66 deletions(-) diff --git a/docs/administration/permissions.md b/docs/administration/permissions.md index c7c8996dc..97b691c1d 100644 --- a/docs/administration/permissions.md +++ b/docs/administration/permissions.md @@ -2,7 +2,7 @@ NetBox v2.9 introduced a new object-based permissions framework, which replace's Django's built-in permissions model. Object-based permissions enable an administrator to grant users or groups the ability to perform an action on arbitrary subsets of objects in NetBox, rather than all objects of a certain type. For example, it is possible to grant a user permission to view only sites within a particular region, or to modify only VLANs with a numeric ID within a certain range. -{!docs/models/users/objectpermission.md!} +{!models/users/objectpermission.md!} ### Example Constraint Definitions diff --git a/docs/core-functionality/circuits.md b/docs/core-functionality/circuits.md index 51261858c..b1b02e300 100644 --- a/docs/core-functionality/circuits.md +++ b/docs/core-functionality/circuits.md @@ -1,10 +1,10 @@ # Circuits -{!docs/models/circuits/provider.md!} -{!docs/models/circuits/providernetwork.md!} +{!models/circuits/provider.md!} +{!models/circuits/providernetwork.md!} --- -{!docs/models/circuits/circuit.md!} -{!docs/models/circuits/circuittype.md!} -{!docs/models/circuits/circuittermination.md!} +{!models/circuits/circuit.md!} +{!models/circuits/circuittype.md!} +{!models/circuits/circuittermination.md!} diff --git a/docs/core-functionality/device-types.md b/docs/core-functionality/device-types.md index a965f51f0..037d3cfd0 100644 --- a/docs/core-functionality/device-types.md +++ b/docs/core-functionality/device-types.md @@ -1,7 +1,7 @@ # Device Types -{!docs/models/dcim/devicetype.md!} -{!docs/models/dcim/manufacturer.md!} +{!models/dcim/devicetype.md!} +{!models/dcim/manufacturer.md!} --- @@ -30,11 +30,11 @@ Once component templates have been created, every new device that you create as !!! note Assignment of components from templates occurs only at the time of device creation. If you modify the templates of a device type, it will not affect devices which have already been created. However, you always have the option of adding, modifying, or deleting components on existing devices. -{!docs/models/dcim/consoleporttemplate.md!} -{!docs/models/dcim/consoleserverporttemplate.md!} -{!docs/models/dcim/powerporttemplate.md!} -{!docs/models/dcim/poweroutlettemplate.md!} -{!docs/models/dcim/interfacetemplate.md!} -{!docs/models/dcim/frontporttemplate.md!} -{!docs/models/dcim/rearporttemplate.md!} -{!docs/models/dcim/devicebaytemplate.md!} +{!models/dcim/consoleporttemplate.md!} +{!models/dcim/consoleserverporttemplate.md!} +{!models/dcim/powerporttemplate.md!} +{!models/dcim/poweroutlettemplate.md!} +{!models/dcim/interfacetemplate.md!} +{!models/dcim/frontporttemplate.md!} +{!models/dcim/rearporttemplate.md!} +{!models/dcim/devicebaytemplate.md!} diff --git a/docs/core-functionality/devices.md b/docs/core-functionality/devices.md index e05d6efd3..67e3612b9 100644 --- a/docs/core-functionality/devices.md +++ b/docs/core-functionality/devices.md @@ -1,8 +1,8 @@ # Devices and Cabling -{!docs/models/dcim/device.md!} -{!docs/models/dcim/devicerole.md!} -{!docs/models/dcim/platform.md!} +{!models/dcim/device.md!} +{!models/dcim/devicerole.md!} +{!models/dcim/platform.md!} --- @@ -10,20 +10,20 @@ Device components represent discrete objects within a device which are used to terminate cables, house child devices, or track resources. -{!docs/models/dcim/consoleport.md!} -{!docs/models/dcim/consoleserverport.md!} -{!docs/models/dcim/powerport.md!} -{!docs/models/dcim/poweroutlet.md!} -{!docs/models/dcim/interface.md!} -{!docs/models/dcim/frontport.md!} -{!docs/models/dcim/rearport.md!} -{!docs/models/dcim/devicebay.md!} -{!docs/models/dcim/inventoryitem.md!} +{!models/dcim/consoleport.md!} +{!models/dcim/consoleserverport.md!} +{!models/dcim/powerport.md!} +{!models/dcim/poweroutlet.md!} +{!models/dcim/interface.md!} +{!models/dcim/frontport.md!} +{!models/dcim/rearport.md!} +{!models/dcim/devicebay.md!} +{!models/dcim/inventoryitem.md!} --- -{!docs/models/dcim/virtualchassis.md!} +{!models/dcim/virtualchassis.md!} --- -{!docs/models/dcim/cable.md!} +{!models/dcim/cable.md!} diff --git a/docs/core-functionality/ipam.md b/docs/core-functionality/ipam.md index 00267dfe0..c1e77069e 100644 --- a/docs/core-functionality/ipam.md +++ b/docs/core-functionality/ipam.md @@ -1,19 +1,19 @@ # IP Address Management -{!docs/models/ipam/aggregate.md!} -{!docs/models/ipam/rir.md!} +{!models/ipam/aggregate.md!} +{!models/ipam/rir.md!} --- -{!docs/models/ipam/prefix.md!} -{!docs/models/ipam/role.md!} +{!models/ipam/prefix.md!} +{!models/ipam/role.md!} --- -{!docs/models/ipam/iprange.md!} -{!docs/models/ipam/ipaddress.md!} +{!models/ipam/iprange.md!} +{!models/ipam/ipaddress.md!} --- -{!docs/models/ipam/vrf.md!} -{!docs/models/ipam/routetarget.md!} +{!models/ipam/vrf.md!} +{!models/ipam/routetarget.md!} diff --git a/docs/core-functionality/power.md b/docs/core-functionality/power.md index 571109936..bdefb2afd 100644 --- a/docs/core-functionality/power.md +++ b/docs/core-functionality/power.md @@ -1,8 +1,8 @@ # Power Tracking -{!docs/models/dcim/powerpanel.md!} -{!docs/models/dcim/powerfeed.md!} +{!models/dcim/powerpanel.md!} +{!models/dcim/powerfeed.md!} # Example Power Topology -![Power distribution model](../../media/power_distribution.png) +![Power distribution model](/media/power_distribution.png) diff --git a/docs/core-functionality/services.md b/docs/core-functionality/services.md index 4d4256081..2e7aaf65a 100644 --- a/docs/core-functionality/services.md +++ b/docs/core-functionality/services.md @@ -1,3 +1,3 @@ # Service Mapping -{!docs/models/ipam/service.md!} +{!models/ipam/service.md!} diff --git a/docs/core-functionality/sites-and-racks.md b/docs/core-functionality/sites-and-racks.md index 1b5ee3ad1..c78f2120a 100644 --- a/docs/core-functionality/sites-and-racks.md +++ b/docs/core-functionality/sites-and-racks.md @@ -1,12 +1,12 @@ # Sites and Racks -{!docs/models/dcim/region.md!} -{!docs/models/dcim/sitegroup.md!} -{!docs/models/dcim/site.md!} -{!docs/models/dcim/location.md!} +{!models/dcim/region.md!} +{!models/dcim/sitegroup.md!} +{!models/dcim/site.md!} +{!models/dcim/location.md!} --- -{!docs/models/dcim/rack.md!} -{!docs/models/dcim/rackrole.md!} -{!docs/models/dcim/rackreservation.md!} +{!models/dcim/rack.md!} +{!models/dcim/rackrole.md!} +{!models/dcim/rackreservation.md!} diff --git a/docs/core-functionality/tenancy.md b/docs/core-functionality/tenancy.md index 540955698..fbe1ea8b9 100644 --- a/docs/core-functionality/tenancy.md +++ b/docs/core-functionality/tenancy.md @@ -1,4 +1,4 @@ # Tenancy Assignment -{!docs/models/tenancy/tenant.md!} -{!docs/models/tenancy/tenantgroup.md!} +{!models/tenancy/tenant.md!} +{!models/tenancy/tenantgroup.md!} diff --git a/docs/core-functionality/virtualization.md b/docs/core-functionality/virtualization.md index f406a59f3..220030ab2 100644 --- a/docs/core-functionality/virtualization.md +++ b/docs/core-functionality/virtualization.md @@ -1,10 +1,10 @@ # Virtualization -{!docs/models/virtualization/cluster.md!} -{!docs/models/virtualization/clustertype.md!} -{!docs/models/virtualization/clustergroup.md!} +{!models/virtualization/cluster.md!} +{!models/virtualization/clustertype.md!} +{!models/virtualization/clustergroup.md!} --- -{!docs/models/virtualization/virtualmachine.md!} -{!docs/models/virtualization/vminterface.md!} +{!models/virtualization/virtualmachine.md!} +{!models/virtualization/vminterface.md!} diff --git a/docs/core-functionality/vlans.md b/docs/core-functionality/vlans.md index cf1c73fe3..d69128765 100644 --- a/docs/core-functionality/vlans.md +++ b/docs/core-functionality/vlans.md @@ -1,4 +1,4 @@ # VLAN Management -{!docs/models/ipam/vlan.md!} -{!docs/models/ipam/vlangroup.md!} +{!models/ipam/vlan.md!} +{!models/ipam/vlangroup.md!} diff --git a/docs/customization/custom-scripts.md b/docs/customization/custom-scripts.md index a27bcab83..252e65f90 100644 --- a/docs/customization/custom-scripts.md +++ b/docs/customization/custom-scripts.md @@ -226,7 +226,7 @@ An IPv4 or IPv6 network with a mask. Returns a `netaddr.IPNetwork` object. Two a !!! note To run a custom script, a user must be assigned the `extras.run_script` permission. This is achieved by assigning the user (or group) a permission on the Script object and specifying the `run` action in the admin UI as shown below. - ![Adding the run action to a permission](../../media/admin_ui_run_permission.png) + ![Adding the run action to a permission](/media/admin_ui_run_permission.png) ### Via the Web UI diff --git a/docs/customization/reports.md b/docs/customization/reports.md index 0d75abd21..2fead68ec 100644 --- a/docs/customization/reports.md +++ b/docs/customization/reports.md @@ -104,7 +104,7 @@ Once you have created a report, it will appear in the reports list. Initially, r !!! note To run a report, a user must be assigned the `extras.run_report` permission. This is achieved by assigning the user (or group) a permission on the Report object and specifying the `run` action in the admin UI as shown below. - ![Adding the run action to a permission](../../media/admin_ui_run_permission.png) + ![Adding the run action to a permission](/media/admin_ui_run_permission.png) ### Via the Web UI diff --git a/docs/models/dcim/cable.md b/docs/models/dcim/cable.md index 753ab6f7f..18ac1002e 100644 --- a/docs/models/dcim/cable.md +++ b/docs/models/dcim/cable.md @@ -25,7 +25,7 @@ A cable may be traced from either of its endpoints by clicking the "trace" butto In the example below, three individual cables comprise a path between devices A and D: -![Cable path](../../media/models/dcim_cable_trace.png) +![Cable path](/media/models/dcim_cable_trace.png) Traced from Interface 1 on Device A, NetBox will show the following path: diff --git a/docs/rest-api/authentication.md b/docs/rest-api/authentication.md index 93c1ce303..1571f15fa 100644 --- a/docs/rest-api/authentication.md +++ b/docs/rest-api/authentication.md @@ -2,7 +2,7 @@ The NetBox REST API primarily employs token-based authentication. For convenience, cookie-based authentication can also be used when navigating the browsable API. -{!docs/models/users/token.md!} +{!models/users/token.md!} ## Authenticating to the API diff --git a/mkdocs.yml b/mkdocs.yml index f4c0cb137..7244c36d6 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -3,9 +3,6 @@ site_dir: netbox/project-static/docs site_url: https://netbox.readthedocs.io/ repo_name: netbox-community/netbox repo_url: https://github.com/netbox-community/netbox -python: - install: - - requirements: docs/requirements.txt theme: name: material icon: @@ -24,13 +21,14 @@ extra: - icon: fontawesome/brands/github link: https://github.com/netbox-community/netbox - icon: fontawesome/brands/slack - link: https://slack.netbox.dev + link: https://netdev.chat/ extra_css: - extra.css markdown_extensions: - admonition - attr_list - markdown_include.include: + base_path: 'docs/' headingOffset: 1 - pymdownx.emoji: emoji_index: !!python/name:materialx.emoji.twemoji From 25f501fb12bc33d10765e41fdea00ec7de22d4e6 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Wed, 15 Sep 2021 12:41:21 -0400 Subject: [PATCH 19/43] Remove obsolete responsive_table.html template --- .../templates/dcim/inc/devicetype_component_table.html | 10 ++++++---- netbox/templates/home.html | 4 ++-- netbox/templates/import_success.html | 5 ++++- netbox/templates/inc/responsive_table.html | 5 ----- netbox/templates/ipam/aggregate.html | 2 +- netbox/templates/ipam/vlan.html | 5 +++-- netbox/templates/utilities/obj_table.html | 9 +++++++-- netbox/templates/virtualization/cluster/devices.html | 5 +++-- .../virtualization/cluster/virtual_machines.html | 5 +++-- 9 files changed, 29 insertions(+), 21 deletions(-) delete mode 100644 netbox/templates/inc/responsive_table.html diff --git a/netbox/templates/dcim/inc/devicetype_component_table.html b/netbox/templates/dcim/inc/devicetype_component_table.html index dd79a668c..900e0f818 100644 --- a/netbox/templates/dcim/inc/devicetype_component_table.html +++ b/netbox/templates/dcim/inc/devicetype_component_table.html @@ -1,4 +1,6 @@ {% load helpers %} +{% load render_table from django_tables2 %} + {% if perms.dcim.change_devicetype %} {% csrf_token %} @@ -6,8 +8,8 @@
{{ title }}
-
- {% include 'inc/responsive_table.html' %} +
+ {% render_table table 'inc/table.html' %}
-
- {% include 'inc/responsive_table.html' with table=changelog_table %} +
+ {% render_table changelog_table 'inc/table.html' %}
diff --git a/netbox/templates/import_success.html b/netbox/templates/import_success.html index 6feb7dab5..122555b81 100644 --- a/netbox/templates/import_success.html +++ b/netbox/templates/import_success.html @@ -1,9 +1,12 @@ {% extends 'base/layout.html' %} +{% load render_table from django_tables2 %} {% block title %}Import Completed{% endblock %} {% block content %} - {% include 'inc/responsive_table.html' %} +
+ {% render_table table 'inc/table.html' %} +
{% if return_url %} View All {% endif %} diff --git a/netbox/templates/inc/responsive_table.html b/netbox/templates/inc/responsive_table.html deleted file mode 100644 index a6aaf5a6f..000000000 --- a/netbox/templates/inc/responsive_table.html +++ /dev/null @@ -1,5 +0,0 @@ -{% load render_table from django_tables2 %} - -
- {% render_table table 'inc/table.html' %} -
diff --git a/netbox/templates/ipam/aggregate.html b/netbox/templates/ipam/aggregate.html index 43760ed80..29aeec1ef 100644 --- a/netbox/templates/ipam/aggregate.html +++ b/netbox/templates/ipam/aggregate.html @@ -77,6 +77,6 @@
{% include 'utilities/obj_table.html' with table=prefix_table heading='Child Prefixes' bulk_edit_url='ipam:prefix_bulk_edit' bulk_delete_url='ipam:prefix_bulk_delete' %} -
+
{% endblock %} diff --git a/netbox/templates/ipam/vlan.html b/netbox/templates/ipam/vlan.html index 33541827f..5ecd6efed 100644 --- a/netbox/templates/ipam/vlan.html +++ b/netbox/templates/ipam/vlan.html @@ -1,5 +1,6 @@ {% extends 'ipam/vlan/base.html' %} {% load helpers %} +{% load render_table from django_tables2 %} {% load plugins %} {% block content %} @@ -92,8 +93,8 @@
Prefixes
-
- {% include 'inc/responsive_table.html' with table=prefix_table %} +
+ {% render_table prefix_table 'inc/table.html' %}
{% if perms.ipam.add_prefix %} {% endif %} - {% include table_template|default:'inc/responsive_table.html' %} +
+ {% render_table table 'inc/table.html' %} +
{% block extra_actions %}{% endblock %} @@ -48,7 +51,9 @@ {% else %} - {% include table_template|default:'inc/responsive_table.html' %} +
+ {% render_table table 'inc/table.html' %} +
{% endif %} diff --git a/netbox/templates/virtualization/cluster/devices.html b/netbox/templates/virtualization/cluster/devices.html index 658162243..9f4b7fc3e 100644 --- a/netbox/templates/virtualization/cluster/devices.html +++ b/netbox/templates/virtualization/cluster/devices.html @@ -1,5 +1,6 @@ {% extends 'virtualization/cluster/base.html' %} {% load helpers %} +{% load render_table from django_tables2 %} {% block content %}
@@ -10,8 +11,8 @@
{% csrf_token %} -
- {% include 'inc/responsive_table.html' with table=devices_table %} +
+ {% render_table devices_table 'inc/table.html' %}
{% if perms.virtualization.change_cluster %}
Name{{ object.name }}{{ object.name }}
Tenant
- + {% endblock %} From 574b57eadb86bdd449935a0bdc9f5754e4fa2750 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Thu, 16 Sep 2021 11:03:55 -0400 Subject: [PATCH 23/43] Treat compiled JS/CSS as binary when diffing --- .gitattributes | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.gitattributes b/.gitattributes index 9ad1ee25e..d431ac5f6 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,5 +1,5 @@ *.sh text eol=lf -# Treat minified or packed JS/CSS files as binary, as they're not meant to be human-readable -*.min.* binary -*.map binary -*.pack.js binary +# Treat compiled JS/CSS files as binary, as they're not meant to be human-readable +netbox/project-static/dist/*.css binary +netbox/project-static/dist/*.js binary +netbox/project-static/dist/*.js.map binary From 6f24a938d9be7edaca410e94ff548652f508be9f Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Thu, 16 Sep 2021 11:08:05 -0400 Subject: [PATCH 24/43] Fixes #7273: Disable automatic sorting of select options fetched via API --- netbox/dcim/forms.py | 4 +-- netbox/project-static/dist/netbox.js | Bin 322940 -> 322742 bytes netbox/project-static/dist/netbox.js.map | Bin 311224 -> 311063 bytes .../src/select/api/apiSelect.ts | 24 +----------------- 4 files changed, 2 insertions(+), 26 deletions(-) diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index c1f8eccf8..61e07c0bb 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -2202,9 +2202,7 @@ class DeviceForm(BootstrapMixin, TenancyForm, CustomFieldModelForm): api_url='/api/dcim/racks/{{rack}}/elevation/', attrs={ 'disabled-indicator': 'device', - 'data-query-param-face': "[\"$face\"]", - # The UI will not sort this element's options. - 'pre-sorted': '' + 'data-query-param-face': "[\"$face\"]" } ) ) diff --git a/netbox/project-static/dist/netbox.js b/netbox/project-static/dist/netbox.js index 33d004108f7a2d8bbc864171bef2575120e8ae90..0b93ac20f577abe745b0007fbc1c7c9e549acf64 100644 GIT binary patch delta 40 wcmex!OL*H&;f5B*7N!>FEiCL+)8(pILbtD}VtK;0{ZS)J9_RLwZWfLt09ry1;{X5v delta 230 zcmdmXQ~1v<;f5B*7N!>FEiCL+yah$6!TCicsVPc2iiXqwRFEiBII!bToNPCAi}j*&X9j*gX1I?j$|)5FqP4k~(AIq6h7 zIXY%K>v%ePXF2Pb06F;}j=!O^j=Q7Bbk__P)$J29SZY}qeW&x~vV<}fIB(yX%c9E8 qQs$!LzWq-L%RC;gj0&eJXB}V1%!=(x8d<#AnX)Xmf9zm6ZUz8k5G_{# delta 301 zcmbRKPiV(~p@tU57N!>FEiBII!r7ihPCAi}j*&X9j*gX1I?j%k)5FqP4(j<=IqBp& zIXV_P>3BMN0~J*QIhoENPL{Kd36PTy;)EMI>$p1xOt;8jQB?&R;jNSC=$NRJ4wTP! z((!e4E^!8uK;cl39iEQvY1?BmSn6091E=54VF?Y>chd0&NryW+2LTNNY6Th+4iR&9 zbaV!B-GTa?feL|4SBOlqqhmT&z9&%Lv&3b)Q7(%ryIhV7#BQ+R$gXhB1M1!$UBWVt ehpVL0sS4!A(#q}3jV#{mOl8{J13Flan*jhf5LTi9 diff --git a/netbox/project-static/src/select/api/apiSelect.ts b/netbox/project-static/src/select/api/apiSelect.ts index 6d83daaac..032fc83fa 100644 --- a/netbox/project-static/src/select/api/apiSelect.ts +++ b/netbox/project-static/src/select/api/apiSelect.ts @@ -149,13 +149,6 @@ export class APISelect { */ private more: Nullable = null; - /** - * This element's options come from the server pre-sorted and should not be sorted client-side. - * Determined by the existence of the `pre-sorted` attribute on the base `` element, - * the options will *not* be sorted. + * Apply new options to both the SlimSelect instance and this manager's state. */ private set options(optionsIn: Option[]) { let newOptions = optionsIn; @@ -304,12 +291,6 @@ export class APISelect { if (this.nullOption !== null) { newOptions = [this.nullOption, ...newOptions]; } - // Sort options unless this element is pre-sorted. - if (!this.preSorted) { - newOptions = newOptions.sort((a, b) => - a.text.toLowerCase() > b.text.toLowerCase() ? 1 : -1, - ); - } // Deduplicate options each time they're set. const deduplicated = uniqueByProperty(newOptions, 'value'); // Determine if the new options have a placeholder. @@ -471,9 +452,6 @@ export class APISelect { if (typeof result._depth === 'number' && result._depth > 0) { // If the object has a `_depth` property, indent its display text. - if (!this.preSorted) { - this.preSorted = true; - } text = `${'─'.repeat(result._depth)} ${text}`; } const data = {} as Record; From 0b2862be54be6b1f9f511d0a0675acac878dad47 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Thu, 16 Sep 2021 14:03:28 -0400 Subject: [PATCH 25/43] Changelog for #5775 --- docs/release-notes/version-3.0.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes/version-3.0.md b/docs/release-notes/version-3.0.md index 9288425b5..a646d2a73 100644 --- a/docs/release-notes/version-3.0.md +++ b/docs/release-notes/version-3.0.md @@ -12,6 +12,7 @@ ### Bug Fixes +* [#5775](https://github.com/netbox-community/netbox/issues/5775) - Enable group synchronization for remote authentication backend * [#7167](https://github.com/netbox-community/netbox/issues/7167) - Ensure consistent font size when using monospace formatting * [#7226](https://github.com/netbox-community/netbox/issues/7226) - Exempt GraphQL API requests from CSRF inspection * [#7248](https://github.com/netbox-community/netbox/issues/7248) - Fix global search results section links From 42afd80e82ff2cc760577ffe64b6150f0795f87b Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Thu, 16 Sep 2021 14:28:17 -0400 Subject: [PATCH 26/43] Changelog for #5775 --- docs/release-notes/version-3.0.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes/version-3.0.md b/docs/release-notes/version-3.0.md index 9288425b5..76b9c8524 100644 --- a/docs/release-notes/version-3.0.md +++ b/docs/release-notes/version-3.0.md @@ -4,6 +4,7 @@ ### Enhancements +* [#5775](https://github.com/netbox-community/netbox/issues/5775) - Enable synchronization of groups for remote authentication backend * [#6387](https://github.com/netbox-community/netbox/issues/6387) - Add xDSL interface type * [#6988](https://github.com/netbox-community/netbox/issues/6988) - Order tenants alphabetically without regard to group assignment * [#7032](https://github.com/netbox-community/netbox/issues/7032) - Add URM port types From e3e005e3278bfa710cb7ce2f3e3ce8c131c3f0a5 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Thu, 16 Sep 2021 14:41:01 -0400 Subject: [PATCH 27/43] Fixes #7266: Tweak font color for form field placeholder text --- docs/release-notes/version-3.0.md | 1 + netbox/project-static/dist/netbox-dark.css | Bin 807537 -> 807537 bytes netbox/project-static/dist/netbox-light.css | Bin 502362 -> 502362 bytes netbox/project-static/dist/netbox-print.css | Bin 1658484 -> 1658484 bytes netbox/project-static/styles/select.scss | 4 ++-- netbox/project-static/styles/theme-dark.scss | 2 +- netbox/project-static/styles/theme-light.scss | 1 + 7 files changed, 5 insertions(+), 3 deletions(-) diff --git a/docs/release-notes/version-3.0.md b/docs/release-notes/version-3.0.md index 76b9c8524..04bd95a8c 100644 --- a/docs/release-notes/version-3.0.md +++ b/docs/release-notes/version-3.0.md @@ -17,6 +17,7 @@ * [#7226](https://github.com/netbox-community/netbox/issues/7226) - Exempt GraphQL API requests from CSRF inspection * [#7248](https://github.com/netbox-community/netbox/issues/7248) - Fix global search results section links * [#7279](https://github.com/netbox-community/netbox/issues/7279) - Fix exception when tracing cable with no associated path +* [#7266](https://github.com/netbox-community/netbox/issues/7266) - Tweak font color for form field placeholder text --- diff --git a/netbox/project-static/dist/netbox-dark.css b/netbox/project-static/dist/netbox-dark.css index 196966fea5aa79954b36720afe3a87bdb484140b..7104afe1cd0acd9d1528247471a7f000c350b6e5 100644 GIT binary patch delta 118 zcmeyk#PH)1!wu={CjVXM*t}w0`-*jpK+FWh%s|Wn#H>Kf2E^>!SFGcxE1mA4%Bi&6 zLxq!(&BW5wz|?%ZpDJg91e7z~j)zffdS5uJ%Jc$zPX6iZgE@KHIqf)smLUpTAE^a6WM{^{$3 WIeFSS?Kpv$3y8V5bK3D_hyVbDG%yqZ diff --git a/netbox/project-static/dist/netbox-light.css b/netbox/project-static/dist/netbox-light.css index b915a414f6b04f25980b73e5d93b7cea95d3564c..c9b3b9453d2b1d8d9203ebd4f65093a96199c0bf 100644 GIT binary patch delta 83 zcmcb0Mef!WxeY~$tcfW}rb*4^iS6Zyj6lo;#LPgcSYZr|u4WN>MEp@gu~c4l$mJVtgCOH%_=bL;8yNx~A_CrAjt)no(P hmNNZ8l!RFOOebL=76D>WAO`WpfmmYuOee|P+yDouJKz8S delta 159 zcmew|C-uvm)P^mLYg|~(lFd!c+c&x}0x=U1GXpUT5VHa?8xXStF$WNH0x=g5a|1CC z5c2{t9}x2cu>cSYZr|u4WN>Nv-!sCz(^Zp%g|>+c7cx$NC?>45eX@k`D@``AO)1kK eL`jIX&vX(7Vi6z~1!53i9Ec^h&vcT!%?$u(Ej&p8 diff --git a/netbox/project-static/styles/select.scss b/netbox/project-static/styles/select.scss index a532d1b69..e306e7c69 100644 --- a/netbox/project-static/styles/select.scss +++ b/netbox/project-static/styles/select.scss @@ -19,7 +19,7 @@ $spacing-s: $input-padding-x; --nbx-select-option-selected-bg: #{$gray-300}; --nbx-select-option-hover-bg: #{$blue}; --nbx-select-option-hover-color: #{$white}; - --nbx-select-placeholder-color: #{$gray-600}; + --nbx-select-placeholder-color: #{$gray-500}; --nbx-select-value-color: #{$white}; &[data-netbox-color-mode='dark'] { // Dark Mode Variables. @@ -27,7 +27,7 @@ $spacing-s: $input-padding-x; --nbx-select-option-selected-bg: #{$gray-500}; --nbx-select-option-hover-bg: #{$blue-200}; --nbx-select-option-hover-color: #{color-contrast($blue-200)}; - --nbx-select-placeholder-color: #{$gray-500}; + --nbx-select-placeholder-color: #{$gray-700}; --nbx-select-value-color: #{$black}; } } diff --git a/netbox/project-static/styles/theme-dark.scss b/netbox/project-static/styles/theme-dark.scss index 716e27df0..c7c0cd76e 100644 --- a/netbox/project-static/styles/theme-dark.scss +++ b/netbox/project-static/styles/theme-dark.scss @@ -82,7 +82,7 @@ $input-border-color: $gray-700; $input-focus-bg: $input-bg; $input-focus-border-color: tint-color($component-active-bg, 10%); $input-focus-color: $input-color; -$input-placeholder-color: $gray-300; +$input-placeholder-color: $gray-700; $input-plaintext-color: $body-color; $form-check-input-active-filter: brightness(90%); diff --git a/netbox/project-static/styles/theme-light.scss b/netbox/project-static/styles/theme-light.scss index 48af7de1a..d4b9303bd 100644 --- a/netbox/project-static/styles/theme-light.scss +++ b/netbox/project-static/styles/theme-light.scss @@ -40,6 +40,7 @@ $list-group-disabled-color: $gray-500; $table-flush-header-bg: $gray-100; +$input-placeholder-color: $gray-500; $form-select-disabled-color: $gray-600; // Tabbed content From cc0830bf281fd0fd85df349d50e3108d06930278 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Thu, 16 Sep 2021 16:04:46 -0400 Subject: [PATCH 28/43] Closes #7087: Add search/filter forms for all organizational models --- docs/release-notes/version-3.0.md | 1 + netbox/circuits/forms.py | 12 ++++++ netbox/circuits/views.py | 2 + netbox/dcim/forms.py | 36 ++++++++++++++++++ netbox/dcim/views.py | 6 +++ netbox/ipam/forms.py | 27 ++++++++++++- netbox/ipam/views.py | 2 + netbox/project-static/dist/rack_elevation.css | Bin 1511 -> 1533 bytes netbox/virtualization/forms.py | 24 ++++++++++++ netbox/virtualization/views.py | 4 ++ 10 files changed, 112 insertions(+), 2 deletions(-) diff --git a/docs/release-notes/version-3.0.md b/docs/release-notes/version-3.0.md index 04bd95a8c..5d104de04 100644 --- a/docs/release-notes/version-3.0.md +++ b/docs/release-notes/version-3.0.md @@ -8,6 +8,7 @@ * [#6387](https://github.com/netbox-community/netbox/issues/6387) - Add xDSL interface type * [#6988](https://github.com/netbox-community/netbox/issues/6988) - Order tenants alphabetically without regard to group assignment * [#7032](https://github.com/netbox-community/netbox/issues/7032) - Add URM port types +* [#7087](https://github.com/netbox-community/netbox/issues/7087) - Add search/filter forms for all organizational models * [#7208](https://github.com/netbox-community/netbox/issues/7208) - Add navigation breadcrumbs for custom scripts & reports * [#7239](https://github.com/netbox-community/netbox/issues/7239) - Redirect global search to filtered object list when an object type is selected diff --git a/netbox/circuits/forms.py b/netbox/circuits/forms.py index 56cd46d4a..f43a3cfff 100644 --- a/netbox/circuits/forms.py +++ b/netbox/circuits/forms.py @@ -266,6 +266,18 @@ class CircuitTypeCSVForm(CustomFieldModelCSVForm): } +class CircuitTypeFilterForm(BootstrapMixin, CustomFieldModelFilterForm): + model = CircuitType + field_groups = [ + ['q'], + ] + q = forms.CharField( + required=False, + widget=forms.TextInput(attrs={'placeholder': _('All Fields')}), + label=_('Search') + ) + + # # Circuits # diff --git a/netbox/circuits/views.py b/netbox/circuits/views.py index dfbfe68a4..3460d4626 100644 --- a/netbox/circuits/views.py +++ b/netbox/circuits/views.py @@ -144,6 +144,8 @@ class CircuitTypeListView(generic.ObjectListView): queryset = CircuitType.objects.annotate( circuit_count=count_related(Circuit, 'type') ) + filterset = filtersets.CircuitTypeFilterSet + filterset_form = forms.CircuitTypeFilterForm table = tables.CircuitTypeTable diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index c1f8eccf8..935c3c9b3 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -696,6 +696,18 @@ class RackRoleBulkEditForm(BootstrapMixin, CustomFieldModelBulkEditForm): nullable_fields = ['color', 'description'] +class RackRoleFilterForm(BootstrapMixin, CustomFieldModelFilterForm): + model = RackRole + field_groups = [ + ['q'], + ] + q = forms.CharField( + required=False, + widget=forms.TextInput(attrs={'placeholder': _('All Fields')}), + label=_('Search') + ) + + # # Racks # @@ -1240,6 +1252,18 @@ class ManufacturerBulkEditForm(BootstrapMixin, CustomFieldModelBulkEditForm): nullable_fields = ['description'] +class ManufacturerFilterForm(BootstrapMixin, CustomFieldModelFilterForm): + model = Manufacturer + field_groups = [ + ['q'], + ] + q = forms.CharField( + required=False, + widget=forms.TextInput(attrs={'placeholder': _('All Fields')}), + label=_('Search') + ) + + # # Device types # @@ -2076,6 +2100,18 @@ class DeviceRoleBulkEditForm(BootstrapMixin, CustomFieldModelBulkEditForm): nullable_fields = ['color', 'description'] +class DeviceRoleFilterForm(BootstrapMixin, CustomFieldModelFilterForm): + model = DeviceRole + field_groups = [ + ['q'], + ] + q = forms.CharField( + required=False, + widget=forms.TextInput(attrs={'placeholder': _('All Fields')}), + label=_('Search') + ) + + # # Platforms # diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index f9e1cc9fe..4305d3c63 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -440,6 +440,8 @@ class RackRoleListView(generic.ObjectListView): queryset = RackRole.objects.annotate( rack_count=count_related(Rack, 'role') ) + filterset = filtersets.RackRoleFilterSet + filterset_form = forms.RackRoleFilterForm table = tables.RackRoleTable @@ -684,6 +686,8 @@ class ManufacturerListView(generic.ObjectListView): inventoryitem_count=count_related(InventoryItem, 'manufacturer'), platform_count=count_related(Platform, 'manufacturer') ) + filterset = filtersets.ManufacturerFilterSet + filterset_form = forms.ManufacturerFilterForm table = tables.ManufacturerTable @@ -1149,6 +1153,8 @@ class DeviceRoleListView(generic.ObjectListView): device_count=count_related(Device, 'device_role'), vm_count=count_related(VirtualMachine, 'role') ) + filterset = filtersets.DeviceRoleFilterSet + filterset_form = forms.DeviceRoleFilterForm table = tables.DeviceRoleTable diff --git a/netbox/ipam/forms.py b/netbox/ipam/forms.py index 4d5b3ad73..c72884b3c 100644 --- a/netbox/ipam/forms.py +++ b/netbox/ipam/forms.py @@ -256,7 +256,17 @@ class RIRBulkEditForm(BootstrapMixin, CustomFieldModelBulkEditForm): nullable_fields = ['is_private', 'description'] -class RIRFilterForm(BootstrapMixin, forms.Form): +class RIRFilterForm(BootstrapMixin, CustomFieldModelFilterForm): + model = RIR + field_groups = [ + ['q'], + ['is_private'], + ] + q = forms.CharField( + required=False, + widget=forms.TextInput(attrs={'placeholder': _('All Fields')}), + label=_('Search') + ) is_private = forms.NullBooleanField( required=False, label=_('Private'), @@ -413,6 +423,18 @@ class RoleBulkEditForm(BootstrapMixin, CustomFieldModelBulkEditForm): nullable_fields = ['description'] +class RoleFilterForm(BootstrapMixin, CustomFieldModelFilterForm): + model = Role + field_groups = [ + ['q'], + ] + q = forms.CharField( + required=False, + widget=forms.TextInput(attrs={'placeholder': _('All Fields')}), + label=_('Search') + ) + + # # Prefixes # @@ -1460,11 +1482,12 @@ class VLANGroupBulkEditForm(BootstrapMixin, CustomFieldModelBulkEditForm): nullable_fields = ['site', 'description'] -class VLANGroupFilterForm(BootstrapMixin, forms.Form): +class VLANGroupFilterForm(BootstrapMixin, CustomFieldModelFilterForm): field_groups = [ ['q'], ['region', 'sitegroup', 'site', 'location', 'rack'] ] + model = VLANGroup q = forms.CharField( required=False, widget=forms.TextInput(attrs={'placeholder': _('All Fields')}), diff --git a/netbox/ipam/views.py b/netbox/ipam/views.py index f84760418..64ca5bc43 100644 --- a/netbox/ipam/views.py +++ b/netbox/ipam/views.py @@ -283,6 +283,8 @@ class RoleListView(generic.ObjectListView): prefix_count=count_related(Prefix, 'role'), vlan_count=count_related(VLAN, 'role') ) + filterset = filtersets.RoleFilterSet + filterset_form = forms.RoleFilterForm table = tables.RoleTable diff --git a/netbox/project-static/dist/rack_elevation.css b/netbox/project-static/dist/rack_elevation.css index 4f9361489cf7fe2a5553150afc12f6de625c9072..eb9a6e237188777af3d911fb7b51f184a5793a38 100644 GIT binary patch delta 30 lcmaFP{g-=#GP7V}b$(fDQCd!Zxm8AHN=j Date: Thu, 16 Sep 2021 16:14:20 -0400 Subject: [PATCH 29/43] Correct changelog for #7210; revert static asset change --- docs/release-notes/version-3.0.md | 2 +- netbox/project-static/dist/rack_elevation.css | Bin 1533 -> 1511 bytes 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/release-notes/version-3.0.md b/docs/release-notes/version-3.0.md index 5d104de04..cf0698707 100644 --- a/docs/release-notes/version-3.0.md +++ b/docs/release-notes/version-3.0.md @@ -8,8 +8,8 @@ * [#6387](https://github.com/netbox-community/netbox/issues/6387) - Add xDSL interface type * [#6988](https://github.com/netbox-community/netbox/issues/6988) - Order tenants alphabetically without regard to group assignment * [#7032](https://github.com/netbox-community/netbox/issues/7032) - Add URM port types -* [#7087](https://github.com/netbox-community/netbox/issues/7087) - Add search/filter forms for all organizational models * [#7208](https://github.com/netbox-community/netbox/issues/7208) - Add navigation breadcrumbs for custom scripts & reports +* [#7210](https://github.com/netbox-community/netbox/issues/7210) - Add search/filter forms for all organizational models * [#7239](https://github.com/netbox-community/netbox/issues/7239) - Redirect global search to filtered object list when an object type is selected ### Bug Fixes diff --git a/netbox/project-static/dist/rack_elevation.css b/netbox/project-static/dist/rack_elevation.css index eb9a6e237188777af3d911fb7b51f184a5793a38..4f9361489cf7fe2a5553150afc12f6de625c9072 100644 GIT binary patch delta 12 Tcmey%{hWJ)GV^9j<|9l1B1Z&< delta 30 lcmaFP{g-=#GP7V}b$(fDQCd!Zxm8AHN=j Date: Fri, 17 Sep 2021 10:16:06 -0400 Subject: [PATCH 30/43] Fixes #7282: Fix KeyError exception when INSECURE_SKIP_TLS_VERIFY is true --- docs/release-notes/version-3.0.md | 3 ++- netbox/netbox/settings.py | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/release-notes/version-3.0.md b/docs/release-notes/version-3.0.md index cf0698707..8fc5bb958 100644 --- a/docs/release-notes/version-3.0.md +++ b/docs/release-notes/version-3.0.md @@ -17,8 +17,9 @@ * [#7167](https://github.com/netbox-community/netbox/issues/7167) - Ensure consistent font size when using monospace formatting * [#7226](https://github.com/netbox-community/netbox/issues/7226) - Exempt GraphQL API requests from CSRF inspection * [#7248](https://github.com/netbox-community/netbox/issues/7248) - Fix global search results section links -* [#7279](https://github.com/netbox-community/netbox/issues/7279) - Fix exception when tracing cable with no associated path * [#7266](https://github.com/netbox-community/netbox/issues/7266) - Tweak font color for form field placeholder text +* [#7279](https://github.com/netbox-community/netbox/issues/7279) - Fix exception when tracing cable with no associated path +* [#7282](https://github.com/netbox-community/netbox/issues/7282) - Fix KeyError exception when `INSECURE_SKIP_TLS_VERIFY` is true --- diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py index e1373d118..06e3e0ab7 100644 --- a/netbox/netbox/settings.py +++ b/netbox/netbox/settings.py @@ -262,6 +262,7 @@ if CACHING_REDIS_SENTINELS: CACHES['default']['OPTIONS']['CLIENT_CLASS'] = 'django_redis.client.SentinelClient' CACHES['default']['OPTIONS']['SENTINELS'] = CACHING_REDIS_SENTINELS if CACHING_REDIS_SKIP_TLS_VERIFY: + CACHES['default']['OPTIONS'].setdefault('CONNECTION_POOL_KWARGS', {}) CACHES['default']['OPTIONS']['CONNECTION_POOL_KWARGS']['ssl_cert_reqs'] = False From e67c965180f95f6c1bf6f198aaff10aa9f606347 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Fri, 17 Sep 2021 10:30:31 -0400 Subject: [PATCH 31/43] Closes #7087: Add local_context_data filter for virtual machines list --- docs/release-notes/version-3.0.md | 1 + netbox/virtualization/forms.py | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/release-notes/version-3.0.md b/docs/release-notes/version-3.0.md index 8fc5bb958..f80ebb73d 100644 --- a/docs/release-notes/version-3.0.md +++ b/docs/release-notes/version-3.0.md @@ -8,6 +8,7 @@ * [#6387](https://github.com/netbox-community/netbox/issues/6387) - Add xDSL interface type * [#6988](https://github.com/netbox-community/netbox/issues/6988) - Order tenants alphabetically without regard to group assignment * [#7032](https://github.com/netbox-community/netbox/issues/7032) - Add URM port types +* [#7087](https://github.com/netbox-community/netbox/issues/7087) - Add `local_context_data` filter for virtual machines list * [#7208](https://github.com/netbox-community/netbox/issues/7208) - Add navigation breadcrumbs for custom scripts & reports * [#7210](https://github.com/netbox-community/netbox/issues/7210) - Add search/filter forms for all organizational models * [#7239](https://github.com/netbox-community/netbox/issues/7239) - Redirect global search to filtered object list when an object type is selected diff --git a/netbox/virtualization/forms.py b/netbox/virtualization/forms.py index 1ecef4280..bf5dec00c 100644 --- a/netbox/virtualization/forms.py +++ b/netbox/virtualization/forms.py @@ -9,7 +9,7 @@ from dcim.forms import InterfaceCommonForm, INTERFACE_MODE_HELP_TEXT from dcim.models import Device, DeviceRole, Platform, Rack, Region, Site, SiteGroup from extras.forms import ( AddRemoveTagsForm, CustomFieldModelBulkEditForm, CustomFieldModelCSVForm, CustomFieldModelForm, - CustomFieldModelFilterForm, CustomFieldsMixin, + CustomFieldModelFilterForm, CustomFieldsMixin, LocalConfigContextFilterForm, ) from extras.models import Tag from ipam.models import IPAddress, VLAN, VLANGroup @@ -569,13 +569,13 @@ class VirtualMachineBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldM ] -class VirtualMachineFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldModelFilterForm): +class VirtualMachineFilterForm(BootstrapMixin, LocalConfigContextFilterForm, TenancyFilterForm, CustomFieldModelFilterForm): model = VirtualMachine field_groups = [ ['q', 'tag'], ['cluster_group_id', 'cluster_type_id', 'cluster_id'], ['region_id', 'site_group_id', 'site_id'], - ['status', 'role_id', 'platform_id', 'mac_address', 'has_primary_ip'], + ['status', 'role_id', 'platform_id', 'mac_address', 'has_primary_ip', 'local_context_data'], ['tenant_group_id', 'tenant_id'], ] q = forms.CharField( From 16d8981a3fa55bbb32213a2ddc62206307f6b1bd Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Fri, 17 Sep 2021 12:04:22 -0400 Subject: [PATCH 32/43] Closes #7284: Include comments field in table/export for all appropriate models --- docs/release-notes/version-3.0.md | 1 + netbox/circuits/tables.py | 12 ++++++++---- netbox/dcim/tables/devices.py | 5 +++-- netbox/dcim/tables/devicetypes.py | 5 +++-- netbox/dcim/tables/power.py | 5 +++-- netbox/dcim/tables/racks.py | 7 ++++--- netbox/dcim/tables/sites.py | 5 +++-- netbox/extras/tables.py | 3 ++- netbox/tenancy/tables.py | 7 +++++-- netbox/utilities/tables.py | 23 ++++++++++++++++++++++- netbox/virtualization/tables.py | 9 ++++++--- 11 files changed, 60 insertions(+), 22 deletions(-) diff --git a/docs/release-notes/version-3.0.md b/docs/release-notes/version-3.0.md index f80ebb73d..76cc7cc45 100644 --- a/docs/release-notes/version-3.0.md +++ b/docs/release-notes/version-3.0.md @@ -12,6 +12,7 @@ * [#7208](https://github.com/netbox-community/netbox/issues/7208) - Add navigation breadcrumbs for custom scripts & reports * [#7210](https://github.com/netbox-community/netbox/issues/7210) - Add search/filter forms for all organizational models * [#7239](https://github.com/netbox-community/netbox/issues/7239) - Redirect global search to filtered object list when an object type is selected +* [#7284](https://github.com/netbox-community/netbox/issues/7284) - Include comments field in table/export for all appropriate models ### Bug Fixes diff --git a/netbox/circuits/tables.py b/netbox/circuits/tables.py index 41a3aed7f..c3e616d8a 100644 --- a/netbox/circuits/tables.py +++ b/netbox/circuits/tables.py @@ -2,7 +2,7 @@ import django_tables2 as tables from django_tables2.utils import Accessor from tenancy.tables import TenantColumn -from utilities.tables import BaseTable, ButtonsColumn, ChoiceFieldColumn, TagColumn, ToggleColumn +from utilities.tables import BaseTable, ButtonsColumn, ChoiceFieldColumn, MarkdownColumn, TagColumn, ToggleColumn from .models import * @@ -28,6 +28,7 @@ class ProviderTable(BaseTable): accessor=Accessor('count_circuits'), verbose_name='Circuits' ) + comments = MarkdownColumn() tags = TagColumn( url_name='circuits:provider_list' ) @@ -35,7 +36,8 @@ class ProviderTable(BaseTable): class Meta(BaseTable.Meta): model = Provider fields = ( - 'pk', 'name', 'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact', 'circuit_count', 'tags', + 'pk', 'name', 'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact', 'circuit_count', 'comments', + 'tags', ) default_columns = ('pk', 'name', 'asn', 'account', 'circuit_count') @@ -52,13 +54,14 @@ class ProviderNetworkTable(BaseTable): provider = tables.Column( linkify=True ) + comments = MarkdownColumn() tags = TagColumn( url_name='circuits:providernetwork_list' ) class Meta(BaseTable.Meta): model = ProviderNetwork - fields = ('pk', 'name', 'provider', 'description', 'tags') + fields = ('pk', 'name', 'provider', 'description', 'comments', 'tags') default_columns = ('pk', 'name', 'provider', 'description') @@ -105,6 +108,7 @@ class CircuitTable(BaseTable): template_code=CIRCUITTERMINATION_LINK, verbose_name='Side Z' ) + comments = MarkdownColumn() tags = TagColumn( url_name='circuits:circuit_list' ) @@ -113,7 +117,7 @@ class CircuitTable(BaseTable): model = Circuit fields = ( 'pk', 'cid', 'provider', 'type', 'status', 'tenant', 'termination_a', 'termination_z', 'install_date', - 'commit_rate', 'description', 'tags', + 'commit_rate', 'description', 'comments', 'tags', ) default_columns = ( 'pk', 'cid', 'provider', 'type', 'status', 'tenant', 'termination_a', 'termination_z', 'description', diff --git a/netbox/dcim/tables/devices.py b/netbox/dcim/tables/devices.py index 352ac7d5c..dda0af2b4 100644 --- a/netbox/dcim/tables/devices.py +++ b/netbox/dcim/tables/devices.py @@ -9,7 +9,7 @@ from dcim.models import ( from tenancy.tables import TenantColumn from utilities.tables import ( BaseTable, BooleanColumn, ButtonsColumn, ChoiceFieldColumn, ColorColumn, ColoredLabelColumn, LinkedCountColumn, - TagColumn, ToggleColumn, + MarkdownColumn, TagColumn, ToggleColumn, ) from .template_code import ( CABLETERMINATION, CONSOLEPORT_BUTTONS, CONSOLESERVERPORT_BUTTONS, DEVICE_LINK, DEVICEBAY_BUTTONS, DEVICEBAY_STATUS, @@ -187,6 +187,7 @@ class DeviceTable(BaseTable): vc_priority = tables.Column( verbose_name='VC Priority' ) + comments = MarkdownColumn() tags = TagColumn( url_name='dcim:device_list' ) @@ -196,7 +197,7 @@ class DeviceTable(BaseTable): fields = ( 'pk', 'name', 'status', 'tenant', 'device_role', 'manufacturer', 'device_type', 'platform', 'serial', 'asset_tag', 'site', 'location', 'rack', 'position', 'face', 'primary_ip', 'primary_ip4', 'primary_ip6', - 'cluster', 'virtual_chassis', 'vc_position', 'vc_priority', 'tags', + 'cluster', 'virtual_chassis', 'vc_position', 'vc_priority', 'comments', 'tags', ) default_columns = ( 'pk', 'name', 'status', 'tenant', 'site', 'location', 'rack', 'device_role', 'manufacturer', 'device_type', diff --git a/netbox/dcim/tables/devicetypes.py b/netbox/dcim/tables/devicetypes.py index 6720baf66..3b11a180b 100644 --- a/netbox/dcim/tables/devicetypes.py +++ b/netbox/dcim/tables/devicetypes.py @@ -5,7 +5,7 @@ from dcim.models import ( Manufacturer, PowerOutletTemplate, PowerPortTemplate, RearPortTemplate, ) from utilities.tables import ( - BaseTable, BooleanColumn, ButtonsColumn, ColorColumn, LinkedCountColumn, TagColumn, ToggleColumn, + BaseTable, BooleanColumn, ButtonsColumn, ColorColumn, LinkedCountColumn, MarkdownColumn, TagColumn, ToggleColumn, ) __all__ = ( @@ -68,6 +68,7 @@ class DeviceTypeTable(BaseTable): url_params={'device_type_id': 'pk'}, verbose_name='Instances' ) + comments = MarkdownColumn() tags = TagColumn( url_name='dcim:devicetype_list' ) @@ -76,7 +77,7 @@ class DeviceTypeTable(BaseTable): model = DeviceType fields = ( 'pk', 'model', 'manufacturer', 'slug', 'part_number', 'u_height', 'is_full_depth', 'subdevice_role', - 'instance_count', 'tags', + 'comments', 'instance_count', 'tags', ) default_columns = ( 'pk', 'model', 'manufacturer', 'part_number', 'u_height', 'is_full_depth', 'instance_count', diff --git a/netbox/dcim/tables/power.py b/netbox/dcim/tables/power.py index 1c4d6e921..b8e032e7f 100644 --- a/netbox/dcim/tables/power.py +++ b/netbox/dcim/tables/power.py @@ -1,7 +1,7 @@ import django_tables2 as tables from dcim.models import PowerFeed, PowerPanel -from utilities.tables import BaseTable, ChoiceFieldColumn, LinkedCountColumn, TagColumn, ToggleColumn +from utilities.tables import BaseTable, ChoiceFieldColumn, LinkedCountColumn, MarkdownColumn, TagColumn, ToggleColumn from .devices import CableTerminationTable __all__ = ( @@ -62,6 +62,7 @@ class PowerFeedTable(CableTerminationTable): available_power = tables.Column( verbose_name='Available power (VA)' ) + comments = MarkdownColumn() tags = TagColumn( url_name='dcim:powerfeed_list' ) @@ -71,7 +72,7 @@ class PowerFeedTable(CableTerminationTable): fields = ( 'pk', 'name', 'power_panel', 'rack', 'status', 'type', 'supply', 'voltage', 'amperage', 'phase', 'max_utilization', 'mark_connected', 'cable', 'cable_color', 'cable_peer', 'connection', 'available_power', - 'tags', + 'comments', 'tags', ) default_columns = ( 'pk', 'name', 'power_panel', 'rack', 'status', 'type', 'supply', 'voltage', 'amperage', 'phase', 'cable', diff --git a/netbox/dcim/tables/racks.py b/netbox/dcim/tables/racks.py index ef0517dfc..39a76eade 100644 --- a/netbox/dcim/tables/racks.py +++ b/netbox/dcim/tables/racks.py @@ -4,8 +4,8 @@ from django_tables2.utils import Accessor from dcim.models import Rack, RackReservation, RackRole from tenancy.tables import TenantColumn from utilities.tables import ( - BaseTable, ButtonsColumn, ChoiceFieldColumn, ColorColumn, ColoredLabelColumn, LinkedCountColumn, TagColumn, - ToggleColumn, UtilizationColumn, + BaseTable, ButtonsColumn, ChoiceFieldColumn, ColorColumn, ColoredLabelColumn, LinkedCountColumn, MarkdownColumn, + TagColumn, ToggleColumn, UtilizationColumn, ) __all__ = ( @@ -67,6 +67,7 @@ class RackTable(BaseTable): class RackDetailTable(RackTable): + comments = MarkdownColumn() device_count = LinkedCountColumn( viewname='dcim:device_list', url_params={'rack_id': 'pk'}, @@ -87,7 +88,7 @@ class RackDetailTable(RackTable): class Meta(RackTable.Meta): fields = ( 'pk', 'name', 'site', 'location', 'status', 'facility_id', 'tenant', 'role', 'serial', 'asset_tag', 'type', - 'width', 'u_height', 'device_count', 'get_utilization', 'get_power_utilization', 'tags', + 'width', 'u_height', 'comments', 'device_count', 'get_utilization', 'get_power_utilization', 'tags', ) default_columns = ( 'pk', 'name', 'site', 'location', 'status', 'facility_id', 'tenant', 'role', 'u_height', 'device_count', diff --git a/netbox/dcim/tables/sites.py b/netbox/dcim/tables/sites.py index 47c912354..37fa019a1 100644 --- a/netbox/dcim/tables/sites.py +++ b/netbox/dcim/tables/sites.py @@ -3,7 +3,7 @@ import django_tables2 as tables from dcim.models import Location, Region, Site, SiteGroup from tenancy.tables import TenantColumn from utilities.tables import ( - BaseTable, ButtonsColumn, ChoiceFieldColumn, LinkedCountColumn, MPTTColumn, TagColumn, ToggleColumn, + BaseTable, ButtonsColumn, ChoiceFieldColumn, LinkedCountColumn, MarkdownColumn, MPTTColumn, TagColumn, ToggleColumn, ) from .template_code import LOCATION_ELEVATIONS @@ -76,6 +76,7 @@ class SiteTable(BaseTable): linkify=True ) tenant = TenantColumn() + comments = MarkdownColumn() tags = TagColumn( url_name='dcim:site_list' ) @@ -85,7 +86,7 @@ class SiteTable(BaseTable): fields = ( 'pk', 'name', 'slug', 'status', 'facility', 'region', 'group', 'tenant', 'asn', 'time_zone', 'description', 'physical_address', 'shipping_address', 'latitude', 'longitude', 'contact_name', 'contact_phone', - 'contact_email', 'tags', + 'contact_email', 'comments', 'tags', ) default_columns = ('pk', 'name', 'status', 'facility', 'region', 'group', 'tenant', 'asn', 'description') diff --git a/netbox/extras/tables.py b/netbox/extras/tables.py index 699bffc9f..c556e312f 100644 --- a/netbox/extras/tables.py +++ b/netbox/extras/tables.py @@ -3,7 +3,7 @@ from django.conf import settings from utilities.tables import ( BaseTable, BooleanColumn, ButtonsColumn, ChoiceFieldColumn, ColorColumn, ContentTypeColumn, ContentTypesColumn, - ToggleColumn, + MarkdownColumn, ToggleColumn, ) from .models import * @@ -232,6 +232,7 @@ class JournalEntryTable(ObjectJournalTable): orderable=False, verbose_name='Object' ) + comments = MarkdownColumn() class Meta(BaseTable.Meta): model = JournalEntry diff --git a/netbox/tenancy/tables.py b/netbox/tenancy/tables.py index 803675bfd..961d02dcc 100644 --- a/netbox/tenancy/tables.py +++ b/netbox/tenancy/tables.py @@ -1,6 +1,8 @@ import django_tables2 as tables -from utilities.tables import BaseTable, ButtonsColumn, LinkedCountColumn, MPTTColumn, TagColumn, ToggleColumn +from utilities.tables import ( + BaseTable, ButtonsColumn, LinkedCountColumn, MarkdownColumn, MPTTColumn, TagColumn, ToggleColumn, +) from .models import Tenant, TenantGroup @@ -60,11 +62,12 @@ class TenantTable(BaseTable): name = tables.Column( linkify=True ) + comments = MarkdownColumn() tags = TagColumn( url_name='tenancy:tenant_list' ) class Meta(BaseTable.Meta): model = Tenant - fields = ('pk', 'name', 'slug', 'group', 'description', 'tags') + fields = ('pk', 'name', 'slug', 'group', 'description', 'comments', 'tags') default_columns = ('pk', 'name', 'group', 'description') diff --git a/netbox/utilities/tables.py b/netbox/utilities/tables.py index ed1dcf5ce..4d8a60114 100644 --- a/netbox/utilities/tables.py +++ b/netbox/utilities/tables.py @@ -12,7 +12,6 @@ from django_tables2.data import TableQuerysetData from django_tables2.utils import Accessor from extras.models import CustomField -from extras.utils import FeatureQuery from .utils import content_type_name from .paginator import EnhancedPaginator, get_paginate_count @@ -395,6 +394,28 @@ class UtilizationColumn(tables.TemplateColumn): return f'{value}%' +class MarkdownColumn(tables.TemplateColumn): + """ + Render a Markdown string. + """ + template_code = """ + {% load helpers %} + {% if value %} + {{ value|render_markdown }} + {% else %} + — + {% endif %} + """ + + def __init__(self): + super().__init__( + template_code=self.template_code + ) + + def value(self, value): + return value + + # # Pagination # diff --git a/netbox/virtualization/tables.py b/netbox/virtualization/tables.py index b5fb93e5c..c6d42d3c1 100644 --- a/netbox/virtualization/tables.py +++ b/netbox/virtualization/tables.py @@ -3,7 +3,8 @@ from django.conf import settings from dcim.tables.devices import BaseInterfaceTable from tenancy.tables import TenantColumn from utilities.tables import ( - BaseTable, ButtonsColumn, ChoiceFieldColumn, ColoredLabelColumn, LinkedCountColumn, TagColumn, ToggleColumn, + BaseTable, ButtonsColumn, ChoiceFieldColumn, ColoredLabelColumn, LinkedCountColumn, MarkdownColumn, TagColumn, + ToggleColumn, ) from .models import Cluster, ClusterGroup, ClusterType, VirtualMachine, VMInterface @@ -91,13 +92,14 @@ class ClusterTable(BaseTable): url_params={'cluster_id': 'pk'}, verbose_name='VMs' ) + comments = MarkdownColumn() tags = TagColumn( url_name='virtualization:cluster_list' ) class Meta(BaseTable.Meta): model = Cluster - fields = ('pk', 'name', 'type', 'group', 'tenant', 'site', 'device_count', 'vm_count', 'tags') + fields = ('pk', 'name', 'type', 'group', 'tenant', 'site', 'comments', 'device_count', 'vm_count', 'tags') default_columns = ('pk', 'name', 'type', 'group', 'tenant', 'site', 'device_count', 'vm_count') @@ -143,6 +145,7 @@ class VirtualMachineDetailTable(VirtualMachineTable): order_by=('primary_ip6', 'primary_ip4'), verbose_name='IP Address' ) + comments = MarkdownColumn() tags = TagColumn( url_name='virtualization:virtualmachine_list' ) @@ -151,7 +154,7 @@ class VirtualMachineDetailTable(VirtualMachineTable): model = VirtualMachine fields = ( 'pk', 'name', 'status', 'cluster', 'role', 'tenant', 'platform', 'vcpus', 'memory', 'disk', 'primary_ip4', - 'primary_ip6', 'primary_ip', 'tags', + 'primary_ip6', 'primary_ip', 'comments', 'tags', ) default_columns = ( 'pk', 'name', 'status', 'cluster', 'role', 'tenant', 'vcpus', 'memory', 'disk', 'primary_ip', From 84f3ab90df90e7928957f628a2ca2eeae59a593e Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Fri, 17 Sep 2021 12:11:08 -0400 Subject: [PATCH 33/43] Changelog for #7273 --- docs/release-notes/version-3.0.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes/version-3.0.md b/docs/release-notes/version-3.0.md index d314e3740..ee4f7aebe 100644 --- a/docs/release-notes/version-3.0.md +++ b/docs/release-notes/version-3.0.md @@ -21,6 +21,7 @@ * [#7226](https://github.com/netbox-community/netbox/issues/7226) - Exempt GraphQL API requests from CSRF inspection * [#7248](https://github.com/netbox-community/netbox/issues/7248) - Fix global search results section links * [#7266](https://github.com/netbox-community/netbox/issues/7266) - Tweak font color for form field placeholder text +* [#7273](https://github.com/netbox-community/netbox/issues/7273) - Fix natural ordering of device components in UI form fields * [#7279](https://github.com/netbox-community/netbox/issues/7279) - Fix exception when tracing cable with no associated path * [#7282](https://github.com/netbox-community/netbox/issues/7282) - Fix KeyError exception when `INSECURE_SKIP_TLS_VERIFY` is true From 5e2967996816ed3d4e4a0dc998968f77de2dcfc6 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Fri, 17 Sep 2021 13:55:32 -0400 Subject: [PATCH 34/43] Merge 'detail' tables into primaries for each model --- netbox/dcim/tables/racks.py | 15 +---- netbox/dcim/views.py | 4 +- netbox/ipam/tables.py | 71 ++++++---------------- netbox/ipam/views.py | 44 ++++++++------ netbox/netbox/constants.py | 4 +- netbox/templates/ipam/prefix/prefixes.html | 4 +- netbox/virtualization/tables.py | 29 +++------ netbox/virtualization/views.py | 8 ++- 8 files changed, 67 insertions(+), 112 deletions(-) diff --git a/netbox/dcim/tables/racks.py b/netbox/dcim/tables/racks.py index 39a76eade..fcc3ed4d2 100644 --- a/netbox/dcim/tables/racks.py +++ b/netbox/dcim/tables/racks.py @@ -10,7 +10,6 @@ from utilities.tables import ( __all__ = ( 'RackTable', - 'RackDetailTable', 'RackReservationTable', 'RackRoleTable', ) @@ -56,17 +55,6 @@ class RackTable(BaseTable): template_code="{{ record.u_height }}U", verbose_name='Height' ) - - class Meta(BaseTable.Meta): - model = Rack - fields = ( - 'pk', 'name', 'site', 'location', 'status', 'facility_id', 'tenant', 'role', 'serial', 'asset_tag', 'type', - 'width', 'u_height', - ) - default_columns = ('pk', 'name', 'site', 'location', 'status', 'facility_id', 'tenant', 'role', 'u_height') - - -class RackDetailTable(RackTable): comments = MarkdownColumn() device_count = LinkedCountColumn( viewname='dcim:device_list', @@ -85,7 +73,8 @@ class RackDetailTable(RackTable): url_name='dcim:rack_list' ) - class Meta(RackTable.Meta): + class Meta(BaseTable.Meta): + model = Rack fields = ( 'pk', 'name', 'site', 'location', 'status', 'facility_id', 'tenant', 'role', 'serial', 'asset_tag', 'type', 'width', 'u_height', 'comments', 'device_count', 'get_utilization', 'get_power_utilization', 'tags', diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index 4305d3c63..fbdd30a3d 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -455,6 +455,8 @@ class RackRoleView(generic.ObjectView): racks_table = tables.RackTable(racks) racks_table.columns.hide('role') + racks_table.columns.hide('get_utilization') + racks_table.columns.hide('get_power_utilization') paginate_table(racks_table, request) return { @@ -505,7 +507,7 @@ class RackListView(generic.ObjectListView): ) filterset = filtersets.RackFilterSet filterset_form = forms.RackFilterForm - table = tables.RackDetailTable + table = tables.RackTable class RackElevationListView(generic.ObjectListView): diff --git a/netbox/ipam/tables.py b/netbox/ipam/tables.py index 5afb6a9c9..a39df2601 100644 --- a/netbox/ipam/tables.py +++ b/netbox/ipam/tables.py @@ -215,13 +215,6 @@ class AggregateTable(BaseTable): format="Y-m-d", verbose_name='Added' ) - - class Meta(BaseTable.Meta): - model = Aggregate - fields = ('pk', 'prefix', 'rir', 'tenant', 'date_added', 'description') - - -class AggregateDetailTable(AggregateTable): child_count = tables.Column( verbose_name='Prefixes' ) @@ -233,7 +226,8 @@ class AggregateDetailTable(AggregateTable): url_name='ipam:aggregate_list' ) - class Meta(AggregateTable.Meta): + class Meta(BaseTable.Meta): + model = Aggregate fields = ('pk', 'prefix', 'rir', 'tenant', 'child_count', 'utilization', 'date_added', 'description', 'tags') default_columns = ('pk', 'prefix', 'rir', 'tenant', 'child_count', 'utilization', 'date_added', 'description') @@ -332,20 +326,6 @@ class PrefixTable(BaseTable): mark_utilized = BooleanColumn( verbose_name='Marked Utilized' ) - - class Meta(BaseTable.Meta): - model = Prefix - fields = ( - 'pk', 'prefix', 'prefix_flat', 'status', 'depth', 'children', 'vrf', 'tenant', 'site', 'vlan', 'role', - 'is_pool', 'mark_utilized', 'description', - ) - default_columns = ('pk', 'prefix', 'status', 'vrf', 'tenant', 'site', 'vlan', 'role', 'description') - row_attrs = { - 'class': lambda record: 'success' if not record.pk else '', - } - - -class PrefixDetailTable(PrefixTable): utilization = PrefixUtilizationColumn( accessor='get_utilization', orderable=False @@ -354,7 +334,8 @@ class PrefixDetailTable(PrefixTable): url_name='ipam:prefix_list' ) - class Meta(PrefixTable.Meta): + class Meta(BaseTable.Meta): + model = Prefix fields = ( 'pk', 'prefix', 'prefix_flat', 'status', 'children', 'vrf', 'utilization', 'tenant', 'site', 'vlan', 'role', 'is_pool', 'mark_utilized', 'description', 'tags', @@ -362,6 +343,9 @@ class PrefixDetailTable(PrefixTable): default_columns = ( 'pk', 'prefix', 'status', 'children', 'vrf', 'utilization', 'tenant', 'site', 'vlan', 'role', 'description', ) + row_attrs = { + 'class': lambda record: 'success' if not record.pk else '', + } # @@ -427,25 +411,11 @@ class IPAddressTable(BaseTable): orderable=False, verbose_name='Device/VM' ) - - class Meta(BaseTable.Meta): - model = IPAddress - fields = ( - 'pk', 'address', 'vrf', 'status', 'role', 'tenant', 'assigned_object', 'assigned_object_parent', 'dns_name', - 'description', - ) - row_attrs = { - 'class': lambda record: 'success' if not isinstance(record, IPAddress) else '', - } - - -class IPAddressDetailTable(IPAddressTable): nat_inside = tables.Column( linkify=True, orderable=False, verbose_name='NAT (Inside)' ) - tenant = TenantColumn() assigned = BooleanColumn( accessor='assigned_object_id', verbose_name='Assigned' @@ -454,14 +424,18 @@ class IPAddressDetailTable(IPAddressTable): url_name='ipam:ipaddress_list' ) - class Meta(IPAddressTable.Meta): + class Meta(BaseTable.Meta): + model = IPAddress fields = ( - 'pk', 'address', 'vrf', 'status', 'role', 'tenant', 'nat_inside', 'assigned', 'dns_name', - 'description', 'tags', + 'pk', 'address', 'vrf', 'status', 'role', 'tenant', 'nat_inside', 'assigned', 'dns_name', 'description', + 'tags', ) default_columns = ( 'pk', 'address', 'vrf', 'status', 'role', 'tenant', 'assigned', 'dns_name', 'description', ) + row_attrs = { + 'class': lambda record: 'success' if not isinstance(record, IPAddress) else '', + } class IPAddressAssignTable(BaseTable): @@ -554,29 +528,22 @@ class VLANTable(BaseTable): role = tables.TemplateColumn( template_code=VLAN_ROLE_LINK ) - - class Meta(BaseTable.Meta): - model = VLAN - fields = ('pk', 'vid', 'name', 'site', 'group', 'tenant', 'status', 'role', 'description') - row_attrs = { - 'class': lambda record: 'success' if not isinstance(record, VLAN) else '', - } - - -class VLANDetailTable(VLANTable): prefixes = tables.TemplateColumn( template_code=VLAN_PREFIXES, orderable=False, verbose_name='Prefixes' ) - tenant = TenantColumn() tags = TagColumn( url_name='ipam:vlan_list' ) - class Meta(VLANTable.Meta): + class Meta(BaseTable.Meta): + model = VLAN fields = ('pk', 'vid', 'name', 'site', 'group', 'prefixes', 'tenant', 'status', 'role', 'description', 'tags') default_columns = ('pk', 'vid', 'name', 'site', 'group', 'prefixes', 'tenant', 'status', 'role', 'description') + row_attrs = { + 'class': lambda record: 'success' if not isinstance(record, VLAN) else '', + } class VLANMembersTable(BaseTable): diff --git a/netbox/ipam/views.py b/netbox/ipam/views.py index 64ca5bc43..6d49cd24f 100644 --- a/netbox/ipam/views.py +++ b/netbox/ipam/views.py @@ -158,6 +158,7 @@ class RIRView(generic.ObjectView): aggregates_table = tables.AggregateTable(aggregates) aggregates_table.columns.hide('rir') + aggregates_table.columns.hide('utilization') paginate_table(aggregates_table, request) return { @@ -207,7 +208,7 @@ class AggregateListView(generic.ObjectListView): ) filterset = filtersets.AggregateFilterSet filterset_form = forms.AggregateFilterForm - table = tables.AggregateDetailTable + table = tables.AggregateTable class AggregateView(generic.ObjectView): @@ -227,7 +228,7 @@ class AggregateView(generic.ObjectView): if request.GET.get('show_available', 'true') == 'true': child_prefixes = add_available_prefixes(instance.prefix, child_prefixes) - prefix_table = tables.PrefixDetailTable(child_prefixes) + prefix_table = tables.PrefixTable(child_prefixes, exclude=('utilization',)) if request.user.has_perm('ipam.change_prefix') or request.user.has_perm('ipam.delete_prefix'): prefix_table.columns.show('pk') paginate_table(prefix_table, request) @@ -296,8 +297,7 @@ class RoleView(generic.ObjectView): role=instance ) - prefixes_table = tables.PrefixTable(prefixes) - prefixes_table.columns.hide('role') + prefixes_table = tables.PrefixTable(prefixes, exclude=('role', 'utilization')) paginate_table(prefixes_table, request) return { @@ -340,7 +340,7 @@ class PrefixListView(generic.ObjectListView): queryset = Prefix.objects.all() filterset = filtersets.PrefixFilterSet filterset_form = forms.PrefixFilterForm - table = tables.PrefixDetailTable + table = tables.PrefixTable template_name = 'ipam/prefix_list.html' @@ -363,8 +363,11 @@ class PrefixView(generic.ObjectView): ).prefetch_related( 'site', 'role' ) - parent_prefix_table = tables.PrefixTable(list(parent_prefixes), orderable=False) - parent_prefix_table.exclude = ('vrf',) + parent_prefix_table = tables.PrefixTable( + list(parent_prefixes), + exclude=('vrf', 'utilization'), + orderable=False + ) # Duplicate prefixes table duplicate_prefixes = Prefix.objects.restrict(request.user, 'view').filter( @@ -374,8 +377,11 @@ class PrefixView(generic.ObjectView): ).prefetch_related( 'site', 'role' ) - duplicate_prefix_table = tables.PrefixTable(list(duplicate_prefixes), orderable=False) - duplicate_prefix_table.exclude = ('vrf',) + duplicate_prefix_table = tables.PrefixTable( + list(duplicate_prefixes), + exclude=('vrf', 'utilization'), + orderable=False + ) return { 'aggregate': aggregate, @@ -398,7 +404,7 @@ class PrefixPrefixesView(generic.ObjectView): if child_prefixes and request.GET.get('show_available', 'true') == 'true': child_prefixes = add_available_prefixes(instance.prefix, child_prefixes) - table = tables.PrefixDetailTable(child_prefixes, user=request.user) + table = tables.PrefixTable(child_prefixes, user=request.user, exclude=('utilization',)) if request.user.has_perm('ipam.change_prefix') or request.user.has_perm('ipam.delete_prefix'): table.columns.show('pk') paginate_table(table, request) @@ -601,7 +607,7 @@ class IPAddressListView(generic.ObjectListView): queryset = IPAddress.objects.all() filterset = filtersets.IPAddressFilterSet filterset_form = forms.IPAddressFilterForm - table = tables.IPAddressDetailTable + table = tables.IPAddressTable class IPAddressView(generic.ObjectView): @@ -615,8 +621,11 @@ class IPAddressView(generic.ObjectView): ).prefetch_related( 'site', 'role' ) - parent_prefixes_table = tables.PrefixTable(list(parent_prefixes), orderable=False) - parent_prefixes_table.exclude = ('vrf',) + parent_prefixes_table = tables.PrefixTable( + list(parent_prefixes), + exclude=('vrf', 'utilization'), + orderable=False + ) # Duplicate IPs table duplicate_ips = IPAddress.objects.restrict(request.user, 'view').filter( @@ -767,11 +776,9 @@ class VLANGroupView(generic.ObjectView): vlans_count = vlans.count() vlans = add_available_vlans(vlans, vlan_group=instance) - vlans_table = tables.VLANDetailTable(vlans) + vlans_table = tables.VLANTable(vlans, exclude=('site', 'group', 'prefixes')) if request.user.has_perm('ipam.change_vlan') or request.user.has_perm('ipam.delete_vlan'): vlans_table.columns.show('pk') - vlans_table.columns.hide('site') - vlans_table.columns.hide('group') paginate_table(vlans_table, request) # Compile permissions list for rendering the object table @@ -828,7 +835,7 @@ class VLANListView(generic.ObjectListView): queryset = VLAN.objects.all() filterset = filtersets.VLANFilterSet filterset_form = forms.VLANFilterForm - table = tables.VLANDetailTable + table = tables.VLANTable class VLANView(generic.ObjectView): @@ -838,8 +845,7 @@ class VLANView(generic.ObjectView): prefixes = Prefix.objects.restrict(request.user, 'view').filter(vlan=instance).prefetch_related( 'vrf', 'site', 'role' ) - prefix_table = tables.PrefixTable(list(prefixes), orderable=False) - prefix_table.exclude = ('vlan',) + prefix_table = tables.PrefixTable(list(prefixes), exclude=('vlan', 'utilization'), orderable=False) return { 'prefix_table': prefix_table, diff --git a/netbox/netbox/constants.py b/netbox/netbox/constants.py index 31e56fb1f..ec6daa021 100644 --- a/netbox/netbox/constants.py +++ b/netbox/netbox/constants.py @@ -21,7 +21,7 @@ from tenancy.tables import TenantTable from utilities.utils import count_related from virtualization.filtersets import ClusterFilterSet, VirtualMachineFilterSet from virtualization.models import Cluster, VirtualMachine -from virtualization.tables import ClusterTable, VirtualMachineDetailTable +from virtualization.tables import ClusterTable, VirtualMachineTable SEARCH_MAX_RESULTS = 15 SEARCH_TYPES = OrderedDict(( @@ -130,7 +130,7 @@ SEARCH_TYPES = OrderedDict(( 'cluster', 'tenant', 'platform', 'primary_ip4', 'primary_ip6', ), 'filterset': VirtualMachineFilterSet, - 'table': VirtualMachineDetailTable, + 'table': VirtualMachineTable, 'url': 'virtualization:virtualmachine_list', }), # IPAM diff --git a/netbox/templates/ipam/prefix/prefixes.html b/netbox/templates/ipam/prefix/prefixes.html index e6c109a39..d10ca79db 100644 --- a/netbox/templates/ipam/prefix/prefixes.html +++ b/netbox/templates/ipam/prefix/prefixes.html @@ -15,9 +15,9 @@ {% block content %}
- {% include 'inc/table_controls.html' with table_modal="PrefixDetailTable_config" %} + {% include 'inc/table_controls.html' with table_modal="PrefixTable_config" %} {% include 'utilities/obj_table.html' with heading='Child Prefixes' bulk_edit_url='ipam:prefix_bulk_edit' bulk_delete_url='ipam:prefix_bulk_delete' parent=prefix %}
- {% table_config_form table table_name="PrefixDetailTable" %} + {% table_config_form table table_name="PrefixTable" %} {% endblock %} diff --git a/netbox/virtualization/tables.py b/netbox/virtualization/tables.py index c6d42d3c1..b0e922e71 100644 --- a/netbox/virtualization/tables.py +++ b/netbox/virtualization/tables.py @@ -12,12 +12,13 @@ __all__ = ( 'ClusterTable', 'ClusterGroupTable', 'ClusterTypeTable', - 'VirtualMachineDetailTable', 'VirtualMachineTable', 'VirtualMachineVMInterfaceTable', 'VMInterfaceTable', ) +PRIMARY_IP_ORDERING = ('primary_ip4', 'primary_ip6') if settings.PREFER_IPV4 else ('primary_ip6', 'primary_ip4') + VMINTERFACE_BUTTONS = """ {% if perms.ipam.add_ipaddress %}
@@ -118,13 +119,7 @@ class VirtualMachineTable(BaseTable): ) role = ColoredLabelColumn() tenant = TenantColumn() - - class Meta(BaseTable.Meta): - model = VirtualMachine - fields = ('pk', 'name', 'status', 'cluster', 'role', 'tenant', 'vcpus', 'memory', 'disk') - - -class VirtualMachineDetailTable(VirtualMachineTable): + comments = MarkdownColumn() primary_ip4 = tables.Column( linkify=True, verbose_name='IPv4 Address' @@ -133,19 +128,11 @@ class VirtualMachineDetailTable(VirtualMachineTable): linkify=True, verbose_name='IPv6 Address' ) - if settings.PREFER_IPV4: - primary_ip = tables.Column( - linkify=True, - order_by=('primary_ip4', 'primary_ip6'), - verbose_name='IP Address' - ) - else: - primary_ip = tables.Column( - linkify=True, - order_by=('primary_ip6', 'primary_ip4'), - verbose_name='IP Address' - ) - comments = MarkdownColumn() + primary_ip = tables.Column( + linkify=True, + order_by=PRIMARY_IP_ORDERING, + verbose_name='IP Address' + ) tags = TagColumn( url_name='virtualization:virtualmachine_list' ) diff --git a/netbox/virtualization/views.py b/netbox/virtualization/views.py index 115ca0a29..8315ba0a6 100644 --- a/netbox/virtualization/views.py +++ b/netbox/virtualization/views.py @@ -171,7 +171,11 @@ class ClusterVirtualMachinesView(generic.ObjectView): def get_extra_context(self, request, instance): virtualmachines = VirtualMachine.objects.restrict(request.user, 'view').filter(cluster=instance) - virtualmachines_table = tables.VirtualMachineTable(virtualmachines, orderable=False) + virtualmachines_table = tables.VirtualMachineTable( + virtualmachines, + exclude=('cluster',), + orderable=False + ) return { 'virtualmachines_table': virtualmachines_table, @@ -315,7 +319,7 @@ class VirtualMachineListView(generic.ObjectListView): queryset = VirtualMachine.objects.all() filterset = filtersets.VirtualMachineFilterSet filterset_form = forms.VirtualMachineFilterForm - table = tables.VirtualMachineDetailTable + table = tables.VirtualMachineTable template_name = 'virtualization/virtualmachine_list.html' From 9cb29f48a0d00d63e39177ff60c65525b2cdfe6a Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Fri, 17 Sep 2021 14:25:02 -0400 Subject: [PATCH 35/43] Use exclude() when instantiating tables to omit columns --- netbox/circuits/views.py | 15 +++------------ netbox/dcim/views.py | 24 +++++++----------------- netbox/ipam/views.py | 5 +---- netbox/tenancy/views.py | 4 +--- netbox/virtualization/views.py | 10 +++------- 5 files changed, 15 insertions(+), 43 deletions(-) diff --git a/netbox/circuits/views.py b/netbox/circuits/views.py index 3460d4626..b549b3a01 100644 --- a/netbox/circuits/views.py +++ b/netbox/circuits/views.py @@ -34,9 +34,7 @@ class ProviderView(generic.ObjectView): ).prefetch_related( 'type', 'tenant', 'terminations__site' ) - - circuits_table = tables.CircuitTable(circuits) - circuits_table.columns.hide('provider') + circuits_table = tables.CircuitTable(circuits, exclude=('provider',)) paginate_table(circuits_table, request) return { @@ -97,10 +95,7 @@ class ProviderNetworkView(generic.ObjectView): ).prefetch_related( 'type', 'tenant', 'terminations__site' ) - circuits_table = tables.CircuitTable(circuits) - circuits_table.columns.hide('termination_a') - circuits_table.columns.hide('termination_z') paginate_table(circuits_table, request) return { @@ -153,12 +148,8 @@ class CircuitTypeView(generic.ObjectView): queryset = CircuitType.objects.all() def get_extra_context(self, request, instance): - circuits = Circuit.objects.restrict(request.user, 'view').filter( - type=instance - ) - - circuits_table = tables.CircuitTable(circuits) - circuits_table.columns.hide('type') + circuits = Circuit.objects.restrict(request.user, 'view').filter(type=instance) + circuits_table = tables.CircuitTable(circuits, exclude=('type',)) paginate_table(circuits_table, request) return { diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index fbdd30a3d..63f2be5c8 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -131,8 +131,7 @@ class RegionView(generic.ObjectView): sites = Site.objects.restrict(request.user, 'view').filter( region=instance ) - sites_table = tables.SiteTable(sites) - sites_table.columns.hide('region') + sites_table = tables.SiteTable(sites, exclude=('region',)) paginate_table(sites_table, request) return { @@ -216,8 +215,7 @@ class SiteGroupView(generic.ObjectView): sites = Site.objects.restrict(request.user, 'view').filter( group=instance ) - sites_table = tables.SiteTable(sites) - sites_table.columns.hide('group') + sites_table = tables.SiteTable(sites, exclude=('group',)) paginate_table(sites_table, request) return { @@ -453,10 +451,7 @@ class RackRoleView(generic.ObjectView): role=instance ) - racks_table = tables.RackTable(racks) - racks_table.columns.hide('role') - racks_table.columns.hide('get_utilization') - racks_table.columns.hide('get_power_utilization') + racks_table = tables.RackTable(racks, exclude=('role', 'get_utilization', 'get_power_utilization')) paginate_table(racks_table, request) return { @@ -706,8 +701,7 @@ class ManufacturerView(generic.ObjectView): manufacturer=instance ) - devicetypes_table = tables.DeviceTypeTable(devicetypes) - devicetypes_table.columns.hide('manufacturer') + devicetypes_table = tables.DeviceTypeTable(devicetypes, exclude=('manufacturer',)) paginate_table(devicetypes_table, request) return { @@ -1167,9 +1161,7 @@ class DeviceRoleView(generic.ObjectView): devices = Device.objects.restrict(request.user, 'view').filter( device_role=instance ) - - devices_table = tables.DeviceTable(devices) - devices_table.columns.hide('device_role') + devices_table = tables.DeviceTable(devices, exclude=('device_role',)) paginate_table(devices_table, request) return { @@ -1233,9 +1225,7 @@ class PlatformView(generic.ObjectView): devices = Device.objects.restrict(request.user, 'view').filter( platform=instance ) - - devices_table = tables.DeviceTable(devices) - devices_table.columns.hide('platform') + devices_table = tables.DeviceTable(devices, exclude=('platform',)) paginate_table(devices_table, request) return { @@ -1880,9 +1870,9 @@ class InterfaceView(generic.ObjectView): child_interfaces = Interface.objects.restrict(request.user, 'view').filter(parent=instance) child_interfaces_tables = tables.InterfaceTable( child_interfaces, + exclude=('device', 'parent'), orderable=False ) - child_interfaces_tables.columns.hide('device') # Get assigned VLANs and annotate whether each is tagged or untagged vlans = [] diff --git a/netbox/ipam/views.py b/netbox/ipam/views.py index 6d49cd24f..015d47065 100644 --- a/netbox/ipam/views.py +++ b/netbox/ipam/views.py @@ -155,10 +155,7 @@ class RIRView(generic.ObjectView): aggregates = Aggregate.objects.restrict(request.user, 'view').filter( rir=instance ) - - aggregates_table = tables.AggregateTable(aggregates) - aggregates_table.columns.hide('rir') - aggregates_table.columns.hide('utilization') + aggregates_table = tables.AggregateTable(aggregates, exclude=('rir', 'utilization')) paginate_table(aggregates_table, request) return { diff --git a/netbox/tenancy/views.py b/netbox/tenancy/views.py index 22d669a0d..0b28a62d2 100644 --- a/netbox/tenancy/views.py +++ b/netbox/tenancy/views.py @@ -32,9 +32,7 @@ class TenantGroupView(generic.ObjectView): tenants = Tenant.objects.restrict(request.user, 'view').filter( group=instance ) - - tenants_table = tables.TenantTable(tenants) - tenants_table.columns.hide('group') + tenants_table = tables.TenantTable(tenants, exclude=('group',)) paginate_table(tenants_table, request) return { diff --git a/netbox/virtualization/views.py b/netbox/virtualization/views.py index 8315ba0a6..a8b2b8f1f 100644 --- a/netbox/virtualization/views.py +++ b/netbox/virtualization/views.py @@ -39,9 +39,7 @@ class ClusterTypeView(generic.ObjectView): device_count=count_related(Device, 'cluster'), vm_count=count_related(VirtualMachine, 'cluster') ) - - clusters_table = tables.ClusterTable(clusters) - clusters_table.columns.hide('type') + clusters_table = tables.ClusterTable(clusters, exclude=('type',)) paginate_table(clusters_table, request) return { @@ -103,9 +101,7 @@ class ClusterGroupView(generic.ObjectView): device_count=count_related(Device, 'cluster'), vm_count=count_related(VirtualMachine, 'cluster') ) - - clusters_table = tables.ClusterTable(clusters) - clusters_table.columns.hide('group') + clusters_table = tables.ClusterTable(clusters, exclude=('group',)) paginate_table(clusters_table, request) return { @@ -434,9 +430,9 @@ class VMInterfaceView(generic.ObjectView): child_interfaces = VMInterface.objects.restrict(request.user, 'view').filter(parent=instance) child_interfaces_tables = tables.VMInterfaceTable( child_interfaces, + exclude=('virtual_machine',), orderable=False ) - child_interfaces_tables.columns.hide('virtual_machine') # Get assigned VLANs and annotate whether each is tagged or untagged vlans = [] From 7b3f6f1c673fe602530c7727c94ffbf3de0cdb08 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Fri, 17 Sep 2021 15:37:19 -0400 Subject: [PATCH 36/43] Clean up table classes --- netbox/circuits/tables.py | 8 + netbox/dcim/tables/devices.py | 1 + netbox/extras/tables.py | 13 ++ netbox/ipam/tables/__init__.py | 4 + netbox/ipam/{tables.py => tables/ip.py} | 288 ++---------------------- netbox/ipam/tables/services.py | 35 +++ netbox/ipam/tables/vlans.py | 203 +++++++++++++++++ netbox/ipam/tables/vrfs.py | 74 ++++++ netbox/tenancy/tables.py | 6 + 9 files changed, 357 insertions(+), 275 deletions(-) create mode 100644 netbox/ipam/tables/__init__.py rename netbox/ipam/{tables.py => tables/ip.py} (59%) create mode 100644 netbox/ipam/tables/services.py create mode 100644 netbox/ipam/tables/vlans.py create mode 100644 netbox/ipam/tables/vrfs.py diff --git a/netbox/circuits/tables.py b/netbox/circuits/tables.py index c3e616d8a..2e31237b6 100644 --- a/netbox/circuits/tables.py +++ b/netbox/circuits/tables.py @@ -6,6 +6,14 @@ from utilities.tables import BaseTable, ButtonsColumn, ChoiceFieldColumn, Markdo from .models import * +__all__ = ( + 'CircuitTable', + 'CircuitTypeTable', + 'ProviderTable', + 'ProviderNetworkTable', +) + + CIRCUITTERMINATION_LINK = """ {% if value.site %} {{ value.site }} diff --git a/netbox/dcim/tables/devices.py b/netbox/dcim/tables/devices.py index dda0af2b4..306b29b09 100644 --- a/netbox/dcim/tables/devices.py +++ b/netbox/dcim/tables/devices.py @@ -18,6 +18,7 @@ from .template_code import ( ) __all__ = ( + 'BaseInterfaceTable', 'ConsolePortTable', 'ConsoleServerPortTable', 'DeviceBayTable', diff --git a/netbox/extras/tables.py b/netbox/extras/tables.py index c556e312f..20a6ffd8a 100644 --- a/netbox/extras/tables.py +++ b/netbox/extras/tables.py @@ -7,6 +7,19 @@ from utilities.tables import ( ) from .models import * +__all__ = ( + 'ConfigContextTable', + 'CustomFieldTable', + 'CustomLinkTable', + 'ExportTemplateTable', + 'JournalEntryTable', + 'ObjectChangeTable', + 'ObjectJournalTable', + 'TaggedItemTable', + 'TagTable', + 'WebhookTable', +) + CONFIGCONTEXT_ACTIONS = """ {% if perms.extras.change_configcontext %} diff --git a/netbox/ipam/tables/__init__.py b/netbox/ipam/tables/__init__.py new file mode 100644 index 000000000..a280eac1b --- /dev/null +++ b/netbox/ipam/tables/__init__.py @@ -0,0 +1,4 @@ +from .ip import * +from .services import * +from .vlans import * +from .vrfs import * diff --git a/netbox/ipam/tables.py b/netbox/ipam/tables/ip.py similarity index 59% rename from netbox/ipam/tables.py rename to netbox/ipam/tables/ip.py index a39df2601..2e59a681c 100644 --- a/netbox/ipam/tables.py +++ b/netbox/ipam/tables/ip.py @@ -2,14 +2,23 @@ import django_tables2 as tables from django.utils.safestring import mark_safe from django_tables2.utils import Accessor -from dcim.models import Interface from tenancy.tables import TenantColumn from utilities.tables import ( - BaseTable, BooleanColumn, ButtonsColumn, ChoiceFieldColumn, ContentTypeColumn, LinkedCountColumn, TagColumn, + BaseTable, BooleanColumn, ButtonsColumn, ChoiceFieldColumn, LinkedCountColumn, TagColumn, ToggleColumn, UtilizationColumn, ) -from virtualization.models import VMInterface -from .models import * +from ipam.models import * + +__all__ = ( + 'AggregateTable', + 'InterfaceIPAddressTable', + 'IPAddressAssignTable', + 'IPAddressTable', + 'IPRangeTable', + 'PrefixTable', + 'RIRTable', + 'RoleTable', +) AVAILABLE_LABEL = mark_safe('Available') @@ -66,114 +75,6 @@ VRF_LINK = """ {% endif %} """ -VRF_TARGETS = """ -{% for rt in value.all %} - {{ rt }}{% if not forloop.last %}
{% endif %} -{% empty %} - — -{% endfor %} -""" - -VLAN_LINK = """ -{% if record.pk %} - {{ record.vid }} -{% elif perms.ipam.add_vlan %} - {{ record.available }} VLAN{{ record.available|pluralize }} available -{% else %} - {{ record.available }} VLAN{{ record.available|pluralize }} available -{% endif %} -""" - -VLAN_PREFIXES = """ -{% for prefix in record.prefixes.all %} - {{ prefix }}{% if not forloop.last %}
{% endif %} -{% empty %} - — -{% endfor %} -""" - -VLAN_ROLE_LINK = """ -{% if record.role %} - {{ record.role }} -{% else %} - — -{% endif %} -""" - -VLANGROUP_ADD_VLAN = """ -{% with next_vid=record.get_next_available_vid %} - {% if next_vid and perms.ipam.add_vlan %} - - - - {% endif %} -{% endwith %} -""" - -VLAN_MEMBER_TAGGED = """ -{% if record.untagged_vlan_id == object.pk %} - -{% else %} - -{% endif %} -""" - - -# -# VRFs -# - -class VRFTable(BaseTable): - pk = ToggleColumn() - name = tables.Column( - linkify=True - ) - rd = tables.Column( - verbose_name='RD' - ) - tenant = TenantColumn() - enforce_unique = BooleanColumn( - verbose_name='Unique' - ) - import_targets = tables.TemplateColumn( - template_code=VRF_TARGETS, - orderable=False - ) - export_targets = tables.TemplateColumn( - template_code=VRF_TARGETS, - orderable=False - ) - tags = TagColumn( - url_name='ipam:vrf_list' - ) - - class Meta(BaseTable.Meta): - model = VRF - fields = ( - 'pk', 'name', 'rd', 'tenant', 'enforce_unique', 'description', 'import_targets', 'export_targets', 'tags', - ) - default_columns = ('pk', 'name', 'rd', 'tenant', 'description') - - -# -# Route targets -# - -class RouteTargetTable(BaseTable): - pk = ToggleColumn() - name = tables.Column( - linkify=True - ) - tenant = TenantColumn() - tags = TagColumn( - url_name='ipam:vrf_list' - ) - - class Meta(BaseTable.Meta): - model = RouteTarget - fields = ('pk', 'name', 'tenant', 'description', 'tags') - default_columns = ('pk', 'name', 'tenant', 'description') - # # RIRs @@ -475,166 +376,3 @@ class InterfaceIPAddressTable(BaseTable): class Meta(BaseTable.Meta): model = IPAddress fields = ('address', 'vrf', 'status', 'role', 'tenant', 'description') - - -# -# VLAN groups -# - -class VLANGroupTable(BaseTable): - pk = ToggleColumn() - name = tables.Column(linkify=True) - scope_type = ContentTypeColumn() - scope = tables.Column( - linkify=True, - orderable=False - ) - vlan_count = LinkedCountColumn( - viewname='ipam:vlan_list', - url_params={'group_id': 'pk'}, - verbose_name='VLANs' - ) - actions = ButtonsColumn( - model=VLANGroup, - prepend_template=VLANGROUP_ADD_VLAN - ) - - class Meta(BaseTable.Meta): - model = VLANGroup - fields = ('pk', 'name', 'scope_type', 'scope', 'vlan_count', 'slug', 'description', 'actions') - default_columns = ('pk', 'name', 'scope_type', 'scope', 'vlan_count', 'description', 'actions') - - -# -# VLANs -# - -class VLANTable(BaseTable): - pk = ToggleColumn() - vid = tables.TemplateColumn( - template_code=VLAN_LINK, - verbose_name='ID' - ) - site = tables.Column( - linkify=True - ) - group = tables.Column( - linkify=True - ) - tenant = TenantColumn() - status = ChoiceFieldColumn( - default=AVAILABLE_LABEL - ) - role = tables.TemplateColumn( - template_code=VLAN_ROLE_LINK - ) - prefixes = tables.TemplateColumn( - template_code=VLAN_PREFIXES, - orderable=False, - verbose_name='Prefixes' - ) - tags = TagColumn( - url_name='ipam:vlan_list' - ) - - class Meta(BaseTable.Meta): - model = VLAN - fields = ('pk', 'vid', 'name', 'site', 'group', 'prefixes', 'tenant', 'status', 'role', 'description', 'tags') - default_columns = ('pk', 'vid', 'name', 'site', 'group', 'prefixes', 'tenant', 'status', 'role', 'description') - row_attrs = { - 'class': lambda record: 'success' if not isinstance(record, VLAN) else '', - } - - -class VLANMembersTable(BaseTable): - """ - Base table for Interface and VMInterface assignments - """ - name = tables.Column( - linkify=True, - verbose_name='Interface' - ) - tagged = tables.TemplateColumn( - template_code=VLAN_MEMBER_TAGGED, - orderable=False - ) - - -class VLANDevicesTable(VLANMembersTable): - device = tables.Column( - linkify=True - ) - actions = ButtonsColumn(Interface, buttons=['edit']) - - class Meta(BaseTable.Meta): - model = Interface - fields = ('device', 'name', 'tagged', 'actions') - - -class VLANVirtualMachinesTable(VLANMembersTable): - virtual_machine = tables.Column( - linkify=True - ) - actions = ButtonsColumn(VMInterface, buttons=['edit']) - - class Meta(BaseTable.Meta): - model = VMInterface - fields = ('virtual_machine', 'name', 'tagged', 'actions') - - -class InterfaceVLANTable(BaseTable): - """ - List VLANs assigned to a specific Interface. - """ - vid = tables.Column( - linkify=True, - verbose_name='ID' - ) - tagged = BooleanColumn() - site = tables.Column( - linkify=True - ) - group = tables.Column( - accessor=Accessor('group__name'), - verbose_name='Group' - ) - tenant = TenantColumn() - status = ChoiceFieldColumn() - role = tables.TemplateColumn( - template_code=VLAN_ROLE_LINK - ) - - class Meta(BaseTable.Meta): - model = VLAN - fields = ('vid', 'tagged', 'site', 'group', 'name', 'tenant', 'status', 'role', 'description') - - def __init__(self, interface, *args, **kwargs): - self.interface = interface - super().__init__(*args, **kwargs) - - -# -# Services -# - -class ServiceTable(BaseTable): - pk = ToggleColumn() - name = tables.Column( - linkify=True - ) - parent = tables.Column( - linkify=True, - order_by=('device', 'virtual_machine') - ) - ports = tables.TemplateColumn( - template_code='{{ record.port_list }}', - verbose_name='Ports' - ) - tags = TagColumn( - url_name='ipam:service_list' - ) - - class Meta(BaseTable.Meta): - model = Service - fields = ('pk', 'name', 'parent', 'protocol', 'ports', 'ipaddresses', 'description', 'tags') - default_columns = ('pk', 'name', 'parent', 'protocol', 'ports', 'description') diff --git a/netbox/ipam/tables/services.py b/netbox/ipam/tables/services.py new file mode 100644 index 000000000..58c8ea49e --- /dev/null +++ b/netbox/ipam/tables/services.py @@ -0,0 +1,35 @@ +import django_tables2 as tables + +from utilities.tables import BaseTable, TagColumn, ToggleColumn +from ipam.models import * + +__all__ = ( + 'ServiceTable', +) + + +# +# Services +# + +class ServiceTable(BaseTable): + pk = ToggleColumn() + name = tables.Column( + linkify=True + ) + parent = tables.Column( + linkify=True, + order_by=('device', 'virtual_machine') + ) + ports = tables.TemplateColumn( + template_code='{{ record.port_list }}', + verbose_name='Ports' + ) + tags = TagColumn( + url_name='ipam:service_list' + ) + + class Meta(BaseTable.Meta): + model = Service + fields = ('pk', 'name', 'parent', 'protocol', 'ports', 'ipaddresses', 'description', 'tags') + default_columns = ('pk', 'name', 'parent', 'protocol', 'ports', 'description') diff --git a/netbox/ipam/tables/vlans.py b/netbox/ipam/tables/vlans.py new file mode 100644 index 000000000..4219a8a52 --- /dev/null +++ b/netbox/ipam/tables/vlans.py @@ -0,0 +1,203 @@ +import django_tables2 as tables +from django.utils.safestring import mark_safe +from django_tables2.utils import Accessor + +from dcim.models import Interface +from tenancy.tables import TenantColumn +from utilities.tables import ( + BaseTable, BooleanColumn, ButtonsColumn, ChoiceFieldColumn, ContentTypeColumn, LinkedCountColumn, TagColumn, + ToggleColumn, +) +from virtualization.models import VMInterface +from ipam.models import * + +__all__ = ( + 'InterfaceVLANTable', + 'VLANDevicesTable', + 'VLANGroupTable', + 'VLANMembersTable', + 'VLANTable', + 'VLANVirtualMachinesTable', +) + +AVAILABLE_LABEL = mark_safe('Available') + +VLAN_LINK = """ +{% if record.pk %} + {{ record.vid }} +{% elif perms.ipam.add_vlan %} + {{ record.available }} VLAN{{ record.available|pluralize }} available +{% else %} + {{ record.available }} VLAN{{ record.available|pluralize }} available +{% endif %} +""" + +VLAN_PREFIXES = """ +{% for prefix in record.prefixes.all %} + {{ prefix }}{% if not forloop.last %}
{% endif %} +{% empty %} + — +{% endfor %} +""" + +VLAN_ROLE_LINK = """ +{% if record.role %} + {{ record.role }} +{% else %} + — +{% endif %} +""" + +VLANGROUP_ADD_VLAN = """ +{% with next_vid=record.get_next_available_vid %} + {% if next_vid and perms.ipam.add_vlan %} + + + + {% endif %} +{% endwith %} +""" + +VLAN_MEMBER_TAGGED = """ +{% if record.untagged_vlan_id == object.pk %} + +{% else %} + +{% endif %} +""" + + +# +# VLAN groups +# + +class VLANGroupTable(BaseTable): + pk = ToggleColumn() + name = tables.Column(linkify=True) + scope_type = ContentTypeColumn() + scope = tables.Column( + linkify=True, + orderable=False + ) + vlan_count = LinkedCountColumn( + viewname='ipam:vlan_list', + url_params={'group_id': 'pk'}, + verbose_name='VLANs' + ) + actions = ButtonsColumn( + model=VLANGroup, + prepend_template=VLANGROUP_ADD_VLAN + ) + + class Meta(BaseTable.Meta): + model = VLANGroup + fields = ('pk', 'name', 'scope_type', 'scope', 'vlan_count', 'slug', 'description', 'actions') + default_columns = ('pk', 'name', 'scope_type', 'scope', 'vlan_count', 'description', 'actions') + + +# +# VLANs +# + +class VLANTable(BaseTable): + pk = ToggleColumn() + vid = tables.TemplateColumn( + template_code=VLAN_LINK, + verbose_name='ID' + ) + site = tables.Column( + linkify=True + ) + group = tables.Column( + linkify=True + ) + tenant = TenantColumn() + status = ChoiceFieldColumn( + default=AVAILABLE_LABEL + ) + role = tables.TemplateColumn( + template_code=VLAN_ROLE_LINK + ) + prefixes = tables.TemplateColumn( + template_code=VLAN_PREFIXES, + orderable=False, + verbose_name='Prefixes' + ) + tags = TagColumn( + url_name='ipam:vlan_list' + ) + + class Meta(BaseTable.Meta): + model = VLAN + fields = ('pk', 'vid', 'name', 'site', 'group', 'prefixes', 'tenant', 'status', 'role', 'description', 'tags') + default_columns = ('pk', 'vid', 'name', 'site', 'group', 'prefixes', 'tenant', 'status', 'role', 'description') + row_attrs = { + 'class': lambda record: 'success' if not isinstance(record, VLAN) else '', + } + + +class VLANMembersTable(BaseTable): + """ + Base table for Interface and VMInterface assignments + """ + name = tables.Column( + linkify=True, + verbose_name='Interface' + ) + tagged = tables.TemplateColumn( + template_code=VLAN_MEMBER_TAGGED, + orderable=False + ) + + +class VLANDevicesTable(VLANMembersTable): + device = tables.Column( + linkify=True + ) + actions = ButtonsColumn(Interface, buttons=['edit']) + + class Meta(BaseTable.Meta): + model = Interface + fields = ('device', 'name', 'tagged', 'actions') + + +class VLANVirtualMachinesTable(VLANMembersTable): + virtual_machine = tables.Column( + linkify=True + ) + actions = ButtonsColumn(VMInterface, buttons=['edit']) + + class Meta(BaseTable.Meta): + model = VMInterface + fields = ('virtual_machine', 'name', 'tagged', 'actions') + + +class InterfaceVLANTable(BaseTable): + """ + List VLANs assigned to a specific Interface. + """ + vid = tables.Column( + linkify=True, + verbose_name='ID' + ) + tagged = BooleanColumn() + site = tables.Column( + linkify=True + ) + group = tables.Column( + accessor=Accessor('group__name'), + verbose_name='Group' + ) + tenant = TenantColumn() + status = ChoiceFieldColumn() + role = tables.TemplateColumn( + template_code=VLAN_ROLE_LINK + ) + + class Meta(BaseTable.Meta): + model = VLAN + fields = ('vid', 'tagged', 'site', 'group', 'name', 'tenant', 'status', 'role', 'description') + + def __init__(self, interface, *args, **kwargs): + self.interface = interface + super().__init__(*args, **kwargs) diff --git a/netbox/ipam/tables/vrfs.py b/netbox/ipam/tables/vrfs.py new file mode 100644 index 000000000..bea2a6b1f --- /dev/null +++ b/netbox/ipam/tables/vrfs.py @@ -0,0 +1,74 @@ +import django_tables2 as tables + +from tenancy.tables import TenantColumn +from utilities.tables import BaseTable, BooleanColumn, TagColumn, ToggleColumn +from ipam.models import * + +__all__ = ( + 'RouteTargetTable', + 'VRFTable', +) + +VRF_TARGETS = """ +{% for rt in value.all %} + {{ rt }}{% if not forloop.last %}
{% endif %} +{% empty %} + — +{% endfor %} +""" + + +# +# VRFs +# + +class VRFTable(BaseTable): + pk = ToggleColumn() + name = tables.Column( + linkify=True + ) + rd = tables.Column( + verbose_name='RD' + ) + tenant = TenantColumn() + enforce_unique = BooleanColumn( + verbose_name='Unique' + ) + import_targets = tables.TemplateColumn( + template_code=VRF_TARGETS, + orderable=False + ) + export_targets = tables.TemplateColumn( + template_code=VRF_TARGETS, + orderable=False + ) + tags = TagColumn( + url_name='ipam:vrf_list' + ) + + class Meta(BaseTable.Meta): + model = VRF + fields = ( + 'pk', 'name', 'rd', 'tenant', 'enforce_unique', 'description', 'import_targets', 'export_targets', 'tags', + ) + default_columns = ('pk', 'name', 'rd', 'tenant', 'description') + + +# +# Route targets +# + +class RouteTargetTable(BaseTable): + pk = ToggleColumn() + name = tables.Column( + linkify=True + ) + tenant = TenantColumn() + tags = TagColumn( + url_name='ipam:vrf_list' + ) + + class Meta(BaseTable.Meta): + model = RouteTarget + fields = ('pk', 'name', 'tenant', 'description', 'tags') + default_columns = ('pk', 'name', 'tenant', 'description') diff --git a/netbox/tenancy/tables.py b/netbox/tenancy/tables.py index 961d02dcc..c62c641d1 100644 --- a/netbox/tenancy/tables.py +++ b/netbox/tenancy/tables.py @@ -5,6 +5,12 @@ from utilities.tables import ( ) from .models import Tenant, TenantGroup +__all__ = ( + 'TenantColumn', + 'TenantGroupTable', + 'TenantTable', +) + # # Table columns From 713e79c1a91581aef3730d8fdda9075b8e244f4c Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Fri, 17 Sep 2021 16:55:32 -0400 Subject: [PATCH 37/43] Fixes #7298: Restore missing object names from applied object list filters --- docs/release-notes/version-3.0.md | 1 + netbox/utilities/forms/utils.py | 44 +++++++++++++----------- netbox/utilities/templatetags/helpers.py | 2 +- 3 files changed, 26 insertions(+), 21 deletions(-) diff --git a/docs/release-notes/version-3.0.md b/docs/release-notes/version-3.0.md index ee4f7aebe..644a2e652 100644 --- a/docs/release-notes/version-3.0.md +++ b/docs/release-notes/version-3.0.md @@ -24,6 +24,7 @@ * [#7273](https://github.com/netbox-community/netbox/issues/7273) - Fix natural ordering of device components in UI form fields * [#7279](https://github.com/netbox-community/netbox/issues/7279) - Fix exception when tracing cable with no associated path * [#7282](https://github.com/netbox-community/netbox/issues/7282) - Fix KeyError exception when `INSECURE_SKIP_TLS_VERIFY` is true +* [#7298](https://github.com/netbox-community/netbox/issues/7298) - Restore missing object names from applied object list filters --- diff --git a/netbox/utilities/forms/utils.py b/netbox/utilities/forms/utils.py index bb1f56c4d..7d2b79f02 100644 --- a/netbox/utilities/forms/utils.py +++ b/netbox/utilities/forms/utils.py @@ -122,28 +122,32 @@ def get_selected_values(form, field_name): form.is_valid() filter_data = form.cleaned_data.get(field_name) field = form.fields[field_name] - # Selection field - if hasattr(field, 'choices'): - try: - choices = unpack_grouped_choices(field.choices) - - if hasattr(field, 'null_option'): - # If the field has a `null_option` attribute set and it is selected, - # add it to the field's grouped choices. - if field.null_option is not None and None in filter_data: - choices.append((settings.FILTERS_NULL_CHOICE_VALUE, field.null_option)) - - return [ - label for value, label in choices if str(value) in filter_data or None in filter_data - ] - except TypeError: - # Field uses dynamic choices. Show all that have been populated. - return [ - subwidget.choice_label for subwidget in form[field_name].subwidgets - ] # Non-selection field - return [str(filter_data)] + if not hasattr(field, 'choices'): + return [str(filter_data)] + + # Get choice labels + if type(field.choices) is forms.models.ModelChoiceIterator: + # Field uses dynamic choices: show all that have been populated on the widget + values = [ + subwidget.choice_label for subwidget in form[field_name].subwidgets + ] + + else: + # Static selection field + choices = unpack_grouped_choices(field.choices) + values = [ + label for value, label in choices if str(value) in filter_data or None in filter_data + ] + + if hasattr(field, 'null_option'): + # If the field has a `null_option` attribute set and it is selected, + # add it to the field's grouped choices. + if field.null_option is not None and None in filter_data: + values.append(field.null_option) + + return values def add_blank_choice(choices): diff --git a/netbox/utilities/templatetags/helpers.py b/netbox/utilities/templatetags/helpers.py index 1b13fb2a8..615595f0f 100644 --- a/netbox/utilities/templatetags/helpers.py +++ b/netbox/utilities/templatetags/helpers.py @@ -411,10 +411,10 @@ def applied_filters(form, query_params): Display the active filters for a given filter form. """ form.is_valid() - querydict = query_params.copy() applied_filters = [] for filter_name in form.changed_data: + querydict = query_params.copy() if filter_name not in querydict: continue From 030c573037a905e8d03b036c3b35ec0dd61418df Mon Sep 17 00:00:00 2001 From: royreznik Date: Mon, 20 Sep 2021 02:44:40 -0700 Subject: [PATCH 38/43] =?UTF-8?q?Fixes=20#7228:=20Round=20=C2=B0F=20temper?= =?UTF-8?q?ature=20to=20one=20decimal=20place?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- netbox/project-static/dist/status.js | Bin 129191 -> 129226 bytes netbox/project-static/dist/status.js.map | Bin 128379 -> 128409 bytes netbox/project-static/src/util.ts | 2 +- 3 files changed, 1 insertion(+), 1 deletion(-) diff --git a/netbox/project-static/dist/status.js b/netbox/project-static/dist/status.js index c817efb4cf7b98cf4060b108185cccf73303d3ed..bffd74dca943abdafb329e987f45620e25d2aa5f 100644 GIT binary patch delta 4794 zcmZu#dvp`$l|R=u2F#{BYzK^Ouw_M_kw)Ve!5}jc4mRM%_86=-fUMXWX(UgOW)#hs zLAFKMnB>re<~2>HDWxRY9H6f}5E~_gkT%>A5Y4?63 zS)o1gAK(2R_ujdW-@V@_AG|j6?7n$b}=8&g8Tn8@)smDH|a z>TZ=w;e1S%wsS^(S$Ts_wTNv_J588HnL~pEIO|opVI&FkfzdOeB@dO<3iO1)aojdV z3fDC|92B}~qdT?*lXb(KV9aS)y^QBl{$`ZsmNz?HDSu1ARfnWC(2*3iv7}t5qj&~Smyqn`Q7oFY3Lp(h} zKB#lUZu~~diKa7yT#(Wkp6-w$R0_oh>D=f1I+F@m_Vp1CH?fKubf;)b3itZ${jsZ@w&$FvY1iKwDRMGJfaYv0b=H7PERE3(LSGnhM-)yCQAn5e48wVLuw zL(_*v4vbS?vrs3=suq+(q7tc#rkOAo8Iwbrq{x(Dw6vDrD=;Z$n6<|+`0*Rg3JNqf zK?$_WrrOd>_ZKk;tD4iy=og)IqoB7hsEx8AQHtThV=W>vMM>f+&BVA!N5PuH7cHis zpjs@U8x%9c1s=hR%8kFYRwJGlx7FZS-L}}+&{j25pi&GX&A6wn0rB`mTl)e&pNzUN zm2yV>af;8p*LD$5<|ns~Ez3bwAUERe3sKws?Y3Io z#kD)NQAtHcRu{#sY7T&dT!_+*pSFJoxq~alaeQ~hw`vfMlasd1_~-8Hb9Z(5*01-( z;~rJDA*3H&IYe}S>~W&*MIGz!<++^VxQYOdQ>jAU32JR|DHfAdF{H?0)#l}I;ShO< zW9!T^#O58P_$IVEKzs5Q;+!E+w;-R!LHzv{E^xd4EDN1S?f=hDWqv1K=}{t=d(5vylG$q#!RC zuGO(}o*QNpNkvn%R6-<{V_LtzdXq8KxxlFCTyGS0F38B8Pn68@$y`jMC_!Pi8Fgxf zG155GxYE;XY+gH5Bq+vGsw#7O?TK0S0+$FTRdH=h30hUv?GFVwN$n5z6Qq)Yf0Ze|GL3ZEHS>!OyA*BMB~vdD{%aD%VqFo&if-J zxbTj&mIyS%8)G9)Wjw1(VQrK~)z6L8pqZyem}Z_$U|=LU+ABshPhqu@7|W#GT*zD4 z^C<}rZ!`v9LB$t>YdiAUNhqpB`T@J$HHwY5Bjz6tPa7rC#JDJHB*d{`N=&Z%N>3LjFcH6y`Cv>ZAs8NSu<@KM>UB{>91)^B1U50I zBG&2Y1PGaD#}^h=2^~={MZiLO9afC0B{4EKuV9>+^ehm#C~Yb5#{!(-#RizEOW#Hu z7~Wy85$KL#R?@^cejX=kkJUCcE;KnQWcKga3#GV^*H32kPO}JQ&fnLCW9?LRD^JlA zDVK}E#y^a=DMhD4Y(zzkogDCDgN9U=FI;BbU; z!4fj(9{#i#)t-919mitBIY-as#+N{gJ}(=GkusJw@)E{R5$+a4n$6+hC05*?P?DPJ z{d~%4*h$YA)Eq>@XWzX-9swUd)jM04SWQ$lDrx-gsX8O@^fA&wo*6}^sb?C|)65SJ zEC=uz7Y_!|j@5^1aemjK5YFE?^i@2r`rgIah@R_*N670UaHPBlp?=Rxx1pLBUfO_a zin1Y+fXFLL7V=!Qsns3!M*>Z)E&-e7W{ny~Z}z_8sL&Y=?;ROSH+PM4x|`3u^U5z_ zA&-C-Db18Hrha}BJ^1wJe&Q~FY85W_oH|Z&aK~FG zaI5_Ezt{v0Lz^oC)gSi8JQ4B)xkI8lHPVv=>`8w--G<7Tw>gK-MbJ(wDQr+xrKn8d z+bD_;4;r5!5$yA~YhFF`$lE&!qUN1Nco{3szOVo*olRmURW_j{5@Hhj=%3G?My03D z-G}D)pYKPb_nptoLjDaGDvEf{XnC);2-zOEFokSo7uQ$t*cO;Bju2POZx1j%9B)J} z)>Z5N&VXkS2WBgWMaON^yl%3@hn4a4#RpM8bE$Sd;bO*dpqZ$K)u$UR*li~8|!dQURBB5jbfKcisRnx8r8+{GVSSNdYQO4tWiBELVJ2> zY(L|dG1Nx(va_0StdW_oh&i4v*<$LFkt=ik^3vJpZ_QP%n7kK;u5P|r;ILS%2$4_L z*NZ9@#cq4*>JntUa@C1rEp=LF85ybPNY49%tkzwgK@DV->2~UwoP{ltTZaZ;am52>j@Blcx_~ckS6Va{wIqN zJkC$n5b(&qufk5c`}aT=5B~9ejPTY!_2bsjKfO1jicfM){=3+Klk##yEbF9)*h$ve zgwKr^|2kR>K67y~ybHeU{}#i3tgNhA3WuOlPcrzDX4WMA625&T3*Ek~Zx$>mo#A7Z zNW|RmB-CfW^(EL(bgWna-$!z30h~cqFPFnXgm`wO62{Rc^S>5>6PKk`&{%Y{Pfd?7 zi6A!;ib|womxXHP!I73L(hZ5{g7>)GcnqTD37g^ZjZV1m0X$18&ew0dL|; zUgd0>G8P)eR})SXHfHOj2qD~MBcS5$GZ7GqDuftkW8+FV7;}fEU`&aU zBrlTSaIp|$%%_(^MfQRO_JW7CTj3JwfEdt7Trohjo82*tLEh8BI6RK?-^Ah8Dhr#G z^^_RSw^1f4)A)sDGE5@Zi!$7brhFj747{Msj}^Ebm(7VAB6nvJ`YLjY!b)h2yhoUX zM0C8Igzut`J5_iV$#oUlku1}ILKOiGx(hZR9fO5@ZKN!PL;DTf&+o{7YQZPRo zhX@k)1lW)aOh6nVetZHJRq`>ck4=bbHXPJ~?qEVfkY_)dfaj)Dn|*Ny?3~tU_Uwes zdF|%Wolt|OoZE?L#=UxM=188r52LS}H%-CofA3b@a_jvmNFm>rT_nQUUAv%#NWXNy zwcGpv?7j)3Ayc{}hZP--dGi5ya0Uq3fj!_ZF)NS2JK2qo!G+>{XGk4_e)EAR;G>)g zi_Fsp;O6X8--JWN12ihXqdy4(`Oq)F4FfkGn8;d^4eW*Hyc0Qo^VoM_?ceaV?1M+q z;5YWcpJ$a26G>BYn7=uIHSaTDKL{L>&kn+NWZHHJ{-+etyY+c+k$Nx>!wxXhkD~2Y zj=*Qcoez(q<1+*~&D4GK7*v_<$3P`6$&|U_$55Vq;~2z<2TiZTXQ;{jW4Mmjs_A?a zHWmr8Isf0G*4+Chl$H5dd}mruv%KJ&XgCVaJ^{i!A@nz?^_deVfUW04+=x1==(2s{ zP7wj1<2v6Uac!RvvL{c#rXozlv}z6U*mTmW34$y$anefmhbOJ7BwL86dro3C-F^yl zGuLvYfM-41`WDm@BwE7JUtlZB&zJuPT$r9Woq=`vdG_Hmuo_Dy`_?;9Lc(z7ob~a) zoP#!8wVt;~#3I>mp9h}sUinYhQ|h*Uz$cY4IZRQKlUmtyEA47)b@h+M1u@BX4-EG7 U^8NTPkY>86r8&FveP~?t{~`xly#N3J delta 4759 zcmZu!dvp`$l|R=u2F#{BYzK^Ouw_M_kw#-<6AUsF;a~%9Y!BdR1IUW3$4H(a%_y2N zgKUe?nB>re<~2>HDWxQ74zO)E55z_ZA*9VV%_;0|NK1gWX&N4<$sr9b$+p{eNxSzO z$qMa>{+Rnc?!DiA{OttUn@#;%UYeTv~P*u)qteU-H+@oGDR2p2yB|YFPH0<-Jc4nra|K4_Rp;U|-OcIQ3(lU>A)Ov4 z2l8CNjc25tXgV{<1u34<=}sw1rBQs4&L8LFnY7=sub*(ZiB;5~J55_s$X~E(5M}zk zKHivQg%tv)Qem1M)xvxngKN zEgzIPFir){LW3l$T2Ky)O0*%CVIo{~R1RyBB2$8~q^;C$8HoD$v*@ zCD1OLYRfP^pT{7qY|StupSRP4g5Lh1Ho}HQDUK75HHpR*C55vL6X&9xMQe&DT1>%^ zYO#c`Q_KX4Jc1XM8-Hu7MLaKTufuO``$A(wd(Cu#N;8NwlHM{W0Z*y?!~ z*WuJgBo!H1T@<&fc>oS_VTw0?-0^MX4lWo!CS%+|(n6PcepS!=`-Q6v$Tjxn6 zJgRC#NI$${i0J;v<3!yHI@jH;b9u!H6#<-}(#5hzQH>R6tZ|~XP~k;rrSnP){)FQQR;uWXj%dRMpl2UhxvTU>A@kWzQC=`y ztKyY97hsbqMN_nNQY4mRTEDkylQGmaBJXOwi!%gSAkmCXvtTwJ3lL1DHT4QiD! z+&ta5+}mnwUNcl8D8^!{CVOhl8#5XOE*VUz;+nV;w5qDd7xr_KIuINnNF@dTBEK3l za*eOU{#LWT5j;B6!z!bim|7=Fu@Q~yVMnZii)VT=nL$pCeVy-j1IR{=&UY9v=JIrKQmm1W}X~oT6H#wfsy1`pBU9V#o0z;ER%L~VQ+Cw zP?8?qXb!%Fiq8kvbQZFcR8)!d19rP>1RHN>+!yfYaTpV|O%Xt6ZHw%_eXUSg1}glP3L4$Z-^n{h!EW=u*p#s zu})7VK*&BlHov4s=!|(O0v6Kiuwqm#g^{s&1>^LDXP&^tXiI@F?&kzAHo$B{<`&{W zV28aMQ6fnR7v#&!y{A?(b!~oR>3w*#W9_CPSPm+^~q)svd1P<#Lt%9q>J@g zUukzEjvv_V!0p4k%gVlh{XE8E8;`|A?3~7r_dN7ZOWEMpHbcEo%v&!yLhcR0;Rxr0 zC1lS&_(>_MJ^5${eoGDKEIyx`P!cWrtZW=c+F07GOBg>Tbf*~BYz~JmvEufmlG0S~ zXG2cIPI}s)W+4(j{my0b2>9TMzL~tlYND!9DdP`MG#LITkC6`Y)Ce+7KGlq#X1{-6 z89>OmaL|u-tU6SW<2w$8as1k$FXML2zh0P$=(%=yn7l6hM=DDY>UX_x3#xhUg$<~t zBo`(Lh`zLFzRty3+T0Oe)ZfzP60m7*)~En_v-c%O70+mR@5orX`D>Kp-Fo)zmwpNJ zbp*6XX-<`HQEBY-Oxi=1vOjsbnV{jyF&Cb=@Rb4lO0Nvm>VCJJFFhvWZd2rgHL|yBkuAiSK?&P$>Sskx4roW zE>)iT51YVYX!Av&`Xb)ACrX|mw@Fl&MtYKfJ?YP<+EE$v7U$r(s7UcvQrMuXN->$j zw^0lq9yC5dqS)te)x3Q6p|^GtM9n#a@G_R4d2Sw7I-9~ws%%n8CdCx?(Z8NKg-TDJ zy$8)7I5&Vs?>m>Bf&3fJSC#0TvE<$Q5@fsY{3NnfTv%77V_RUlIYL}=PKCFxJCJ!HZ1W zFTT*H%X!T)&Am=@Z`BySu&@^A;=6=I4*^^+Q-9M#y%c0SJ%Qndl{l+o}AzM62DB!6wv=HJ&syV+O|@0j;5hPmdmi=o`CS_J3Nf{zx#KJ@K@#n4iM zcD~#Q@0amdQ04&}9LEK*33`z{+XT%>&Nji(YF?d~AoXEcBj6o^dDG1UcDVHUTe4 z-wI!uj4W`Ka$oDm(BkCf0qCC+Z{R`0@nbyvW@>5AdWfLLtLx$4DsXMT z52Zrpf91*l`Cu{5YKI8Pbq>LUGcj8yLTp^Y*znZ^^A$}oXk&&zN#n)1F3)9`{aKT_aUoVF&fi`?xg=&#BviYVbx@*ZK5 z64CKu3ciCnZdc)HB-d2vK(bT=3RU}7TR2$dw&#&GF15I;T+3##=v*2gA9H5&9t~ZI+Vfcfigmjb`so z*j(5!kM4vzH0A70L^G~6-UB+4$M3=D^X3hc@ajLim9X4;ZxYhTw`CWJaBkNwSVE*< zxYt^3y$^QZfYFdCU6LaTw#MA+_rd+sK*+7%1MV`j`Ut$8+xQ5aFD-P2^dT59?|TeB z%$u;lJaqtW%suf9I7B=^qY5kf;}9YT{qkF|{`w6QS<7<%z0g{4BF}Fg`!=lk2fiiy z;9)fQwSDl{8D+#o(v%$LZx3M2hs;+G0*B<&gRmW$wjF~1DM$2feHL7#9?ZkA1I*0B zX#3?O@F{WUgQMv9G(pZV4c|BhHD<>#P>D-2Wp4N(ROVhg265s+%d7AyYI6S&uHm(6 zI$wv4C4y|u{cor@_r4Al6(JVinby;+Aow~Oj)F7slAj}l{~@&@bNmfp8}%?Ztd1zU zY#+Z}L;&zOFEmJ;+sB36i8o+V2_|AnwT5_XIbqcV0fm{ww*{W@!3nD>$r2*!t`k^I zx1Pk@%(fgU;+fC2y$SUMiI#BeXV{7g<7GdG^HbxN)3CNM&OLY{;pyn;tK;sH7ggfA K{rDrsEkXcM(HAKI delta 39 rcmbRFn*H}H_J%Etn;wa}J35Bz00~bJ;jQEDk{4y}wEgKL#w|hsLj?~p diff --git a/netbox/project-static/src/util.ts b/netbox/project-static/src/util.ts index d7a8d5b6a..04dfa1d01 100644 --- a/netbox/project-static/src/util.ts +++ b/netbox/project-static/src/util.ts @@ -409,7 +409,7 @@ export function createElement< * @returns Degrees in Fahrenheit. */ export function cToF(celsius: number): number { - return celsius * (9 / 5) + 32; + return Math.round((celsius * (9 / 5) + 32 + Number.EPSILON) * 10) / 10; } /** From 879ffd648b6f93720f2e3c95a815b7615799eb80 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Mon, 20 Sep 2021 09:02:28 -0400 Subject: [PATCH 39/43] Fix 'select all' widget display --- netbox/templates/utilities/obj_table.html | 2 ++ 1 file changed, 2 insertions(+) diff --git a/netbox/templates/utilities/obj_table.html b/netbox/templates/utilities/obj_table.html index c8c0f07f2..99ab5c0c7 100644 --- a/netbox/templates/utilities/obj_table.html +++ b/netbox/templates/utilities/obj_table.html @@ -8,6 +8,7 @@ {% if table.paginator.num_pages > 1 %}
+
{% if bulk_edit_url and permissions.change %}
+
{% endif %} From 8ba5d032800d5560418d4c07ba9977ab82d06b50 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Mon, 20 Sep 2021 10:02:03 -0400 Subject: [PATCH 40/43] Fixes #7301: Fix exception when deleting a large number of child prefixes --- docs/release-notes/version-3.0.md | 1 + netbox/netbox/views/generic.py | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/release-notes/version-3.0.md b/docs/release-notes/version-3.0.md index 644a2e652..4b0a93c75 100644 --- a/docs/release-notes/version-3.0.md +++ b/docs/release-notes/version-3.0.md @@ -25,6 +25,7 @@ * [#7279](https://github.com/netbox-community/netbox/issues/7279) - Fix exception when tracing cable with no associated path * [#7282](https://github.com/netbox-community/netbox/issues/7282) - Fix KeyError exception when `INSECURE_SKIP_TLS_VERIFY` is true * [#7298](https://github.com/netbox-community/netbox/issues/7298) - Restore missing object names from applied object list filters +* [#7301](https://github.com/netbox-community/netbox/issues/7301) - Fix exception when deleting a large number of child prefixes --- diff --git a/netbox/netbox/views/generic.py b/netbox/netbox/views/generic.py index 0d4f89d0f..b05033128 100644 --- a/netbox/netbox/views/generic.py +++ b/netbox/netbox/views/generic.py @@ -1010,10 +1010,10 @@ class BulkDeleteView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View): # Are we deleting *all* objects in the queryset or just a selected subset? if request.POST.get('_all'): + qs = model.objects.all() if self.filterset is not None: - pk_list = [obj.pk for obj in self.filterset(request.GET, model.objects.only('pk')).qs] - else: - pk_list = model.objects.values_list('pk', flat=True) + qs = self.filterset(request.GET, qs).qs + pk_list = qs.only('pk').values_list('pk', flat=True) else: pk_list = [int(pk) for pk in request.POST.getlist('pk')] From 38be0b4976d025857bff111952703b412ce3493e Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Mon, 20 Sep 2021 10:23:29 -0400 Subject: [PATCH 41/43] Changelog for #7228 --- docs/release-notes/version-3.0.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes/version-3.0.md b/docs/release-notes/version-3.0.md index 4b0a93c75..97f2fd4b0 100644 --- a/docs/release-notes/version-3.0.md +++ b/docs/release-notes/version-3.0.md @@ -19,6 +19,7 @@ * [#5775](https://github.com/netbox-community/netbox/issues/5775) - Enable group synchronization for remote authentication backend * [#7167](https://github.com/netbox-community/netbox/issues/7167) - Ensure consistent font size when using monospace formatting * [#7226](https://github.com/netbox-community/netbox/issues/7226) - Exempt GraphQL API requests from CSRF inspection +* [#7228](https://github.com/netbox-community/netbox/issues/7228) - Improve temperature conversions under device status * [#7248](https://github.com/netbox-community/netbox/issues/7248) - Fix global search results section links * [#7266](https://github.com/netbox-community/netbox/issues/7266) - Tweak font color for form field placeholder text * [#7273](https://github.com/netbox-community/netbox/issues/7273) - Fix natural ordering of device components in UI form fields From 214c1d5a50320c3118808e622e805e0fedd636ae Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Mon, 20 Sep 2021 12:44:44 -0400 Subject: [PATCH 42/43] Release v3.0.3 --- .github/ISSUE_TEMPLATE/bug_report.yaml | 2 +- .github/ISSUE_TEMPLATE/feature_request.yaml | 2 +- docs/release-notes/version-3.0.md | 2 +- netbox/netbox/settings.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml index e04b0e602..b3558959f 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yaml +++ b/.github/ISSUE_TEMPLATE/bug_report.yaml @@ -17,7 +17,7 @@ body: What version of NetBox are you currently running? (If you don't have access to the most recent NetBox release, consider testing on our [demo instance](https://demo.netbox.dev/) before opening a bug report to see if your issue has already been addressed.) - placeholder: v3.0.2 + placeholder: v3.0.3 validations: required: true - type: dropdown diff --git a/.github/ISSUE_TEMPLATE/feature_request.yaml b/.github/ISSUE_TEMPLATE/feature_request.yaml index 61a36fb0c..522c328fa 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yaml +++ b/.github/ISSUE_TEMPLATE/feature_request.yaml @@ -14,7 +14,7 @@ body: attributes: label: NetBox version description: What version of NetBox are you currently running? - placeholder: v3.0.2 + placeholder: v3.0.3 validations: required: true - type: dropdown diff --git a/docs/release-notes/version-3.0.md b/docs/release-notes/version-3.0.md index 97f2fd4b0..28ab7f2b4 100644 --- a/docs/release-notes/version-3.0.md +++ b/docs/release-notes/version-3.0.md @@ -1,6 +1,6 @@ # NetBox v3.0 -## v3.0.3 (FUTURE) +## v3.0.3 (2021-09-20) ### Enhancements diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py index 06e3e0ab7..a720eb484 100644 --- a/netbox/netbox/settings.py +++ b/netbox/netbox/settings.py @@ -16,7 +16,7 @@ from django.core.validators import URLValidator # Environment setup # -VERSION = '3.0.3-dev' +VERSION = '3.0.3' # Hostname HOSTNAME = platform.node() From d2dcc51430a0dea8c4cdd6c097b41caf92e0b347 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Mon, 20 Sep 2021 12:55:04 -0400 Subject: [PATCH 43/43] Changelog cleanup --- docs/release-notes/version-3.0.md | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/release-notes/version-3.0.md b/docs/release-notes/version-3.0.md index 28ab7f2b4..522a026e3 100644 --- a/docs/release-notes/version-3.0.md +++ b/docs/release-notes/version-3.0.md @@ -16,7 +16,6 @@ ### Bug Fixes -* [#5775](https://github.com/netbox-community/netbox/issues/5775) - Enable group synchronization for remote authentication backend * [#7167](https://github.com/netbox-community/netbox/issues/7167) - Ensure consistent font size when using monospace formatting * [#7226](https://github.com/netbox-community/netbox/issues/7226) - Exempt GraphQL API requests from CSRF inspection * [#7228](https://github.com/netbox-community/netbox/issues/7228) - Improve temperature conversions under device status