diff --git a/docs/customization/custom-scripts.md b/docs/customization/custom-scripts.md index 54cac48ec..3b56a8459 100644 --- a/docs/customization/custom-scripts.md +++ b/docs/customization/custom-scripts.md @@ -104,6 +104,10 @@ The checkbox to commit database changes when executing a script is checked by de commit_default = False ``` +### `scheduling_enabled` + +By default, a script can be scheduled for execution at a later time. Setting `scheduling_enabled` to False disables this ability: Only immediate execution will be possible. (This also disables the ability to set a recurring execution interval.) + ### `job_timeout` Set the maximum allowed runtime for the script. If not set, `RQ_DEFAULT_TIMEOUT` will be used. diff --git a/netbox/core/views.py b/netbox/core/views.py index 9a5eb0702..d3dc2b1c2 100644 --- a/netbox/core/views.py +++ b/netbox/core/views.py @@ -3,7 +3,6 @@ from django.shortcuts import get_object_or_404, redirect from netbox.views import generic from netbox.views.generic.base import BaseObjectView -from utilities.rqworker import get_queue_for_model, get_workers_for_queue from utilities.utils import count_related from utilities.views import register_model_view from . import filtersets, forms, tables diff --git a/netbox/extras/api/serializers.py b/netbox/extras/api/serializers.py index 6391b294d..be48aed9b 100644 --- a/netbox/extras/api/serializers.py +++ b/netbox/extras/api/serializers.py @@ -478,6 +478,16 @@ class ScriptInputSerializer(serializers.Serializer): schedule_at = serializers.DateTimeField(required=False, allow_null=True) interval = serializers.IntegerField(required=False, allow_null=True) + def validate_schedule_at(self, value): + if value and not self.context['script'].scheduling_enabled: + raise serializers.ValidationError("Scheduling is not enabled for this script.") + return value + + def validate_interval(self, value): + if value and not self.context['script'].scheduling_enabled: + raise serializers.ValidationError("Scheduling is not enabled for this script.") + return value + class ScriptLogMessageSerializer(serializers.Serializer): status = serializers.SerializerMethodField(read_only=True) diff --git a/netbox/extras/api/views.py b/netbox/extras/api/views.py index e132c0327..645100169 100644 --- a/netbox/extras/api/views.py +++ b/netbox/extras/api/views.py @@ -329,7 +329,10 @@ class ScriptViewSet(ViewSet): raise PermissionDenied("This user does not have permission to run scripts.") module, script = self._get_script(pk) - input_serializer = serializers.ScriptInputSerializer(data=request.data) + input_serializer = serializers.ScriptInputSerializer( + data=request.data, + context={'script': script} + ) # Check that at least one RQ worker is running if not Worker.count(get_connection('default')): diff --git a/netbox/extras/forms/scripts.py b/netbox/extras/forms/scripts.py index 29e5f47ab..5677db536 100644 --- a/netbox/extras/forms/scripts.py +++ b/netbox/extras/forms/scripts.py @@ -31,27 +31,24 @@ class ScriptForm(BootstrapMixin, forms.Form): help_text=_("Interval at which this script is re-run (in minutes)") ) - def __init__(self, *args, **kwargs): + def __init__(self, *args, scheduling_enabled=True, **kwargs): super().__init__(*args, **kwargs) # Annotate the current system time for reference now = local_now().strftime('%Y-%m-%d %H:%M:%S') self.fields['_schedule_at'].help_text += f' (current time: {now})' - # Move _commit and _schedule_at to the end of the form - schedule_at = self.fields.pop('_schedule_at') - interval = self.fields.pop('_interval') - commit = self.fields.pop('_commit') - self.fields['_schedule_at'] = schedule_at - self.fields['_interval'] = interval - self.fields['_commit'] = commit + # Remove scheduling fields if scheduling is disabled + if not scheduling_enabled: + self.fields.pop('_schedule_at') + self.fields.pop('_interval') def clean(self): scheduled_time = self.cleaned_data['_schedule_at'] if scheduled_time and scheduled_time < local_now(): raise forms.ValidationError(_('Scheduled time must be in the future.')) - # When interval is used without schedule at, raise an exception + # When interval is used without schedule at, schedule for the current time if self.cleaned_data['_interval'] and not scheduled_time: self.cleaned_data['_schedule_at'] = local_now() diff --git a/netbox/extras/scripts.py b/netbox/extras/scripts.py index db94b6cbf..cebc57af4 100644 --- a/netbox/extras/scripts.py +++ b/netbox/extras/scripts.py @@ -297,6 +297,12 @@ class BaseScript: def full_name(self): return f'{self.module}.{self.class_name}' + @classmethod + def root_module(cls): + return cls.__module__.split(".")[0] + + # Author-defined attributes + @classproperty def name(self): return getattr(self.Meta, 'name', self.__name__) @@ -305,14 +311,26 @@ class BaseScript: def description(self): return getattr(self.Meta, 'description', '') - @classmethod - def root_module(cls): - return cls.__module__.split(".")[0] + @classproperty + def field_order(self): + return getattr(self.Meta, 'field_order', None) + + @classproperty + def fieldsets(self): + return getattr(self.Meta, 'fieldsets', None) + + @classproperty + def commit_default(self): + return getattr(self.Meta, 'commit_default', True) @classproperty def job_timeout(self): return getattr(self.Meta, 'job_timeout', None) + @classproperty + def scheduling_enabled(self): + return getattr(self.Meta, 'scheduling_enabled', True) + @classmethod def _get_vars(cls): vars = {} @@ -328,11 +346,10 @@ class BaseScript: vars[name] = attr # Order variables according to field_order - field_order = getattr(cls.Meta, 'field_order', None) - if not field_order: + if not cls.field_order: return vars ordered_vars = { - field: vars.pop(field) for field in field_order if field in vars + field: vars.pop(field) for field in cls.field_order if field in vars } ordered_vars.update(vars) @@ -341,6 +358,23 @@ class BaseScript: def run(self, data, commit): raise NotImplementedError("The script must define a run() method.") + # Form rendering + + def get_fieldsets(self): + fieldsets = [] + + if self.fieldsets: + fieldsets.extend(self.fieldsets) + else: + fields = (name for name, _ in self._get_vars().items()) + fieldsets.append(('Script Data', fields)) + + # Append the default fieldset if defined in the Meta class + exec_parameters = ('_schedule_at', '_interval', '_commit') if self.scheduling_enabled else ('_commit',) + fieldsets.append(('Script Execution Parameters', exec_parameters)) + + return fieldsets + def as_form(self, data=None, files=None, initial=None): """ Return a Django form suitable for populating the context data required to run this Script. @@ -354,19 +388,7 @@ class BaseScript: form = FormClass(data, files, initial=initial) # Set initial "commit" checkbox state based on the script's Meta parameter - form.fields['_commit'].initial = getattr(self.Meta, 'commit_default', True) - - # Append the default fieldset if defined in the Meta class - default_fieldset = ( - ('Script Execution Parameters', ('_schedule_at', '_interval', '_commit')), - ) - if not hasattr(self.Meta, 'fieldsets'): - fields = ( - name for name, _ in self._get_vars().items() - ) - self.Meta.fieldsets = (('Script Data', fields),) - - self.Meta.fieldsets += default_fieldset + form.fields['_commit'].initial = self.commit_default return form diff --git a/netbox/templates/extras/script.html b/netbox/templates/extras/script.html index 0f74d6091..b7ef2a908 100644 --- a/netbox/templates/extras/script.html +++ b/netbox/templates/extras/script.html @@ -16,27 +16,19 @@ {% csrf_token %}
{% if form.requires_input %} - {% if script.Meta.fieldsets %} - {# Render grouped fields according to declared fieldsets #} - {% for group, fields in script.Meta.fieldsets %} -
-
-
{{ group }}
-
- {% for name in fields %} - {% with field=form|getfield:name %} - {% render_field field %} - {% endwith %} - {% endfor %} + {# Render grouped fields according to declared fieldsets #} + {% for group, fields in script.get_fieldsets %} +
+
+
{{ group }}
- {% endfor %} - {% else %} - {# Render all fields as a single group #} -
-
Script Data
+ {% for name in fields %} + {% with field=form|getfield:name %} + {% render_field field %} + {% endwith %} + {% endfor %}
- {% render_form form %} - {% endif %} + {% endfor %} {% else %}