Merge pull request #8957 from kkthxbye-code/save-job-results

Fix #8956: Save old JobResults
This commit is contained in:
Jeremy Stretch 2022-04-12 11:57:48 -04:00 committed by GitHub
commit 68b8cca540
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 55 additions and 41 deletions

View File

@ -4,6 +4,7 @@ NetBox includes a `housekeeping` management command that should be run nightly.
* Clearing expired authentication sessions from the database
* Deleting changelog records older than the configured [retention time](../configuration/dynamic-settings.md#changelog_retention)
* Deleting job result records older than the configured [retention time](../configuration/dynamic-settings.md#jobresult_retention)
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.

View File

@ -43,6 +43,18 @@ changes in the database indefinitely.
---
## JOBRESULT_RETENTION
Default: 90
The number of days to retain job results (scripts and reports). Set this to `0` to retain
job results in the database indefinitely.
!!! warning
If enabling indefinite job results retention, it is recommended to periodically delete old entries. Otherwise, the database may eventually exceed capacity.
---
## CUSTOM_VALIDATORS
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:

View File

@ -40,7 +40,7 @@ class ConfigRevisionAdmin(admin.ModelAdmin):
'fields': ('DEFAULT_USER_PREFERENCES',),
}),
('Miscellaneous', {
'fields': ('MAINTENANCE_MODE', 'GRAPHQL_ENABLED', 'CHANGELOG_RETENTION', 'MAPS_URL'),
'fields': ('MAINTENANCE_MODE', 'GRAPHQL_ENABLED', 'CHANGELOG_RETENTION', 'JOBRESULT_RETENTION', 'MAPS_URL'),
}),
('Config Revision', {
'fields': ('comment',),

View File

@ -179,7 +179,7 @@ class ReportViewSet(ViewSet):
for r in JobResult.objects.filter(
obj_type=report_content_type,
status__in=JobResultStatusChoices.TERMINAL_STATE_CHOICES
).defer('data')
).order_by('name', '-created').distinct('name').defer('data')
}
# Iterate through all available Reports.
@ -271,7 +271,7 @@ class ScriptViewSet(ViewSet):
for r in JobResult.objects.filter(
obj_type=script_content_type,
status__in=JobResultStatusChoices.TERMINAL_STATE_CHOICES
).defer('data').order_by('created')
).order_by('name', '-created').distinct('name').defer('data')
}
flat_list = []

View File

@ -9,6 +9,7 @@ from django.db import DEFAULT_DB_ALIAS
from django.utils import timezone
from packaging import version
from extras.models import JobResult
from extras.models import ObjectChange
from netbox.config import Config
@ -63,6 +64,33 @@ class Command(BaseCommand):
f"\tSkipping: No retention period specified (CHANGELOG_RETENTION = {config.CHANGELOG_RETENTION})"
)
# Delete expired JobResults
if options['verbosity']:
self.stdout.write("[*] Checking for expired jobresult records")
if config.JOBRESULT_RETENTION:
cutoff = timezone.now() - timedelta(days=config.JOBRESULT_RETENTION)
if options['verbosity'] >= 2:
self.stdout.write(f"\tRetention period: {config.JOBRESULT_RETENTION} days")
self.stdout.write(f"\tCut-off time: {cutoff}")
expired_records = JobResult.objects.filter(created__lt=cutoff).count()
if expired_records:
if options['verbosity']:
self.stdout.write(
f"\tDeleting {expired_records} expired records... ",
self.style.WARNING,
ending=""
)
self.stdout.flush()
JobResult.objects.filter(created__lt=cutoff)._raw_delete(using=DEFAULT_DB_ALIAS)
if options['verbosity']:
self.stdout.write("Done.", self.style.SUCCESS)
elif options['verbosity']:
self.stdout.write("\tNo expired records found.", self.style.SUCCESS)
elif options['verbosity']:
self.stdout.write(
f"\tSkipping: No retention period specified (JOBRESULT_RETENTION = {config.JOBRESULT_RETENTION})"
)
# Check for new releases (if enabled)
if options['verbosity']:
self.stdout.write("[*] Checking for latest release")

View File

