| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135 |
- import logging
- import traceback
- from contextlib import ExitStack
- 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
- from .utils import is_report
- class ScriptJob(JobRunner):
- """
- Script execution job.
- A wrapper for calling Script.run(). This performs error handling and provides a hook for committing changes. It
- exists outside the Script class to ensure it cannot be overridden by a script author.
- """
- class Meta:
- name = 'Run Script'
- def run_script(self, script, request, data, commit):
- """
- Core script execution task. We capture this within a method to allow for conditionally wrapping it with the
- event_tracking context manager (which is bypassed if commit == False).
- Args:
- request: The WSGI request associated with this execution (if any)
- data: A dictionary of data to be passed to the script upon execution
- commit: Passed through to Script.run()
- """
- logger = logging.getLogger(f"netbox.scripts.{script.full_name}")
- logger.info(f"Running script (commit={commit})")
- try:
- try:
- # 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)
- 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:
- logger.warning("Script failed")
- except Exception as e:
- if type(e) is AbortScript:
- msg = _("Script aborted with error: ") + str(e)
- if is_report(type(script)):
- script.log_failure(message=msg)
- else:
- script.log_failure(msg)
- logger.error(f"Script aborted with error: {e}")
- self.logger.error(f"Script aborted with error: {e}")
- else:
- stacktrace = traceback.format_exc()
- script.log_failure(
- message=_("An exception occurred: ") + f"`{type(e).__name__}: {e}`\n```\n{stacktrace}\n```"
- )
- logger.error(f"Exception raised during script execution: {e}")
- self.logger.error(f"Exception raised during script execution: {e}")
- if type(e) is not AbortTransaction:
- script.log_info(message=_("Database changes have been reverted due to error."))
- self.logger.info("Database changes have been reverted due to error.")
- # Clear all pending events. Job termination (including setting the status) is handled by the job framework.
- if request:
- clear_events.send(request)
- raise
- # Update the job data regardless of the execution status of the job. Successes should be reported as well as
- # failures.
- finally:
- self.job.data = script.get_job_data()
- def run(self, data, request=None, commit=True, **kwargs):
- """
- Run the script.
- Args:
- job: The Job associated with this execution
- data: A dictionary of data to be passed to the script upon execution
- request: The WSGI request associated with this execution (if any)
- commit: Passed through to Script.run()
- """
- script_model = ScriptModel.objects.get(pk=self.job.object_id)
- self.logger.debug(f"Found ScriptModel ID {script_model.pk}")
- script = script_model.python_class()
- self.logger.debug(f"Loaded script {script.full_name}")
- # Add files to form data
- if request:
- files = request.FILES
- for field_name, fileobj in files.items():
- data[field_name] = fileobj
- # Add the current request as a property of the script
- script.request = request
- self.logger.debug(f"Request ID: {request.id if request else None}")
- if commit:
- self.logger.info("Executing script (commit enabled)")
- 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)
|