2019-08-09 12:33:33 -04:00
|
|
|
from collections import OrderedDict
|
|
|
|
import inspect
|
|
|
|
import pkgutil
|
|
|
|
|
|
|
|
from django import forms
|
|
|
|
from django.conf import settings
|
2019-08-09 13:56:37 -04:00
|
|
|
from django.core.validators import RegexValidator
|
2019-08-12 13:51:25 -04:00
|
|
|
from django.db import transaction
|
2019-08-09 12:33:33 -04:00
|
|
|
|
2019-08-12 14:28:06 -04:00
|
|
|
from utilities.exceptions import AbortTransaction
|
2019-08-09 12:33:33 -04:00
|
|
|
from .constants import LOG_DEFAULT, LOG_FAILURE, LOG_INFO, LOG_SUCCESS, LOG_WARNING
|
|
|
|
from .forms import ScriptForm
|
|
|
|
|
|
|
|
|
2019-08-09 16:34:01 -04:00
|
|
|
__all__ = [
|
|
|
|
'Script',
|
|
|
|
'StringVar',
|
|
|
|
'IntegerVar',
|
|
|
|
'BooleanVar',
|
|
|
|
'ObjectVar',
|
|
|
|
]
|
|
|
|
|
|
|
|
|
2019-08-09 12:33:33 -04:00
|
|
|
#
|
|
|
|
# Script variables
|
|
|
|
#
|
|
|
|
|
|
|
|
class ScriptVariable:
|
2019-08-09 13:56:37 -04:00
|
|
|
"""
|
|
|
|
Base model for script variables
|
|
|
|
"""
|
2019-08-09 12:33:33 -04:00
|
|
|
form_field = forms.CharField
|
|
|
|
|
2019-08-09 13:56:37 -04:00
|
|
|
def __init__(self, label='', description='', default=None, required=True):
|
2019-08-09 12:33:33 -04:00
|
|
|
|
|
|
|
# Default field attributes
|
2019-08-09 13:56:37 -04:00
|
|
|
self.field_attrs = {
|
|
|
|
'help_text': description,
|
|
|
|
'required': required
|
|
|
|
}
|
2019-08-09 12:33:33 -04:00
|
|
|
if label:
|
|
|
|
self.field_attrs['label'] = label
|
2019-08-09 13:56:37 -04:00
|
|
|
if default:
|
|
|
|
self.field_attrs['initial'] = default
|
2019-08-09 12:33:33 -04:00
|
|
|
|
|
|
|
def as_field(self):
|
|
|
|
"""
|
|
|
|
Render the variable as a Django form field.
|
|
|
|
"""
|
|
|
|
return self.form_field(**self.field_attrs)
|
|
|
|
|
|
|
|
|
|
|
|
class StringVar(ScriptVariable):
|
2019-08-09 13:56:37 -04:00
|
|
|
"""
|
|
|
|
Character string representation. Can enforce minimum/maximum length and/or regex validation.
|
|
|
|
"""
|
|
|
|
def __init__(self, min_length=None, max_length=None, regex=None, *args, **kwargs):
|
|
|
|
super().__init__(*args, **kwargs)
|
|
|
|
|
|
|
|
# Optional minimum/maximum lengths
|
|
|
|
if min_length:
|
|
|
|
self.field_attrs['min_length'] = min_length
|
|
|
|
if max_length:
|
|
|
|
self.field_attrs['max_length'] = max_length
|
|
|
|
|
|
|
|
# Optional regular expression validation
|
|
|
|
if regex:
|
|
|
|
self.field_attrs['validators'] = [
|
|
|
|
RegexValidator(
|
|
|
|
regex=regex,
|
|
|
|
message='Invalid value. Must match regex: {}'.format(regex),
|
|
|
|
code='invalid'
|
|
|
|
)
|
|
|
|
]
|
2019-08-09 12:33:33 -04:00
|
|
|
|
|
|
|
|
|
|
|
class IntegerVar(ScriptVariable):
|
2019-08-09 13:56:37 -04:00
|
|
|
"""
|
|
|
|
Integer representation. Can enforce minimum/maximum values.
|
|
|
|
"""
|
2019-08-09 12:33:33 -04:00
|
|
|
form_field = forms.IntegerField
|
|
|
|
|
2019-08-09 13:56:37 -04:00
|
|
|
def __init__(self, min_value=None, max_value=None, *args, **kwargs):
|
|
|
|
super().__init__(*args, **kwargs)
|
|
|
|
|
|
|
|
# Optional minimum/maximum values
|
|
|
|
if min_value:
|
|
|
|
self.field_attrs['min_value'] = min_value
|
|
|
|
if max_value:
|
|
|
|
self.field_attrs['max_value'] = max_value
|
|
|
|
|
2019-08-09 12:33:33 -04:00
|
|
|
|
|
|
|
class BooleanVar(ScriptVariable):
|
2019-08-09 13:56:37 -04:00
|
|
|
"""
|
|
|
|
Boolean representation (true/false). Renders as a checkbox.
|
|
|
|
"""
|
2019-08-09 16:45:00 -04:00
|
|
|
form_field = forms.BooleanField
|
|
|
|
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
|
|
super().__init__(*args, **kwargs)
|
|
|
|
|
|
|
|
# Boolean fields cannot be required
|
|
|
|
self.field_attrs['required'] = False
|
2019-08-09 12:33:33 -04:00
|
|
|
|
|
|
|
|
|
|
|
class ObjectVar(ScriptVariable):
|
2019-08-09 13:56:37 -04:00
|
|
|
"""
|
|
|
|
NetBox object representation. The provided QuerySet will determine the choices available.
|
|
|
|
"""
|
2019-08-09 12:33:33 -04:00
|
|
|
form_field = forms.ModelChoiceField
|
|
|
|
|
|
|
|
def __init__(self, queryset, *args, **kwargs):
|
|
|
|
super().__init__(*args, **kwargs)
|
|
|
|
|
2019-08-09 13:56:37 -04:00
|
|
|
# Queryset for field choices
|
2019-08-09 12:33:33 -04:00
|
|
|
self.field_attrs['queryset'] = queryset
|
|
|
|
|
|
|
|
|
|
|
|
class Script:
|
|
|
|
"""
|
|
|
|
Custom scripts inherit this object.
|
|
|
|
"""
|
2019-08-12 13:16:18 -04:00
|
|
|
class Meta:
|
|
|
|
pass
|
|
|
|
|
2019-08-09 12:33:33 -04:00
|
|
|
def __init__(self):
|
|
|
|
|
|
|
|
# Initiate the log
|
|
|
|
self.log = []
|
|
|
|
|
|
|
|
# Grab some info about the script
|
|
|
|
self.filename = inspect.getfile(self.__class__)
|
|
|
|
self.source = inspect.getsource(self.__class__)
|
|
|
|
|
|
|
|
def __str__(self):
|
2019-08-12 13:16:18 -04:00
|
|
|
return getattr(self.Meta, 'name', self.__class__.__name__)
|
2019-08-09 12:33:33 -04:00
|
|
|
|
|
|
|
def _get_vars(self):
|
2019-08-09 16:34:01 -04:00
|
|
|
vars = OrderedDict()
|
|
|
|
|
2019-08-12 13:16:18 -04:00
|
|
|
# Infer order from Meta.fields (Python 3.5 and lower)
|
2019-08-12 16:59:09 -04:00
|
|
|
fields = getattr(self.Meta, 'fields', [])
|
2019-08-12 13:16:18 -04:00
|
|
|
for name in fields:
|
|
|
|
vars[name] = getattr(self, name)
|
2019-08-09 16:34:01 -04:00
|
|
|
|
|
|
|
# Default to order of declaration on class
|
|
|
|
for name, attr in self.__class__.__dict__.items():
|
|
|
|
if name not in vars and issubclass(attr.__class__, ScriptVariable):
|
|
|
|
vars[name] = attr
|
|
|
|
|
|
|
|
return vars
|
2019-08-09 12:33:33 -04:00
|
|
|
|
2019-08-09 13:56:37 -04:00
|
|
|
def run(self, data):
|
2019-08-09 12:33:33 -04:00
|
|
|
raise NotImplementedError("The script must define a run() method.")
|
|
|
|
|
|
|
|
def as_form(self, data=None):
|
|
|
|
"""
|
|
|
|
Return a Django form suitable for populating the context data required to run this Script.
|
|
|
|
"""
|
|
|
|
vars = self._get_vars()
|
|
|
|
form = ScriptForm(vars, data)
|
|
|
|
|
|
|
|
return form
|
|
|
|
|
|
|
|
# Logging
|
|
|
|
|
|
|
|
def log_debug(self, message):
|
|
|
|
self.log.append((LOG_DEFAULT, message))
|
|
|
|
|
|
|
|
def log_success(self, message):
|
|
|
|
self.log.append((LOG_SUCCESS, message))
|
|
|
|
|
|
|
|
def log_info(self, message):
|
|
|
|
self.log.append((LOG_INFO, message))
|
|
|
|
|
|
|
|
def log_warning(self, message):
|
|
|
|
self.log.append((LOG_WARNING, message))
|
|
|
|
|
|
|
|
def log_failure(self, message):
|
|
|
|
self.log.append((LOG_FAILURE, message))
|
|
|
|
|
|
|
|
|
|
|
|
#
|
|
|
|
# Functions
|
|
|
|
#
|
|
|
|
|
|
|
|
def is_script(obj):
|
|
|
|
"""
|
|
|
|
Returns True if the object is a Script.
|
|
|
|
"""
|
|
|
|
return obj in Script.__subclasses__()
|
|
|
|
|
|
|
|
|
|
|
|
def is_variable(obj):
|
|
|
|
"""
|
|
|
|
Returns True if the object is a ScriptVariable.
|
|
|
|
"""
|
|
|
|
return isinstance(obj, ScriptVariable)
|
|
|
|
|
|
|
|
|
2019-08-12 14:28:06 -04:00
|
|
|
def run_script(script, data, commit=True):
|
2019-08-12 13:51:25 -04:00
|
|
|
"""
|
2019-08-12 14:28:06 -04:00
|
|
|
A wrapper for calling Script.run(). This performs error handling and provides a hook for committing changes. It
|
|
|
|
exists outside of the Script class to ensure it cannot be overridden by a script author.
|
2019-08-12 13:51:25 -04:00
|
|
|
"""
|
2019-08-12 14:28:06 -04:00
|
|
|
output = None
|
|
|
|
|
2019-08-12 13:51:25 -04:00
|
|
|
try:
|
|
|
|
with transaction.atomic():
|
2019-08-12 14:28:06 -04:00
|
|
|
output = script.run(data)
|
|
|
|
if not commit:
|
|
|
|
raise AbortTransaction()
|
|
|
|
except AbortTransaction:
|
|
|
|
pass
|
2019-08-12 13:51:25 -04:00
|
|
|
except Exception as e:
|
|
|
|
script.log_failure(
|
2019-08-12 14:28:06 -04:00
|
|
|
"An exception occurred. {}: {}".format(type(e).__name__, e)
|
2019-08-12 13:51:25 -04:00
|
|
|
)
|
2019-08-12 14:28:06 -04:00
|
|
|
commit = False
|
|
|
|
finally:
|
|
|
|
if not commit:
|
|
|
|
script.log_info(
|
|
|
|
"Database changes have been reverted automatically."
|
|
|
|
)
|
|
|
|
|
|
|
|
return output
|
2019-08-12 13:51:25 -04:00
|
|
|
|
|
|
|
|
2019-08-09 12:33:33 -04:00
|
|
|
def get_scripts():
|
|
|
|
scripts = OrderedDict()
|
|
|
|
|
|
|
|
# Iterate through all modules within the reports path. These are the user-created files in which reports are
|
|
|
|
# defined.
|
|
|
|
for importer, module_name, _ in pkgutil.iter_modules([settings.SCRIPTS_ROOT]):
|
|
|
|
module = importer.find_module(module_name).load_module(module_name)
|
2019-08-13 09:09:12 -04:00
|
|
|
if hasattr(module, 'name'):
|
|
|
|
module_name = module.name
|
2019-08-09 12:33:33 -04:00
|
|
|
module_scripts = OrderedDict()
|
|
|
|
for name, cls in inspect.getmembers(module, is_script):
|
|
|
|
module_scripts[name] = cls
|
|
|
|
scripts[module_name] = module_scripts
|
|
|
|
|
|
|
|
return scripts
|