mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-14 01:41:22 -06:00
Add scheduling_enabled parameter for scripts
This commit is contained in:
parent
014a5d10d1
commit
197c6a1cbf
@ -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.
|
||||||
|
@ -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
|
||||||
|
@ -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)
|
||||||
|
@ -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')):
|
||||||
|
@ -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
|
# Remove scheduling fields if scheduling is disabled
|
||||||
schedule_at = self.fields.pop('_schedule_at')
|
if not scheduling_enabled:
|
||||||
interval = self.fields.pop('_interval')
|
self.fields.pop('_schedule_at')
|
||||||
commit = self.fields.pop('_commit')
|
self.fields.pop('_interval')
|
||||||
self.fields['_schedule_at'] = schedule_at
|
|
||||||
self.fields['_interval'] = interval
|
|
||||||
self.fields['_commit'] = commit
|
|
||||||
|
|
||||||
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()
|
||||||
|
|
||||||
|
@ -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
|
@classproperty
|
||||||
def root_module(cls):
|
def field_order(self):
|
||||||
return cls.__module__.split(".")[0]
|
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 cls.field_order:
|
||||||
if not 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)
|
form.fields['_commit'].initial = self.commit_default
|
||||||
|
|
||||||
# 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
|
|
||||||
|
|
||||||
return form
|
return form
|
||||||
|
|
||||||
|
@ -16,9 +16,8 @@
|
|||||||
{% 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 #}
|
{# Render grouped fields according to declared fieldsets #}
|
||||||
{% for group, fields in script.Meta.fieldsets %}
|
{% for group, fields in script.get_fieldsets %}
|
||||||
<div class="field-group mb-5">
|
<div class="field-group mb-5">
|
||||||
<div class="row mb-2">
|
<div class="row mb-2">
|
||||||
<h5 class="offset-sm-3">{{ group }}</h5>
|
<h5 class="offset-sm-3">{{ group }}</h5>
|
||||||
@ -30,13 +29,6 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% else %}
|
|
||||||
{# Render all fields as a single group #}
|
|
||||||
<div class="row mb-2">
|
|
||||||
<h5 class="offset-sm-3">Script Data</h5>
|
|
||||||
</div>
|
|
||||||
{% render_form form %}
|
|
||||||
{% endif %}
|
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="alert alert-info">
|
<div class="alert alert-info">
|
||||||
<i class="mdi mdi-information"></i>
|
<i class="mdi mdi-information"></i>
|
||||||
|
Loading…
Reference in New Issue
Block a user