Skip to content

Commit 68b8cca

Browse files
Merge pull request #8957 from kkthxbye-code/save-job-results
Fix #8956: Save old JobResults
2 parents 8781d03 + c216405 commit 68b8cca

File tree

11 files changed

+55
-41
lines changed

11 files changed

+55
-41
lines changed

docs/administration/housekeeping.md

+1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ NetBox includes a `housekeeping` management command that should be run nightly.
44

55
* Clearing expired authentication sessions from the database
66
* Deleting changelog records older than the configured [retention time](../configuration/dynamic-settings.md#changelog_retention)
7+
* Deleting job result records older than the configured [retention time](../configuration/dynamic-settings.md#jobresult_retention)
78

89
This command can be invoked directly, or by using the shell script provided at `/opt/netbox/contrib/netbox-housekeeping.sh`. This script can be linked from your cron scheduler's daily jobs directory (e.g. `/etc/cron.daily`) or referenced directly within the cron configuration file.
910

docs/configuration/dynamic-settings.md

+12
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,18 @@ changes in the database indefinitely.
4343

4444
---
4545

46+
## JOBRESULT_RETENTION
47+
48+
Default: 90
49+
50+
The number of days to retain job results (scripts and reports). Set this to `0` to retain
51+
job results in the database indefinitely.
52+
53+
!!! warning
54+
If enabling indefinite job results retention, it is recommended to periodically delete old entries. Otherwise, the database may eventually exceed capacity.
55+
56+
---
57+
4658
## CUSTOM_VALIDATORS
4759

4860
This is a mapping of models to [custom validators](../customization/custom-validation.md) that have been defined locally to enforce custom validation logic. An example is provided below:

netbox/extras/admin.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ class ConfigRevisionAdmin(admin.ModelAdmin):
4040
'fields': ('DEFAULT_USER_PREFERENCES',),
4141
}),
4242
('Miscellaneous', {
43-
'fields': ('MAINTENANCE_MODE', 'GRAPHQL_ENABLED', 'CHANGELOG_RETENTION', 'MAPS_URL'),
43+
'fields': ('MAINTENANCE_MODE', 'GRAPHQL_ENABLED', 'CHANGELOG_RETENTION', 'JOBRESULT_RETENTION', 'MAPS_URL'),
4444
}),
4545
('Config Revision', {
4646
'fields': ('comment',),

netbox/extras/api/views.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,7 @@ def list(self, request):
179179
for r in JobResult.objects.filter(
180180
obj_type=report_content_type,
181181
status__in=JobResultStatusChoices.TERMINAL_STATE_CHOICES
182-
).defer('data')
182+
).order_by('name', '-created').distinct('name').defer('data')
183183
}
184184

185185
# Iterate through all available Reports.
@@ -271,7 +271,7 @@ def list(self, request):
271271
for r in JobResult.objects.filter(
272272
obj_type=script_content_type,
273273
status__in=JobResultStatusChoices.TERMINAL_STATE_CHOICES
274-
).defer('data').order_by('created')
274+
).order_by('name', '-created').distinct('name').defer('data')
275275
}
276276

277277
flat_list = []

netbox/extras/management/commands/housekeeping.py

+28
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from django.utils import timezone
1010
from packaging import version
1111

12+
from extras.models import JobResult
1213
from extras.models import ObjectChange
1314
from netbox.config import Config
1415

@@ -63,6 +64,33 @@ def handle(self, *args, **options):
6364
f"\tSkipping: No retention period specified (CHANGELOG_RETENTION = {config.CHANGELOG_RETENTION})"
6465
)
6566

67+
# Delete expired JobResults
68+
if options['verbosity']:
69+
self.stdout.write("[*] Checking for expired jobresult records")
70+
if config.JOBRESULT_RETENTION:
71+
cutoff = timezone.now() - timedelta(days=config.JOBRESULT_RETENTION)
72+
if options['verbosity'] >= 2:
73+
self.stdout.write(f"\tRetention period: {config.JOBRESULT_RETENTION} days")
74+
self.stdout.write(f"\tCut-off time: {cutoff}")
75+
expired_records = JobResult.objects.filter(created__lt=cutoff).count()
76+
if expired_records:
77+
if options['verbosity']:
78+
self.stdout.write(
79+
f"\tDeleting {expired_records} expired records... ",
80+
self.style.WARNING,
81+
ending=""
82+
)
83+
self.stdout.flush()
84+
JobResult.objects.filter(created__lt=cutoff)._raw_delete(using=DEFAULT_DB_ALIAS)
85+
if options['verbosity']:
86+
self.stdout.write("Done.", self.style.SUCCESS)
87+
elif options['verbosity']:
88+
self.stdout.write("\tNo expired records found.", self.style.SUCCESS)
89+
elif options['verbosity']:
90+
self.stdout.write(
91+
f"\tSkipping: No retention period specified (JOBRESULT_RETENTION = {config.JOBRESULT_RETENTION})"
92+
)
93+
6694
# Check for new releases (if enabled)
6795
if options['verbosity']:
6896
self.stdout.write("[*] Checking for latest release")

netbox/extras/management/commands/runscript.py

