mirror of
https://github.com/netbox-community/netbox.git
synced 2024-05-10 07:54:54 +00:00
Merge branch 'develop' into feature
This commit is contained in:
2
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
2
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
@ -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
|
||||
|
2
.github/ISSUE_TEMPLATE/feature_request.yaml
vendored
2
.github/ISSUE_TEMPLATE/feature_request.yaml
vendored
@ -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
|
||||
|
@ -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
|
||||
```
|
||||
|
@ -1,6 +1,32 @@
|
||||
# NetBox v3.3
|
||||
|
||||
## v3.3.8 (FUTURE)
|
||||
## v3.3.9 (FUTURE)
|
||||
|
||||
---
|
||||
|
||||
## 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
|
||||
|
||||
---
|
||||
|
||||
|
@ -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',
|
||||
(
|
||||
|
@ -885,26 +885,38 @@ 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'
|
||||
}
|
||||
)
|
||||
comments = CommentField()
|
||||
|
||||
fieldsets = (
|
||||
('Power Panel', ('region', 'site', 'power_panel', 'description')),
|
||||
('Power Feed', ('rack', 'name', 'status', 'type', 'mark_connected', 'tags')),
|
||||
('Power Panel', ('region', 'site', 'power_panel')),
|
||||
('Power Feed', ('location', 'rack', 'name', 'status', 'type', 'description', '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',
|
||||
'phase', 'voltage', 'amperage', 'max_utilization', 'description', 'comments', 'tags',
|
||||
'region', 'site_group', 'site', 'power_panel', 'location', 'rack', 'name', 'status', 'type',
|
||||
'mark_connected', 'supply', 'phase', 'voltage', 'amperage', 'max_utilization', 'description', 'comments',
|
||||
'tags',
|
||||
]
|
||||
widgets = {
|
||||
'status': StaticSelect(),
|
||||
|
@ -166,7 +166,7 @@ class CableTraceSVG:
|
||||
"""
|
||||
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
|
||||
|
@ -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([])
|
||||
|
@ -7,14 +7,14 @@ from django.dispatch import receiver, Signal
|
||||
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 @@ from .webhooks import enqueue_object, get_snapshots, serialize_for_webhook
|
||||
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 handle_changed_object(sender, instance, **kwargs):
|
||||
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 handle_changed_object(sender, instance, **kwargs):
|
||||
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([])
|
||||
|
||||
|
||||
#
|
||||
|
@ -480,6 +480,7 @@ class ServiceTemplateFilterForm(NetBoxModelFilterSetForm):
|
||||
|
||||
class ServiceFilterForm(ServiceTemplateFilterForm):
|
||||
model = Service
|
||||
tag = TagFilterField(model)
|
||||
|
||||
|
||||
class L2VPNFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
|
||||
|
@ -558,6 +558,11 @@ class FHRPGroupForm(NetBoxModelForm):
|
||||
'protocol', 'group_id', 'auth_type', 'auth_key', 'name', 'ip_vrf', 'ip_address', 'ip_status', 'description',
|
||||
'comments', 'tags',
|
||||
)
|
||||
widgets = {
|
||||
'protocol': StaticSelect(),
|
||||
'auth_type': StaticSelect(),
|
||||
'ip_status': StaticSelect(),
|
||||
}
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
instance = super().save(*args, **kwargs)
|
||||
|
@ -1,3 +0,0 @@
|
||||
import threading
|
||||
|
||||
thread_locals = threading.local()
|
||||
|
@ -24,6 +24,7 @@ AUTH_BACKEND_ATTRS = {
|
||||
'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'),
|
||||
|
10
netbox/netbox/context.py
Normal file
10
netbox/netbox/context.py
Normal file
@ -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')
|
@ -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)
|
@ -76,11 +76,11 @@ AUTH_PASSWORD_VALIDATORS = getattr(configuration, 'AUTH_PASSWORD_VALIDATORS', []
|
||||
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')
|
||||
@ -126,8 +126,6 @@ SENTRY_TRACES_SAMPLE_RATE = getattr(configuration, 'SENTRY_TRACES_SAMPLE_RATE',
|
||||
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')
|
||||
@ -402,6 +400,7 @@ STATIC_URL = f'/{BASE_PATH}static/'
|
||||
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
|
||||
)
|
||||
|
||||
|
72
netbox/project-static/js/setmode.js
Normal file
72
netbox/project-static/js/setmode.js
Normal file
@ -0,0 +1,72 @@
|
||||
/**
|
||||
* Set the color mode on the `<html/>` 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);
|
||||
};
|
@ -26,78 +26,15 @@
|
||||
{# Page title #}
|
||||
<title>{% block title %}Home{% endblock %} | NetBox</title>
|
||||
|
||||
<script
|
||||
type="text/javascript"
|
||||
src="{% static 'setmode.js' %}"
|
||||
onerror="window.location='{% url 'media_failure' %}?filename=setmode.js'">
|
||||
</script>
|
||||
|
||||
<script type="text/javascript">
|
||||
/**
|
||||
* Set the color mode on the `<html/>` 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 () {
|
||||
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);
|
||||
initMode()
|
||||
})();
|
||||
window.CSRF_TOKEN = "{{ csrf_token }}";
|
||||
</script>
|
||||
|
@ -64,19 +64,19 @@
|
||||
<h5 class="card-header">Environment</h5>
|
||||
<div class="card-body">
|
||||
<table class="table">
|
||||
<tr id="status-cpu" class="bg-light">
|
||||
<tr id="status-cpu">
|
||||
<th colspan="2"><i class="mdi mdi-gauge"></i> CPU</th>
|
||||
</tr>
|
||||
<tr id="status-memory" class="bg-light">
|
||||
<tr id="status-memory">
|
||||
<th colspan="2"><i class="mdi mdi-chip"></i> Memory</th>
|
||||
</tr>
|
||||
<tr id="status-temperature" class="bg-light">
|
||||
<tr id="status-temperature">
|
||||
<th colspan="2"><i class="mdi mdi-thermometer"></i> Temperature</th>
|
||||
</tr>
|
||||
<tr id="status-fans" class="bg-light">
|
||||
<tr id="status-fans">
|
||||
<th colspan="2"><i class="mdi mdi-fan"></i> Fans</th>
|
||||
</tr>
|
||||
<tr id="status-power" class="bg-light">
|
||||
<tr id="status-power">
|
||||
<th colspan="2"><i class="mdi mdi-power"></i> Power</th>
|
||||
</tr>
|
||||
<tr class="napalm-table-placeholder d-none invisible">
|
||||
|
@ -7,6 +7,10 @@
|
||||
<td>Site</td>
|
||||
<td>{{ terminations.0.device.site|linkify }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Location</td>
|
||||
<td>{{ terminations.0.device.location|linkify|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Rack</td>
|
||||
<td>{{ terminations.0.device.rack|linkify|placeholder }}</td>
|
||||
|
@ -4,10 +4,24 @@
|
||||
{% load render_table from django_tables2 %}
|
||||
|
||||
{% block extra_controls %}
|
||||
{% if perms.dcim.add_devicetype %}
|
||||
<a href="{% url 'dcim:devicetype_add' %}?manufacturer={{ object.pk }}" class="btn btn-sm btn-primary">
|
||||
<span class="mdi mdi-plus-thick" aria-hidden="true"></span> Add Device Type
|
||||
</a>
|
||||
{% if perms.dcim.add_devicetype or perms.dcim.add_moduletype %}
|
||||
<div class="dropdown">
|
||||
<button id="add-components" type="button" class="btn btn-sm btn-primary dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
<i class="mdi mdi-plus-thick" aria-hidden="true"></i> Add
|
||||
</button>
|
||||
<ul class="dropdown-menu" aria-labeled-by="add-components">
|
||||
{% if perms.dcim.add_devicetype %}
|
||||
<li><a class="dropdown-item" href="{% url 'dcim:devicetype_add' %}?manufacturer={{ object.pk }}">
|
||||
Add Device Type
|
||||
</a></li>
|
||||
{% endif %}
|
||||
{% if perms.dcim.add_moduletype %}
|
||||
<li><a class="dropdown-item" href="{% url 'dcim:moduletype_add' %}?manufacturer={{ object.pk }}">
|
||||
Add Module Type
|
||||
</a></li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock extra_controls %}
|
||||
|
||||
|
@ -77,10 +77,10 @@
|
||||
</button>
|
||||
<ul class="dropdown-menu dropdown-menu-end">
|
||||
<li>
|
||||
<a href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.powerportport&a_terminations={{ object.pk }}&termination_b_type=dcim.poweroutlet&termination_b_site={{ object.device.site.pk }}&termination_b_rack={{ object.device.rack.pk }}&return_url={{ object.get_absolute_url }}" class="dropdown-item">Power Outlet</a>
|
||||
<a href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.powerport&a_terminations={{ object.pk }}&b_terminations_type=dcim.poweroutlet&termination_b_site={{ object.device.site.pk }}&termination_b_rack={{ object.device.rack.pk }}&return_url={{ object.get_absolute_url }}" class="dropdown-item">Power Outlet</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.powerportport&a_terminations={{ object.pk }}&termination_b_type=dcim.powerfeed&termination_b_site={{ object.device.site.pk }}&termination_b_rack={{ object.device.rack.pk }}&return_url={{ object.get_absolute_url }}" class="dropdown-item">Power Feed</a>
|
||||
<a href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.powerport&a_terminations={{ object.pk }}&b_terminations_type=dcim.powerfeed&termination_b_site={{ object.device.site.pk }}&termination_b_rack={{ object.device.rack.pk }}&return_url={{ object.get_absolute_url }}" class="dropdown-item">Power Feed</a>
|
||||
</li>
|
||||
</ul>
|
||||
</span>
|
||||
|
@ -70,64 +70,65 @@ Context:
|
||||
{% applied_filters model filter_form request.GET %}
|
||||
{% endif %}
|
||||
|
||||
{# "Select all" form #}
|
||||
{% if table.paginator.num_pages > 1 %}
|
||||
<div id="select-all-box" class="d-none card noprint">
|
||||
<form method="post" class="form col-md-12">
|
||||
{% csrf_token %}
|
||||
<div class="card-body">
|
||||
<div class="float-end">
|
||||
<form method="post" class="form form-horizontal">
|
||||
{% csrf_token %}
|
||||
{# "Select all" form #}
|
||||
{% if table.paginator.num_pages > 1 %}
|
||||
<div id="select-all-box" class="d-none card noprint">
|
||||
<div class="form col-md-12">
|
||||
<div class="card-body">
|
||||
<div class="float-end">
|
||||
{% if 'bulk_edit' in actions %}
|
||||
{% bulk_edit_button model query_params=request.GET %}
|
||||
{% endif %}
|
||||
{% if 'bulk_delete' in actions %}
|
||||
{% bulk_delete_button model query_params=request.GET %}
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input type="checkbox" id="select-all" name="_all" class="form-check-input" />
|
||||
<label for="select-all" class="form-check-label">
|
||||
Select <strong>all {{ table.rows|length }} {{ table.data.verbose_name_plural }}</strong> matching query
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# Object table controls #}
|
||||
{% include 'inc/table_controls_htmx.html' with table_modal="ObjectTable_config" %}
|
||||
|
||||
<div class="form form-horizontal">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="return_url" value="{% if return_url %}{{ return_url }}{% else %}{{ request.path }}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}{% endif %}" />
|
||||
|
||||
{# Object table #}
|
||||
|
||||
{% if prerequisite_model %}
|
||||
{% include 'inc/missing_prerequisites.html' %}
|
||||
{% endif %}
|
||||
|
||||
<div class="card">
|
||||
<div class="card-body" id="object_list">
|
||||
{% include 'htmx/table.html' %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# Form buttons #}
|
||||
<div class="noprint bulk-buttons">
|
||||
<div class="bulk-button-group">
|
||||
{% block bulk_buttons %}
|
||||
{% if 'bulk_edit' in actions %}
|
||||
{% bulk_edit_button model query_params=request.GET %}
|
||||
{% endif %}
|
||||
{% if 'bulk_delete' in actions %}
|
||||
{% bulk_delete_button model query_params=request.GET %}
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input type="checkbox" id="select-all" name="_all" class="form-check-input" />
|
||||
<label for="select-all" class="form-check-label">
|
||||
Select <strong>all {{ table.rows|length }} {{ table.data.verbose_name_plural }}</strong> matching query
|
||||
</label>
|
||||
</div>
|
||||
{% endblock %}
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# Object table controls #}
|
||||
{% include 'inc/table_controls_htmx.html' with table_modal="ObjectTable_config" %}
|
||||
|
||||
<form method="post" class="form form-horizontal">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="return_url" value="{% if return_url %}{{ return_url }}{% else %}{{ request.path }}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}{% endif %}" />
|
||||
|
||||
{# Object table #}
|
||||
|
||||
{% if prerequisite_model %}
|
||||
{% include 'inc/missing_prerequisites.html' %}
|
||||
{% endif %}
|
||||
|
||||
<div class="card">
|
||||
<div class="card-body" id="object_list">
|
||||
{% include 'htmx/table.html' %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# Form buttons #}
|
||||
<div class="noprint bulk-buttons">
|
||||
<div class="bulk-button-group">
|
||||
{% block bulk_buttons %}
|
||||
{% if 'bulk_edit' in actions %}
|
||||
{% bulk_edit_button model query_params=request.GET %}
|
||||
{% endif %}
|
||||
{% if 'bulk_delete' in actions %}
|
||||
{% bulk_delete_button model query_params=request.GET %}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
|
||||
</div>
|
||||
|
@ -25,7 +25,7 @@
|
||||
<tr>
|
||||
<th scope="row">Assignments</th>
|
||||
<td>
|
||||
<a href="{% url 'tenancy:contact_list' %}?role={{ object.slug }}">{{ assignment_count }}</a>
|
||||
{{ assignment_count }}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
@ -97,6 +97,12 @@
|
||||
<h2><a href="{% url 'ipam:vlan_list' %}?tenant_id={{ object.pk }}" class="stat-btn btn {% if stats.vlan_count %}btn-primary{% else %}btn-outline-dark{% endif %} btn-lg">{{ stats.vlan_count }}</a></h2>
|
||||
<p>VLANs</p>
|
||||
</div>
|
||||
|
||||
<div class="col col-md-4 text-center">
|
||||
<h2><a href="{% url 'ipam:l2vpn_list' %}?tenant_id={{ object.pk }}" class="stat-btn btn {% if stats.l2vpn_count %}btn-primary{% else %}btn-outline-dark{% endif %} btn-lg">{{ stats.l2vpn_count }}</a></h2>
|
||||
<p>L2VPNs</p>
|
||||
</div>
|
||||
|
||||
<div class="col col-md-4 text-center">
|
||||
<h2><a href="{% url 'circuits:circuit_list' %}?tenant_id={{ object.pk }}" class="stat-btn btn {% if stats.circuit_count %}btn-primary{% else %}btn-outline-dark{% endif %} btn-lg">{{ stats.circuit_count }}</a></h2>
|
||||
<p>Circuits</p>
|
||||
|
@ -3,7 +3,7 @@ from django.shortcuts import get_object_or_404
|
||||
|
||||
from circuits.models import Circuit
|
||||
from dcim.models import Cable, Device, Location, Rack, RackReservation, Site, VirtualDeviceContext
|
||||
from ipam.models import Aggregate, IPAddress, IPRange, Prefix, VLAN, VRF, ASN
|
||||
from ipam.models import Aggregate, ASN, IPAddress, IPRange, L2VPN, Prefix, VLAN, VRF
|
||||
from netbox.views import generic
|
||||
from utilities.utils import count_related
|
||||
from utilities.views import register_model_view
|
||||
@ -116,6 +116,7 @@ class TenantView(generic.ObjectView):
|
||||
'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(),
|
||||
|
@ -468,6 +468,7 @@ def copy_safe_request(request):
|
||||
}
|
||||
return NetBoxFakeRequest({
|
||||
'META': meta,
|
||||
'COOKIES': request.COOKIES,
|
||||
'POST': request.POST,
|
||||
'GET': request.GET,
|
||||
'FILES': request.FILES,
|
||||
|
@ -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==3.0.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
|
||||
|
Reference in New Issue
Block a user