From 347dc826d7875c0a6dbbaa9a9e98d8b5037cfceb 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 68ac7e6d09a8358dd8a0b2ae36b2d09c57559d7d 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 3dcd933d8c54b58adb544dfb3ed362cb3a1aec54 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 d3b8bb5ba3f0f2d516b9586890cc2aeba33461cc 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 1ecd60b72ee08468fff2eecff5f8f1982bfeb39f 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 1fd5ad47cb867eb9e52b4f6c3aeda7f17dc9019d 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 55b0529909d66ed56e7f2d41bfdfc38960bd973e 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 c452e401c3957489a2b74dac0d21ade6a123404a 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 a0f304e4cbbb840951b55566bfea91e06589e166 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 e854085dce151c79c124735d5b7cf3db89300997 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 0ee12cf2be9320080fa7a56e91d7ba92c5c47795 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 77f0bd141e0c6a7cf37dde46ee17085dd9b0cea2 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 520118981f178f750b368b9f440bd84afcbcfed8 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 02b2a3b2f8627e12277988f161a6c258bb6962de 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 3cd90c34d1a9247ffa8280d4ae336eab95d739ec 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 2d1dd7e6ab70f60f7644ad9cb034fe8ca22bf655 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 e74c9348225927ced775ef3aad58b6848470e6ad 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 afbc275ba69befe282202c334bd800b7bbae71e1 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 2197fafb4dc0469c56e54fdb8437816855a4df16 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 820a4b25039e939cff7bc7c685592a53cea1b94d 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 019a37b7316cd1c14a879c4f1b1b7161e8bb7817 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 02fe6ef3a5286402349d25e3c4bbc500be75181b 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 e745955c5475dae71348c6ef4cf882e5f0af9077 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 554916342199ee5e5598c7e86c75afa7e0a8927b 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 2e4045199b319b6db70844524cf70ecda7c59039 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 ccd250b431403998ea138009e14bcce314283bcf 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 847cd313dd73f444fa41b8a4b6af3aebf5022d6c 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 b08a4fe8602d29c88e175818841e3c67c5019e3c 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 f2331d09d932550a13b2e27b33ba37e56975c3f1 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 8879e0d9fbd76e71769d14cdf56ccf37fe55c7a4 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 de4b7df06eddb1bde1d9585324d866dcb799c14b 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 9d6d97413b772197888f48962403b98bb570e7c7 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 5dc2f40dbb85b827e801caf3edcd6f7360dd337a 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 fd491929d9ec95021cbda435b23a3cda378713f2 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 731f3012a51e4c02eed17ef1696fdb8fd225dd56 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 a2913b0b6bef672160000f0c4b25673808943ff2 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 0f6f4005a58f34de757fbb9310305c824661f270 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 81b5f8dca3b0b9d72393c495ad3f90a0cff76780 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