diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml index 4de82d4e336..3beec4cf7a9 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yaml +++ b/.github/ISSUE_TEMPLATE/bug_report.yaml @@ -14,7 +14,7 @@ body: attributes: label: NetBox version description: What version of NetBox are you currently running? - placeholder: v3.3.7 + placeholder: v3.3.8 validations: required: true - type: dropdown diff --git a/.github/ISSUE_TEMPLATE/feature_request.yaml b/.github/ISSUE_TEMPLATE/feature_request.yaml index 5f0a17aa7c3..6688de9fef0 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.3.7 + placeholder: v3.3.8 validations: required: true - type: dropdown diff --git a/docs/installation/3-netbox.md b/docs/installation/3-netbox.md index f42e28debfc..72c054e5b7b 100644 --- a/docs/installation/3-netbox.md +++ b/docs/installation/3-netbox.md @@ -36,7 +36,7 @@ This documentation provides two options for installing NetBox: from a downloadab Download the [latest stable release](https://github.com/netbox-community/netbox/releases) from GitHub as a tarball or ZIP archive and extract it to your desired path. In this example, we'll use `/opt/netbox` as the NetBox root. ```no-highlight -sudo wget https://github.com/netbox-community/netbox/archive/vX.Y.Z.tar.gz +sudo wget https://github.com/netbox-community/netbox/archive/refs/tags/vX.Y.Z.tar.gz sudo tar -xzf vX.Y.Z.tar.gz -C /opt sudo ln -s /opt/netbox-X.Y.Z/ /opt/netbox ``` diff --git a/docs/release-notes/version-3.3.md b/docs/release-notes/version-3.3.md index fe02827d6a1..4894690defb 100644 --- a/docs/release-notes/version-3.3.md +++ b/docs/release-notes/version-3.3.md @@ -1,5 +1,31 @@ # NetBox v3.3 +## v3.3.8 (2022-11-16) + +### Enhancements + +* [#10356](https://github.com/netbox-community/netbox/issues/10356) - Add backplane Ethernet interface types +* [#10902](https://github.com/netbox-community/netbox/issues/10902) - Add location selector to power feed form +* [#10904](https://github.com/netbox-community/netbox/issues/10904) - Use front/rear port colors in cable trace SVG +* [#10914](https://github.com/netbox-community/netbox/issues/10914) - Include "add module type" button on manufacturer view +* [#10915](https://github.com/netbox-community/netbox/issues/10915) - Add count of L2VPNs to tenant view +* [#10919](https://github.com/netbox-community/netbox/issues/10919) - Include device location under cable view +* [#10920](https://github.com/netbox-community/netbox/issues/10920) - Include request cookies when queuing a custom script + +### Bug Fixes + +* [#9439](https://github.com/netbox-community/netbox/issues/9439) - Ensure thread safety of change logging functions +* [#10709](https://github.com/netbox-community/netbox/issues/10709) - Correct UI display for `azuread-v2-tenant-oauth2` SSO backend +* [#10829](https://github.com/netbox-community/netbox/issues/10829) - Fix bulk edit/delete buttons ad top of object lists +* [#10837](https://github.com/netbox-community/netbox/issues/10837) - Correct cookie paths when `BASE_PATH` is set +* [#10874](https://github.com/netbox-community/netbox/issues/10874) - Remove erroneous link for contact assignment count +* [#10881](https://github.com/netbox-community/netbox/issues/10881) - Fix dark mode coloring for data on device status page +* [#10891](https://github.com/netbox-community/netbox/issues/10891) - Populate tag selection list for service filter form +* [#10897](https://github.com/netbox-community/netbox/issues/10897) - Fix form widget styling on FHRP group form +* [#10910](https://github.com/netbox-community/netbox/issues/10910) - Fix cable creation links on power port view + +--- + ## v3.3.7 (2022-11-01) ### Bug Fixes diff --git a/netbox/dcim/choices.py b/netbox/dcim/choices.py index 7d35a40f9c6..14ddc9930e9 100644 --- a/netbox/dcim/choices.py +++ b/netbox/dcim/choices.py @@ -783,6 +783,17 @@ class InterfaceTypeChoices(ChoiceSet): TYPE_400GE_QSFP_DD = '400gbase-x-qsfpdd' TYPE_400GE_OSFP = '400gbase-x-osfp' + # Ethernet Backplane + TYPE_1GE_KX = '1000base-kx' + TYPE_10GE_KR = '10gbase-kr' + TYPE_10GE_KX4 = '10gbase-kx4' + TYPE_25GE_KR = '25gbase-kr' + TYPE_40GE_KR4 = '40gbase-kr4' + TYPE_50GE_KR = '50gbase-kr' + TYPE_100GE_KP4 = '100gbase-kp4' + TYPE_100GE_KR2 = '100gbase-kr2' + TYPE_100GE_KR4 = '100gbase-kr4' + # Wireless TYPE_80211A = 'ieee802.11a' TYPE_80211G = 'ieee802.11g' @@ -911,6 +922,20 @@ class InterfaceTypeChoices(ChoiceSet): (TYPE_400GE_OSFP, 'OSFP (400GE)'), ) ), + ( + 'Ethernet (backplane)', + ( + (TYPE_1GE_KX, '1000BASE-KX (1GE)'), + (TYPE_10GE_KR, '10GBASE-KR (10GE)'), + (TYPE_10GE_KX4, '10GBASE-KX4 (10GE)'), + (TYPE_25GE_KR, '25GBASE-KR (25GE)'), + (TYPE_40GE_KR4, '40GBASE-KR4 (40GE)'), + (TYPE_50GE_KR, '50GBASE-KR (50GE)'), + (TYPE_100GE_KP4, '100GBASE-KP4 (100GE)'), + (TYPE_100GE_KR2, '100GBASE-KR2 (100GE)'), + (TYPE_100GE_KR4, '100GBASE-KR4 (100GE)'), + ) + ), ( 'Wireless', ( diff --git a/netbox/dcim/forms/models.py b/netbox/dcim/forms/models.py index 8f1626361eb..da50b8f2ae0 100644 --- a/netbox/dcim/forms/models.py +++ b/netbox/dcim/forms/models.py @@ -877,10 +877,21 @@ class PowerFeedForm(NetBoxModelForm): 'site_id': '$site' } ) + location = DynamicModelChoiceField( + queryset=Location.objects.all(), + required=False, + query_params={ + 'site_id': '$site' + }, + initial_params={ + 'racks': '$rack' + } + ) rack = DynamicModelChoiceField( queryset=Rack.objects.all(), required=False, query_params={ + 'location_id': '$location', 'site_id': '$site' } ) @@ -888,14 +899,14 @@ class PowerFeedForm(NetBoxModelForm): fieldsets = ( ('Power Panel', ('region', 'site', 'power_panel')), - ('Power Feed', ('rack', 'name', 'status', 'type', 'mark_connected', 'tags')), + ('Power Feed', ('location', 'rack', 'name', 'status', 'type', 'mark_connected', 'tags')), ('Characteristics', ('supply', 'voltage', 'amperage', 'phase', 'max_utilization')), ) class Meta: model = PowerFeed fields = [ - 'region', 'site_group', 'site', 'power_panel', 'rack', 'name', 'status', 'type', 'mark_connected', 'supply', + 'region', 'site_group', 'site', 'power_panel', 'location', 'rack', 'name', 'status', 'type', 'mark_connected', 'supply', 'phase', 'voltage', 'amperage', 'max_utilization', 'comments', 'tags', ] widgets = { diff --git a/netbox/dcim/svg/cables.py b/netbox/dcim/svg/cables.py index 9a847acc9b5..33adef798a0 100644 --- a/netbox/dcim/svg/cables.py +++ b/netbox/dcim/svg/cables.py @@ -166,7 +166,7 @@ def _get_color(cls, instance): """ if hasattr(instance, 'parent_object'): # Termination - return 'f0f0f0' + return getattr(instance, 'color', 'f0f0f0') or 'f0f0f0' if hasattr(instance, 'device_role'): # Device return instance.device_role.color diff --git a/netbox/extras/context_managers.py b/netbox/extras/context_managers.py index 9f73fe9c39e..32323999efe 100644 --- a/netbox/extras/context_managers.py +++ b/netbox/extras/context_managers.py @@ -1,10 +1,6 @@ from contextlib import contextmanager -from django.db.models.signals import m2m_changed, pre_delete, post_save - -from extras.signals import clear_webhooks, clear_webhook_queue, handle_changed_object, handle_deleted_object -from netbox import thread_locals -from netbox.request_context import set_request +from netbox.context import current_request, webhooks_queue from .webhooks import flush_webhooks @@ -16,27 +12,14 @@ def change_logging(request): :param request: WSGIRequest object with a unique `id` set """ - set_request(request) - thread_locals.webhook_queue = [] - - # Connect our receivers to the post_save and post_delete signals. - post_save.connect(handle_changed_object, dispatch_uid='handle_changed_object') - m2m_changed.connect(handle_changed_object, dispatch_uid='handle_changed_object') - pre_delete.connect(handle_deleted_object, dispatch_uid='handle_deleted_object') - clear_webhooks.connect(clear_webhook_queue, dispatch_uid='clear_webhook_queue') + current_request.set(request) + webhooks_queue.set([]) yield - # Disconnect change logging signals. This is necessary to avoid recording any errant - # changes during test cleanup. - post_save.disconnect(handle_changed_object, dispatch_uid='handle_changed_object') - m2m_changed.disconnect(handle_changed_object, dispatch_uid='handle_changed_object') - pre_delete.disconnect(handle_deleted_object, dispatch_uid='handle_deleted_object') - clear_webhooks.disconnect(clear_webhook_queue, dispatch_uid='clear_webhook_queue') - # Flush queued webhooks to RQ - flush_webhooks(thread_locals.webhook_queue) - del thread_locals.webhook_queue + flush_webhooks(webhooks_queue.get()) - # Clear the request from thread-local storage - set_request(None) + # Clear context vars + current_request.set(None) + webhooks_queue.set([]) diff --git a/netbox/extras/signals.py b/netbox/extras/signals.py index aff350cc4f3..31e0c126cf7 100644 --- a/netbox/extras/signals.py +++ b/netbox/extras/signals.py @@ -7,14 +7,14 @@ from django_prometheus.models import model_deletes, model_inserts, model_updates from extras.validators import CustomValidator -from netbox import thread_locals from netbox.config import get_config -from netbox.request_context import get_request +from netbox.context import current_request, webhooks_queue from netbox.signals import post_clean from .choices import ObjectChangeActionChoices from .models import ConfigRevision, CustomField, ObjectChange from .webhooks import enqueue_object, get_snapshots, serialize_for_webhook + # # Change logging/webhooks # @@ -23,22 +23,32 @@ clear_webhooks = Signal() +def is_same_object(instance, webhook_data, request_id): + """ + Compare the given instance to the most recent queued webhook object, returning True + if they match. This check is used to avoid creating duplicate webhook entries. + """ + return ( + ContentType.objects.get_for_model(instance) == webhook_data['content_type'] and + instance.pk == webhook_data['object_id'] and + request_id == webhook_data['request_id'] + ) + + +@receiver((post_save, m2m_changed)) def handle_changed_object(sender, instance, **kwargs): """ Fires when an object is created or updated. """ + m2m_changed = False + if not hasattr(instance, 'to_objectchange'): return - request = get_request() - m2m_changed = False - - def is_same_object(instance, webhook_data): - return ( - ContentType.objects.get_for_model(instance) == webhook_data['content_type'] and - instance.pk == webhook_data['object_id'] and - request.id == webhook_data['request_id'] - ) + # Get the current request, or bail if not set + request = current_request.get() + if request is None: + return # Determine the type of change being made if kwargs.get('created'): @@ -69,13 +79,14 @@ def is_same_object(instance, webhook_data): objectchange.save() # If this is an M2M change, update the previously queued webhook (from post_save) - webhook_queue = thread_locals.webhook_queue - if m2m_changed and webhook_queue and is_same_object(instance, webhook_queue[-1]): + queue = webhooks_queue.get() + if m2m_changed and queue and is_same_object(instance, queue[-1], request.id): instance.refresh_from_db() # Ensure that we're working with fresh M2M assignments - webhook_queue[-1]['data'] = serialize_for_webhook(instance) - webhook_queue[-1]['snapshots']['postchange'] = get_snapshots(instance, action)['postchange'] + queue[-1]['data'] = serialize_for_webhook(instance) + queue[-1]['snapshots']['postchange'] = get_snapshots(instance, action)['postchange'] else: - enqueue_object(webhook_queue, instance, request.user, request.id, action) + enqueue_object(queue, instance, request.user, request.id, action) + webhooks_queue.set(queue) # Increment metric counters if action == ObjectChangeActionChoices.ACTION_CREATE: @@ -84,6 +95,7 @@ def is_same_object(instance, webhook_data): model_updates.labels(instance._meta.model_name).inc() +@receiver(pre_delete) def handle_deleted_object(sender, instance, **kwargs): """ Fires when an object is deleted. @@ -91,7 +103,10 @@ def handle_deleted_object(sender, instance, **kwargs): if not hasattr(instance, 'to_objectchange'): return - request = get_request() + # Get the current request, or bail if not set + request = current_request.get() + if request is None: + return # Record an ObjectChange if applicable if hasattr(instance, 'to_objectchange'): @@ -101,22 +116,22 @@ def handle_deleted_object(sender, instance, **kwargs): objectchange.save() # Enqueue webhooks - webhook_queue = thread_locals.webhook_queue - enqueue_object(webhook_queue, instance, request.user, request.id, ObjectChangeActionChoices.ACTION_DELETE) + queue = webhooks_queue.get() + enqueue_object(queue, instance, request.user, request.id, ObjectChangeActionChoices.ACTION_DELETE) + webhooks_queue.set(queue) # Increment metric counters model_deletes.labels(instance._meta.model_name).inc() +@receiver(clear_webhooks) def clear_webhook_queue(sender, **kwargs): """ Delete any queued webhooks (e.g. because of an aborted bulk transaction) """ logger = logging.getLogger('webhooks') - webhook_queue = thread_locals.webhook_queue - - logger.info(f"Clearing {len(webhook_queue)} queued webhooks ({sender})") - webhook_queue.clear() + logger.info(f"Clearing {len(webhooks_queue.get())} queued webhooks ({sender})") + webhooks_queue.set([]) # diff --git a/netbox/ipam/forms/filtersets.py b/netbox/ipam/forms/filtersets.py index ecf63b49f36..9566e69a9a5 100644 --- a/netbox/ipam/forms/filtersets.py +++ b/netbox/ipam/forms/filtersets.py @@ -478,6 +478,7 @@ class ServiceTemplateFilterForm(NetBoxModelFilterSetForm): class ServiceFilterForm(ServiceTemplateFilterForm): model = Service + tag = TagFilterField(model) class L2VPNFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): diff --git a/netbox/ipam/forms/models.py b/netbox/ipam/forms/models.py index 1986b159086..95723f80d93 100644 --- a/netbox/ipam/forms/models.py +++ b/netbox/ipam/forms/models.py @@ -549,6 +549,11 @@ class Meta: fields = ( 'protocol', 'group_id', 'auth_type', 'auth_key', 'description', 'ip_vrf', 'ip_address', 'ip_status', 'tags', ) + widgets = { + 'protocol': StaticSelect(), + 'auth_type': StaticSelect(), + 'ip_status': StaticSelect(), + } def save(self, *args, **kwargs): instance = super().save(*args, **kwargs) diff --git a/netbox/netbox/__init__.py b/netbox/netbox/__init__.py index 5cf43102523..e69de29bb2d 100644 --- a/netbox/netbox/__init__.py +++ b/netbox/netbox/__init__.py @@ -1,3 +0,0 @@ -import threading - -thread_locals = threading.local() diff --git a/netbox/netbox/authentication.py b/netbox/netbox/authentication.py index a7e56e2799e..89d71b8152a 100644 --- a/netbox/netbox/authentication.py +++ b/netbox/netbox/authentication.py @@ -24,6 +24,7 @@ 'azuread-oauth2': ('Microsoft Azure AD', 'microsoft'), 'azuread-b2c-oauth2': ('Microsoft Azure AD', 'microsoft'), 'azuread-tenant-oauth2': ('Microsoft Azure AD', 'microsoft'), + 'azuread-v2-tenant-oauth2': ('Microsoft Azure AD', 'microsoft'), 'bitbucket': ('BitBucket', 'bitbucket'), 'bitbucket-oauth2': ('BitBucket', 'bitbucket'), 'digitalocean': ('DigitalOcean', 'digital-ocean'), diff --git a/netbox/netbox/context.py b/netbox/netbox/context.py new file mode 100644 index 00000000000..b5e4dc28ed4 --- /dev/null +++ b/netbox/netbox/context.py @@ -0,0 +1,10 @@ +from contextvars import ContextVar + +__all__ = ( + 'current_request', + 'webhooks_queue', +) + + +current_request = ContextVar('current_request', default=None) +webhooks_queue = ContextVar('webhooks_queue') diff --git a/netbox/netbox/request_context.py b/netbox/netbox/request_context.py deleted file mode 100644 index 41e8283e8c5..00000000000 --- a/netbox/netbox/request_context.py +++ /dev/null @@ -1,9 +0,0 @@ -from netbox import thread_locals - - -def set_request(request): - thread_locals.request = request - - -def get_request(): - return getattr(thread_locals, 'request', None) diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py index 5241737223a..46663f08c4f 100644 --- a/netbox/netbox/settings.py +++ b/netbox/netbox/settings.py @@ -29,7 +29,7 @@ # Environment setup # -VERSION = '3.3.7' +VERSION = '3.3.8' # Hostname HOSTNAME = platform.node() @@ -81,11 +81,11 @@ BASE_PATH = getattr(configuration, 'BASE_PATH', '') if BASE_PATH: BASE_PATH = BASE_PATH.strip('/') + '/' # Enforce trailing slash only +CSRF_COOKIE_PATH = LANGUAGE_COOKIE_PATH = SESSION_COOKIE_PATH = f'/{BASE_PATH.rstrip("/")}' CORS_ORIGIN_ALLOW_ALL = getattr(configuration, 'CORS_ORIGIN_ALLOW_ALL', False) CORS_ORIGIN_REGEX_WHITELIST = getattr(configuration, 'CORS_ORIGIN_REGEX_WHITELIST', []) CORS_ORIGIN_WHITELIST = getattr(configuration, 'CORS_ORIGIN_WHITELIST', []) CSRF_COOKIE_NAME = getattr(configuration, 'CSRF_COOKIE_NAME', 'csrftoken') -CSRF_COOKIE_PATH = BASE_PATH or '/' CSRF_TRUSTED_ORIGINS = getattr(configuration, 'CSRF_TRUSTED_ORIGINS', []) DATE_FORMAT = getattr(configuration, 'DATE_FORMAT', 'N j, Y') DATETIME_FORMAT = getattr(configuration, 'DATETIME_FORMAT', 'N j, Y g:i a') @@ -130,8 +130,6 @@ SENTRY_TAGS = getattr(configuration, 'SENTRY_TAGS', {}) SESSION_FILE_PATH = getattr(configuration, 'SESSION_FILE_PATH', None) SESSION_COOKIE_NAME = getattr(configuration, 'SESSION_COOKIE_NAME', 'sessionid') -SESSION_COOKIE_PATH = BASE_PATH or '/' -LANGUAGE_COOKIE_PATH = BASE_PATH or '/' SHORT_DATE_FORMAT = getattr(configuration, 'SHORT_DATE_FORMAT', 'Y-m-d') SHORT_DATETIME_FORMAT = getattr(configuration, 'SHORT_DATETIME_FORMAT', 'Y-m-d H:i') SHORT_TIME_FORMAT = getattr(configuration, 'SHORT_TIME_FORMAT', 'H:i:s') @@ -407,6 +405,7 @@ def _setting(name, default=None): STATICFILES_DIRS = ( os.path.join(BASE_DIR, 'project-static', 'dist'), os.path.join(BASE_DIR, 'project-static', 'img'), + os.path.join(BASE_DIR, 'project-static', 'js'), ('docs', os.path.join(BASE_DIR, 'project-static', 'docs')), # Prefix with /docs ) diff --git a/netbox/project-static/js/setmode.js b/netbox/project-static/js/setmode.js new file mode 100644 index 00000000000..8441a542f68 --- /dev/null +++ b/netbox/project-static/js/setmode.js @@ -0,0 +1,72 @@ +/** + * Set the color mode on the `
` element and in local storage. + * + * @param mode {"dark" | "light"} NetBox Color Mode. + * @param inferred {boolean} Value is inferred from browser/system preference. + */ +function setMode(mode, inferred) { + document.documentElement.setAttribute("data-netbox-color-mode", mode); + localStorage.setItem("netbox-color-mode", mode); + localStorage.setItem("netbox-color-mode-inferred", inferred); +} +/** + * Determine the best initial color mode to use prior to rendering. + */ +function initMode() { + try { + // Browser prefers dark color scheme. + var preferDark = window.matchMedia("(prefers-color-scheme: dark)").matches; + // Browser prefers light color scheme. + var preferLight = window.matchMedia("(prefers-color-scheme: light)").matches; + // Client NetBox color-mode override. + var clientMode = localStorage.getItem("netbox-color-mode"); + // NetBox server-rendered value. + var serverMode = document.documentElement.getAttribute("data-netbox-color-mode"); + // Color mode is inferred from browser/system preference and not deterministically set by + // the client or server. + var inferred = JSON.parse(localStorage.getItem("netbox-color-mode-inferred")); + + if (inferred === true && (serverMode === "light" || serverMode === "dark")) { + // The color mode was previously inferred from browser/system preference, but + // the server now has a value, so we should use the server's value. + return setMode(serverMode, false); + } + if (clientMode === null && (serverMode === "light" || serverMode === "dark")) { + // If the client mode is not set but the server mode is, use the server mode. + return setMode(serverMode, false); + } + if (clientMode !== null && serverMode === "unset") { + // The color mode has been set, deterministically or otherwise, and the server + // has no preference or has not been set. Use the client mode, but allow it to + /// be overridden by the server if/when a server value exists. + return setMode(clientMode, true); + } + if ( + clientMode !== null && + (serverMode === "light" || serverMode === "dark") && + clientMode !== serverMode + ) { + // If the client mode is set and is different than the server mode (which is also set), + // use the client mode over the server mode, as it should be more recent. + return setMode(clientMode, false); + } + if (clientMode === serverMode) { + // If the client and server modes match, use that value. + return setMode(clientMode, false); + } + if (preferDark && serverMode === "unset") { + // If the server mode is not set but the browser prefers dark mode, use dark mode, but + // allow it to be overridden by an explicit preference. + return setMode("dark", true); + } + if (preferLight && serverMode === "unset") { + // If the server mode is not set but the browser prefers light mode, use light mode, + // but allow it to be overridden by an explicit preference. + return setMode("light", true); + } + } catch (error) { + // In the event of an error, log it to the console and set the mode to light mode. + console.error(error); + } + return setMode("light", true); +}; diff --git a/netbox/templates/base/base.html b/netbox/templates/base/base.html index 8ba47dde36f..138a92b6d56 100644 --- a/netbox/templates/base/base.html +++ b/netbox/templates/base/base.html @@ -26,78 +26,15 @@ {# Page title #}CPU | |
---|---|
Memory | |
Temperature | |
Fans | |
Power | |
Site | {{ terminations.0.device.site|linkify }} |
Location | +{{ terminations.0.device.location|linkify|placeholder }} | +
Rack | {{ terminations.0.device.rack|linkify|placeholder }} | diff --git a/netbox/templates/dcim/manufacturer.html b/netbox/templates/dcim/manufacturer.html index 43d16afcb81..ad975f2eaba 100644 --- a/netbox/templates/dcim/manufacturer.html +++ b/netbox/templates/dcim/manufacturer.html @@ -4,10 +4,24 @@ {% load render_table from django_tables2 %} {% block extra_controls %} - {% if perms.dcim.add_devicetype %} - - Add Device Type - + {% if perms.dcim.add_devicetype or perms.dcim.add_moduletype %} +
VLANs
L2VPNs
+Circuits
diff --git a/netbox/tenancy/views.py b/netbox/tenancy/views.py index d8b810ad9ca..03b454321e7 100644 --- a/netbox/tenancy/views.py +++ b/netbox/tenancy/views.py @@ -4,7 +4,7 @@ from circuits.models import Circuit from dcim.models import Cable, Device, Location, Rack, RackReservation, Site -from ipam.models import Aggregate, IPAddress, IPRange, Prefix, VLAN, VRF, ASN +from ipam.models import Aggregate, IPAddress, IPRange, L2VPN, Prefix, VLAN, VRF, ASN from netbox.views import generic from utilities.utils import count_related from virtualization.models import VirtualMachine, Cluster @@ -111,6 +111,7 @@ def get_extra_context(self, request, instance): 'iprange_count': IPRange.objects.restrict(request.user, 'view').filter(tenant=instance).count(), 'ipaddress_count': IPAddress.objects.restrict(request.user, 'view').filter(tenant=instance).count(), 'vlan_count': VLAN.objects.restrict(request.user, 'view').filter(tenant=instance).count(), + 'l2vpn_count': L2VPN.objects.restrict(request.user, 'view').filter(tenant=instance).count(), 'circuit_count': Circuit.objects.restrict(request.user, 'view').filter(tenant=instance).count(), 'virtualmachine_count': VirtualMachine.objects.restrict(request.user, 'view').filter(tenant=instance).count(), 'cluster_count': Cluster.objects.restrict(request.user, 'view').filter(tenant=instance).count(), diff --git a/netbox/utilities/utils.py b/netbox/utilities/utils.py index 69ab615fcbb..ba3388c7575 100644 --- a/netbox/utilities/utils.py +++ b/netbox/utilities/utils.py @@ -410,6 +410,7 @@ def copy_safe_request(request): } return NetBoxFakeRequest({ 'META': meta, + 'COOKIES': request.COOKIES, 'POST': request.POST, 'GET': request.GET, 'FILES': request.FILES, diff --git a/requirements.txt b/requirements.txt index 73abfa25937..8e89b47c5af 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,7 +9,7 @@ django-pglocks==1.0.4 django-prometheus==2.2.0 django-redis==5.2.0 django-rich==1.4.0 -django-rq==2.5.1 +django-rq==2.6.0 django-tables2==2.4.1 django-taggit==3.0.0 django-timezone-field==5.0 @@ -19,13 +19,13 @@ graphene-django==2.15.0 gunicorn==20.1.0 Jinja2==3.1.2 Markdown==3.3.7 -mkdocs-material==8.5.7 +mkdocs-material==8.5.10 mkdocstrings[python-legacy]==0.19.0 netaddr==0.8.0 Pillow==9.3.0 psycopg2-binary==2.9.5 PyYAML==6.0 -sentry-sdk==1.10.1 +sentry-sdk==1.11.0 social-auth-app-django==5.0.0 social-auth-core[openidconnect]==4.3.0 svgwrite==1.4.3