|
|
@@ -0,0 +1,158 @@
|
|
|
+import json
|
|
|
+import logging
|
|
|
+import sys
|
|
|
+import traceback
|
|
|
+import uuid
|
|
|
+
|
|
|
+from django.contrib.auth.models import User
|
|
|
+from django.contrib.contenttypes.models import ContentType
|
|
|
+from django.core.management.base import BaseCommand, CommandError
|
|
|
+from django.db import transaction
|
|
|
+
|
|
|
+from extras.api.serializers import ScriptOutputSerializer
|
|
|
+from extras.choices import JobResultStatusChoices
|
|
|
+from extras.context_managers import change_logging
|
|
|
+from extras.models import JobResult
|
|
|
+from extras.scripts import get_script
|
|
|
+from utilities.exceptions import AbortTransaction
|
|
|
+from utilities.utils import NetBoxFakeRequest
|
|
|
+
|
|
|
+
|
|
|
+class Command(BaseCommand):
|
|
|
+ help = "Run a script in Netbox"
|
|
|
+
|
|
|
+ def add_arguments(self, parser):
|
|
|
+ parser.add_argument(
|
|
|
+ '--loglevel',
|
|
|
+ help="Logging Level (default: info)",
|
|
|
+ dest='loglevel',
|
|
|
+ default='info',
|
|
|
+ choices=['debug', 'info', 'warning', 'error', 'critical'])
|
|
|
+ parser.add_argument('--commit', help="Commit this script to database", action='store_true')
|
|
|
+ parser.add_argument('--user', help="User script is running as")
|
|
|
+ parser.add_argument('--data', help="Data as a string encapsulated JSON blob")
|
|
|
+ parser.add_argument('script', help="Script to run")
|
|
|
+
|
|
|
+ def handle(self, *args, **options):
|
|
|
+ def _run_script():
|
|
|
+ """
|
|
|
+ Core script execution task. We capture this within a subfunction to allow for conditionally wrapping it with
|
|
|
+ the change_logging context manager (which is bypassed if commit == False).
|
|
|
+ """
|
|
|
+ try:
|
|
|
+ with transaction.atomic():
|
|
|
+ script.output = script.run(data=data, commit=commit)
|
|
|
+ job_result.set_status(JobResultStatusChoices.STATUS_COMPLETED)
|
|
|
+
|
|
|
+ if not commit:
|
|
|
+ raise AbortTransaction()
|
|
|
+
|
|
|
+ except AbortTransaction:
|
|
|
+ script.log_info("Database changes have been reverted automatically.")
|
|
|
+
|
|
|
+ except Exception as e:
|
|
|
+ stacktrace = traceback.format_exc()
|
|
|
+ script.log_failure(
|
|
|
+ f"An exception occurred: `{type(e).__name__}: {e}`\n```\n{stacktrace}\n```"
|
|
|
+ )
|
|
|
+ script.log_info("Database changes have been reverted due to error.")
|
|
|
+ logger.error(f"Exception raised during script execution: {e}")
|
|
|
+ job_result.set_status(JobResultStatusChoices.STATUS_ERRORED)
|
|
|
+
|
|
|
+ finally:
|
|
|
+ job_result.data = ScriptOutputSerializer(script).data
|
|
|
+ job_result.save()
|
|
|
+
|
|
|
+ logger.info(f"Script completed in {job_result.duration}")
|
|
|
+
|
|
|
+ # Params
|
|
|
+ script = options['script']
|
|
|
+ loglevel = options['loglevel']
|
|
|
+ commit = options['commit']
|
|
|
+ try:
|
|
|
+ data = json.loads(options['data'])
|
|
|
+ except TypeError:
|
|
|
+ data = {}
|
|
|
+
|
|
|
+ module, name = script.split('.', 1)
|
|
|
+
|
|
|
+ # Take user from command line if provided and exists, other
|
|
|
+ if options['user']:
|
|
|
+ try:
|
|
|
+ user = User.objects.get(username=options['user'])
|
|
|
+ except User.DoesNotExist:
|
|
|
+ user = User.objects.filter(is_superuser=True).order_by('pk')[0]
|
|
|
+ else:
|
|
|
+ user = User.objects.filter(is_superuser=True).order_by('pk')[0]
|
|
|
+
|
|
|
+ # Setup logging to Stdout
|
|
|
+ formatter = logging.Formatter(f'[%(asctime)s][%(levelname)s] - %(message)s')
|
|
|
+ stdouthandler = logging.StreamHandler(sys.stdout)
|
|
|
+ stdouthandler.setLevel(logging.DEBUG)
|
|
|
+ stdouthandler.setFormatter(formatter)
|
|
|
+
|
|
|
+ logger = logging.getLogger(f"netbox.scripts.{module}.{name}")
|
|
|
+ logger.addHandler(stdouthandler)
|
|
|
+
|
|
|
+ try:
|
|
|
+ logger.setLevel({
|
|
|
+ 'critical': logging.CRITICAL,
|
|
|
+ 'debug': logging.DEBUG,
|
|
|
+ 'error': logging.ERROR,
|
|
|
+ 'fatal': logging.FATAL,
|
|
|
+ 'info': logging.INFO,
|
|
|
+ 'warning': logging.WARNING,
|
|
|
+ }[loglevel])
|
|
|
+ except KeyError:
|
|
|
+ raise CommandError(f"Invalid log level: {loglevel}")
|
|
|
+
|
|
|
+ # Get the script
|
|
|
+ script = get_script(module, name)()
|
|
|
+ # Parse the parameters
|
|
|
+ form = script.as_form(data, None)
|
|
|
+
|
|
|
+ script_content_type = ContentType.objects.get(app_label='extras', model='script')
|
|
|
+
|
|
|
+ # Delete any previous terminal state results
|
|
|
+ JobResult.objects.filter(
|
|
|
+ obj_type=script_content_type,
|
|
|
+ name=script.full_name,
|
|
|
+ status__in=JobResultStatusChoices.TERMINAL_STATE_CHOICES
|
|
|
+ ).delete()
|
|
|
+
|
|
|
+ # Create the job result
|
|
|
+ job_result = JobResult.objects.create(
|
|
|
+ name=script.full_name,
|
|
|
+ obj_type=script_content_type,
|
|
|
+ user=User.objects.filter(is_superuser=True).order_by('pk')[0],
|
|
|
+ job_id=uuid.uuid4()
|
|
|
+ )
|
|
|
+
|
|
|
+ request = NetBoxFakeRequest({
|
|
|
+ 'META': {},
|
|
|
+ 'POST': data,
|
|
|
+ 'GET': {},
|
|
|
+ 'FILES': {},
|
|
|
+ 'user': user,
|
|
|
+ 'path': '',
|
|
|
+ 'id': job_result.job_id
|
|
|
+ })
|
|
|
+
|
|
|
+ if form.is_valid():
|
|
|
+ job_result.status = JobResultStatusChoices.STATUS_RUNNING
|
|
|
+ job_result.save()
|
|
|
+
|
|
|
+ logger.info(f"Running script (commit={commit})")
|
|
|
+ script.request = request
|
|
|
+
|
|
|
+ # Execute the script. If commit is True, wrap it with the change_logging context manager to ensure we process
|
|
|
+ # change logging, webhooks, etc.
|
|
|
+ with change_logging(request):
|
|
|
+ _run_script()
|
|
|
+ else:
|
|
|
+ logger.error('Data is not valid:')
|
|
|
+ for field, errors in form.errors.get_json_data().items():
|
|
|
+ for error in errors:
|
|
|
+ logger.error(f'\t{field}: {error.get("message")}')
|
|
|
+ job_result.status = JobResultStatusChoices.STATUS_ERRORED
|
|
|
+ job_result.save()
|