Browse Source

Add scheduling_enabled parameter for scripts

jeremystretch 2 years ago
parent
commit
197c6a1cbf

+ 4 - 0
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
 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`
 ### `job_timeout`
 
 
 Set the maximum allowed runtime for the script. If not set, `RQ_DEFAULT_TIMEOUT` will be used.
 Set the maximum allowed runtime for the script. If not set, `RQ_DEFAULT_TIMEOUT` will be used.

+ 0 - 1
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 import generic
 from netbox.views.generic.base import BaseObjectView
 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.utils import count_related
 from utilities.views import register_model_view
 from utilities.views import register_model_view
 from . import filtersets, forms, tables
 from . import filtersets, forms, tables

+ 10 - 0
netbox/extras/api/serializers.py

@@ -478,6 +478,16 @@ class ScriptInputSerializer(serializers.Serializer):
     schedule_at = serializers.DateTimeField(required=False, allow_null=True)
     schedule_at = serializers.DateTimeField(required=False, allow_null=True)
     interval = serializers.IntegerField(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):
 class ScriptLogMessageSerializer(serializers.Serializer):
     status = serializers.SerializerMethodField(read_only=True)
     status = serializers.SerializerMethodField(read_only=True)

+ 4 - 1
netbox/extras/api/views.py

@@ -329,7 +329,10 @@ class ScriptViewSet(ViewSet):
             raise PermissionDenied("This user does not have permission to run scripts.")
             raise PermissionDenied("This user does not have permission to run scripts.")
 
 
         module, script = self._get_script(pk)
         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
         # Check that at least one RQ worker is running
         if not Worker.count(get_connection('default')):
         if not Worker.count(get_connection('default')):

+ 6 - 9
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)")
         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)
         super().__init__(*args, **kwargs)
 
 
         # Annotate the current system time for reference
         # Annotate the current system time for reference
         now = local_now().strftime('%Y-%m-%d %H:%M:%S')
         now = local_now().strftime('%Y-%m-%d %H:%M:%S')
         self.fields['_schedule_at'].help_text += f' (current time: <strong>{now}</strong>)'
         self.fields['_schedule_at'].help_text += f' (current time: <strong>{now}</strong>)'
 
 
-        # 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):
     def clean(self):
         scheduled_time = self.cleaned_data['_schedule_at']
         scheduled_time = self.cleaned_data['_schedule_at']
         if scheduled_time and scheduled_time < local_now():
         if scheduled_time and scheduled_time < local_now():
             raise forms.ValidationError(_('Scheduled time must be in the future.'))
             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:
         if self.cleaned_data['_interval'] and not scheduled_time:
             self.cleaned_data['_schedule_at'] = local_now()
             self.cleaned_data['_schedule_at'] = local_now()
 
 

+ 41 - 19
netbox/extras/scripts.py

@@ -297,6 +297,12 @@ class BaseScript:
     def full_name(self):
     def full_name(self):
         return f'{self.module}.{self.class_name}'
         return f'{self.module}.{self.class_name}'
 
 
+    @classmethod
+    def root_module(cls):
+        return cls.__module__.split(".")[0]
+
+    # Author-defined attributes
+
     @classproperty
     @classproperty
     def name(self):
     def name(self):
         return getattr(self.Meta, 'name', self.__name__)
         return getattr(self.Meta, 'name', self.__name__)
@@ -305,14 +311,26 @@ class BaseScript:
     def description(self):
     def description(self):
         return getattr(self.Meta, 'description', '')
         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
     @classproperty
     def job_timeout(self):
     def job_timeout(self):
         return getattr(self.Meta, 'job_timeout', None)
         return getattr(self.Meta, 'job_timeout', None)
 
 
+    @classproperty
+    def scheduling_enabled(self):
+        return getattr(self.Meta, 'scheduling_enabled', True)
+
     @classmethod
     @classmethod
     def _get_vars(cls):
     def _get_vars(cls):
         vars = {}
         vars = {}
@@ -328,11 +346,10 @@ class BaseScript:
                     vars[name] = attr
                     vars[name] = attr
 
 
         # Order variables according to field_order
         # 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
             return vars
         ordered_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)
         ordered_vars.update(vars)
 
 
@@ -341,6 +358,23 @@ class BaseScript:
     def run(self, data, commit):
     def run(self, data, commit):
         raise NotImplementedError("The script must define a run() method.")
         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):
     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.
         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)
         form = FormClass(data, files, initial=initial)
 
 
         # Set initial "commit" checkbox state based on the script's Meta parameter
         # 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
         return form
 
 

+ 11 - 19
netbox/templates/extras/script.html

@@ -16,27 +16,19 @@
         {% csrf_token %}
         {% csrf_token %}
         <div class="field-group my-4">
         <div class="field-group my-4">
           {% if form.requires_input %}
           {% if form.requires_input %}
-            {% if script.Meta.fieldsets %}
-              {# Render grouped fields according to declared fieldsets #}
-              {% for group, fields in script.Meta.fieldsets %}
-                <div class="field-group mb-5">
-                  <div class="row mb-2">
-                    <h5 class="offset-sm-3">{{ group }}</h5>
-                  </div>
-                  {% 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 %}
+              <div class="field-group mb-5">
+                <div class="row mb-2">
+                  <h5 class="offset-sm-3">{{ group }}</h5>
                 </div>
                 </div>
-              {% endfor %}
-            {% else %}
-              {# Render all fields as a single group #}
-              <div class="row mb-2">
-                <h5 class="offset-sm-3">Script Data</h5>
+                {% for name in fields %}
+                  {% with field=form|getfield:name %}
+                    {% render_field field %}
+                  {% endwith %}
+                {% endfor %}
               </div>
               </div>
-              {% render_form form %}
-            {% endif %}
+            {% endfor %}
           {% else %}
           {% else %}
             <div class="alert alert-info">
             <div class="alert alert-info">
               <i class="mdi mdi-information"></i>
               <i class="mdi mdi-information"></i>