@ -113,13 +113,6 @@ class Command(BaseCommand):
script_content_type = ContentType.objects.get(app_label='extras', model='script')
# Delete any previous terminal state results
JobResult.objects.filter(
obj_type=script_content_type,
name=script.full_name,
status__in=JobResultStatusChoices.TERMINAL_STATE_CHOICES
).delete()
# Create the job result
job_result = JobResult.objects.create(
name=script.full_name,

View File

@ -84,15 +84,6 @@ def run_report(job_result, *args, **kwargs):
job_result.save()
logging.error(f"Error during execution of report {job_result.name}")
# Delete any previous terminal state results
JobResult.objects.filter(
obj_type=job_result.obj_type,
name=job_result.name,
status__in=JobResultStatusChoices.TERMINAL_STATE_CHOICES
).exclude(
pk=job_result.pk
).delete()
class Report(object):
"""

View File

@ -481,15 +481,6 @@ def run_script(data, request, commit=True, *args, **kwargs):
else:
_run_script()
# Delete any previous terminal state results
JobResult.objects.filter(
obj_type=job_result.obj_type,
name=job_result.name,
status__in=JobResultStatusChoices.TERMINAL_STATE_CHOICES
).exclude(
pk=job_result.pk
).delete()
def get_scripts(use_names=False):
"""
@ -497,7 +488,7 @@ def get_scripts(use_names=False):
defined name in place of the actual module name.
"""
scripts = OrderedDict()
# Iterate through all modules within the reports path. These are the user-created files in which reports are
# Iterate through all modules within the scripts path. These are the user-created files in which reports are
# defined.
for importer, module_name, _ in pkgutil.iter_modules([settings.SCRIPTS_ROOT]):
# Remove cached module to ensure consistency with filesystem

View File

@ -524,7 +524,7 @@ class ReportListView(ContentTypePermissionRequiredMixin, View):
for r in JobResult.objects.filter(
obj_type=report_content_type,
status__in=JobResultStatusChoices.TERMINAL_STATE_CHOICES
).defer('data')
).order_by('name', '-created').distinct('name').defer('data')
}
ret = []
@ -656,7 +656,7 @@ class ScriptListView(ContentTypePermissionRequiredMixin, View):
for r in JobResult.objects.filter(
obj_type=script_content_type,
status__in=JobResultStatusChoices.TERMINAL_STATE_CHOICES
).defer('data')
).order_by('name', '-created').distinct('name').defer('data')
}
for _scripts in scripts.values():

View File

@ -187,6 +187,13 @@ PARAMS = (
description="Days to retain changelog history (set to zero for unlimited)",
field=forms.IntegerField
),
ConfigParam(
name='JOBRESULT_RETENTION',
label='Job result retention',
default=90,
description="Days to retain job result history (set to zero for unlimited)",
field=forms.IntegerField
),
ConfigParam(
name='MAPS_URL',
label='Maps URL',

View File

@ -19,8 +19,7 @@ from circuits.models import Circuit, Provider
from dcim.models import (
Cable, ConsolePort, Device, DeviceType, Interface, PowerPanel, PowerFeed, PowerPort, Rack, Site,
)
from extras.choices import JobResultStatusChoices
from extras.models import ObjectChange, JobResult
from extras.models import ObjectChange
from extras.tables import ObjectChangeTable
from ipam.models import Aggregate, IPAddress, IPRange, Prefix, VLAN, VRF
from netbox.constants import SEARCH_MAX_RESULTS, SEARCH_TYPES
@ -48,13 +47,6 @@ class HomeView(View):
pk__lt=F('_path__destination_id')
)
# Report Results
report_content_type = ContentType.objects.get(app_label='extras', model='report')
report_results = JobResult.objects.filter(
obj_type=report_content_type,
status__in=JobResultStatusChoices.TERMINAL_STATE_CHOICES
).defer('data')[:10]
def build_stats():
org = (
("dcim.view_site", "Sites", Site.objects.restrict(request.user, 'view').count),
@ -150,7 +142,6 @@ class HomeView(View):
return render(request, self.template_name, {
'search_form': SearchForm(),
'stats': build_stats(),
'report_results': report_results,
'changelog_table': changelog_table,
'new_release': new_release,
})