scripts.py 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146
  1. import logging
  2. from django.core.files.storage import storages
  3. from django.db import IntegrityError
  4. from django.utils.translation import gettext_lazy as _
  5. from drf_spectacular.utils import extend_schema_field
  6. from rest_framework import serializers
  7. from core.api.serializers_.jobs import JobSerializer
  8. from core.choices import ManagedFileRootPathChoices
  9. from extras.models import Script, ScriptModule
  10. from netbox.api.serializers import ValidatedModelSerializer
  11. from utilities.datetime import local_now
  12. logger = logging.getLogger(__name__)
  13. __all__ = (
  14. 'ScriptDetailSerializer',
  15. 'ScriptInputSerializer',
  16. 'ScriptModuleSerializer',
  17. 'ScriptSerializer',
  18. )
  19. class ScriptModuleSerializer(ValidatedModelSerializer):
  20. file = serializers.FileField(write_only=True)
  21. file_path = serializers.CharField(read_only=True)
  22. class Meta:
  23. model = ScriptModule
  24. fields = ['id', 'display', 'file_path', 'file', 'created', 'last_updated']
  25. brief_fields = ('id', 'display')
  26. def validate(self, data):
  27. # ScriptModule.save() sets file_root; inject it here so full_clean() succeeds.
  28. # Pop 'file' before model instantiation — ScriptModule has no such field.
  29. file = data.pop('file', None)
  30. data['file_root'] = ManagedFileRootPathChoices.SCRIPTS
  31. data = super().validate(data)
  32. data.pop('file_root', None)
  33. if file is not None:
  34. data['file'] = file
  35. return data
  36. def create(self, validated_data):
  37. file = validated_data.pop('file')
  38. storage = storages.create_storage(storages.backends["scripts"])
  39. validated_data['file_path'] = storage.save(file.name, file)
  40. created = False
  41. try:
  42. instance = super().create(validated_data)
  43. created = True
  44. return instance
  45. except IntegrityError as e:
  46. if 'file_path' in str(e):
  47. raise serializers.ValidationError(
  48. _("A script module with this file name already exists.")
  49. )
  50. raise
  51. finally:
  52. if not created and (file_path := validated_data.get('file_path')):
  53. try:
  54. storage.delete(file_path)
  55. except Exception:
  56. logger.warning(f"Failed to delete orphaned script file '{file_path}' from storage.")
  57. class ScriptSerializer(ValidatedModelSerializer):
  58. description = serializers.SerializerMethodField(read_only=True)
  59. vars = serializers.SerializerMethodField(read_only=True)
  60. result = JobSerializer(nested=True, read_only=True)
  61. class Meta:
  62. model = Script
  63. fields = [
  64. 'id', 'url', 'display_url', 'module', 'name', 'description', 'vars', 'result', 'display', 'is_executable',
  65. ]
  66. brief_fields = ('id', 'url', 'display', 'name', 'description')
  67. @extend_schema_field(serializers.JSONField(allow_null=True))
  68. def get_vars(self, obj):
  69. if obj.python_class:
  70. return {
  71. k: v.__class__.__name__ for k, v in obj.python_class()._get_vars().items()
  72. }
  73. return {}
  74. @extend_schema_field(serializers.CharField())
  75. def get_display(self, obj):
  76. return f'{obj.name} ({obj.module})'
  77. @extend_schema_field(serializers.CharField(allow_null=True))
  78. def get_description(self, obj):
  79. if obj.python_class:
  80. return obj.python_class().description
  81. return None
  82. class ScriptDetailSerializer(ScriptSerializer):
  83. result = serializers.SerializerMethodField(read_only=True)
  84. @extend_schema_field(JobSerializer())
  85. def get_result(self, obj):
  86. job = obj.jobs.all().order_by('-created').first()
  87. context = {
  88. 'request': self.context['request']
  89. }
  90. data = JobSerializer(job, context=context).data
  91. return data
  92. class ScriptInputSerializer(serializers.Serializer):
  93. data = serializers.JSONField()
  94. commit = serializers.BooleanField()
  95. schedule_at = serializers.DateTimeField(required=False, allow_null=True)
  96. interval = serializers.IntegerField(required=False, allow_null=True)
  97. def validate_schedule_at(self, value):
  98. """
  99. Validates the specified schedule time for a script execution.
  100. """
  101. if value:
  102. if not self.context['script'].python_class.scheduling_enabled:
  103. raise serializers.ValidationError(_('Scheduling is not enabled for this script.'))
  104. if value < local_now():
  105. raise serializers.ValidationError(_('Scheduled time must be in the future.'))
  106. return value
  107. def validate_interval(self, value):
  108. """
  109. Validates the provided interval based on the script's scheduling configuration.
  110. """
  111. if value and not self.context['script'].python_class.scheduling_enabled:
  112. raise serializers.ValidationError(_('Scheduling is not enabled for this script.'))
  113. return value
  114. def validate(self, data):
  115. """
  116. Validates the given data and ensures the necessary fields are populated.
  117. """
  118. # Set the schedule_at time to now if only an interval is provided
  119. # while handling the case where schedule_at is null.
  120. if data.get('interval') and not data.get('schedule_at'):
  121. data['schedule_at'] = local_now()
  122. return super().validate(data)