jobs.py 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116
  1. import logging
  2. import traceback
  3. from contextlib import ExitStack
  4. from django.db import transaction
  5. from django.utils.translation import gettext as _
  6. from core.signals import clear_events
  7. from extras.models import Script as ScriptModel
  8. from netbox.jobs import JobRunner
  9. from netbox.registry import registry
  10. from utilities.exceptions import AbortScript, AbortTransaction
  11. from .utils import is_report
  12. class ScriptJob(JobRunner):
  13. """
  14. Script execution job.
  15. A wrapper for calling Script.run(). This performs error handling and provides a hook for committing changes. It
  16. exists outside the Script class to ensure it cannot be overridden by a script author.
  17. """
  18. class Meta:
  19. name = 'Run Script'
  20. def run_script(self, script, request, data, commit):
  21. """
  22. Core script execution task. We capture this within a method to allow for conditionally wrapping it with the
  23. event_tracking context manager (which is bypassed if commit == False).
  24. Args:
  25. request: The WSGI request associated with this execution (if any)
  26. data: A dictionary of data to be passed to the script upon execution
  27. commit: Passed through to Script.run()
  28. """
  29. logger = logging.getLogger(f"netbox.scripts.{script.full_name}")
  30. logger.info(f"Running script (commit={commit})")
  31. try:
  32. try:
  33. # A script can modify multiple models so need to do an atomic lock on
  34. # both the default database (for non ChangeLogged models) and potentially
  35. # any other database (for ChangeLogged models)
  36. with transaction.atomic():
  37. script.output = script.run(data, commit)
  38. if not commit:
  39. raise AbortTransaction()
  40. except AbortTransaction:
  41. script.log_info(message=_("Database changes have been reverted automatically."))
  42. if script.failed:
  43. logger.warning("Script failed")
  44. except Exception as e:
  45. if type(e) is AbortScript:
  46. msg = _("Script aborted with error: ") + str(e)
  47. if is_report(type(script)):
  48. script.log_failure(message=msg)
  49. else:
  50. script.log_failure(msg)
  51. logger.error(f"Script aborted with error: {e}")
  52. else:
  53. stacktrace = traceback.format_exc()
  54. script.log_failure(
  55. message=_("An exception occurred: ") + f"`{type(e).__name__}: {e}`\n```\n{stacktrace}\n```"
  56. )
  57. logger.error(f"Exception raised during script execution: {e}")
  58. if type(e) is not AbortTransaction:
  59. script.log_info(message=_("Database changes have been reverted due to error."))
  60. # Clear all pending events. Job termination (including setting the status) is handled by the job framework.
  61. if request:
  62. clear_events.send(request)
  63. raise
  64. # Update the job data regardless of the execution status of the job. Successes should be reported as well as
  65. # failures.
  66. finally:
  67. self.job.data = script.get_job_data()
  68. def run(self, data, request=None, commit=True, **kwargs):
  69. """
  70. Run the script.
  71. Args:
  72. job: The Job associated with this execution
  73. data: A dictionary of data to be passed to the script upon execution
  74. request: The WSGI request associated with this execution (if any)
  75. commit: Passed through to Script.run()
  76. """
  77. script_model = ScriptModel.objects.get(pk=self.job.object_id)
  78. self.logger.debug(f"Found ScriptModel ID {script_model.pk}")
  79. script = script_model.python_class()
  80. self.logger.debug(f"Loaded script {script.full_name}")
  81. # Add files to form data
  82. if request:
  83. files = request.FILES
  84. for field_name, fileobj in files.items():
  85. data[field_name] = fileobj
  86. # Add the current request as a property of the script
  87. script.request = request
  88. self.logger.debug(f"Request ID: {request.id}")
  89. # Execute the script. If commit is True, wrap it with the event_tracking context manager to ensure we process
  90. # change logging, event rules, etc.
  91. if commit:
  92. with ExitStack() as stack:
  93. for request_processor in registry['request_processors']:
  94. stack.enter_context(request_processor(request))
  95. self.run_script(script, request, data, commit)
  96. else:
  97. self.run_script(script, request, data, commit)