Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion docs/customization/custom-scripts.md
Original file line number Diff line number Diff line change
Expand Up @@ -267,7 +267,7 @@ An IPv4 or IPv6 network with a mask. Returns a `netaddr.IPNetwork` object. Two a

### Via the Web UI

Custom scripts can be run via the web UI by navigating to the script, completing any required form data, and clicking the "run script" button.
Custom scripts can be run via the web UI by navigating to the script, completing any required form data, and clicking the "run script" button. It is possible to schedule a script to be executed at specified time in the future. A scheduled script can be canceled by deleting the associated job result object.

### Via the API

Expand All @@ -282,6 +282,8 @@ http://netbox/api/extras/scripts/example.MyReport/ \
--data '{"data": {"foo": "somevalue", "bar": 123}, "commit": true}'
```

Optionally `schedule_at` can be passed in the form data with a datetime string to schedule a script at the specified date and time.

### Via the CLI

Scripts can be run on the CLI by invoking the management command:
Expand Down
4 changes: 3 additions & 1 deletion docs/customization/reports.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ Once you have created a report, it will appear in the reports list. Initially, r

### Via the Web UI

Reports can be run via the web UI by navigating to the report and clicking the "run report" button at top right. Once a report has been run, its associated results will be included in the report view.
Reports can be run via the web UI by navigating to the report and clicking the "run report" button at top right. Once a report has been run, its associated results will be included in the report view. It is possible to schedule a report to be executed at specified time in the future. A scheduled report can be canceled by deleting the associated job result object.

### Via the API

Expand All @@ -152,6 +152,8 @@ Our example report above would be called as:
POST /api/extras/reports/devices.DeviceConnectionsReport/run/
```

Optionally `schedule_at` can be passed in the form data with a datetime string to schedule a script at the specified date and time.

### Via the CLI

Reports can be run on the CLI by invoking the management command:
Expand Down
21 changes: 0 additions & 21 deletions netbox/extras/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -131,24 +131,3 @@ def restore(self, request, pk):
})

return TemplateResponse(request, 'admin/extras/configrevision/restore.html', context)


#
# Reports & scripts
#

@admin.register(JobResult)
class JobResultAdmin(admin.ModelAdmin):
list_display = [
'obj_type', 'name', 'created', 'completed', 'user', 'status',
]
fields = [
'obj_type', 'name', 'created', 'completed', 'user', 'status', 'data', 'job_id'
]
list_filter = [
'status',
]
readonly_fields = fields

def has_add_permission(self, request):
return False
8 changes: 7 additions & 1 deletion netbox/extras/api/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
'ObjectChangeSerializer',
'ReportDetailSerializer',
'ReportSerializer',
'ReportInputSerializer',
'ScriptDetailSerializer',
'ScriptInputSerializer',
'ScriptLogMessageSerializer',
Expand Down Expand Up @@ -362,7 +363,7 @@ class JobResultSerializer(BaseModelSerializer):
class Meta:
model = JobResult
fields = [
'id', 'url', 'display', 'created', 'completed', 'name', 'obj_type', 'status', 'user', 'data', 'job_id',
'id', 'url', 'display', 'created', 'completed', 'scheduled_time', 'name', 'obj_type', 'status', 'user', 'data', 'job_id',
]


Expand All @@ -388,6 +389,10 @@ class ReportDetailSerializer(ReportSerializer):
result = JobResultSerializer()


class ReportInputSerializer(serializers.Serializer):
schedule_at = serializers.DateTimeField(required=False, allow_null=True)


#
# Scripts
#
Expand Down Expand Up @@ -419,6 +424,7 @@ class ScriptDetailSerializer(ScriptSerializer):
class ScriptInputSerializer(serializers.Serializer):
data = serializers.JSONField()
commit = serializers.BooleanField()
schedule_at = serializers.DateTimeField(required=False, allow_null=True)


class ScriptLogMessageSerializer(serializers.Serializer):
Expand Down
31 changes: 20 additions & 11 deletions netbox/extras/api/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -231,19 +231,26 @@ def run(self, request, pk):

# Retrieve and run the Report. This will create a new JobResult.
report = self._retrieve_report(pk)
report_content_type = ContentType.objects.get(app_label='extras', model='report')
job_result = JobResult.enqueue_job(
run_report,
report.full_name,
report_content_type,
request.user,
job_timeout=report.job_timeout
)
report.result = job_result
input_serializer = serializers.ReportInputSerializer(data=request.data)

