diff --git a/.github/ISSUE_TEMPLATE/01-feature_request.yaml b/.github/ISSUE_TEMPLATE/01-feature_request.yaml index cb04b90d0..fb4bab623 100644 --- a/.github/ISSUE_TEMPLATE/01-feature_request.yaml +++ b/.github/ISSUE_TEMPLATE/01-feature_request.yaml @@ -14,7 +14,7 @@ body: attributes: label: NetBox version description: What version of NetBox are you currently running? - placeholder: v4.1.2 + placeholder: v4.1.3 validations: required: true - type: dropdown diff --git a/.github/ISSUE_TEMPLATE/02-bug_report.yaml b/.github/ISSUE_TEMPLATE/02-bug_report.yaml index 1453c1dfd..f31e024b4 100644 --- a/.github/ISSUE_TEMPLATE/02-bug_report.yaml +++ b/.github/ISSUE_TEMPLATE/02-bug_report.yaml @@ -26,7 +26,7 @@ body: attributes: label: NetBox Version description: What version of NetBox are you currently running? - placeholder: v4.1.2 + placeholder: v4.1.3 validations: required: true - type: dropdown diff --git a/README.md b/README.md index 34975980b..0c793b8a4 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,11 @@ GitHub stars Languages supported CI status -

+

+ NetBox Community | + NetBox Cloud | + NetBox Enterprise +

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. * Check out [our wiki](https://github.com/netbox-community/netbox/wiki/Community-Contributions) for even more projects to get the most out of NetBox! -

- NetBox Cloud
- Looking for a managed solution? Check out NetBox Cloud or NetBox Enterprise! -

- ## Get Involved * Follow [@NetBoxOfficial](https://twitter.com/NetBoxOfficial) on Twitter! diff --git a/docs/features/synchronized-data.md b/docs/features/synchronized-data.md index 8c95c8779..23c79feed 100644 --- a/docs/features/synchronized-data.md +++ b/docs/features/synchronized-data.md @@ -13,6 +13,9 @@ To enable remote data synchronization, the NetBox administrator first designates !!! 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. +!!! 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. The following NetBox models can be associated with replicated data files: diff --git a/docs/media/misc/netbox_cloud.png b/docs/media/misc/netbox_cloud.png deleted file mode 100644 index f9deca674..000000000 Binary files a/docs/media/misc/netbox_cloud.png and /dev/null differ diff --git a/docs/media/misc/netbox_logo.png b/docs/media/misc/netbox_logo.png deleted file mode 100644 index c6e0a58e6..000000000 Binary files a/docs/media/misc/netbox_logo.png and /dev/null differ diff --git a/docs/models/ipam/asn.md b/docs/models/ipam/asn.md index 8de3cfd93..17fa5ebe3 100644 --- a/docs/models/ipam/asn.md +++ b/docs/models/ipam/asn.md @@ -1,6 +1,6 @@ # 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). @@ -8,7 +8,7 @@ ASNs must be globally unique within NetBox, and may be allocated from within a [ ### AS Number -The 32- or 64-bit AS number. +The 16- or 32-bit AS number. ### RIR diff --git a/docs/release-notes/version-4.1.md b/docs/release-notes/version-4.1.md index 0f441aa46..28fc8ff12 100644 --- a/docs/release-notes/version-4.1.md +++ b/docs/release-notes/version-4.1.md @@ -1,5 +1,17 @@ # 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) ### Enhancements diff --git a/netbox/core/data_backends.py b/netbox/core/data_backends.py index 1b64f5f5c..8b36c6995 100644 --- a/netbox/core/data_backends.py +++ b/netbox/core/data_backends.py @@ -8,10 +8,13 @@ from urllib.parse import urlparse from django import forms from django.conf import settings +from django.core.exceptions import ImproperlyConfigured from django.utils.translation import gettext as _ from netbox.data_backends import DataBackend 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 __all__ = ( @@ -67,11 +70,18 @@ class GitBackend(DataBackend): # Initialize backend config config = ConfigDict() + self.use_socks = False # Apply HTTP proxy (if configured) - if settings.HTTP_PROXIES and self.url_scheme in ('http', 'https'): - if proxy := settings.HTTP_PROXIES.get(self.url_scheme): - config.set("http", "proxy", proxy) + if settings.HTTP_PROXIES: + if proxy := settings.HTTP_PROXIES.get(self.url_scheme, None): + 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 @@ -87,6 +97,10 @@ class GitBackend(DataBackend): "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.params.get('username'): clone_args.update( diff --git a/netbox/extras/models/customfields.py b/netbox/extras/models/customfields.py index aff41521b..91de7df0d 100644 --- a/netbox/extras/models/customfields.py +++ b/netbox/extras/models/customfields.py @@ -785,6 +785,12 @@ class CustomFieldChoiceSet(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel def __str__(self): 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): 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: 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): # Sort choices if alphabetical ordering is enforced diff --git a/netbox/extras/tests/test_customfields.py b/netbox/extras/tests/test_customfields.py index 697b756ec..2bc9b5acc 100644 --- a/netbox/extras/tests/test_customfields.py +++ b/netbox/extras/tests/test_customfields.py @@ -343,6 +343,74 @@ class CustomFieldTest(TestCase): instance.refresh_from_db() 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): value = VLAN.objects.create(name='VLAN 1', vid=1).pk diff --git a/netbox/release.yaml b/netbox/release.yaml index fd19bf096..3963fd542 100644 --- a/netbox/release.yaml +++ b/netbox/release.yaml @@ -1,3 +1,3 @@ -version: "4.1.2" +version: "4.1.3" edition: "Community" -published: "2024-09-26" +published: "2024-10-02" diff --git a/netbox/translations/en/LC_MESSAGES/django.po b/netbox/translations/en/LC_MESSAGES/django.po index 02e78c374..d34a314a1 100644 --- a/netbox/translations/en/LC_MESSAGES/django.po +++ b/netbox/translations/en/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\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" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -82,8 +82,8 @@ msgstr "" #: netbox/circuits/choices.py:21 netbox/dcim/choices.py:20 #: netbox/dcim/choices.py:102 netbox/dcim/choices.py:185 -#: netbox/dcim/choices.py:231 netbox/dcim/choices.py:1522 -#: netbox/dcim/choices.py:1598 netbox/dcim/choices.py:1648 +#: netbox/dcim/choices.py:231 netbox/dcim/choices.py:1524 +#: netbox/dcim/choices.py:1600 netbox/dcim/choices.py:1650 #: netbox/virtualization/choices.py:20 netbox/virtualization/choices.py:45 #: netbox/vpn/choices.py:18 msgid "Planned" @@ -96,7 +96,7 @@ msgstr "" #: 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: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/ipam/choices.py:49 netbox/ipam/choices.py:69 #: netbox/ipam/choices.py:154 netbox/templates/extras/configcontext.html:25 @@ -107,8 +107,8 @@ msgid "Active" msgstr "" #: netbox/circuits/choices.py:24 netbox/dcim/choices.py:183 -#: netbox/dcim/choices.py:229 netbox/dcim/choices.py:1596 -#: netbox/dcim/choices.py:1649 netbox/virtualization/choices.py:24 +#: netbox/dcim/choices.py:229 netbox/dcim/choices.py:1598 +#: netbox/dcim/choices.py:1651 netbox/virtualization/choices.py:24 #: netbox/virtualization/choices.py:43 msgid "Offline" msgstr "" @@ -121,7 +121,7 @@ msgstr "" msgid "Decommissioned" 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 msgid "Primary" msgstr "" @@ -1587,7 +1587,7 @@ msgstr "" #: netbox/core/choices.py:22 netbox/core/choices.py:59 #: 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:1599 netbox/virtualization/choices.py:47 +#: netbox/dcim/choices.py:1601 netbox/virtualization/choices.py:47 msgid "Failed" msgstr "" @@ -1653,42 +1653,42 @@ msgstr "" msgid "Cancelled" 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/dcim/interface.html:216 msgid "Local" 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/users/tables.py:31 msgid "Username" 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)" 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/users/forms/model_forms.py:171 msgid "Password" msgstr "" -#: netbox/core/data_backends.py:59 +#: netbox/core/data_backends.py:62 msgid "Branch" msgstr "" -#: netbox/core/data_backends.py:106 +#: netbox/core/data_backends.py:120 #, python-brace-format msgid "Fetching remote data failed ({name}): {error}" msgstr "" -#: netbox/core/data_backends.py:119 +#: netbox/core/data_backends.py:133 msgid "AWS access key ID" msgstr "" -#: netbox/core/data_backends.py:123 +#: netbox/core/data_backends.py:137 msgid "AWS secret access key" msgstr "" @@ -1911,7 +1911,7 @@ msgstr "" msgid "Rack Elevations" 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:1375 netbox/dcim/tables/racks.py:158 #: netbox/netbox/navigation/menu.py:291 netbox/netbox/navigation/menu.py:295 @@ -2477,7 +2477,7 @@ msgid "Staging" msgstr "" #: 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 msgid "Decommissioning" msgstr "" @@ -2541,7 +2541,7 @@ msgstr "" msgid "Millimeters" msgstr "" -#: netbox/dcim/choices.py:115 netbox/dcim/choices.py:1545 +#: netbox/dcim/choices.py:115 netbox/dcim/choices.py:1547 msgid "Inches" msgstr "" @@ -2630,7 +2630,7 @@ msgid "Side to rear" msgstr "" #: netbox/dcim/choices.py:209 netbox/dcim/choices.py:253 -#: netbox/dcim/choices.py:1295 +#: netbox/dcim/choices.py:1297 msgid "Passive" msgstr "" @@ -2659,8 +2659,8 @@ msgid "Proprietary" msgstr "" #: netbox/dcim/choices.py:575 netbox/dcim/choices.py:818 -#: netbox/dcim/choices.py:1211 netbox/dcim/choices.py:1213 -#: netbox/dcim/choices.py:1439 netbox/dcim/choices.py:1441 +#: netbox/dcim/choices.py:1213 netbox/dcim/choices.py:1215 +#: netbox/dcim/choices.py:1441 netbox/dcim/choices.py:1443 #: netbox/netbox/navigation/menu.py:200 msgid "Other" msgstr "" @@ -2673,11 +2673,11 @@ msgstr "" msgid "Physical" msgstr "" -#: netbox/dcim/choices.py:849 netbox/dcim/choices.py:1016 +#: netbox/dcim/choices.py:849 netbox/dcim/choices.py:1017 msgid "Virtual" 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/model_forms.py:988 netbox/dcim/forms/model_forms.py:1397 #: netbox/netbox/navigation/menu.py:140 netbox/netbox/navigation/menu.py:144 @@ -2685,11 +2685,11 @@ msgstr "" msgid "Wireless" msgstr "" -#: netbox/dcim/choices.py:1014 +#: netbox/dcim/choices.py:1015 msgid "Virtual interfaces" 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/tables/devices.py:657 netbox/templates/dcim/interface.html:106 #: netbox/templates/virtualization/vminterface.html:43 @@ -2699,27 +2699,27 @@ msgstr "" msgid "Bridge" msgstr "" -#: netbox/dcim/choices.py:1018 +#: netbox/dcim/choices.py:1019 msgid "Link Aggregation Group (LAG)" msgstr "" -#: netbox/dcim/choices.py:1022 +#: netbox/dcim/choices.py:1023 msgid "Ethernet (fixed)" msgstr "" -#: netbox/dcim/choices.py:1037 +#: netbox/dcim/choices.py:1038 msgid "Ethernet (modular)" msgstr "" -#: netbox/dcim/choices.py:1073 +#: netbox/dcim/choices.py:1075 msgid "Ethernet (backplane)" msgstr "" -#: netbox/dcim/choices.py:1105 +#: netbox/dcim/choices.py:1107 msgid "Cellular" 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:1542 #: netbox/templates/dcim/inventoryitem.html:52 @@ -2727,130 +2727,130 @@ msgstr "" msgid "Serial" msgstr "" -#: netbox/dcim/choices.py:1172 +#: netbox/dcim/choices.py:1174 msgid "Coaxial" msgstr "" -#: netbox/dcim/choices.py:1192 +#: netbox/dcim/choices.py:1194 msgid "Stacking" msgstr "" -#: netbox/dcim/choices.py:1242 +#: netbox/dcim/choices.py:1244 msgid "Half" msgstr "" -#: netbox/dcim/choices.py:1243 +#: netbox/dcim/choices.py:1245 msgid "Full" 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 msgid "Auto" msgstr "" -#: netbox/dcim/choices.py:1255 +#: netbox/dcim/choices.py:1257 msgid "Access" 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/templates/dcim/inc/interface_vlans_table.html:7 msgid "Tagged" msgstr "" -#: netbox/dcim/choices.py:1257 +#: netbox/dcim/choices.py:1259 msgid "Tagged (All)" msgstr "" -#: netbox/dcim/choices.py:1286 +#: netbox/dcim/choices.py:1288 msgid "IEEE Standard" msgstr "" -#: netbox/dcim/choices.py:1297 +#: netbox/dcim/choices.py:1299 msgid "Passive 24V (2-pair)" msgstr "" -#: netbox/dcim/choices.py:1298 +#: netbox/dcim/choices.py:1300 msgid "Passive 24V (4-pair)" msgstr "" -#: netbox/dcim/choices.py:1299 +#: netbox/dcim/choices.py:1301 msgid "Passive 48V (2-pair)" msgstr "" -#: netbox/dcim/choices.py:1300 +#: netbox/dcim/choices.py:1302 msgid "Passive 48V (4-pair)" msgstr "" -#: netbox/dcim/choices.py:1370 netbox/dcim/choices.py:1480 +#: netbox/dcim/choices.py:1372 netbox/dcim/choices.py:1482 msgid "Copper" msgstr "" -#: netbox/dcim/choices.py:1393 +#: netbox/dcim/choices.py:1395 msgid "Fiber Optic" msgstr "" -#: netbox/dcim/choices.py:1426 netbox/dcim/choices.py:1509 +#: netbox/dcim/choices.py:1428 netbox/dcim/choices.py:1511 msgid "USB" msgstr "" -#: netbox/dcim/choices.py:1496 +#: netbox/dcim/choices.py:1498 msgid "Fiber" 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" msgstr "" -#: netbox/dcim/choices.py:1540 netbox/wireless/choices.py:497 +#: netbox/dcim/choices.py:1542 netbox/wireless/choices.py:497 msgid "Kilometers" 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 msgid "Meters" msgstr "" -#: netbox/dcim/choices.py:1542 +#: netbox/dcim/choices.py:1544 msgid "Centimeters" msgstr "" -#: netbox/dcim/choices.py:1543 netbox/wireless/choices.py:499 +#: netbox/dcim/choices.py:1545 netbox/wireless/choices.py:499 msgid "Miles" 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 msgid "Feet" 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 msgid "Kilograms" msgstr "" -#: netbox/dcim/choices.py:1561 +#: netbox/dcim/choices.py:1563 msgid "Grams" 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 msgid "Pounds" msgstr "" -#: netbox/dcim/choices.py:1563 +#: netbox/dcim/choices.py:1565 msgid "Ounces" msgstr "" -#: netbox/dcim/choices.py:1610 +#: netbox/dcim/choices.py:1612 msgid "Redundant" msgstr "" -#: netbox/dcim/choices.py:1631 +#: netbox/dcim/choices.py:1633 msgid "Single phase" msgstr "" -#: netbox/dcim/choices.py:1632 +#: netbox/dcim/choices.py:1634 msgid "Three-phase" msgstr "" @@ -8233,10 +8233,17 @@ msgstr "" msgid "custom field choice sets" msgstr "" -#: netbox/extras/models/customfields.py:819 +#: netbox/extras/models/customfields.py:825 msgid "Must define base or extra choices." 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 msgid "layout" msgstr "" @@ -13749,11 +13756,6 @@ msgstr "" msgid "Disk Space" msgstr "" -#: netbox/templates/virtualization/cluster.html:72 -msgctxt "Abbreviation for gigabyte" -msgid "GB" -msgstr "" - #: netbox/templates/virtualization/cluster/base.html:18 msgid "Add Virtual Machine" msgstr "" @@ -14493,7 +14495,7 @@ msgid "Invalid value for a multiple choice field: {value}" msgstr "" #: netbox/utilities/forms/fields/csv.py:57 -#: netbox/utilities/forms/fields/csv.py:74 +#: netbox/utilities/forms/fields/csv.py:78 #, python-format msgid "Object not found: %(value)s" msgstr "" @@ -14504,11 +14506,16 @@ msgid "" "\"{value}\" is not a unique value for this field; multiple objects were found" msgstr "" -#: netbox/utilities/forms/fields/csv.py:97 -msgid "Object type must be specified as \".\"" +#: netbox/utilities/forms/fields/csv.py:69 +#, python-brace-format +msgid "\"{field_name}\" is an invalid accessor field name." msgstr "" #: netbox/utilities/forms/fields/csv.py:101 +msgid "Object type must be specified as \".\"" +msgstr "" + +#: netbox/utilities/forms/fields/csv.py:105 msgid "Invalid object type" msgstr "" diff --git a/netbox/utilities/constants.py b/netbox/utilities/constants.py index c7c26f6b3..2b93f2b96 100644 --- a/netbox/utilities/constants.py +++ b/netbox/utilities/constants.py @@ -93,3 +93,7 @@ HTML_ALLOWED_ATTRIBUTES = { "td": {"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'] diff --git a/netbox/utilities/socks.py b/netbox/utilities/socks.py new file mode 100644 index 000000000..bb0b6b250 --- /dev/null +++ b/netbox/utilities/socks.py @@ -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, + } diff --git a/requirements.txt b/requirements.txt index e8b9ca38b..17ec31097 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,7 +10,7 @@ django-prometheus==2.3.1 django-redis==5.4.0 django-rich==1.11.0 django-rq==2.10.2 -django-taggit==6.0.0 +django-taggit==6.1.0 django-tables2==2.7.0 django-timezone-field==7.0 djangorestframework==3.15.2 @@ -20,12 +20,12 @@ feedparser==6.0.11 gunicorn==23.0.0 Jinja2==3.1.4 Markdown==3.7 -mkdocs-material==9.5.38 +mkdocs-material==9.5.39 mkdocstrings[python-legacy]==0.26.1 netaddr==1.3.0 nh3==0.2.18 Pillow==10.4.0 -psycopg[c,pool]==3.2.2 +psycopg[c,pool]==3.2.3 PyYAML==6.0.2 requests==2.32.3 social-auth-app-django==5.4.2