From 701561daa85882deba25ba5fbd4953744f6a2739 Mon Sep 17 00:00:00 2001 From: Arthur Date: Tue, 25 Nov 2025 10:33:46 -0800 Subject: [PATCH 1/4] 20878 use database routing when running script --- netbox/extras/jobs.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/netbox/extras/jobs.py b/netbox/extras/jobs.py index c270833b1e5..d1c5f060737 100644 --- a/netbox/extras/jobs.py +++ b/netbox/extras/jobs.py @@ -2,10 +2,11 @@ import traceback from contextlib import ExitStack -from django.db import transaction +from django.db import router, transaction 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.jobs import JobRunner from netbox.registry import registry @@ -42,7 +43,7 @@ def run_script(self, script, request, data, commit): # 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(): + with transaction.atomic(using=router.db_for_write(Device)): script.output = script.run(data, commit) if not commit: raise AbortTransaction() @@ -108,14 +109,15 @@ def run(self, data, request=None, commit=True, **kwargs): 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. + # Execute the script. Always use request processors to set up the execution context (e.g. branch activation). + # The event_tracking context manager is one of the request processors and will be activated regardless of + # the commit flag, but it only records changes when commit=True. 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']: + stack.enter_context(request_processor(request)) self.run_script(script, request, data, commit) From 6094aa868208edf5b7a6c6c53040fd34063ad84b Mon Sep 17 00:00:00 2001 From: Arthur Date: Tue, 25 Nov 2025 11:16:05 -0800 Subject: [PATCH 2/4] update comments --- netbox/extras/jobs.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/netbox/extras/jobs.py b/netbox/extras/jobs.py index d1c5f060737..930f327ac12 100644 --- a/netbox/extras/jobs.py +++ b/netbox/extras/jobs.py @@ -109,9 +109,6 @@ def run(self, data, request=None, commit=True, **kwargs): script.request = request self.logger.debug(f"Request ID: {request.id if request else None}") - # Execute the script. Always use request processors to set up the execution context (e.g. branch activation). - # The event_tracking context manager is one of the request processors and will be activated regardless of - # the commit flag, but it only records changes when commit=True. if commit: self.logger.info("Executing script (commit enabled)") else: From 5208e544ce005b74fea91828b151e74b53a9f5f5 Mon Sep 17 00:00:00 2001 From: Arthur Date: Mon, 1 Dec 2025 10:49:01 -0800 Subject: [PATCH 3/4] remove event_tracking processing if not commit --- netbox/extras/jobs.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/netbox/extras/jobs.py b/netbox/extras/jobs.py index 930f327ac12..2418c58e4af 100644 --- a/netbox/extras/jobs.py +++ b/netbox/extras/jobs.py @@ -8,6 +8,7 @@ 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 @@ -116,5 +117,7 @@ def run(self, data, request=None, commit=True, **kwargs): 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) From aec8e293bffc27ee58226b6441dc093c84755fa7 Mon Sep 17 00:00:00 2001 From: Arthur Date: Mon, 1 Dec 2025 11:14:06 -0800 Subject: [PATCH 4/4] atomic lock on both default and potentially branch database --- netbox/extras/jobs.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/netbox/extras/jobs.py b/netbox/extras/jobs.py index 2418c58e4af..f64806701bf 100644 --- a/netbox/extras/jobs.py +++ b/netbox/extras/jobs.py @@ -44,10 +44,18 @@ def run_script(self, script, request, data, commit): # 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(using=router.db_for_write(Device)): - script.output = script.run(data, commit) - if not commit: - raise AbortTransaction() + branch_db = router.db_for_write(Device) + with transaction.atomic(using='default'): + # If branch database is different from default, wrap in a second atomic transaction + if branch_db != 'default': + with transaction.atomic(using=branch_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: