runscript.py 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152
  1. import json
  2. import logging
  3. import sys
  4. import traceback
  5. import uuid
  6. from django.contrib.auth.models import User
  7. from django.contrib.contenttypes.models import ContentType
  8. from django.core.management.base import BaseCommand, CommandError
  9. from django.db import transaction
  10. from extras.api.serializers import ScriptOutputSerializer
  11. from extras.choices import JobResultStatusChoices
  12. from extras.context_managers import change_logging
  13. from extras.models import JobResult
  14. from extras.scripts import get_script
  15. from extras.signals import clear_webhooks
  16. from utilities.exceptions import AbortTransaction
  17. from utilities.utils import NetBoxFakeRequest
  18. class Command(BaseCommand):
  19. help = "Run a script in Netbox"
  20. def add_arguments(self, parser):
  21. parser.add_argument(
  22. '--loglevel',
  23. help="Logging Level (default: info)",
  24. dest='loglevel',
  25. default='info',
  26. choices=['debug', 'info', 'warning', 'error', 'critical'])
  27. parser.add_argument('--commit', help="Commit this script to database", action='store_true')
  28. parser.add_argument('--user', help="User script is running as")
  29. parser.add_argument('--data', help="Data as a string encapsulated JSON blob")
  30. parser.add_argument('script', help="Script to run")
  31. def handle(self, *args, **options):
  32. def _run_script():
  33. """
  34. Core script execution task. We capture this within a subfunction to allow for conditionally wrapping it with
  35. the change_logging context manager (which is bypassed if commit == False).
  36. """
  37. try:
  38. with transaction.atomic():
  39. script.output = script.run(data=data, commit=commit)
  40. job_result.set_status(JobResultStatusChoices.STATUS_COMPLETED)
  41. if not commit:
  42. raise AbortTransaction()
  43. except AbortTransaction:
  44. script.log_info("Database changes have been reverted automatically.")
  45. clear_webhooks.send(request)
  46. except Exception as e:
  47. stacktrace = traceback.format_exc()
  48. script.log_failure(
  49. f"An exception occurred: `{type(e).__name__}: {e}`\n```\n{stacktrace}\n```"
  50. )
  51. script.log_info("Database changes have been reverted due to error.")
  52. logger.error(f"Exception raised during script execution: {e}")
  53. job_result.set_status(JobResultStatusChoices.STATUS_ERRORED)
  54. clear_webhooks.send(request)
  55. finally:
  56. job_result.data = ScriptOutputSerializer(script).data
  57. job_result.save()
  58. logger.info(f"Script completed in {job_result.duration}")
  59. # Params
  60. script = options['script']
  61. loglevel = options['loglevel']
  62. commit = options['commit']
  63. try:
  64. data = json.loads(options['data'])
  65. except TypeError:
  66. data = {}
  67. module, name = script.split('.', 1)
  68. # Take user from command line if provided and exists, other
  69. if options['user']:
  70. try:
  71. user = User.objects.get(username=options['user'])
  72. except User.DoesNotExist:
  73. user = User.objects.filter(is_superuser=True).order_by('pk')[0]
  74. else:
  75. user = User.objects.filter(is_superuser=True).order_by('pk')[0]
  76. # Setup logging to Stdout
  77. formatter = logging.Formatter(f'[%(asctime)s][%(levelname)s] - %(message)s')
  78. stdouthandler = logging.StreamHandler(sys.stdout)
  79. stdouthandler.setLevel(logging.DEBUG)
  80. stdouthandler.setFormatter(formatter)
  81. logger = logging.getLogger(f"netbox.scripts.{module}.{name}")
  82. logger.addHandler(stdouthandler)
  83. try:
  84. logger.setLevel({
  85. 'critical': logging.CRITICAL,
  86. 'debug': logging.DEBUG,
  87. 'error': logging.ERROR,
  88. 'fatal': logging.FATAL,
  89. 'info': logging.INFO,
  90. 'warning': logging.WARNING,
  91. }[loglevel])
  92. except KeyError:
  93. raise CommandError(f"Invalid log level: {loglevel}")
  94. # Get the script
  95. script = get_script(module, name)()
  96. # Parse the parameters
  97. form = script.as_form(data, None)
  98. script_content_type = ContentType.objects.get(app_label='extras', model='script')
  99. # Create the job result
  100. job_result = JobResult.objects.create(
  101. name=script.full_name,
  102. obj_type=script_content_type,
  103. user=User.objects.filter(is_superuser=True).order_by('pk')[0],
  104. job_id=uuid.uuid4()
  105. )
  106. request = NetBoxFakeRequest({
  107. 'META': {},
  108. 'POST': data,
  109. 'GET': {},
  110. 'FILES': {},
  111. 'user': user,
  112. 'path': '',
  113. 'id': job_result.job_id
  114. })
  115. if form.is_valid():
  116. job_result.status = JobResultStatusChoices.STATUS_RUNNING
  117. job_result.save()
  118. logger.info(f"Running script (commit={commit})")
  119. script.request = request
  120. # Execute the script. If commit is True, wrap it with the change_logging context manager to ensure we process
  121. # change logging, webhooks, etc.
  122. with change_logging(request):
  123. _run_script()
  124. else:
  125. logger.error('Data is not valid:')
  126. for field, errors in form.errors.get_json_data().items():
  127. for error in errors:
  128. logger.error(f'\t{field}: {error.get("message")}')
  129. job_result.status = JobResultStatusChoices.STATUS_ERRORED
  130. job_result.save()