-7
Original file line numberDiff line numberDiff line change
@@ -113,13 +113,6 @@ def _run_script():
113113

114114
script_content_type = ContentType.objects.get(app_label='extras', model='script')
115115

116-
# Delete any previous terminal state results
117-
JobResult.objects.filter(
118-
obj_type=script_content_type,
119-
name=script.full_name,
120-
status__in=JobResultStatusChoices.TERMINAL_STATE_CHOICES
121-
).delete()
122-
123116
# Create the job result
124117
job_result = JobResult.objects.create(
125118
name=script.full_name,

netbox/extras/reports.py

-9
Original file line numberDiff line numberDiff line change
@@ -84,15 +84,6 @@ def run_report(job_result, *args, **kwargs):
8484
job_result.save()
8585
logging.error(f"Error during execution of report {job_result.name}")
8686

87-
# Delete any previous terminal state results
88-
JobResult.objects.filter(
89-
obj_type=job_result.obj_type,
90-
name=job_result.name,
91-
status__in=JobResultStatusChoices.TERMINAL_STATE_CHOICES
92-
).exclude(
93-
pk=job_result.pk
94-
).delete()
95-
9687

9788
class Report(object):
9889
"""

netbox/extras/scripts.py

+1-10
Original file line numberDiff line numberDiff line change
@@ -481,23 +481,14 @@ def _run_script():
481481
else:
482482
_run_script()
483483

484-
# Delete any previous terminal state results
485-
JobResult.objects.filter(
486-
obj_type=job_result.obj_type,
487-
name=job_result.name,
488-
status__in=JobResultStatusChoices.TERMINAL_STATE_CHOICES
489-
).exclude(
490-
pk=job_result.pk
491-
).delete()
492-
493484

494485
def get_scripts(use_names=False):
495486
"""
496487
Return a dict of dicts mapping all scripts to their modules. Set use_names to True to use each module's human-
497488
defined name in place of the actual module name.
498489
"""
499490
scripts = OrderedDict()
500-
# Iterate through all modules within the reports path. These are the user-created files in which reports are
491+
# Iterate through all modules within the scripts path. These are the user-created files in which reports are
501492
# defined.
502493
for importer, module_name, _ in pkgutil.iter_modules([settings.SCRIPTS_ROOT]):
503494
# Remove cached module to ensure consistency with filesystem

netbox/extras/views.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -524,7 +524,7 @@ def get(self, request):
524524
for r in JobResult.objects.filter(
525525
obj_type=report_content_type,
526526
status__in=JobResultStatusChoices.TERMINAL_STATE_CHOICES
527-
).defer('data')
527+
).order_by('name', '-created').distinct('name').defer('data')
528528
}
529529

530530
ret = []
@@ -656,7 +656,7 @@ def get(self, request):
656656
for r in JobResult.objects.filter(
657657
obj_type=script_content_type,
658658
status__in=JobResultStatusChoices.TERMINAL_STATE_CHOICES
659-
).defer('data')
659+
).order_by('name', '-created').distinct('name').defer('data')
660660
}
661661

662662
for _scripts in scripts.values():

netbox/netbox/config/parameters.py

+7
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,13 @@ def __init__(self, name, label, default, description='', field=None, field_kwarg
187187
description="Days to retain changelog history (set to zero for unlimited)",
188188
field=forms.IntegerField
189189
),
190+
ConfigParam(
191+
name='JOBRESULT_RETENTION',
192+
label='Job result retention',
193+
default=90,
194+
description="Days to retain job result history (set to zero for unlimited)",
195+
field=forms.IntegerField
196+
),
190197
ConfigParam(
191198
name='MAPS_URL',
192199
label='Maps URL',

netbox/netbox/views/__init__.py

+1-10
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,7 @@
1919
from dcim.models import (
2020
Cable, ConsolePort, Device, DeviceType, Interface, PowerPanel, PowerFeed, PowerPort, Rack, Site,
2121
)
22-
from extras.choices import JobResultStatusChoices
23-
from extras.models import ObjectChange, JobResult
22+
from extras.models import ObjectChange
2423
from extras.tables import ObjectChangeTable
2524
from ipam.models import Aggregate, IPAddress, IPRange, Prefix, VLAN, VRF
2625
from netbox.constants import SEARCH_MAX_RESULTS, SEARCH_TYPES
@@ -48,13 +47,6 @@ def get(self, request):
4847
pk__lt=F('_path__destination_id')
4948
)
5049

51-
# Report Results
52-
report_content_type = ContentType.objects.get(app_label='extras', model='report')
53-
report_results = JobResult.objects.filter(
54-
obj_type=report_content_type,
55-
status__in=JobResultStatusChoices.TERMINAL_STATE_CHOICES
56-
).defer('data')[:10]
57-
5850
def build_stats():
5951
org = (
6052
("dcim.view_site", "Sites", Site.objects.restrict(request.user, 'view').count),
@@ -150,7 +142,6 @@ def build_stats():
150142
return render(request, self.template_name, {
151143
'search_form': SearchForm(),
152144
'stats': build_stats(),
153-
'report_results': report_results,
154145
'changelog_table': changelog_table,
155146
'new_release': new_release,
156147
})

0 commit comments

Comments
 (0)