diff --git a/netbox/dcim/migrations/0216_latitude_longitude_validators.py b/netbox/dcim/migrations/0216_latitude_longitude_validators.py index 2621d5019..e3bd2ca78 100644 --- a/netbox/dcim/migrations/0216_latitude_longitude_validators.py +++ b/netbox/dcim/migrations/0216_latitude_longitude_validators.py @@ -1,3 +1,5 @@ +import decimal + import django.core.validators from django.db import migrations, models @@ -17,8 +19,8 @@ class Migration(migrations.Migration): max_digits=8, null=True, validators=[ - django.core.validators.MinValueValidator(-90.0), - django.core.validators.MaxValueValidator(90.0), + django.core.validators.MinValueValidator(decimal.Decimal('-90.0')), + django.core.validators.MaxValueValidator(decimal.Decimal('90.0')) ], ), ), @@ -31,8 +33,8 @@ class Migration(migrations.Migration): max_digits=9, null=True, validators=[ - django.core.validators.MinValueValidator(-180.0), - django.core.validators.MaxValueValidator(180.0), + django.core.validators.MinValueValidator(decimal.Decimal('-180.0')), + django.core.validators.MaxValueValidator(decimal.Decimal('180.0')) ], ), ), @@ -45,8 +47,8 @@ class Migration(migrations.Migration): max_digits=8, null=True, validators=[ - django.core.validators.MinValueValidator(-90.0), - django.core.validators.MaxValueValidator(90.0), + django.core.validators.MinValueValidator(decimal.Decimal('-90.0')), + django.core.validators.MaxValueValidator(decimal.Decimal('90.0')) ], ), ), @@ -59,8 +61,8 @@ class Migration(migrations.Migration): max_digits=9, null=True, validators=[ - django.core.validators.MinValueValidator(-180.0), - django.core.validators.MaxValueValidator(180.0), + django.core.validators.MinValueValidator(decimal.Decimal('-180.0')), + django.core.validators.MaxValueValidator(decimal.Decimal('180.0')) ], ), ), diff --git a/netbox/dcim/models/devices.py b/netbox/dcim/models/devices.py index 6d05d80b9..64d2e346c 100644 --- a/netbox/dcim/models/devices.py +++ b/netbox/dcim/models/devices.py @@ -646,7 +646,10 @@ class Device( decimal_places=6, blank=True, null=True, - validators=[MinValueValidator(-90.0), MaxValueValidator(90.0)], + validators=[ + MinValueValidator(decimal.Decimal('-90.0')), + MaxValueValidator(decimal.Decimal('90.0')) + ], help_text=_("GPS coordinate in decimal format (xx.yyyyyy)") ) longitude = models.DecimalField( @@ -655,7 +658,10 @@ class Device( decimal_places=6, blank=True, null=True, - validators=[MinValueValidator(-180.0), MaxValueValidator(180.0)], + validators=[ + MinValueValidator(decimal.Decimal('-180.0')), + MaxValueValidator(decimal.Decimal('180.0')) + ], help_text=_("GPS coordinate in decimal format (xx.yyyyyy)") ) services = GenericRelation( diff --git a/netbox/dcim/models/sites.py b/netbox/dcim/models/sites.py index f9aa47874..d18c22c7e 100644 --- a/netbox/dcim/models/sites.py +++ b/netbox/dcim/models/sites.py @@ -1,3 +1,5 @@ +import decimal + from django.contrib.contenttypes.fields import GenericRelation from django.core.exceptions import ValidationError from django.core.validators import MaxValueValidator, MinValueValidator @@ -211,7 +213,10 @@ class Site(ContactsMixin, ImageAttachmentsMixin, PrimaryModel): decimal_places=6, blank=True, null=True, - validators=[MinValueValidator(-90.0), MaxValueValidator(90.0)], + validators=[ + MinValueValidator(decimal.Decimal('-90.0')), + MaxValueValidator(decimal.Decimal('90.0')) + ], help_text=_('GPS coordinate in decimal format (xx.yyyyyy)') ) longitude = models.DecimalField( @@ -220,7 +225,10 @@ class Site(ContactsMixin, ImageAttachmentsMixin, PrimaryModel): decimal_places=6, blank=True, null=True, - validators=[MinValueValidator(-180.0), MaxValueValidator(180.0)], + validators=[ + MinValueValidator(decimal.Decimal('-180.0')), + MaxValueValidator(decimal.Decimal('180.0')) + ], help_text=_('GPS coordinate in decimal format (xx.yyyyyy)') ) diff --git a/netbox/extras/jobs.py b/netbox/extras/jobs.py index c270833b1..5b57cbce4 100644 --- a/netbox/extras/jobs.py +++ b/netbox/extras/jobs.py @@ -2,11 +2,14 @@ import logging import traceback from contextlib import ExitStack -from django.db import transaction +from django.db import router, transaction +from django.db import DEFAULT_DB_ALIAS from django.utils.translation import gettext as _ from core.signals import clear_events +from dcim.models import Device from extras.models import Script as ScriptModel +from netbox.context_managers import event_tracking from netbox.jobs import JobRunner from netbox.registry import registry from utilities.exceptions import AbortScript, AbortTransaction @@ -42,10 +45,21 @@ class ScriptJob(JobRunner): # A script can modify multiple models so need to do an atomic lock on # both the default database (for non ChangeLogged models) and potentially # any other database (for ChangeLogged models) - with transaction.atomic(): - script.output = script.run(data, commit) - if not commit: - raise AbortTransaction() + changeloged_db = router.db_for_write(Device) + with transaction.atomic(using=DEFAULT_DB_ALIAS): + # If branch database is different from default, wrap in a second atomic transaction + # Note: Don't add any extra code between the two atomic transactions, + # otherwise the changes might get committed to the default database + # if there are any raised exceptions. + if changeloged_db != DEFAULT_DB_ALIAS: + with transaction.atomic(using=changeloged_db): + script.output = script.run(data, commit) + if not commit: + raise AbortTransaction() + else: + script.output = script.run(data, commit) + if not commit: + raise AbortTransaction() except AbortTransaction: script.log_info(message=_("Database changes have been reverted automatically.")) if script.failed: @@ -108,14 +122,14 @@ class ScriptJob(JobRunner): script.request = request self.logger.debug(f"Request ID: {request.id if request else None}") - # Execute the script. If commit is True, wrap it with the event_tracking context manager to ensure we process - # change logging, event rules, etc. if commit: self.logger.info("Executing script (commit enabled)") - with ExitStack() as stack: - for request_processor in registry['request_processors']: - stack.enter_context(request_processor(request)) - self.run_script(script, request, data, commit) else: self.logger.warning("Executing script (commit disabled)") + + with ExitStack() as stack: + for request_processor in registry['request_processors']: + if not commit and request_processor is event_tracking: + continue + stack.enter_context(request_processor(request)) self.run_script(script, request, data, commit) diff --git a/netbox/netbox/views/generic/object_views.py b/netbox/netbox/views/generic/object_views.py index 897191592..5038d0ecc 100644 --- a/netbox/netbox/views/generic/object_views.py +++ b/netbox/netbox/views/generic/object_views.py @@ -559,6 +559,7 @@ class ComponentCreateView(GetReturnURLMixin, BaseObjectView): form.instance._replicated_base = hasattr(self.form, "replication_fields") if form.is_valid(): + changelog_message = form.cleaned_data.pop('changelog_message', '') new_components = [] data = deepcopy(request.POST) pattern_count = len(form.cleaned_data[self.form.replication_fields[0]]) @@ -585,6 +586,9 @@ class ComponentCreateView(GetReturnURLMixin, BaseObjectView): # Create the new components new_objs = [] for component_form in new_components: + # Record changelog message (if any) + if changelog_message: + component_form.instance._changelog_message = changelog_message obj = component_form.save() new_objs.append(obj) diff --git a/netbox/translations/en/LC_MESSAGES/django.po b/netbox/translations/en/LC_MESSAGES/django.po index 35e29b71c..86056682d 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: 2025-11-26 05:01+0000\n" +"POT-Creation-Date: 2025-12-04 05:01+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -1469,10 +1469,10 @@ msgstr "" #: netbox/core/models/jobs.py:95 netbox/dcim/models/cables.py:51 #: netbox/dcim/models/device_components.py:488 #: netbox/dcim/models/device_components.py:1319 -#: netbox/dcim/models/devices.py:580 netbox/dcim/models/devices.py:1196 +#: netbox/dcim/models/devices.py:580 netbox/dcim/models/devices.py:1202 #: netbox/dcim/models/modules.py:210 netbox/dcim/models/power.py:94 #: netbox/dcim/models/racks.py:294 netbox/dcim/models/racks.py:677 -#: netbox/dcim/models/sites.py:155 netbox/dcim/models/sites.py:273 +#: netbox/dcim/models/sites.py:157 netbox/dcim/models/sites.py:281 #: netbox/ipam/models/ip.py:243 netbox/ipam/models/ip.py:529 #: netbox/ipam/models/ip.py:758 netbox/ipam/models/vlans.py:228 #: netbox/virtualization/models/clusters.py:70 @@ -1603,10 +1603,10 @@ msgstr "" #: netbox/core/models/jobs.py:56 #: netbox/dcim/models/device_component_templates.py:44 #: netbox/dcim/models/device_components.py:53 netbox/dcim/models/devices.py:524 -#: netbox/dcim/models/devices.py:1122 netbox/dcim/models/devices.py:1191 +#: netbox/dcim/models/devices.py:1128 netbox/dcim/models/devices.py:1197 #: netbox/dcim/models/modules.py:32 netbox/dcim/models/power.py:38 #: netbox/dcim/models/power.py:89 netbox/dcim/models/racks.py:263 -#: netbox/dcim/models/sites.py:143 netbox/extras/models/configs.py:36 +#: netbox/dcim/models/sites.py:145 netbox/extras/models/configs.py:36 #: netbox/extras/models/configs.py:78 netbox/extras/models/configs.py:272 #: netbox/extras/models/customfields.py:94 netbox/extras/models/models.py:60 #: netbox/extras/models/models.py:165 netbox/extras/models/models.py:308 @@ -1637,7 +1637,7 @@ msgid "Full name of the provider" msgstr "" #: netbox/circuits/models/providers.py:28 netbox/dcim/models/devices.py:89 -#: netbox/dcim/models/racks.py:143 netbox/dcim/models/sites.py:150 +#: netbox/dcim/models/racks.py:143 netbox/dcim/models/sites.py:152 #: netbox/extras/models/models.py:474 netbox/ipam/models/asns.py:24 #: netbox/ipam/models/vlans.py:43 netbox/netbox/models/__init__.py:146 #: netbox/netbox/models/__init__.py:195 netbox/tenancy/models/tenants.py:25 @@ -3816,8 +3816,8 @@ msgstr "" #: netbox/dcim/filtersets.py:1197 netbox/dcim/forms/filtersets.py:848 #: netbox/dcim/forms/filtersets.py:1473 netbox/dcim/forms/filtersets.py:1688 -#: netbox/dcim/forms/model_forms.py:1900 netbox/dcim/models/devices.py:1292 -#: netbox/dcim/models/devices.py:1312 netbox/virtualization/filtersets.py:201 +#: netbox/dcim/forms/model_forms.py:1900 netbox/dcim/models/devices.py:1298 +#: netbox/dcim/models/devices.py:1318 netbox/virtualization/filtersets.py:201 #: netbox/virtualization/filtersets.py:273 #: netbox/virtualization/forms/filtersets.py:178 #: netbox/virtualization/forms/filtersets.py:231 @@ -6857,12 +6857,12 @@ msgstr "" msgid "rack face" msgstr "" -#: netbox/dcim/models/devices.py:598 netbox/dcim/models/devices.py:1212 +#: netbox/dcim/models/devices.py:598 netbox/dcim/models/devices.py:1218 #: netbox/virtualization/models/virtualmachines.py:94 msgid "primary IPv4" msgstr "" -#: netbox/dcim/models/devices.py:606 netbox/dcim/models/devices.py:1220 +#: netbox/dcim/models/devices.py:606 netbox/dcim/models/devices.py:1226 #: netbox/virtualization/models/virtualmachines.py:102 msgid "primary IPv6" msgstr "" @@ -6887,191 +6887,191 @@ msgstr "" msgid "Virtual chassis master election priority" msgstr "" -#: netbox/dcim/models/devices.py:644 netbox/dcim/models/sites.py:209 +#: netbox/dcim/models/devices.py:644 netbox/dcim/models/sites.py:211 msgid "latitude" msgstr "" -#: netbox/dcim/models/devices.py:650 netbox/dcim/models/devices.py:659 -#: netbox/dcim/models/sites.py:215 netbox/dcim/models/sites.py:224 +#: netbox/dcim/models/devices.py:653 netbox/dcim/models/devices.py:665 +#: netbox/dcim/models/sites.py:220 netbox/dcim/models/sites.py:232 msgid "GPS coordinate in decimal format (xx.yyyyyy)" msgstr "" -#: netbox/dcim/models/devices.py:653 netbox/dcim/models/sites.py:218 +#: netbox/dcim/models/devices.py:656 netbox/dcim/models/sites.py:223 msgid "longitude" msgstr "" -#: netbox/dcim/models/devices.py:733 +#: netbox/dcim/models/devices.py:739 msgid "Device name must be unique per site." msgstr "" -#: netbox/dcim/models/devices.py:744 +#: netbox/dcim/models/devices.py:750 msgid "device" msgstr "" -#: netbox/dcim/models/devices.py:745 +#: netbox/dcim/models/devices.py:751 msgid "devices" msgstr "" -#: netbox/dcim/models/devices.py:764 +#: netbox/dcim/models/devices.py:770 #, python-brace-format msgid "Rack {rack} does not belong to site {site}." msgstr "" -#: netbox/dcim/models/devices.py:769 +#: netbox/dcim/models/devices.py:775 #, python-brace-format msgid "Location {location} does not belong to site {site}." msgstr "" -#: netbox/dcim/models/devices.py:775 +#: netbox/dcim/models/devices.py:781 #, python-brace-format msgid "Rack {rack} does not belong to location {location}." msgstr "" -#: netbox/dcim/models/devices.py:782 +#: netbox/dcim/models/devices.py:788 msgid "Cannot select a rack face without assigning a rack." msgstr "" -#: netbox/dcim/models/devices.py:786 +#: netbox/dcim/models/devices.py:792 msgid "Cannot select a rack position without assigning a rack." msgstr "" -#: netbox/dcim/models/devices.py:792 +#: netbox/dcim/models/devices.py:798 msgid "Position must be in increments of 0.5 rack units." msgstr "" -#: netbox/dcim/models/devices.py:796 +#: netbox/dcim/models/devices.py:802 msgid "Must specify rack face when defining rack position." msgstr "" -#: netbox/dcim/models/devices.py:804 +#: netbox/dcim/models/devices.py:810 #, python-brace-format msgid "A 0U device type ({device_type}) cannot be assigned to a rack position." msgstr "" -#: netbox/dcim/models/devices.py:815 +#: netbox/dcim/models/devices.py:821 msgid "" "Child device types cannot be assigned to a rack face. This is an attribute " "of the parent device." msgstr "" -#: netbox/dcim/models/devices.py:822 +#: netbox/dcim/models/devices.py:828 msgid "" "Child device types cannot be assigned to a rack position. This is an " "attribute of the parent device." msgstr "" -#: netbox/dcim/models/devices.py:836 +#: netbox/dcim/models/devices.py:842 #, python-brace-format msgid "" "U{position} is already occupied or does not have sufficient space to " "accommodate this device type: {device_type} ({u_height}U)" msgstr "" -#: netbox/dcim/models/devices.py:851 +#: netbox/dcim/models/devices.py:857 #, python-brace-format msgid "{ip} is not an IPv4 address." msgstr "" -#: netbox/dcim/models/devices.py:863 netbox/dcim/models/devices.py:881 +#: netbox/dcim/models/devices.py:869 netbox/dcim/models/devices.py:887 #, python-brace-format msgid "The specified IP address ({ip}) is not assigned to this device." msgstr "" -#: netbox/dcim/models/devices.py:869 +#: netbox/dcim/models/devices.py:875 #, python-brace-format msgid "{ip} is not an IPv6 address." msgstr "" -#: netbox/dcim/models/devices.py:899 +#: netbox/dcim/models/devices.py:905 #, python-brace-format msgid "" "The assigned platform is limited to {platform_manufacturer} device types, " "but this device's type belongs to {devicetype_manufacturer}." msgstr "" -#: netbox/dcim/models/devices.py:910 +#: netbox/dcim/models/devices.py:916 #, python-brace-format msgid "The assigned cluster belongs to a different site ({site})" msgstr "" -#: netbox/dcim/models/devices.py:917 +#: netbox/dcim/models/devices.py:923 #, python-brace-format msgid "The assigned cluster belongs to a different location ({location})" msgstr "" -#: netbox/dcim/models/devices.py:925 +#: netbox/dcim/models/devices.py:931 msgid "A device assigned to a virtual chassis must have its position defined." msgstr "" -#: netbox/dcim/models/devices.py:931 +#: netbox/dcim/models/devices.py:937 #, python-brace-format msgid "" "Device cannot be removed from virtual chassis {virtual_chassis} because it " "is currently designated as its master." msgstr "" -#: netbox/dcim/models/devices.py:1127 +#: netbox/dcim/models/devices.py:1133 msgid "domain" msgstr "" -#: netbox/dcim/models/devices.py:1140 netbox/dcim/models/devices.py:1141 +#: netbox/dcim/models/devices.py:1146 netbox/dcim/models/devices.py:1147 msgid "virtual chassis" msgstr "" -#: netbox/dcim/models/devices.py:1153 +#: netbox/dcim/models/devices.py:1159 #, python-brace-format msgid "The selected master ({master}) is not assigned to this virtual chassis." msgstr "" -#: netbox/dcim/models/devices.py:1168 +#: netbox/dcim/models/devices.py:1174 #, python-brace-format msgid "" "Unable to delete virtual chassis {self}. There are member interfaces which " "form a cross-chassis LAG interfaces." msgstr "" -#: netbox/dcim/models/devices.py:1201 netbox/vpn/models/l2vpn.py:42 +#: netbox/dcim/models/devices.py:1207 netbox/vpn/models/l2vpn.py:42 msgid "identifier" msgstr "" -#: netbox/dcim/models/devices.py:1202 +#: netbox/dcim/models/devices.py:1208 msgid "Numeric identifier unique to the parent device" msgstr "" -#: netbox/dcim/models/devices.py:1230 netbox/extras/models/customfields.py:231 +#: netbox/dcim/models/devices.py:1236 netbox/extras/models/customfields.py:231 #: netbox/extras/models/models.py:111 netbox/extras/models/models.py:800 #: netbox/netbox/models/__init__.py:120 netbox/netbox/models/__init__.py:155 msgid "comments" msgstr "" -#: netbox/dcim/models/devices.py:1246 +#: netbox/dcim/models/devices.py:1252 msgid "virtual device context" msgstr "" -#: netbox/dcim/models/devices.py:1247 +#: netbox/dcim/models/devices.py:1253 msgid "virtual device contexts" msgstr "" -#: netbox/dcim/models/devices.py:1276 +#: netbox/dcim/models/devices.py:1282 #, python-brace-format msgid "{ip} is not an IPv{family} address." msgstr "" -#: netbox/dcim/models/devices.py:1282 +#: netbox/dcim/models/devices.py:1288 msgid "Primary IP address must belong to an interface on the assigned device." msgstr "" -#: netbox/dcim/models/devices.py:1313 +#: netbox/dcim/models/devices.py:1319 msgid "MAC addresses" msgstr "" -#: netbox/dcim/models/devices.py:1345 +#: netbox/dcim/models/devices.py:1351 msgid "" "Cannot unassign MAC Address while it is designated as the primary MAC for an " "object" msgstr "" -#: netbox/dcim/models/devices.py:1349 +#: netbox/dcim/models/devices.py:1355 msgid "" "Cannot reassign MAC Address while it is designated as the primary MAC for an " "object" @@ -7378,91 +7378,91 @@ msgstr "" msgid "The following units have already been reserved: {unit_list}" msgstr "" -#: netbox/dcim/models/sites.py:54 +#: netbox/dcim/models/sites.py:56 msgid "A top-level region with this name already exists." msgstr "" -#: netbox/dcim/models/sites.py:64 +#: netbox/dcim/models/sites.py:66 msgid "A top-level region with this slug already exists." msgstr "" -#: netbox/dcim/models/sites.py:67 +#: netbox/dcim/models/sites.py:69 msgid "region" msgstr "" -#: netbox/dcim/models/sites.py:68 +#: netbox/dcim/models/sites.py:70 msgid "regions" msgstr "" -#: netbox/dcim/models/sites.py:110 +#: netbox/dcim/models/sites.py:112 msgid "A top-level site group with this name already exists." msgstr "" -#: netbox/dcim/models/sites.py:120 +#: netbox/dcim/models/sites.py:122 msgid "A top-level site group with this slug already exists." msgstr "" -#: netbox/dcim/models/sites.py:123 +#: netbox/dcim/models/sites.py:125 msgid "site group" msgstr "" -#: netbox/dcim/models/sites.py:124 +#: netbox/dcim/models/sites.py:126 msgid "site groups" msgstr "" -#: netbox/dcim/models/sites.py:146 +#: netbox/dcim/models/sites.py:148 msgid "Full name of the site" msgstr "" -#: netbox/dcim/models/sites.py:182 netbox/dcim/models/sites.py:286 +#: netbox/dcim/models/sites.py:184 netbox/dcim/models/sites.py:294 msgid "facility" msgstr "" -#: netbox/dcim/models/sites.py:185 netbox/dcim/models/sites.py:289 +#: netbox/dcim/models/sites.py:187 netbox/dcim/models/sites.py:297 msgid "Local facility ID or description" msgstr "" -#: netbox/dcim/models/sites.py:197 +#: netbox/dcim/models/sites.py:199 msgid "physical address" msgstr "" -#: netbox/dcim/models/sites.py:200 +#: netbox/dcim/models/sites.py:202 msgid "Physical location of the building" msgstr "" -#: netbox/dcim/models/sites.py:203 +#: netbox/dcim/models/sites.py:205 msgid "shipping address" msgstr "" -#: netbox/dcim/models/sites.py:206 +#: netbox/dcim/models/sites.py:208 msgid "If different from the physical address" msgstr "" -#: netbox/dcim/models/sites.py:248 +#: netbox/dcim/models/sites.py:256 msgid "site" msgstr "" -#: netbox/dcim/models/sites.py:249 +#: netbox/dcim/models/sites.py:257 msgid "sites" msgstr "" -#: netbox/dcim/models/sites.py:322 +#: netbox/dcim/models/sites.py:330 msgid "A location with this name already exists within the specified site." msgstr "" -#: netbox/dcim/models/sites.py:332 +#: netbox/dcim/models/sites.py:340 msgid "A location with this slug already exists within the specified site." msgstr "" -#: netbox/dcim/models/sites.py:335 +#: netbox/dcim/models/sites.py:343 msgid "location" msgstr "" -#: netbox/dcim/models/sites.py:336 +#: netbox/dcim/models/sites.py:344 msgid "locations" msgstr "" -#: netbox/dcim/models/sites.py:347 +#: netbox/dcim/models/sites.py:355 #, python-brace-format msgid "Parent location ({parent}) must belong to the same site ({site})." msgstr "" @@ -8985,19 +8985,19 @@ msgstr "" msgid "Interval at which this script is re-run (in minutes)" msgstr "" -#: netbox/extras/jobs.py:50 +#: netbox/extras/jobs.py:64 msgid "Database changes have been reverted automatically." msgstr "" -#: netbox/extras/jobs.py:56 +#: netbox/extras/jobs.py:70 msgid "Script aborted with error: " msgstr "" -#: netbox/extras/jobs.py:67 +#: netbox/extras/jobs.py:81 msgid "An exception occurred: " msgstr "" -#: netbox/extras/jobs.py:73 +#: netbox/extras/jobs.py:87 msgid "Database changes have been reverted due to error." msgstr ""