diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml index be2aacff5..5e456d0df 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.5.1 + placeholder: v3.5.3 validations: required: true - type: dropdown diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 1f8fdebd4..e6a5e76c2 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -3,10 +3,13 @@ blank_issues_enabled: false contact_links: - name: 📖 Contributing Policy url: https://github.com/netbox-community/netbox/blob/develop/CONTRIBUTING.md - about: "Please read through our contributing policy before opening an issue or pull request" + about: "Please read through our contributing policy before opening an issue or pull request." - name: ❓ Discussion url: https://github.com/netbox-community/netbox/discussions - about: "If you're just looking for help, try starting a discussion instead" + about: "If you're just looking for help, try starting a discussion instead." + - name: 💡 Plugin Idea + url: https://plugin-ideas.netbox.dev + about: "Have an idea for a plugin? Head over to the ideas board!" - name: 💬 Community Slack - url: https://netdev.chat/ - about: "Join #netbox on the NetDev Community Slack for assistance with installation issues and other problems" + url: https://netdev.chat + about: "Join #netbox on the NetDev Community Slack for assistance with installation issues and other problems." diff --git a/.github/ISSUE_TEMPLATE/feature_request.yaml b/.github/ISSUE_TEMPLATE/feature_request.yaml index fcb3516b4..e317dd64c 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.5.1 + placeholder: v3.5.3 validations: required: true - type: dropdown diff --git a/README.md b/README.md index 480f0f856..6e2b34fb8 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,10 @@
The premiere source of truth powering network automation
+{{ context_data|pprint }}+
{{ context_data|pprint }}+
MAC Address | -{{ object.mac_address|placeholder }} | +{{ object.mac_address|placeholder }} |
---|---|---|
WWN | -{{ object.wwn|placeholder }} | +{{ object.wwn|placeholder }} |
VRF | diff --git a/netbox/templates/extras/dashboard/widgets/objectcounts.html b/netbox/templates/extras/dashboard/widgets/objectcounts.html index d0e604c9a..8b68dc166 100644 --- a/netbox/templates/extras/dashboard/widgets/objectcounts.html +++ b/netbox/templates/extras/dashboard/widgets/objectcounts.html @@ -1,10 +1,8 @@ -{% load helpers %} - {% if counts %}||
MAC Address | -{{ object.mac_address|placeholder }} | +{{ object.mac_address|placeholder }} |
802.1Q Mode | diff --git a/netbox/tenancy/views.py b/netbox/tenancy/views.py index 5f8a7e314..bbe901bde 100644 --- a/netbox/tenancy/views.py +++ b/netbox/tenancy/views.py @@ -15,21 +15,31 @@ from .models import * class ObjectContactsView(generic.ObjectChildrenView): - child_model = Contact - table = tables.ContactTable - filterset = filtersets.ContactFilterSet + child_model = ContactAssignment + table = tables.ContactAssignmentTable + filterset = filtersets.ContactAssignmentFilterSet template_name = 'tenancy/object_contacts.html' tab = ViewTab( label=_('Contacts'), badge=lambda obj: obj.contacts.count(), - permission='tenancy.view_contact', + permission='tenancy.view_contactassignment', weight=5000 ) def get_children(self, request, parent): - return Contact.objects.annotate( - assignment_count=count_related(ContactAssignment, 'contact') - ).restrict(request.user, 'view').filter(assignments__object_id=parent.pk) + return ContactAssignment.objects.restrict(request.user, 'view').filter( + content_type=ContentType.objects.get_for_model(parent), + object_id=parent.pk + ) + + def get_table(self, *args, **kwargs): + table = super().get_table(*args, **kwargs) + + # Hide object columns + table.columns.hide('content_type') + table.columns.hide('object') + + return table def get_extra_context(self, request, instance): return { diff --git a/netbox/users/signals.py b/netbox/users/signals.py index 8915af1dc..98036d5d1 100644 --- a/netbox/users/signals.py +++ b/netbox/users/signals.py @@ -1,10 +1,18 @@ import logging from django.dispatch import receiver from django.contrib.auth.signals import user_login_failed +from utilities.request import get_client_ip @receiver(user_login_failed) def log_user_login_failed(sender, credentials, request, **kwargs): logger = logging.getLogger('netbox.auth.login') username = credentials.get("username") - logger.info(f"Failed login attempt for username: {username}") + if client_ip := get_client_ip(request): + logger.info(f"Failed login attempt for username: {username} from {client_ip}") + else: + logger.warning( + "Client IP address could not be determined for validation. Check that the HTTP server is properly " + "configured to pass the required header(s)." + ) + logger.info(f"Failed login attempt for username: {username}") diff --git a/netbox/utilities/rqworker.py b/netbox/utilities/rqworker.py index 5866dfee0..61f594767 100644 --- a/netbox/utilities/rqworker.py +++ b/netbox/utilities/rqworker.py @@ -1,11 +1,12 @@ from django_rq.queues import get_connection -from rq import Worker +from rq import Retry, Worker from netbox.config import get_config from netbox.constants import RQ_QUEUE_DEFAULT __all__ = ( 'get_queue_for_model', + 'get_rq_retry', 'get_workers_for_queue', ) @@ -22,3 +23,14 @@ def get_workers_for_queue(queue_name): Returns True if a worker process is currently servicing the specified queue. """ return Worker.count(get_connection(queue_name)) + + +def get_rq_retry(): + """ + If RQ_RETRY_MAX is defined and greater than zero, instantiate and return a Retry object to be + used when queuing a job. Otherwise, return None. + """ + retry_max = get_config().RQ_RETRY_MAX + retry_interval = get_config().RQ_RETRY_INTERVAL + if retry_max: + return Retry(max=retry_max, interval=retry_interval) diff --git a/netbox/utilities/templates/builtins/htmx_table.html b/netbox/utilities/templates/builtins/htmx_table.html new file mode 100644 index 000000000..7e871931c --- /dev/null +++ b/netbox/utilities/templates/builtins/htmx_table.html @@ -0,0 +1,4 @@ + diff --git a/netbox/utilities/templatetags/builtins/tags.py b/netbox/utilities/templatetags/builtins/tags.py index cdc517b97..dc86586e7 100644 --- a/netbox/utilities/templatetags/builtins/tags.py +++ b/netbox/utilities/templatetags/builtins/tags.py @@ -1,4 +1,5 @@ from django import template +from django.http import QueryDict __all__ = ( 'badge', @@ -74,3 +75,22 @@ def checkmark(value, show_false=True, true='Yes', false='No'): 'true_label': true, 'false_label': false, } + + +@register.inclusion_tag('builtins/htmx_table.html', takes_context=True) +def htmx_table(context, viewname, return_url=None, **kwargs): + """ + Embed an object list table retrieved using HTMX. Any extra keyword arguments are passed as URL query parameters. + + Args: + context: The current request context + viewname: The name of the view to use for the HTMX request (e.g. `dcim:site_list`) + return_url: The URL to pass as the `return_url`. If not provided, the current request's path will be used. + """ + url_params = QueryDict(mutable=True) + url_params.update(kwargs) + url_params['return_url'] = return_url or context['request'].path + return { + 'viewname': viewname, + 'url_params': url_params, + } diff --git a/netbox/utilities/utils.py b/netbox/utilities/utils.py index b1504e62f..4b4a2631a 100644 --- a/netbox/utilities/utils.py +++ b/netbox/utilities/utils.py @@ -302,7 +302,7 @@ def to_meters(length, unit): if unit == CableLengthUnitChoices.UNIT_FOOT: return length * Decimal(0.3048) if unit == CableLengthUnitChoices.UNIT_INCH: - return length * Decimal(0.3048) * 12 + return length * Decimal(0.0254) raise ValueError(f"Unknown unit {unit}. Must be 'km', 'm', 'cm', 'mi', 'ft', or 'in'.") diff --git a/netbox/virtualization/forms/bulk_import.py b/netbox/virtualization/forms/bulk_import.py index a229bd935..15651f2ae 100644 --- a/netbox/virtualization/forms/bulk_import.py +++ b/netbox/virtualization/forms/bulk_import.py @@ -65,7 +65,7 @@ class ClusterImportForm(NetBoxModelImportForm): class Meta: model = Cluster - fields = ('name', 'type', 'group', 'status', 'site', 'description', 'comments', 'tags') + fields = ('name', 'type', 'group', 'status', 'site', 'tenant', 'description', 'comments', 'tags') class VirtualMachineImportForm(NetBoxModelImportForm): diff --git a/requirements.txt b/requirements.txt index c3d9c8c38..ee6a79635 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,8 +1,8 @@ bleach==6.0.0 -boto3==1.26.127 +boto3==1.26.145 Django==4.1.9 -django-cors-headers==3.14.0 -django-debug-toolbar==4.0.0 +django-cors-headers==4.0.0 +django-debug-toolbar==4.1.0 django-filter==23.2 django-graphiql-debug-toolbar==0.2.0 django-mptt==0.14 @@ -10,26 +10,26 @@ django-pglocks==1.0.4 django-prometheus==2.3.1 django-redis==5.2.0 django-rich==1.5.0 -django-rq==2.8.0 +django-rq==2.8.1 django-tables2==2.5.3 django-taggit==4.0.0 django-timezone-field==5.0 djangorestframework==3.14.0 drf-spectacular==0.26.2 -drf-spectacular-sidecar==2023.5.1 +drf-spectacular-sidecar==2023.6.1 dulwich==0.21.5 feedparser==6.0.10 graphene-django==3.0.0 gunicorn==20.1.0 Jinja2==3.1.2 Markdown==3.3.7 -mkdocs-material==9.1.9 -mkdocstrings[python-legacy]==0.21.2 +mkdocs-material==9.1.15 +mkdocstrings[python-legacy]==0.22.0 netaddr==0.8.0 Pillow==9.5.0 psycopg2-binary==2.9.6 PyYAML==6.0 -sentry-sdk==1.22.1 +sentry-sdk==1.25.0 social-auth-app-django==5.2.0 social-auth-core[openidconnect]==4.4.2 svgwrite==1.4.3