serializer = serializers.ReportDetailSerializer(report, context={'request': request})
if input_serializer.is_valid():
schedule_at = input_serializer.validated_data.get('schedule_at')

return Response(serializer.data)
report_content_type = ContentType.objects.get(app_label='extras', model='report')
job_result = JobResult.enqueue_job(
run_report,
report.full_name,
report_content_type,
request.user,
job_timeout=report.job_timeout,
schedule_at=schedule_at,
)
report.result = job_result

serializer = serializers.ReportDetailSerializer(report, context={'request': request})

return Response(serializer.data)
return Response(input_serializer.errors, status=status.HTTP_400_BAD_REQUEST)


#
Expand Down Expand Up @@ -312,6 +319,7 @@ def post(self, request, pk):
if input_serializer.is_valid():
data = input_serializer.data['data']
commit = input_serializer.data['commit']
schedule_at = input_serializer.validated_data.get('schedule_at')

script_content_type = ContentType.objects.get(app_label='extras', model='script')
job_result = JobResult.enqueue_job(
Expand All @@ -323,6 +331,7 @@ def post(self, request, pk):
request=copy_safe_request(request),
commit=commit,
job_timeout=script.job_timeout,
schedule_at=schedule_at,
)
script.result = job_result
serializer = serializers.ScriptDetailSerializer(script, context={'request': request})
Expand Down
2 changes: 2 additions & 0 deletions netbox/extras/choices.py
Original file line number Diff line number Diff line change
Expand Up @@ -141,13 +141,15 @@ class LogLevelChoices(ChoiceSet):
class JobResultStatusChoices(ChoiceSet):

STATUS_PENDING = 'pending'
STATUS_SCHEDULED = 'scheduled'
STATUS_RUNNING = 'running'
STATUS_COMPLETED = 'completed'
STATUS_ERRORED = 'errored'
STATUS_FAILED = 'failed'

