mirror of
https://github.com/netbox-community/netbox.git
synced 2025-08-18 05:28:16 -06:00
Merge branch 'develop' into 13983-csv-string-delimiter3
This commit is contained in:
commit
a533275649
13
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
13
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
@ -10,16 +10,25 @@ body:
|
|||||||
installation. If you're having trouble with installation or just looking for
|
installation. If you're having trouble with installation or just looking for
|
||||||
assistance with using NetBox, please visit our
|
assistance with using NetBox, please visit our
|
||||||
[discussion forum](https://github.com/netbox-community/netbox/discussions) instead.
|
[discussion forum](https://github.com/netbox-community/netbox/discussions) instead.
|
||||||
|
- type: dropdown
|
||||||
|
attributes:
|
||||||
|
label: Deployment Type
|
||||||
|
description: How are you running NetBox?
|
||||||
|
options:
|
||||||
|
- Self-hosted
|
||||||
|
- NetBox Cloud
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
- type: input
|
- type: input
|
||||||
attributes:
|
attributes:
|
||||||
label: NetBox version
|
label: NetBox Version
|
||||||
description: What version of NetBox are you currently running?
|
description: What version of NetBox are you currently running?
|
||||||
placeholder: v3.6.6
|
placeholder: v3.6.6
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: dropdown
|
- type: dropdown
|
||||||
attributes:
|
attributes:
|
||||||
label: Python version
|
label: Python Version
|
||||||
description: What version of Python are you currently running?
|
description: What version of Python are you currently running?
|
||||||
options:
|
options:
|
||||||
- "3.8"
|
- "3.8"
|
||||||
|
@ -36,6 +36,8 @@ NetBox users are welcome to participate in either role, on stage or in the crowd
|
|||||||
|
|
||||||
## :bug: Reporting Bugs
|
## :bug: Reporting Bugs
|
||||||
|
|
||||||
|
:warning: Bug reports are used to call attention to some unintended or unexpected behavior in NetBox, such as when an error occurs or when the result of taking some action is inconsistent with the documentation. **Bug reports may not be used to suggest new functionality**; please see "feature requests" below if that is your goal.
|
||||||
|
|
||||||
* First, ensure that you're running the [latest stable version](https://github.com/netbox-community/netbox/releases) of NetBox. If you're running an older version, it's likely that the bug has already been fixed.
|
* First, ensure that you're running the [latest stable version](https://github.com/netbox-community/netbox/releases) of NetBox. If you're running an older version, it's likely that the bug has already been fixed.
|
||||||
|
|
||||||
* Next, search our [issues list](https://github.com/netbox-community/netbox/issues?q=is%3Aissue) to see if the bug you've found has already been reported. If you come across a bug report that seems to match, please click "add a reaction" in the top right corner of the issue and add a thumbs up (:thumbsup:). This will help draw more attention to it. Any comments you can add to provide additional information or context would also be much appreciated.
|
* Next, search our [issues list](https://github.com/netbox-community/netbox/issues?q=is%3Aissue) to see if the bug you've found has already been reported. If you come across a bug report that seems to match, please click "add a reaction" in the top right corner of the issue and add a thumbs up (:thumbsup:). This will help draw more attention to it. Any comments you can add to provide additional information or context would also be much appreciated.
|
||||||
|
@ -2,6 +2,19 @@
|
|||||||
|
|
||||||
## v3.6.7 (FUTURE)
|
## v3.6.7 (FUTURE)
|
||||||
|
|
||||||
|
### Enhancements
|
||||||
|
|
||||||
|
* [#14390](https://github.com/netbox-community/netbox/issues/14390) - Add `classes` parameter to `copy_content` template tag
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* [#14249](https://github.com/netbox-community/netbox/issues/14249) - Fix server error when authenticating via IP-restricted API tokens using IPv6
|
||||||
|
* [#14392](https://github.com/netbox-community/netbox/issues/14392) - Fix bulk operations for plugin models under admin UI
|
||||||
|
* [#14397](https://github.com/netbox-community/netbox/issues/14397) - Fix exception on non-JSON request to `/available-ips/` API endpoints
|
||||||
|
* [#14401](https://github.com/netbox-community/netbox/issues/14401) - Rack `starting_unit` cannot be zero
|
||||||
|
* [#14432](https://github.com/netbox-community/netbox/issues/14432) - Populate custom field default values for components when creating a device
|
||||||
|
* [#14448](https://github.com/netbox-community/netbox/issues/14448) - Fix exception when creating a power feed with rack and panel in different sites
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## v3.6.6 (2023-11-29)
|
## v3.6.6 (2023-11-29)
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import json
|
import json
|
||||||
|
import re
|
||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
@ -95,8 +96,8 @@ class CustomFieldChoiceSetForm(BootstrapMixin, forms.ModelForm):
|
|||||||
required=False,
|
required=False,
|
||||||
help_text=mark_safe(_(
|
help_text=mark_safe(_(
|
||||||
'Enter one choice per line. An optional label may be specified for each choice by appending it with a '
|
'Enter one choice per line. An optional label may be specified for each choice by appending it with a '
|
||||||
'comma. Example:'
|
'colon. Example:'
|
||||||
) + ' <code>choice1,First Choice</code>')
|
) + ' <code>choice1:First Choice</code>')
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@ -107,7 +108,7 @@ class CustomFieldChoiceSetForm(BootstrapMixin, forms.ModelForm):
|
|||||||
data = []
|
data = []
|
||||||
for line in self.cleaned_data['extra_choices'].splitlines():
|
for line in self.cleaned_data['extra_choices'].splitlines():
|
||||||
try:
|
try:
|
||||||
value, label = line.split(',', maxsplit=1)
|
value, label = re.split(r'(?<!\\):', line, maxsplit=1)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
value, label = line, line
|
value, label = line, line
|
||||||
data.append((value, label))
|
data.append((value, label))
|
||||||
|
@ -102,7 +102,7 @@ class CustomFieldChoiceSetTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
|||||||
|
|
||||||
cls.form_data = {
|
cls.form_data = {
|
||||||
'name': 'Choice Set X',
|
'name': 'Choice Set X',
|
||||||
'extra_choices': '\n'.join(['X1,Choice 1', 'X2,Choice 2', 'X3,Choice 3'])
|
'extra_choices': '\n'.join(['X1:Choice 1', 'X2:Choice 2', 'X3:Choice 3'])
|
||||||
}
|
}
|
||||||
|
|
||||||
cls.csv_data = (
|
cls.csv_data = (
|
||||||
@ -125,6 +125,13 @@ class CustomFieldChoiceSetTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
|||||||
'description': 'New description',
|
'description': 'New description',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# This is here as extra_choices field splits on colon, but is returned
|
||||||
|
# from DB as comma separated.
|
||||||
|
def assertInstanceEqual(self, instance, data, exclude=None, api=False):
|
||||||
|
if 'extra_choices' in data:
|
||||||
|
data['extra_choices'] = data['extra_choices'].replace(':', ',')
|
||||||
|
return super().assertInstanceEqual(instance, data, exclude, api)
|
||||||
|
|
||||||
|
|
||||||
class CustomLinkTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
class CustomLinkTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||||
model = CustomLink
|
model = CustomLink
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
from copy import deepcopy
|
||||||
|
|
||||||
from django.core.exceptions import ObjectDoesNotExist, PermissionDenied
|
from django.core.exceptions import ObjectDoesNotExist, PermissionDenied
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
from django.shortcuts import get_object_or_404
|
from django.shortcuts import get_object_or_404
|
||||||
@ -290,7 +292,7 @@ class AvailableObjectsView(ObjectValidationMixin, APIView):
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Prepare object data for deserialization
|
# Prepare object data for deserialization
|
||||||
requested_objects = self.prep_object_data(requested_objects, available_objects, parent)
|
requested_objects = self.prep_object_data(deepcopy(requested_objects), available_objects, parent)
|
||||||
|
|
||||||
# Initialize the serializer with a list or a single object depending on what was requested
|
# Initialize the serializer with a list or a single object depending on what was requested
|
||||||
serializer_class = get_serializer_for_model(self.queryset.model)
|
serializer_class = get_serializer_for_model(self.queryset.model)
|
||||||
|
@ -818,7 +818,7 @@ class L2VPNTerminationForm(NetBoxModelForm):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = L2VPNTermination
|
model = L2VPNTermination
|
||||||
fields = ('l2vpn', )
|
fields = ('l2vpn', 'tags')
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
instance = kwargs.get('instance')
|
instance = kwargs.get('instance')
|
||||||
|
@ -73,12 +73,15 @@ class L2VPNTerminationTable(NetBoxTable):
|
|||||||
orderable=False,
|
orderable=False,
|
||||||
verbose_name=_('Object Site')
|
verbose_name=_('Object Site')
|
||||||
)
|
)
|
||||||
|
tags = columns.TagColumn(
|
||||||
|
url_name='ipam:l2vpntermination_list'
|
||||||
|
)
|
||||||
|
|
||||||
class Meta(NetBoxTable.Meta):
|
class Meta(NetBoxTable.Meta):
|
||||||
model = L2VPNTermination
|
model = L2VPNTermination
|
||||||
fields = (
|
fields = (
|
||||||
'pk', 'l2vpn', 'assigned_object_type', 'assigned_object', 'assigned_object_parent', 'assigned_object_site',
|
'pk', 'l2vpn', 'assigned_object_type', 'assigned_object', 'assigned_object_parent', 'assigned_object_site',
|
||||||
'actions',
|
'tags', 'actions',
|
||||||
)
|
)
|
||||||
default_columns = (
|
default_columns = (
|
||||||
'pk', 'l2vpn', 'assigned_object_type', 'assigned_object_parent', 'assigned_object', 'actions',
|
'pk', 'l2vpn', 'assigned_object_type', 'assigned_object_parent', 'assigned_object', 'actions',
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
{% comment %}
|
{% comment %}
|
||||||
Include a hidden field of the same name to ensure that unchecked checkboxes
|
Include a hidden field of the same name to ensure that unchecked checkboxes
|
||||||
are always included in the submitted form data.
|
are always included in the submitted form data. Omit fields names
|
||||||
|
_selected_action to avoid breaking the admin UI.
|
||||||
{% endcomment %}
|
{% endcomment %}
|
||||||
<input type="hidden" name="{{ widget.name }}" value="">
|
{% if widget.name != '_selected_action' %}<input type="hidden" name="{{ widget.name }}" value="">{% endif %}
|
||||||
{% include "django/forms/widgets/input.html" %}
|
{% include "django/forms/widgets/input.html" %}
|
||||||
|
@ -65,5 +65,5 @@ class ChoicesWidget(forms.Textarea):
|
|||||||
if not value:
|
if not value:
|
||||||
return None
|
return None
|
||||||
if type(value) is list:
|
if type(value) is list:
|
||||||
return '\n'.join([f'{k},{v}' for k, v in value])
|
return '\n'.join([f'{k}:{v}' for k, v in value])
|
||||||
return value
|
return value
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
from netaddr import IPAddress
|
from netaddr import AddrFormatError, IPAddress
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
'get_client_ip',
|
'get_client_ip',
|
||||||
@ -17,11 +18,18 @@ def get_client_ip(request, additional_headers=()):
|
|||||||
)
|
)
|
||||||
for header in HTTP_HEADERS:
|
for header in HTTP_HEADERS:
|
||||||
if header in request.META:
|
if header in request.META:
|
||||||
client_ip = request.META[header].split(',')[0].partition(':')[0]
|
ip = request.META[header].split(',')[0].strip()
|
||||||
try:
|
try:
|
||||||
return IPAddress(client_ip)
|
return IPAddress(ip)
|
||||||
except ValueError:
|
except AddrFormatError:
|
||||||
raise ValueError(f"Invalid IP address set for {header}: {client_ip}")
|
# Parse the string with urlparse() to remove port number or any other cruft
|
||||||
|
ip = urlparse(f'//{ip}').hostname
|
||||||
|
|
||||||
|
try:
|
||||||
|
return IPAddress(ip)
|
||||||
|
except AddrFormatError:
|
||||||
|
# We did our best
|
||||||
|
raise ValueError(f"Invalid IP address set for {header}: {ip}")
|
||||||
|
|
||||||
# Could not determine the client IP address from request headers
|
# Could not determine the client IP address from request headers
|
||||||
return None
|
return None
|
||||||
|
28
netbox/utilities/tests/test_request.py
Normal file
28
netbox/utilities/tests/test_request.py
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
from django.test import TestCase, RequestFactory
|
||||||
|
|
||||||
|
from netaddr import IPAddress
|
||||||
|
from utilities.request import get_client_ip
|
||||||
|
|
||||||
|
|
||||||
|
class GetClientIPTests(TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.factory = RequestFactory()
|
||||||
|
|
||||||
|
def test_ipv4_address(self):
|
||||||
|
request = self.factory.get('/', HTTP_X_FORWARDED_FOR='192.168.1.1')
|
||||||
|
self.assertEqual(get_client_ip(request), IPAddress('192.168.1.1'))
|
||||||
|
request = self.factory.get('/', HTTP_X_FORWARDED_FOR='192.168.1.1:8080')
|
||||||
|
self.assertEqual(get_client_ip(request), IPAddress('192.168.1.1'))
|
||||||
|
|
||||||
|
def test_ipv6_address(self):
|
||||||
|
request = self.factory.get('/', HTTP_X_FORWARDED_FOR='2001:db8::8a2e:370:7334')
|
||||||
|
self.assertEqual(get_client_ip(request), IPAddress('2001:db8::8a2e:370:7334'))
|
||||||
|
request = self.factory.get('/', HTTP_X_FORWARDED_FOR='[2001:db8::8a2e:370:7334]')
|
||||||
|
self.assertEqual(get_client_ip(request), IPAddress('2001:db8::8a2e:370:7334'))
|
||||||
|
request = self.factory.get('/', HTTP_X_FORWARDED_FOR='[2001:db8::8a2e:370:7334]:8080')
|
||||||
|
self.assertEqual(get_client_ip(request), IPAddress('2001:db8::8a2e:370:7334'))
|
||||||
|
|
||||||
|
def test_invalid_ip_address(self):
|
||||||
|
request = self.factory.get('/', HTTP_X_FORWARDED_FOR='invalid_ip')
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
get_client_ip(request)
|
Loading…
Reference in New Issue
Block a user