mirror of
https://github.com/netbox-community/netbox.git
synced 2024-05-10 07:54:54 +00:00
#8366: Add started field to JobResult
This commit is contained in:
@ -131,7 +131,7 @@ This release introduces a new programmatic API that enables plugins and custom s
|
|||||||
* extras.ExportTemplate
|
* extras.ExportTemplate
|
||||||
* Renamed `content_type` field to `content_types`
|
* Renamed `content_type` field to `content_types`
|
||||||
* extras.JobResult
|
* extras.JobResult
|
||||||
* Added the `scheduled` field
|
* Added `scheduled` and `started` datetime fields
|
||||||
* ipam.Aggregate
|
* ipam.Aggregate
|
||||||
* Added a `comments` field
|
* Added a `comments` field
|
||||||
* ipam.ASN
|
* ipam.ASN
|
||||||
|
@ -385,8 +385,8 @@ class JobResultSerializer(BaseModelSerializer):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = JobResult
|
model = JobResult
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'url', 'display', 'status', 'created', 'scheduled', 'completed', 'name', 'obj_type', 'user', 'data',
|
'id', 'url', 'display', 'status', 'created', 'scheduled', 'started', 'completed', 'name', 'obj_type',
|
||||||
'job_id',
|
'user', 'data', 'job_id',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@ -503,15 +503,6 @@ class JobResultFilterSet(BaseFilterSet):
|
|||||||
field_name='created',
|
field_name='created',
|
||||||
lookup_expr='gte'
|
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 = django_filters.DateTimeFilter()
|
scheduled = django_filters.DateTimeFilter()
|
||||||
scheduled__before = django_filters.DateTimeFilter(
|
scheduled__before = django_filters.DateTimeFilter(
|
||||||
field_name='scheduled',
|
field_name='scheduled',
|
||||||
@ -521,6 +512,24 @@ class JobResultFilterSet(BaseFilterSet):
|
|||||||
field_name='scheduled',
|
field_name='scheduled',
|
||||||
lookup_expr='gte'
|
lookup_expr='gte'
|
||||||
)
|
)
|
||||||
|
started = django_filters.DateTimeFilter()
|
||||||
|
started__before = django_filters.DateTimeFilter(
|
||||||
|
field_name='started',
|
||||||
|
lookup_expr='lte'
|
||||||
|
)
|
||||||
|
started__after = django_filters.DateTimeFilter(
|
||||||
|
field_name='started',
|
||||||
|
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'
|
||||||
|
)
|
||||||
status = django_filters.MultipleChoiceFilter(
|
status = django_filters.MultipleChoiceFilter(
|
||||||
choices=JobResultStatusChoices,
|
choices=JobResultStatusChoices,
|
||||||
null_value=None
|
null_value=None
|
||||||
@ -528,9 +537,7 @@ class JobResultFilterSet(BaseFilterSet):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = JobResult
|
model = JobResult
|
||||||
fields = [
|
fields = ('id', 'status', 'user', 'obj_type', 'name')
|
||||||
'id', 'status', 'created', 'scheduled', 'completed', 'user', 'obj_type', 'name'
|
|
||||||
]
|
|
||||||
|
|
||||||
def search(self, queryset, name, value):
|
def search(self, queryset, name, value):
|
||||||
if not value.strip():
|
if not value.strip():
|
||||||
|
@ -73,11 +73,10 @@ class JobResultFilterForm(SavedFiltersMixin, FilterForm):
|
|||||||
(None, ('q', 'filter_id')),
|
(None, ('q', 'filter_id')),
|
||||||
('Attributes', ('obj_type', 'status')),
|
('Attributes', ('obj_type', 'status')),
|
||||||
('Creation', (
|
('Creation', (
|
||||||
'created__before', 'created__after', 'completed__before', 'completed__after', 'scheduled__before',
|
'created__before', 'created__after', 'scheduled__before', 'scheduled__after', 'started__before',
|
||||||
'scheduled__after', 'user',
|
'started__after', 'completed__before', 'completed__after', 'user',
|
||||||
)),
|
)),
|
||||||
)
|
)
|
||||||
|
|
||||||
obj_type = ContentTypeChoiceField(
|
obj_type = ContentTypeChoiceField(
|
||||||
label=_('Object Type'),
|
label=_('Object Type'),
|
||||||
queryset=ContentType.objects.all(),
|
queryset=ContentType.objects.all(),
|
||||||
@ -96,14 +95,6 @@ class JobResultFilterForm(SavedFiltersMixin, FilterForm):
|
|||||||
required=False,
|
required=False,
|
||||||
widget=DateTimePicker()
|
widget=DateTimePicker()
|
||||||
)
|
)
|
||||||
completed__after = forms.DateTimeField(
|
|
||||||
required=False,
|
|
||||||
widget=DateTimePicker()
|
|
||||||
)
|
|
||||||
completed__before = forms.DateTimeField(
|
|
||||||
required=False,
|
|
||||||
widget=DateTimePicker()
|
|
||||||
)
|
|
||||||
scheduled__after = forms.DateTimeField(
|
scheduled__after = forms.DateTimeField(
|
||||||
required=False,
|
required=False,
|
||||||
widget=DateTimePicker()
|
widget=DateTimePicker()
|
||||||
@ -112,6 +103,22 @@ class JobResultFilterForm(SavedFiltersMixin, FilterForm):
|
|||||||
required=False,
|
required=False,
|
||||||
widget=DateTimePicker()
|
widget=DateTimePicker()
|
||||||
)
|
)
|
||||||
|
started__after = forms.DateTimeField(
|
||||||
|
required=False,
|
||||||
|
widget=DateTimePicker()
|
||||||
|
)
|
||||||
|
started__before = forms.DateTimeField(
|
||||||
|
required=False,
|
||||||
|
widget=DateTimePicker()
|
||||||
|
)
|
||||||
|
completed__after = forms.DateTimeField(
|
||||||
|
required=False,
|
||||||
|
widget=DateTimePicker()
|
||||||
|
)
|
||||||
|
completed__before = forms.DateTimeField(
|
||||||
|
required=False,
|
||||||
|
widget=DateTimePicker()
|
||||||
|
)
|
||||||
user = DynamicModelMultipleChoiceField(
|
user = DynamicModelMultipleChoiceField(
|
||||||
queryset=User.objects.all(),
|
queryset=User.objects.all(),
|
||||||
required=False,
|
required=False,
|
||||||
|
@ -13,6 +13,11 @@ class Migration(migrations.Migration):
|
|||||||
name='scheduled',
|
name='scheduled',
|
||||||
field=models.DateTimeField(blank=True, null=True),
|
field=models.DateTimeField(blank=True, null=True),
|
||||||
),
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='jobresult',
|
||||||
|
name='started',
|
||||||
|
field=models.DateTimeField(blank=True, null=True),
|
||||||
|
),
|
||||||
migrations.AlterModelOptions(
|
migrations.AlterModelOptions(
|
||||||
name='jobresult',
|
name='jobresult',
|
||||||
options={'ordering': ['-created']},
|
options={'ordering': ['-created']},
|
@ -12,7 +12,7 @@ class Migration(migrations.Migration):
|
|||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('contenttypes', '0002_remove_content_type_name'),
|
('contenttypes', '0002_remove_content_type_name'),
|
||||||
('extras', '0079_jobresult_scheduled'),
|
('extras', '0079_scheduled_jobs'),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
|
@ -585,6 +585,10 @@ class JobResult(models.Model):
|
|||||||
null=True,
|
null=True,
|
||||||
blank=True
|
blank=True
|
||||||
)
|
)
|
||||||
|
started = models.DateTimeField(
|
||||||
|
null=True,
|
||||||
|
blank=True
|
||||||
|
)
|
||||||
completed = models.DateTimeField(
|
completed = models.DateTimeField(
|
||||||
null=True,
|
null=True,
|
||||||
blank=True
|
blank=True
|
||||||
@ -639,9 +643,18 @@ class JobResult(models.Model):
|
|||||||
|
|
||||||
return f"{int(minutes)} minutes, {seconds:.2f} seconds"
|
return f"{int(minutes)} minutes, {seconds:.2f} seconds"
|
||||||
|
|
||||||
|
def start(self):
|
||||||
|
"""
|
||||||
|
Record the job's start time and update its status to "running."
|
||||||
|
"""
|
||||||
|
if self.started is None:
|
||||||
|
self.started = timezone.now()
|
||||||
|
self.status = JobResultStatusChoices.STATUS_RUNNING
|
||||||
|
JobResult.objects.filter(pk=self.pk).update(started=self.started, status=self.status)
|
||||||
|
|
||||||
def set_status(self, status):
|
def set_status(self, status):
|
||||||
"""
|
"""
|
||||||
Helper method to change the status of the job result. If the target status is terminal, the completion
|
Helper method to change the status of the job result. If the target status is terminal, the completion
|
||||||
time is also set.
|
time is also set.
|
||||||
"""
|
"""
|
||||||
self.status = status
|
self.status = status
|
||||||
|
@ -83,6 +83,7 @@ def run_report(job_result, *args, **kwargs):
|
|||||||
report = get_report(module_name, report_name)
|
report = get_report(module_name, report_name)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
job_result.start()
|
||||||
report.run(job_result)
|
report.run(job_result)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
job_result.set_status(JobResultStatusChoices.STATUS_ERRORED)
|
job_result.set_status(JobResultStatusChoices.STATUS_ERRORED)
|
||||||
|
@ -433,16 +433,14 @@ def is_variable(obj):
|
|||||||
def run_script(data, request, commit=True, *args, **kwargs):
|
def run_script(data, request, commit=True, *args, **kwargs):
|
||||||
"""
|
"""
|
||||||
A wrapper for calling Script.run(). This performs error handling and provides a hook for committing changes. It
|
A wrapper for calling Script.run(). This performs error handling and provides a hook for committing changes. It
|
||||||
exists outside of the Script class to ensure it cannot be overridden by a script author.
|
exists outside the Script class to ensure it cannot be overridden by a script author.
|
||||||
"""
|
"""
|
||||||
job_result = kwargs.pop('job_result')
|
job_result = kwargs.pop('job_result')
|
||||||
|
job_result.start()
|
||||||
|
|
||||||
module, script_name = job_result.name.split('.', 1)
|
module, script_name = job_result.name.split('.', 1)
|
||||||
|
|
||||||
script = get_script(module, script_name)()
|
script = get_script(module, script_name)()
|
||||||
|
|
||||||
job_result.status = JobResultStatusChoices.STATUS_RUNNING
|
|
||||||
job_result.save()
|
|
||||||
|
|
||||||
logger = logging.getLogger(f"netbox.scripts.{module}.{script_name}")
|
logger = logging.getLogger(f"netbox.scripts.{module}.{script_name}")
|
||||||
logger.info(f"Running script (commit={commit})")
|
logger.info(f"Running script (commit={commit})")
|
||||||
|
|
||||||
|
@ -49,9 +49,11 @@ class JobResultTable(NetBoxTable):
|
|||||||
class Meta(NetBoxTable.Meta):
|
class Meta(NetBoxTable.Meta):
|
||||||
model = JobResult
|
model = JobResult
|
||||||
fields = (
|
fields = (
|
||||||
'pk', 'id', 'name', 'obj_type', 'status', 'created', 'scheduled', 'completed', 'user', 'job_id',
|
'pk', 'id', 'name', 'obj_type', 'status', 'created', 'scheduled', 'started', 'completed', 'user', 'job_id',
|
||||||
|
)
|
||||||
|
default_columns = (
|
||||||
|
'pk', 'id', 'name', 'obj_type', 'status', 'created', 'scheduled', 'started', 'completed', 'user',
|
||||||
)
|
)
|
||||||
default_columns = ('pk', 'id', 'name', 'obj_type', 'status', 'created', 'scheduled', 'completed', 'user',)
|
|
||||||
|
|
||||||
|
|
||||||
class CustomLinkTable(NetBoxTable):
|
class CustomLinkTable(NetBoxTable):
|
||||||
|
@ -726,7 +726,7 @@ class ReportResultView(ContentTypePermissionRequiredMixin, View):
|
|||||||
'report': report,
|
'report': report,
|
||||||
'result': result,
|
'result': result,
|
||||||
})
|
})
|
||||||
if result.completed:
|
if result.completed or not result.started:
|
||||||
response.status_code = 286
|
response.status_code = 286
|
||||||
return response
|
return response
|
||||||
|
|
||||||
@ -860,7 +860,7 @@ class ScriptResultView(ContentTypePermissionRequiredMixin, GetScriptMixin, View)
|
|||||||
'script': script,
|
'script': script,
|
||||||
'result': result,
|
'result': result,
|
||||||
})
|
})
|
||||||
if result.completed:
|
if result.completed or not result.started:
|
||||||
response.status_code = 286
|
response.status_code = 286
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
@ -1,9 +1,12 @@
|
|||||||
{% load helpers %}
|
{% load helpers %}
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
Initiated: <strong>{{ result.created|annotated_date }}</strong>
|
{% if result.started %}
|
||||||
{% if result.scheduled %}
|
Started: <strong>{{ result.started|annotated_date }}</strong>
|
||||||
|
{% elif result.scheduled %}
|
||||||
Scheduled for: <strong>{{ result.scheduled|annotated_date }}</strong>
|
Scheduled for: <strong>{{ result.scheduled|annotated_date }}</strong>
|
||||||
|
{% else %}
|
||||||
|
Created: <strong>{{ result.created|annotated_date }}</strong>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if result.completed %}
|
{% if result.completed %}
|
||||||
Duration: <strong>{{ result.duration }}</strong>
|
Duration: <strong>{{ result.duration }}</strong>
|
||||||
@ -71,6 +74,6 @@
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% else %}
|
{% elif result.started %}
|
||||||
{% include 'extras/inc/result_pending.html' %}
|
{% include 'extras/inc/result_pending.html' %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -2,9 +2,12 @@
|
|||||||
{% load log_levels %}
|
{% load log_levels %}
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
Initiated: <strong>{{ result.created|annotated_date }}</strong>
|
{% if result.started %}
|
||||||
{% if result.scheduled %}
|
Started: <strong>{{ result.started|annotated_date }}</strong>
|
||||||
|
{% elif result.scheduled %}
|
||||||
Scheduled for: <strong>{{ result.scheduled|annotated_date }}</strong>
|
Scheduled for: <strong>{{ result.scheduled|annotated_date }}</strong>
|
||||||
|
{% else %}
|
||||||
|
Created: <strong>{{ result.created|annotated_date }}</strong>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if result.completed %}
|
{% if result.completed %}
|
||||||
Duration: <strong>{{ result.duration }}</strong>
|
Duration: <strong>{{ result.duration }}</strong>
|
||||||
@ -48,6 +51,6 @@
|
|||||||
{% else %}
|
{% else %}
|
||||||
<p class="text-muted">None</p>
|
<p class="text-muted">None</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% else %}
|
{% elif result.started %}
|
||||||
{% include 'extras/inc/result_pending.html' %}
|
{% include 'extras/inc/result_pending.html' %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
{% block content-wrapper %}
|
{% block content-wrapper %}
|
||||||
<div class="row p-3">
|
<div class="row p-3">
|
||||||
<div class="col col-md-12"{% if not result.completed %} hx-get="{% url 'extras:report_result' job_result_pk=result.pk %}" hx-trigger="every 3s"{% endif %}>
|
<div class="col col-md-12"{% if not result.completed %} hx-get="{% url 'extras:report_result' job_result_pk=result.pk %}" hx-trigger="every 5s"{% endif %}>
|
||||||
{% include 'extras/htmx/report_result.html' %}
|
{% include 'extras/htmx/report_result.html' %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -47,7 +47,7 @@
|
|||||||
<div class="tab-content mb-3">
|
<div class="tab-content mb-3">
|
||||||
<div role="tabpanel" class="tab-pane active" id="log">
|
<div role="tabpanel" class="tab-pane active" id="log">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col col-md-12"{% if not result.completed %} hx-get="{% url 'extras:script_result' job_result_pk=result.pk %}" hx-trigger="every 3s"{% endif %}>
|
<div class="col col-md-12"{% if not result.completed %} hx-get="{% url 'extras:script_result' job_result_pk=result.pk %}" hx-trigger="every 5s"{% endif %}>
|
||||||
{% include 'extras/htmx/script_result.html' %}
|
{% include 'extras/htmx/script_result.html' %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
Reference in New Issue
Block a user