CHOICES = (
(STATUS_PENDING, 'Pending'),
(STATUS_SCHEDULED, 'Scheduled'),
(STATUS_RUNNING, 'Running'),
(STATUS_COMPLETED, 'Completed'),
(STATUS_ERRORED, 'Errored'),
Expand Down
31 changes: 29 additions & 2 deletions netbox/extras/filtersets.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
'ConfigContextFilterSet',
'ContentTypeFilterSet',
'CustomFieldFilterSet',
'JobResultFilterSet',
'CustomLinkFilterSet',
'ExportTemplateFilterSet',
'ImageAttachmentFilterSet',
Expand Down Expand Up @@ -435,7 +436,32 @@ class JobResultFilterSet(BaseFilterSet):
label='Search',
)
created = django_filters.DateTimeFilter()
created__before = django_filters.DateTimeFilter(
field_name='created',
lookup_expr='lte'
)
created__after = django_filters.DateTimeFilter(
field_name='created',
lookup_expr='gte'
)
completed = django_filters.DateTimeFilter()
completed__before = django_filters.DateTimeFilter(
field_name='completed',
lookup_expr='lte'
)
completed__after = django_filters.DateTimeFilter(
field_name='completed',
lookup_expr='gte'
)
scheduled_time = django_filters.DateTimeFilter()
scheduled_time__before = django_filters.DateTimeFilter(
field_name='scheduled_time',
lookup_expr='lte'
)
scheduled_time__after = django_filters.DateTimeFilter(
field_name='scheduled_time',
lookup_expr='gte'
)
status = django_filters.MultipleChoiceFilter(
choices=JobResultStatusChoices,
null_value=None
Expand All @@ -444,14 +470,15 @@ class JobResultFilterSet(BaseFilterSet):
class Meta:
model = JobResult
fields = [
'id', 'created', 'completed', 'status', 'user', 'obj_type', 'name'
'id', 'created', 'completed', 'scheduled_time', 'status', 'user', 'obj_type', 'name'
]

def search(self, queryset, name, value):
if not value.strip():
return queryset
return queryset.filter(
Q(user__username__icontains=value)
Q(user__username__icontains=value) |
Q(name__icontains=value)
)


Expand Down
53 changes: 53 additions & 0 deletions netbox/extras/forms/filtersets.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
__all__ = (
'ConfigContextFilterForm',
'CustomFieldFilterForm',
'JobResultFilterForm',
'CustomLinkFilterForm',
'ExportTemplateFilterForm',
'JournalEntryFilterForm',
Expand Down Expand Up @@ -65,6 +66,58 @@ class CustomFieldFilterForm(FilterForm):
)


class JobResultFilterForm(FilterForm):
fieldsets = (
(None, ('q',)),
('Attributes', ('obj_type', 'status')),
('Creation', ('created__before', 'created__after', 'completed__before', 'completed__after',
'scheduled_time__before', 'scheduled_time__after', 'user')),
)

obj_type = ContentTypeChoiceField(
label=_('Object Type'),
queryset=ContentType.objects.all(),
limit_choices_to=FeatureQuery('job_results'), # TODO: This doesn't actually work
required=False,
)
status = MultipleChoiceField(
choices=JobResultStatusChoices,
required=False
)
created__after = forms.DateTimeField(
required=False,
widget=DateTimePicker()
)
created__before = forms.DateTimeField(
required=False,
widget=DateTimePicker()
)
completed__after = forms.DateTimeField(
required=False,
widget=DateTimePicker()
)
completed__before = forms.DateTimeField(
required=False,
widget=DateTimePicker()
)
scheduled_time__after = forms.DateTimeField(
required=False,
widget=DateTimePicker()
)
scheduled_time__before = forms.DateTimeField(
required=False,
widget=DateTimePicker()
)
user = DynamicModelMultipleChoiceField(
queryset=User.objects.all(),
required=False,
label=_('User'),
widget=APISelectMultiple(
api_url='/api/users/users/',
)
)


class CustomLinkFilterForm(FilterForm):
fieldsets = (
(None, ('q',)),
Expand Down
16 changes: 16 additions & 0 deletions netbox/extras/forms/reports.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
from django import forms

from utilities.forms import BootstrapMixin, DateTimePicker

__all__ = (
'ReportForm',
)


class ReportForm(BootstrapMixin, forms.Form):
schedule_at = forms.DateTimeField(
required=False,
widget=DateTimePicker(),
label="Schedule at",
help_text="Schedule execution of report to a set time",
)
16 changes: 12 additions & 4 deletions netbox/extras/forms/scripts.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from django import forms

from utilities.forms import BootstrapMixin
from utilities.forms import BootstrapMixin, DateTimePicker

__all__ = (
'ScriptForm',
Expand All @@ -14,17 +14,25 @@ class ScriptForm(BootstrapMixin, forms.Form):
label="Commit changes",
help_text="Commit changes to the database (uncheck for a dry-run)"
)
_schedule_at = forms.DateTimeField(
required=False,
widget=DateTimePicker(),
label="Schedule at",
help_text="Schedule execution of script to a set time",
)

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)

# Move _commit to the end of the form
# Move _commit and _schedule_at to the end of the form
schedule_at = self.fields.pop('_schedule_at')
commit = self.fields.pop('_commit')
self.fields['_schedule_at'] = schedule_at
self.fields['_commit'] = commit

@property
def requires_input(self):
"""
A boolean indicating whether the form requires user input (ignore the _commit field).
A boolean indicating whether the form requires user input (ignore the _commit and _schedule_at fields).
"""
return bool(len(self.fields) > 1)
return bool(len(self.fields) > 2)
2 changes: 1 addition & 1 deletion netbox/extras/management/commands/housekeeping.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ def handle(self, *args, **options):
ending=""
)
self.stdout.flush()
JobResult.objects.filter(created__lt=cutoff)._raw_delete(using=DEFAULT_DB_ALIAS)
JobResult.objects.filter(created__lt=cutoff).delete()
if options['verbosity']:
self.stdout.write("Done.", self.style.SUCCESS)
elif options['verbosity']:
Expand Down
2 changes: 2 additions & 0 deletions netbox/extras/management/commands/rqworker.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ class Command(_Command):
of only the 'default' queue).
"""
def handle(self, *args, **options):
# Run the worker with scheduler functionality
options['with_scheduler'] = True

# If no queues have been specified on the command line, listen on all configured queues.
if len(args) < 1:
Expand Down
17 changes: 17 additions & 0 deletions netbox/extras/migrations/0079_change_jobresult_order.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Generated by Django 4.1.1 on 2022-10-09 18:37

from django.db import migrations


class Migration(migrations.Migration):

dependencies = [
('extras', '0078_unique_constraints'),
]

operations = [
migrations.AlterModelOptions(
name='jobresult',
options={'ordering': ['-created']},
),
]
Loading