Merge pull request #17658 from netbox-community/develop

Release v4.1.3
This commit is contained in:
Jeremy Stretch 2024-10-02 10:10:06 -04:00 committed by GitHub
commit 6ea0c0c3e9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 329 additions and 89 deletions

View File

@ -14,7 +14,7 @@ body:
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: v4.1.2 placeholder: v4.1.3
validations: validations:
required: true required: true
- type: dropdown - type: dropdown

View File

@ -26,7 +26,7 @@ body:
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: v4.1.2 placeholder: v4.1.3
validations: validations:
required: true required: true
- type: dropdown - type: dropdown

View File

@ -7,7 +7,11 @@
<a href="https://github.com/netbox-community/netbox/stargazers"><img src="https://img.shields.io/github/stars/netbox-community/netbox?style=flat" alt="GitHub stars" /></a> <a href="https://github.com/netbox-community/netbox/stargazers"><img src="https://img.shields.io/github/stars/netbox-community/netbox?style=flat" alt="GitHub stars" /></a>
<a href="https://explore.transifex.com/netbox-community/netbox/"><img src="https://img.shields.io/badge/languages-15-blue" alt="Languages supported" /></a> <a href="https://explore.transifex.com/netbox-community/netbox/"><img src="https://img.shields.io/badge/languages-15-blue" alt="Languages supported" /></a>
<a href="https://github.com/netbox-community/netbox/actions/workflows/ci.yml"><img src="https://github.com/netbox-community/netbox/workflows/CI/badge.svg?branch=master" alt="CI status" /></a> <a href="https://github.com/netbox-community/netbox/actions/workflows/ci.yml"><img src="https://github.com/netbox-community/netbox/workflows/CI/badge.svg?branch=master" alt="CI status" /></a>
<p></p> <p>
<strong><a href="https://github.com/netbox-community/netbox/">NetBox Community</a></strong> |
<strong><a href="https://netboxlabs.com/netbox-cloud/">NetBox Cloud</a></strong> |
<strong><a href="https://netboxlabs.com/netbox-enterprise/">NetBox Enterprise</a></strong>
</p>
</div> </div>
NetBox exists to empower network engineers. Since its release in 2016, it has become the go-to solution for modeling and documenting network infrastructure for thousands of organizations worldwide. As a successor to legacy IPAM and DCIM applications, NetBox provides a cohesive, extensive, and accessible data model for all things networked. By providing a single robust user interface and programmable APIs for everything from cable maps to device configurations, NetBox serves as the central source of truth for the modern network. NetBox exists to empower network engineers. Since its release in 2016, it has become the go-to solution for modeling and documenting network infrastructure for thousands of organizations worldwide. As a successor to legacy IPAM and DCIM applications, NetBox provides a cohesive, extensive, and accessible data model for all things networked. By providing a single robust user interface and programmable APIs for everything from cable maps to device configurations, NetBox serves as the central source of truth for the modern network.
@ -81,11 +85,6 @@ NetBox automatically logs the creation, modification, and deletion of all manage
* The [official documentation](https://docs.netbox.dev) offers a comprehensive introduction. * The [official documentation](https://docs.netbox.dev) offers a comprehensive introduction.
* Check out [our wiki](https://github.com/netbox-community/netbox/wiki/Community-Contributions) for even more projects to get the most out of NetBox! * Check out [our wiki](https://github.com/netbox-community/netbox/wiki/Community-Contributions) for even more projects to get the most out of NetBox!
<p align="center">
<a href="https://netboxlabs.com/netbox-cloud/"><img src="docs/media/misc/netbox_cloud.png" alt="NetBox Cloud" /></a><br />
Looking for a managed solution? Check out <strong><a href="https://netboxlabs.com/netbox-cloud/">NetBox Cloud</a></strong> or <strong><a href="https://netboxlabs.com/netbox-enterprise/">NetBox Enterprise</a></strong>!
</p>
## Get Involved ## Get Involved
* Follow [@NetBoxOfficial](https://twitter.com/NetBoxOfficial) on Twitter! * Follow [@NetBoxOfficial](https://twitter.com/NetBoxOfficial) on Twitter!

View File

@ -13,6 +13,9 @@ To enable remote data synchronization, the NetBox administrator first designates
!!! info !!! info
Data backends which connect to external sources typically require the installation of one or more supporting Python libraries. The Git backend requires the [`dulwich`](https://www.dulwich.io/) package, and the S3 backend requires the [`boto3`](https://boto3.amazonaws.com/v1/documentation/api/latest/index.html) package. These must be installed within NetBox's environment to enable these backends. Data backends which connect to external sources typically require the installation of one or more supporting Python libraries. The Git backend requires the [`dulwich`](https://www.dulwich.io/) package, and the S3 backend requires the [`boto3`](https://boto3.amazonaws.com/v1/documentation/api/latest/index.html) package. These must be installed within NetBox's environment to enable these backends.
!!! info
If you are configuring Git and have `HTTP_PROXIES` configured to use the SOCKS protocol, you will also need to install the [`python_socks`](https://pypi.org/project/python-socks/) Python library.
Each type of remote source has its own configuration parameters. For instance, a git source will ask the user to specify a branch and authentication credentials. Once the source has been created, a synchronization job is run to automatically replicate remote files in the local database. Each type of remote source has its own configuration parameters. For instance, a git source will ask the user to specify a branch and authentication credentials. Once the source has been created, a synchronization job is run to automatically replicate remote files in the local database.
The following NetBox models can be associated with replicated data files: The following NetBox models can be associated with replicated data files:

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

View File

@ -1,6 +1,6 @@
# ASNs # ASNs
An Autonomous System Number (ASN) is a numeric identifier used in the BGP protocol to identify which [autonomous system](https://en.wikipedia.org/wiki/Autonomous_system_%28Internet%29) a particular prefix is originating and transiting through. NetBox support both 32- and 64- ASNs. An Autonomous System Number (ASN) is a numeric identifier used in the Border Gateway Protocol (BGP) to identify which [autonomous system](https://en.wikipedia.org/wiki/Autonomous_system_%28Internet%29) a particular prefix is originating from or transiting through. NetBox supports both 16- and 32-bit ASNs.
ASNs must be globally unique within NetBox, and may be allocated from within a [defined range](./asnrange.md). Each ASN may be assigned to multiple [sites](../dcim/site.md). ASNs must be globally unique within NetBox, and may be allocated from within a [defined range](./asnrange.md). Each ASN may be assigned to multiple [sites](../dcim/site.md).
@ -8,7 +8,7 @@ ASNs must be globally unique within NetBox, and may be allocated from within a [
### AS Number ### AS Number
The 32- or 64-bit AS number. The 16- or 32-bit AS number.
### RIR ### RIR

View File

@ -1,5 +1,17 @@
# NetBox v4.1 # NetBox v4.1
## v4.1.3 (2024-10-02)
### Enhancements
* [#17639](https://github.com/netbox-community/netbox/issues/17639) - Add SOCKS support to proxy settings for Git remote data sources
### Bug Fixes
* [#17558](https://github.com/netbox-community/netbox/issues/17558) - Raise validation error when attempting to remove a custom field choice in use
---
## v4.1.2 (2024-09-26) ## v4.1.2 (2024-09-26)
### Enhancements ### Enhancements

View File

@ -8,10 +8,13 @@ from urllib.parse import urlparse
from django import forms from django import forms
from django.conf import settings from django.conf import settings
from django.core.exceptions import ImproperlyConfigured
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
from netbox.data_backends import DataBackend from netbox.data_backends import DataBackend
from netbox.utils import register_data_backend from netbox.utils import register_data_backend
from utilities.constants import HTTP_PROXY_SUPPORTED_SCHEMAS, HTTP_PROXY_SUPPORTED_SOCK_SCHEMAS
from utilities.socks import ProxyPoolManager
from .exceptions import SyncError from .exceptions import SyncError
__all__ = ( __all__ = (
@ -67,11 +70,18 @@ class GitBackend(DataBackend):
# Initialize backend config # Initialize backend config
config = ConfigDict() config = ConfigDict()
self.use_socks = False
# Apply HTTP proxy (if configured) # Apply HTTP proxy (if configured)
if settings.HTTP_PROXIES and self.url_scheme in ('http', 'https'): if settings.HTTP_PROXIES:
if proxy := settings.HTTP_PROXIES.get(self.url_scheme): if proxy := settings.HTTP_PROXIES.get(self.url_scheme, None):
config.set("http", "proxy", proxy) if urlparse(proxy).scheme not in HTTP_PROXY_SUPPORTED_SCHEMAS:
raise ImproperlyConfigured(f"Unsupported Git DataSource proxy scheme: {urlparse(proxy).scheme}")
if self.url_scheme in ('http', 'https'):
config.set("http", "proxy", proxy)
if urlparse(proxy).scheme in HTTP_PROXY_SUPPORTED_SOCK_SCHEMAS:
self.use_socks = True
return config return config
@ -87,6 +97,10 @@ class GitBackend(DataBackend):
"errstream": porcelain.NoneStream(), "errstream": porcelain.NoneStream(),
} }
# check if using socks for proxy - if so need to use custom pool_manager
if self.use_socks:
clone_args['pool_manager'] = ProxyPoolManager(settings.HTTP_PROXIES.get(self.url_scheme))
if self.url_scheme in ('http', 'https'): if self.url_scheme in ('http', 'https'):
if self.params.get('username'): if self.params.get('username'):
clone_args.update( clone_args.update(

View File

@ -785,6 +785,12 @@ class CustomFieldChoiceSet(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel
def __str__(self): def __str__(self):
return self.name return self.name
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Cache the initial set of choices for comparison under clean()
self._original_extra_choices = self.__dict__.get('extra_choices')
def get_absolute_url(self): def get_absolute_url(self):
return reverse('extras:customfieldchoiceset', args=[self.pk]) return reverse('extras:customfieldchoiceset', args=[self.pk])
@ -818,6 +824,32 @@ class CustomFieldChoiceSet(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel
if not self.base_choices and not self.extra_choices: if not self.base_choices and not self.extra_choices:
raise ValidationError(_("Must define base or extra choices.")) raise ValidationError(_("Must define base or extra choices."))
# Check whether any choices have been removed. If so, check whether any of the removed
# choices are still set in custom field data for any object.
original_choices = set([
c[0] for c in self._original_extra_choices
]) if self._original_extra_choices else set()
current_choices = set([
c[0] for c in self.extra_choices
]) if self.extra_choices else set()
if removed_choices := original_choices - current_choices:
for custom_field in self.choices_for.all():
for object_type in custom_field.object_types.all():
model = object_type.model_class()
for choice in removed_choices:
# Form the query based on the type of custom field
if custom_field.type == CustomFieldTypeChoices.TYPE_MULTISELECT:
query_args = {f"custom_field_data__{custom_field.name}__contains": choice}
else:
query_args = {f"custom_field_data__{custom_field.name}": choice}
# Raise a ValidationError if there are any objects which still reference the removed choice
if model.objects.filter(models.Q(**query_args)).exists():
raise ValidationError(
_(
"Cannot remove choice {choice} as there are {model} objects which reference it."
).format(choice=choice, model=object_type)
)
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
# Sort choices if alphabetical ordering is enforced # Sort choices if alphabetical ordering is enforced

View File

@ -343,6 +343,74 @@ class CustomFieldTest(TestCase):
instance.refresh_from_db() instance.refresh_from_db()
self.assertIsNone(instance.custom_field_data.get(cf.name)) self.assertIsNone(instance.custom_field_data.get(cf.name))
def test_remove_selected_choice(self):
"""
Removing a ChoiceSet choice that is referenced by an object should raise
a ValidationError exception.
"""
CHOICES = (
('a', 'Option A'),
('b', 'Option B'),
('c', 'Option C'),
('d', 'Option D'),
)
# Create a set of custom field choices
choice_set = CustomFieldChoiceSet.objects.create(
name='Custom Field Choice Set 1',
extra_choices=CHOICES
)
# Create a select custom field
cf = CustomField.objects.create(
name='select_field',
type=CustomFieldTypeChoices.TYPE_SELECT,
required=False,
choice_set=choice_set
)
cf.object_types.set([self.object_type])
# Create a multi-select custom field
cf_multiselect = CustomField.objects.create(
name='multiselect_field',
type=CustomFieldTypeChoices.TYPE_MULTISELECT,
required=False,
choice_set=choice_set
)
cf_multiselect.object_types.set([self.object_type])
# Assign a choice for both custom fields on an object
instance = Site.objects.first()
instance.custom_field_data[cf.name] = 'a'
instance.custom_field_data[cf_multiselect.name] = ['b', 'c']
instance.save()
# Attempting to delete a selected choice should fail
with self.assertRaises(ValidationError):
choice_set.extra_choices = (
('b', 'Option B'),
('c', 'Option C'),
('d', 'Option D'),
)
choice_set.full_clean()
# Attempting to delete either of the multi-select choices should fail
with self.assertRaises(ValidationError):
choice_set.extra_choices = (
('a', 'Option A'),
('b', 'Option B'),
('d', 'Option D'),
)
choice_set.full_clean()
# Removing a non-selected choice should succeed
choice_set.extra_choices = (
('a', 'Option A'),
('b', 'Option B'),
('c', 'Option C'),
)
choice_set.full_clean()
def test_object_field(self): def test_object_field(self):
value = VLAN.objects.create(name='VLAN 1', vid=1).pk value = VLAN.objects.create(name='VLAN 1', vid=1).pk

View File

@ -1,3 +1,3 @@
version: "4.1.2" version: "4.1.3"
edition: "Community" edition: "Community"
published: "2024-09-26" published: "2024-10-02"

View File

@ -8,7 +8,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-09-25 05:02+0000\n" "POT-Creation-Date: 2024-10-02 05:01+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
@ -82,8 +82,8 @@ msgstr ""
#: netbox/circuits/choices.py:21 netbox/dcim/choices.py:20 #: netbox/circuits/choices.py:21 netbox/dcim/choices.py:20
#: netbox/dcim/choices.py:102 netbox/dcim/choices.py:185 #: netbox/dcim/choices.py:102 netbox/dcim/choices.py:185
#: netbox/dcim/choices.py:231 netbox/dcim/choices.py:1522 #: netbox/dcim/choices.py:231 netbox/dcim/choices.py:1524
#: netbox/dcim/choices.py:1598 netbox/dcim/choices.py:1648 #: netbox/dcim/choices.py:1600 netbox/dcim/choices.py:1650
#: netbox/virtualization/choices.py:20 netbox/virtualization/choices.py:45 #: netbox/virtualization/choices.py:20 netbox/virtualization/choices.py:45
#: netbox/vpn/choices.py:18 #: netbox/vpn/choices.py:18
msgid "Planned" msgid "Planned"
@ -96,7 +96,7 @@ msgstr ""
#: netbox/circuits/choices.py:23 netbox/core/tables/tasks.py:22 #: netbox/circuits/choices.py:23 netbox/core/tables/tasks.py:22
#: netbox/dcim/choices.py:22 netbox/dcim/choices.py:103 #: netbox/dcim/choices.py:22 netbox/dcim/choices.py:103
#: netbox/dcim/choices.py:184 netbox/dcim/choices.py:230 #: netbox/dcim/choices.py:184 netbox/dcim/choices.py:230
#: netbox/dcim/choices.py:1597 netbox/dcim/choices.py:1647 #: netbox/dcim/choices.py:1599 netbox/dcim/choices.py:1649
#: netbox/extras/tables/tables.py:495 netbox/ipam/choices.py:31 #: netbox/extras/tables/tables.py:495 netbox/ipam/choices.py:31
#: netbox/ipam/choices.py:49 netbox/ipam/choices.py:69 #: netbox/ipam/choices.py:49 netbox/ipam/choices.py:69
#: netbox/ipam/choices.py:154 netbox/templates/extras/configcontext.html:25 #: netbox/ipam/choices.py:154 netbox/templates/extras/configcontext.html:25
@ -107,8 +107,8 @@ msgid "Active"
msgstr "" msgstr ""
#: netbox/circuits/choices.py:24 netbox/dcim/choices.py:183 #: netbox/circuits/choices.py:24 netbox/dcim/choices.py:183
#: netbox/dcim/choices.py:229 netbox/dcim/choices.py:1596 #: netbox/dcim/choices.py:229 netbox/dcim/choices.py:1598
#: netbox/dcim/choices.py:1649 netbox/virtualization/choices.py:24 #: netbox/dcim/choices.py:1651 netbox/virtualization/choices.py:24
#: netbox/virtualization/choices.py:43 #: netbox/virtualization/choices.py:43
msgid "Offline" msgid "Offline"
msgstr "" msgstr ""
@ -121,7 +121,7 @@ msgstr ""
msgid "Decommissioned" msgid "Decommissioned"
msgstr "" msgstr ""
#: netbox/circuits/choices.py:90 netbox/dcim/choices.py:1609 #: netbox/circuits/choices.py:90 netbox/dcim/choices.py:1611
#: netbox/tenancy/choices.py:17 #: netbox/tenancy/choices.py:17
msgid "Primary" msgid "Primary"
msgstr "" msgstr ""
@ -1587,7 +1587,7 @@ msgstr ""
#: netbox/core/choices.py:22 netbox/core/choices.py:59 #: netbox/core/choices.py:22 netbox/core/choices.py:59
#: netbox/core/constants.py:20 netbox/core/tables/tasks.py:34 #: netbox/core/constants.py:20 netbox/core/tables/tasks.py:34
#: netbox/dcim/choices.py:187 netbox/dcim/choices.py:233 #: netbox/dcim/choices.py:187 netbox/dcim/choices.py:233
#: netbox/dcim/choices.py:1599 netbox/virtualization/choices.py:47 #: netbox/dcim/choices.py:1601 netbox/virtualization/choices.py:47
msgid "Failed" msgid "Failed"
msgstr "" msgstr ""
@ -1653,42 +1653,42 @@ msgstr ""
msgid "Cancelled" msgid "Cancelled"
msgstr "" msgstr ""
#: netbox/core/data_backends.py:29 netbox/core/tables/plugins.py:51 #: netbox/core/data_backends.py:32 netbox/core/tables/plugins.py:51
#: netbox/templates/core/plugin.html:87 #: netbox/templates/core/plugin.html:87
#: netbox/templates/dcim/interface.html:216 #: netbox/templates/dcim/interface.html:216
msgid "Local" msgid "Local"
msgstr "" msgstr ""
#: netbox/core/data_backends.py:47 netbox/core/tables/change_logging.py:20 #: netbox/core/data_backends.py:50 netbox/core/tables/change_logging.py:20
#: netbox/templates/account/profile.html:15 netbox/templates/users/user.html:17 #: netbox/templates/account/profile.html:15 netbox/templates/users/user.html:17
#: netbox/users/tables.py:31 #: netbox/users/tables.py:31
msgid "Username" msgid "Username"
msgstr "" msgstr ""
#: netbox/core/data_backends.py:49 netbox/core/data_backends.py:55 #: netbox/core/data_backends.py:52 netbox/core/data_backends.py:58
msgid "Only used for cloning with HTTP(S)" msgid "Only used for cloning with HTTP(S)"
msgstr "" msgstr ""
#: netbox/core/data_backends.py:53 netbox/templates/account/base.html:23 #: netbox/core/data_backends.py:56 netbox/templates/account/base.html:23
#: netbox/templates/account/password.html:12 #: netbox/templates/account/password.html:12
#: netbox/users/forms/model_forms.py:171 #: netbox/users/forms/model_forms.py:171
msgid "Password" msgid "Password"
msgstr "" msgstr ""
#: netbox/core/data_backends.py:59 #: netbox/core/data_backends.py:62
msgid "Branch" msgid "Branch"
msgstr "" msgstr ""
#: netbox/core/data_backends.py:106 #: netbox/core/data_backends.py:120
#, python-brace-format #, python-brace-format
msgid "Fetching remote data failed ({name}): {error}" msgid "Fetching remote data failed ({name}): {error}"
msgstr "" msgstr ""
#: netbox/core/data_backends.py:119 #: netbox/core/data_backends.py:133
msgid "AWS access key ID" msgid "AWS access key ID"
msgstr "" msgstr ""
#: netbox/core/data_backends.py:123 #: netbox/core/data_backends.py:137
msgid "AWS secret access key" msgid "AWS secret access key"
msgstr "" msgstr ""
@ -1911,7 +1911,7 @@ msgstr ""
msgid "Rack Elevations" msgid "Rack Elevations"
msgstr "" msgstr ""
#: netbox/core/forms/model_forms.py:157 netbox/dcim/choices.py:1510 #: netbox/core/forms/model_forms.py:157 netbox/dcim/choices.py:1512
#: netbox/dcim/forms/bulk_edit.py:969 netbox/dcim/forms/bulk_edit.py:1357 #: netbox/dcim/forms/bulk_edit.py:969 netbox/dcim/forms/bulk_edit.py:1357
#: netbox/dcim/forms/bulk_edit.py:1375 netbox/dcim/tables/racks.py:158 #: netbox/dcim/forms/bulk_edit.py:1375 netbox/dcim/tables/racks.py:158
#: netbox/netbox/navigation/menu.py:291 netbox/netbox/navigation/menu.py:295 #: netbox/netbox/navigation/menu.py:291 netbox/netbox/navigation/menu.py:295
@ -2477,7 +2477,7 @@ msgid "Staging"
msgstr "" msgstr ""
#: netbox/dcim/choices.py:23 netbox/dcim/choices.py:189 #: netbox/dcim/choices.py:23 netbox/dcim/choices.py:189
#: netbox/dcim/choices.py:234 netbox/dcim/choices.py:1523 #: netbox/dcim/choices.py:234 netbox/dcim/choices.py:1525
#: netbox/virtualization/choices.py:23 netbox/virtualization/choices.py:48 #: netbox/virtualization/choices.py:23 netbox/virtualization/choices.py:48
msgid "Decommissioning" msgid "Decommissioning"
msgstr "" msgstr ""
@ -2541,7 +2541,7 @@ msgstr ""
msgid "Millimeters" msgid "Millimeters"
msgstr "" msgstr ""
#: netbox/dcim/choices.py:115 netbox/dcim/choices.py:1545 #: netbox/dcim/choices.py:115 netbox/dcim/choices.py:1547
msgid "Inches" msgid "Inches"
msgstr "" msgstr ""
@ -2630,7 +2630,7 @@ msgid "Side to rear"
msgstr "" msgstr ""
#: netbox/dcim/choices.py:209 netbox/dcim/choices.py:253 #: netbox/dcim/choices.py:209 netbox/dcim/choices.py:253
#: netbox/dcim/choices.py:1295 #: netbox/dcim/choices.py:1297
msgid "Passive" msgid "Passive"
msgstr "" msgstr ""
@ -2659,8 +2659,8 @@ msgid "Proprietary"
msgstr "" msgstr ""
#: netbox/dcim/choices.py:575 netbox/dcim/choices.py:818 #: netbox/dcim/choices.py:575 netbox/dcim/choices.py:818
#: netbox/dcim/choices.py:1211 netbox/dcim/choices.py:1213 #: netbox/dcim/choices.py:1213 netbox/dcim/choices.py:1215
#: netbox/dcim/choices.py:1439 netbox/dcim/choices.py:1441 #: netbox/dcim/choices.py:1441 netbox/dcim/choices.py:1443
#: netbox/netbox/navigation/menu.py:200 #: netbox/netbox/navigation/menu.py:200
msgid "Other" msgid "Other"
msgstr "" msgstr ""
@ -2673,11 +2673,11 @@ msgstr ""
msgid "Physical" msgid "Physical"
msgstr "" msgstr ""
#: netbox/dcim/choices.py:849 netbox/dcim/choices.py:1016 #: netbox/dcim/choices.py:849 netbox/dcim/choices.py:1017
msgid "Virtual" msgid "Virtual"
msgstr "" msgstr ""
#: netbox/dcim/choices.py:850 netbox/dcim/choices.py:1089 #: netbox/dcim/choices.py:850 netbox/dcim/choices.py:1091
#: netbox/dcim/forms/bulk_edit.py:1515 netbox/dcim/forms/filtersets.py:1330 #: netbox/dcim/forms/bulk_edit.py:1515 netbox/dcim/forms/filtersets.py:1330
#: netbox/dcim/forms/model_forms.py:988 netbox/dcim/forms/model_forms.py:1397 #: netbox/dcim/forms/model_forms.py:988 netbox/dcim/forms/model_forms.py:1397
#: netbox/netbox/navigation/menu.py:140 netbox/netbox/navigation/menu.py:144 #: netbox/netbox/navigation/menu.py:140 netbox/netbox/navigation/menu.py:144
@ -2685,11 +2685,11 @@ msgstr ""
msgid "Wireless" msgid "Wireless"
msgstr "" msgstr ""
#: netbox/dcim/choices.py:1014 #: netbox/dcim/choices.py:1015
msgid "Virtual interfaces" msgid "Virtual interfaces"
msgstr "" msgstr ""
#: netbox/dcim/choices.py:1017 netbox/dcim/forms/bulk_edit.py:1410 #: netbox/dcim/choices.py:1018 netbox/dcim/forms/bulk_edit.py:1410
#: netbox/dcim/forms/bulk_import.py:840 netbox/dcim/forms/model_forms.py:974 #: netbox/dcim/forms/bulk_import.py:840 netbox/dcim/forms/model_forms.py:974
#: netbox/dcim/tables/devices.py:657 netbox/templates/dcim/interface.html:106 #: netbox/dcim/tables/devices.py:657 netbox/templates/dcim/interface.html:106
#: netbox/templates/virtualization/vminterface.html:43 #: netbox/templates/virtualization/vminterface.html:43
@ -2699,27 +2699,27 @@ msgstr ""
msgid "Bridge" msgid "Bridge"
msgstr "" msgstr ""
#: netbox/dcim/choices.py:1018 #: netbox/dcim/choices.py:1019
msgid "Link Aggregation Group (LAG)" msgid "Link Aggregation Group (LAG)"
msgstr "" msgstr ""
#: netbox/dcim/choices.py:1022 #: netbox/dcim/choices.py:1023
msgid "Ethernet (fixed)" msgid "Ethernet (fixed)"
msgstr "" msgstr ""
#: netbox/dcim/choices.py:1037 #: netbox/dcim/choices.py:1038
msgid "Ethernet (modular)" msgid "Ethernet (modular)"
msgstr "" msgstr ""
#: netbox/dcim/choices.py:1073 #: netbox/dcim/choices.py:1075
msgid "Ethernet (backplane)" msgid "Ethernet (backplane)"
msgstr "" msgstr ""
#: netbox/dcim/choices.py:1105 #: netbox/dcim/choices.py:1107
msgid "Cellular" msgid "Cellular"
msgstr "" msgstr ""
#: netbox/dcim/choices.py:1157 netbox/dcim/forms/filtersets.py:383 #: netbox/dcim/choices.py:1159 netbox/dcim/forms/filtersets.py:383
#: netbox/dcim/forms/filtersets.py:809 netbox/dcim/forms/filtersets.py:963 #: netbox/dcim/forms/filtersets.py:809 netbox/dcim/forms/filtersets.py:963
#: netbox/dcim/forms/filtersets.py:1542 #: netbox/dcim/forms/filtersets.py:1542
#: netbox/templates/dcim/inventoryitem.html:52 #: netbox/templates/dcim/inventoryitem.html:52
@ -2727,130 +2727,130 @@ msgstr ""
msgid "Serial" msgid "Serial"
msgstr "" msgstr ""
#: netbox/dcim/choices.py:1172 #: netbox/dcim/choices.py:1174
msgid "Coaxial" msgid "Coaxial"
msgstr "" msgstr ""
#: netbox/dcim/choices.py:1192 #: netbox/dcim/choices.py:1194
msgid "Stacking" msgid "Stacking"
msgstr "" msgstr ""
#: netbox/dcim/choices.py:1242 #: netbox/dcim/choices.py:1244
msgid "Half" msgid "Half"
msgstr "" msgstr ""
#: netbox/dcim/choices.py:1243 #: netbox/dcim/choices.py:1245
msgid "Full" msgid "Full"
msgstr "" msgstr ""
#: netbox/dcim/choices.py:1244 netbox/netbox/preferences.py:31 #: netbox/dcim/choices.py:1246 netbox/netbox/preferences.py:31
#: netbox/wireless/choices.py:480 #: netbox/wireless/choices.py:480
msgid "Auto" msgid "Auto"
msgstr "" msgstr ""
#: netbox/dcim/choices.py:1255 #: netbox/dcim/choices.py:1257
msgid "Access" msgid "Access"
msgstr "" msgstr ""
#: netbox/dcim/choices.py:1256 netbox/ipam/tables/vlans.py:172 #: netbox/dcim/choices.py:1258 netbox/ipam/tables/vlans.py:172
#: netbox/ipam/tables/vlans.py:217 #: netbox/ipam/tables/vlans.py:217
#: netbox/templates/dcim/inc/interface_vlans_table.html:7 #: netbox/templates/dcim/inc/interface_vlans_table.html:7
msgid "Tagged" msgid "Tagged"
msgstr "" msgstr ""
#: netbox/dcim/choices.py:1257 #: netbox/dcim/choices.py:1259
msgid "Tagged (All)" msgid "Tagged (All)"
msgstr "" msgstr ""
#: netbox/dcim/choices.py:1286 #: netbox/dcim/choices.py:1288
msgid "IEEE Standard" msgid "IEEE Standard"
msgstr "" msgstr ""
#: netbox/dcim/choices.py:1297 #: netbox/dcim/choices.py:1299
msgid "Passive 24V (2-pair)" msgid "Passive 24V (2-pair)"
msgstr "" msgstr ""
#: netbox/dcim/choices.py:1298 #: netbox/dcim/choices.py:1300
msgid "Passive 24V (4-pair)" msgid "Passive 24V (4-pair)"
msgstr "" msgstr ""
#: netbox/dcim/choices.py:1299 #: netbox/dcim/choices.py:1301
msgid "Passive 48V (2-pair)" msgid "Passive 48V (2-pair)"
msgstr "" msgstr ""
#: netbox/dcim/choices.py:1300 #: netbox/dcim/choices.py:1302
msgid "Passive 48V (4-pair)" msgid "Passive 48V (4-pair)"
msgstr "" msgstr ""
#: netbox/dcim/choices.py:1370 netbox/dcim/choices.py:1480 #: netbox/dcim/choices.py:1372 netbox/dcim/choices.py:1482
msgid "Copper" msgid "Copper"
msgstr "" msgstr ""
#: netbox/dcim/choices.py:1393 #: netbox/dcim/choices.py:1395
msgid "Fiber Optic" msgid "Fiber Optic"
msgstr "" msgstr ""
#: netbox/dcim/choices.py:1426 netbox/dcim/choices.py:1509 #: netbox/dcim/choices.py:1428 netbox/dcim/choices.py:1511
msgid "USB" msgid "USB"
msgstr "" msgstr ""
#: netbox/dcim/choices.py:1496 #: netbox/dcim/choices.py:1498
msgid "Fiber" msgid "Fiber"
msgstr "" msgstr ""
#: netbox/dcim/choices.py:1521 netbox/dcim/forms/filtersets.py:1227 #: netbox/dcim/choices.py:1523 netbox/dcim/forms/filtersets.py:1227
msgid "Connected" msgid "Connected"
msgstr "" msgstr ""
#: netbox/dcim/choices.py:1540 netbox/wireless/choices.py:497 #: netbox/dcim/choices.py:1542 netbox/wireless/choices.py:497
msgid "Kilometers" msgid "Kilometers"
msgstr "" msgstr ""
#: netbox/dcim/choices.py:1541 netbox/templates/dcim/cable_trace.html:65 #: netbox/dcim/choices.py:1543 netbox/templates/dcim/cable_trace.html:65
#: netbox/wireless/choices.py:498 #: netbox/wireless/choices.py:498
msgid "Meters" msgid "Meters"
msgstr "" msgstr ""
#: netbox/dcim/choices.py:1542 #: netbox/dcim/choices.py:1544
msgid "Centimeters" msgid "Centimeters"
msgstr "" msgstr ""
#: netbox/dcim/choices.py:1543 netbox/wireless/choices.py:499 #: netbox/dcim/choices.py:1545 netbox/wireless/choices.py:499
msgid "Miles" msgid "Miles"
msgstr "" msgstr ""
#: netbox/dcim/choices.py:1544 netbox/templates/dcim/cable_trace.html:66 #: netbox/dcim/choices.py:1546 netbox/templates/dcim/cable_trace.html:66
#: netbox/wireless/choices.py:500 #: netbox/wireless/choices.py:500
msgid "Feet" msgid "Feet"
msgstr "" msgstr ""
#: netbox/dcim/choices.py:1560 netbox/templates/dcim/device.html:327 #: netbox/dcim/choices.py:1562 netbox/templates/dcim/device.html:327
#: netbox/templates/dcim/rack.html:107 #: netbox/templates/dcim/rack.html:107
msgid "Kilograms" msgid "Kilograms"
msgstr "" msgstr ""
#: netbox/dcim/choices.py:1561 #: netbox/dcim/choices.py:1563
msgid "Grams" msgid "Grams"
msgstr "" msgstr ""
#: netbox/dcim/choices.py:1562 netbox/templates/dcim/device.html:328 #: netbox/dcim/choices.py:1564 netbox/templates/dcim/device.html:328
#: netbox/templates/dcim/rack.html:108 #: netbox/templates/dcim/rack.html:108
msgid "Pounds" msgid "Pounds"
msgstr "" msgstr ""
#: netbox/dcim/choices.py:1563 #: netbox/dcim/choices.py:1565
msgid "Ounces" msgid "Ounces"
msgstr "" msgstr ""
#: netbox/dcim/choices.py:1610 #: netbox/dcim/choices.py:1612
msgid "Redundant" msgid "Redundant"
msgstr "" msgstr ""
#: netbox/dcim/choices.py:1631 #: netbox/dcim/choices.py:1633
msgid "Single phase" msgid "Single phase"
msgstr "" msgstr ""
#: netbox/dcim/choices.py:1632 #: netbox/dcim/choices.py:1634
msgid "Three-phase" msgid "Three-phase"
msgstr "" msgstr ""
@ -8233,10 +8233,17 @@ msgstr ""
msgid "custom field choice sets" msgid "custom field choice sets"
msgstr "" msgstr ""
#: netbox/extras/models/customfields.py:819 #: netbox/extras/models/customfields.py:825
msgid "Must define base or extra choices." msgid "Must define base or extra choices."
msgstr "" msgstr ""
#: netbox/extras/models/customfields.py:849
#, python-brace-format
msgid ""
"Cannot remove choice {choice} as there are {model} objects which reference "
"it."
msgstr ""
#: netbox/extras/models/dashboard.py:18 #: netbox/extras/models/dashboard.py:18
msgid "layout" msgid "layout"
msgstr "" msgstr ""
@ -13749,11 +13756,6 @@ msgstr ""
msgid "Disk Space" msgid "Disk Space"
msgstr "" msgstr ""
#: netbox/templates/virtualization/cluster.html:72
msgctxt "Abbreviation for gigabyte"
msgid "GB"
msgstr ""
#: netbox/templates/virtualization/cluster/base.html:18 #: netbox/templates/virtualization/cluster/base.html:18
msgid "Add Virtual Machine" msgid "Add Virtual Machine"
msgstr "" msgstr ""
@ -14493,7 +14495,7 @@ msgid "Invalid value for a multiple choice field: {value}"
msgstr "" msgstr ""
#: netbox/utilities/forms/fields/csv.py:57 #: netbox/utilities/forms/fields/csv.py:57
#: netbox/utilities/forms/fields/csv.py:74 #: netbox/utilities/forms/fields/csv.py:78
#, python-format #, python-format
msgid "Object not found: %(value)s" msgid "Object not found: %(value)s"
msgstr "" msgstr ""
@ -14504,11 +14506,16 @@ msgid ""
"\"{value}\" is not a unique value for this field; multiple objects were found" "\"{value}\" is not a unique value for this field; multiple objects were found"
msgstr "" msgstr ""
#: netbox/utilities/forms/fields/csv.py:97 #: netbox/utilities/forms/fields/csv.py:69
msgid "Object type must be specified as \"<app>.<model>\"" #, python-brace-format
msgid "\"{field_name}\" is an invalid accessor field name."
msgstr "" msgstr ""
#: netbox/utilities/forms/fields/csv.py:101 #: netbox/utilities/forms/fields/csv.py:101
msgid "Object type must be specified as \"<app>.<model>\""
msgstr ""
#: netbox/utilities/forms/fields/csv.py:105
msgid "Invalid object type" msgid "Invalid object type"
msgstr "" msgstr ""

View File

@ -93,3 +93,7 @@ HTML_ALLOWED_ATTRIBUTES = {
"td": {"align"}, "td": {"align"},
"th": {"align"}, "th": {"align"},
} }
HTTP_PROXY_SUPPORTED_SOCK_SCHEMAS = ['socks4', 'socks4a', 'socks4h', 'socks5', 'socks5a', 'socks5h']
HTTP_PROXY_SOCK_RDNS_SCHEMAS = ['socks4h', 'socks4a', 'socks5h', 'socks5a']
HTTP_PROXY_SUPPORTED_SCHEMAS = ['http', 'https', 'socks4', 'socks4a', 'socks4h', 'socks5', 'socks5a', 'socks5h']

101
netbox/utilities/socks.py Normal file
View File

@ -0,0 +1,101 @@
import logging
from urllib.parse import urlparse
from urllib3 import PoolManager, HTTPConnectionPool, HTTPSConnectionPool
from urllib3.connection import HTTPConnection, HTTPSConnection
from .constants import HTTP_PROXY_SOCK_RDNS_SCHEMAS
logger = logging.getLogger('netbox.utilities')
class ProxyHTTPConnection(HTTPConnection):
"""
A Proxy connection class that uses a SOCK proxy - used to create
a urllib3 PoolManager that routes connections via the proxy.
This is for an HTTP (not HTTPS) connection
"""
use_rdns = False
def __init__(self, *args, **kwargs):
socks_options = kwargs.pop('_socks_options')
self._proxy_url = socks_options['proxy_url']
super().__init__(*args, **kwargs)
def _new_conn(self):
try:
from python_socks.sync import Proxy
except ModuleNotFoundError as e:
logger.info("Configuring an HTTP proxy using SOCKS requires the python_socks library. Check that it has been installed.")
raise e
proxy = Proxy.from_url(self._proxy_url, rdns=self.use_rdns)
return proxy.connect(
dest_host=self.host,
dest_port=self.port,
timeout=self.timeout
)
class ProxyHTTPSConnection(ProxyHTTPConnection, HTTPSConnection):
"""
A Proxy connection class for an HTTPS (not HTTP) connection.
"""
pass
class RdnsProxyHTTPConnection(ProxyHTTPConnection):
"""
A Proxy connection class for an HTTP remote-dns connection.
I.E. socks4a, socks4h, socks5a, socks5h
"""
use_rdns = True
class RdnsProxyHTTPSConnection(ProxyHTTPSConnection):
"""
A Proxy connection class for an HTTPS remote-dns connection.
I.E. socks4a, socks4h, socks5a, socks5h
"""
use_rdns = True
class ProxyHTTPConnectionPool(HTTPConnectionPool):
ConnectionCls = ProxyHTTPConnection
class ProxyHTTPSConnectionPool(HTTPSConnectionPool):
ConnectionCls = ProxyHTTPSConnection
class RdnsProxyHTTPConnectionPool(HTTPConnectionPool):
ConnectionCls = RdnsProxyHTTPConnection
class RdnsProxyHTTPSConnectionPool(HTTPSConnectionPool):
ConnectionCls = RdnsProxyHTTPSConnection
class ProxyPoolManager(PoolManager):
def __init__(self, proxy_url, timeout=5, num_pools=10, headers=None, **connection_pool_kw):
# python_socks uses rdns param to denote remote DNS parsing and
# doesn't accept the 'h' or 'a' in the proxy URL
if use_rdns := urlparse(proxy_url).scheme in HTTP_PROXY_SOCK_RDNS_SCHEMAS:
proxy_url = proxy_url.replace('socks5h:', 'socks5:').replace('socks5a:', 'socks5:')
proxy_url = proxy_url.replace('socks4h:', 'socks4:').replace('socks4a:', 'socks4:')
connection_pool_kw['_socks_options'] = {'proxy_url': proxy_url}
connection_pool_kw['timeout'] = timeout
super().__init__(num_pools, headers, **connection_pool_kw)
if use_rdns:
self.pool_classes_by_scheme = {
'http': RdnsProxyHTTPConnectionPool,
'https': RdnsProxyHTTPSConnectionPool,
}
else:
self.pool_classes_by_scheme = {
'http': ProxyHTTPConnectionPool,
'https': ProxyHTTPSConnectionPool,
}

View File

@ -10,7 +10,7 @@ django-prometheus==2.3.1
django-redis==5.4.0 django-redis==5.4.0
django-rich==1.11.0 django-rich==1.11.0
django-rq==2.10.2 django-rq==2.10.2
django-taggit==6.0.0 django-taggit==6.1.0
django-tables2==2.7.0 django-tables2==2.7.0
django-timezone-field==7.0 django-timezone-field==7.0
djangorestframework==3.15.2 djangorestframework==3.15.2
@ -20,12 +20,12 @@ feedparser==6.0.11
gunicorn==23.0.0 gunicorn==23.0.0
Jinja2==3.1.4 Jinja2==3.1.4
Markdown==3.7 Markdown==3.7
mkdocs-material==9.5.38 mkdocs-material==9.5.39
mkdocstrings[python-legacy]==0.26.1 mkdocstrings[python-legacy]==0.26.1
netaddr==1.3.0 netaddr==1.3.0
nh3==0.2.18 nh3==0.2.18
Pillow==10.4.0 Pillow==10.4.0
psycopg[c,pool]==3.2.2 psycopg[c,pool]==3.2.3
PyYAML==6.0.2 PyYAML==6.0.2
requests==2.32.3 requests==2.32.3
social-auth-app-django==5.4.2 social-auth-app-django==5.4.2