scripts.py 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256
  1. from collections import OrderedDict
  2. import inspect
  3. import pkgutil
  4. from django import forms
  5. from django.conf import settings
  6. from django.core.validators import RegexValidator
  7. from django.db import transaction
  8. from ipam.formfields import IPFormField
  9. from utilities.exceptions import AbortTransaction
  10. from .constants import LOG_DEFAULT, LOG_FAILURE, LOG_INFO, LOG_SUCCESS, LOG_WARNING
  11. from .forms import ScriptForm
  12. __all__ = [
  13. 'Script',
  14. 'StringVar',
  15. 'IntegerVar',
  16. 'BooleanVar',
  17. 'ObjectVar',
  18. 'IPNetworkVar',
  19. ]
  20. #
  21. # Script variables
  22. #
  23. class ScriptVariable:
  24. """
  25. Base model for script variables
  26. """
  27. form_field = forms.CharField
  28. def __init__(self, label='', description='', default=None, required=True):
  29. # Default field attributes
  30. self.field_attrs = {
  31. 'help_text': description,
  32. 'required': required
  33. }
  34. if label:
  35. self.field_attrs['label'] = label
  36. if default:
  37. self.field_attrs['initial'] = default
  38. def as_field(self):
  39. """
  40. Render the variable as a Django form field.
  41. """
  42. return self.form_field(**self.field_attrs)
  43. class StringVar(ScriptVariable):
  44. """
  45. Character string representation. Can enforce minimum/maximum length and/or regex validation.
  46. """
  47. def __init__(self, min_length=None, max_length=None, regex=None, *args, **kwargs):
  48. super().__init__(*args, **kwargs)
  49. # Optional minimum/maximum lengths
  50. if min_length:
  51. self.field_attrs['min_length'] = min_length
  52. if max_length:
  53. self.field_attrs['max_length'] = max_length
  54. # Optional regular expression validation
  55. if regex:
  56. self.field_attrs['validators'] = [
  57. RegexValidator(
  58. regex=regex,
  59. message='Invalid value. Must match regex: {}'.format(regex),
  60. code='invalid'
  61. )
  62. ]
  63. class IntegerVar(ScriptVariable):
  64. """
  65. Integer representation. Can enforce minimum/maximum values.
  66. """
  67. form_field = forms.IntegerField
  68. def __init__(self, min_value=None, max_value=None, *args, **kwargs):
  69. super().__init__(*args, **kwargs)
  70. # Optional minimum/maximum values
  71. if min_value:
  72. self.field_attrs['min_value'] = min_value
  73. if max_value:
  74. self.field_attrs['max_value'] = max_value
  75. class BooleanVar(ScriptVariable):
  76. """
  77. Boolean representation (true/false). Renders as a checkbox.
  78. """
  79. form_field = forms.BooleanField
  80. def __init__(self, *args, **kwargs):
  81. super().__init__(*args, **kwargs)
  82. # Boolean fields cannot be required
  83. self.field_attrs['required'] = False
  84. class ObjectVar(ScriptVariable):
  85. """
  86. NetBox object representation. The provided QuerySet will determine the choices available.
  87. """
  88. form_field = forms.ModelChoiceField
  89. def __init__(self, queryset, *args, **kwargs):
  90. super().__init__(*args, **kwargs)
  91. # Queryset for field choices
  92. self.field_attrs['queryset'] = queryset
  93. class IPNetworkVar(ScriptVariable):
  94. """
  95. An IPv4 or IPv6 prefix.
  96. """
  97. form_field = IPFormField
  98. #
  99. # Scripts
  100. #
  101. class Script:
  102. """
  103. Custom scripts inherit this object.
  104. """
  105. class Meta:
  106. pass
  107. def __init__(self):
  108. # Initiate the log
  109. self.log = []
  110. # Grab some info about the script
  111. self.filename = inspect.getfile(self.__class__)
  112. self.source = inspect.getsource(self.__class__)
  113. def __str__(self):
  114. return getattr(self.Meta, 'name', self.__class__.__name__)
  115. def _get_vars(self):
  116. vars = OrderedDict()
  117. # Infer order from Meta.fields (Python 3.5 and lower)
  118. fields = getattr(self.Meta, 'fields', [])
  119. for name in fields:
  120. vars[name] = getattr(self, name)
  121. # Default to order of declaration on class
  122. for name, attr in self.__class__.__dict__.items():
  123. if name not in vars and issubclass(attr.__class__, ScriptVariable):
  124. vars[name] = attr
  125. return vars
  126. def run(self, data):
  127. raise NotImplementedError("The script must define a run() method.")
  128. def as_form(self, data=None):
  129. """
  130. Return a Django form suitable for populating the context data required to run this Script.
  131. """
  132. vars = self._get_vars()
  133. form = ScriptForm(vars, data)
  134. return form
  135. # Logging
  136. def log_debug(self, message):
  137. self.log.append((LOG_DEFAULT, message))
  138. def log_success(self, message):
  139. self.log.append((LOG_SUCCESS, message))
  140. def log_info(self, message):
  141. self.log.append((LOG_INFO, message))
  142. def log_warning(self, message):
  143. self.log.append((LOG_WARNING, message))
  144. def log_failure(self, message):
  145. self.log.append((LOG_FAILURE, message))
  146. #
  147. # Functions
  148. #
  149. def is_script(obj):
  150. """
  151. Returns True if the object is a Script.
  152. """
  153. return obj in Script.__subclasses__()
  154. def is_variable(obj):
  155. """
  156. Returns True if the object is a ScriptVariable.
  157. """
  158. return isinstance(obj, ScriptVariable)
  159. def run_script(script, data, commit=True):
  160. """
  161. A wrapper for calling Script.run(). This performs error handling and provides a hook for committing changes. It
  162. exists outside of the Script class to ensure it cannot be overridden by a script author.
  163. """
  164. output = None
  165. try:
  166. with transaction.atomic():
  167. output = script.run(data)
  168. if not commit:
  169. raise AbortTransaction()
  170. except AbortTransaction:
  171. pass
  172. except Exception as e:
  173. script.log_failure(
  174. "An exception occurred. {}: {}".format(type(e).__name__, e)
  175. )
  176. commit = False
  177. finally:
  178. if not commit:
  179. script.log_info(
  180. "Database changes have been reverted automatically."
  181. )
  182. return output
  183. def get_scripts():
  184. scripts = OrderedDict()
  185. # Iterate through all modules within the reports path. These are the user-created files in which reports are
  186. # defined.
  187. for importer, module_name, _ in pkgutil.iter_modules([settings.SCRIPTS_ROOT]):
  188. module = importer.find_module(module_name).load_module(module_name)
  189. if hasattr(module, 'name'):
  190. module_name = module.name
  191. module_scripts = OrderedDict()
  192. for name, cls in inspect.getmembers(module, is_script):
  193. module_scripts[name] = cls
  194. scripts[module_name] = module_scripts
  195. return scripts