|
|
@@ -1,18 +1,15 @@
|
|
|
import json
|
|
|
-import uuid
|
|
|
|
|
|
-import django_rq
|
|
|
from django.conf import settings
|
|
|
from django.contrib import admin
|
|
|
from django.contrib.auth.models import User
|
|
|
from django.contrib.contenttypes.fields import GenericForeignKey
|
|
|
from django.contrib.contenttypes.models import ContentType
|
|
|
from django.core.cache import cache
|
|
|
-from django.core.validators import MinValueValidator, ValidationError
|
|
|
+from django.core.validators import ValidationError
|
|
|
from django.db import models
|
|
|
from django.http import HttpResponse, QueryDict
|
|
|
from django.urls import reverse
|
|
|
-from django.urls.exceptions import NoReverseMatch
|
|
|
from django.utils import timezone
|
|
|
from django.utils.formats import date_format
|
|
|
from django.utils.translation import gettext as _
|
|
|
@@ -22,14 +19,11 @@ from extras.choices import *
|
|
|
from extras.conditions import ConditionSet
|
|
|
from extras.constants import *
|
|
|
from extras.utils import FeatureQuery, image_upload
|
|
|
-from netbox.config import get_config
|
|
|
-from netbox.constants import RQ_QUEUE_DEFAULT
|
|
|
from netbox.models import ChangeLoggedModel
|
|
|
from netbox.models.features import (
|
|
|
CloningMixin, CustomFieldsMixin, CustomLinksMixin, ExportTemplatesMixin, SyncedDataMixin, TagsMixin,
|
|
|
)
|
|
|
from utilities.querysets import RestrictedQuerySet
|
|
|
-from utilities.rqworker import get_queue_for_model
|
|
|
from utilities.utils import render_jinja2
|
|
|
|
|
|
__all__ = (
|
|
|
@@ -37,7 +31,6 @@ __all__ = (
|
|
|
'CustomLink',
|
|
|
'ExportTemplate',
|
|
|
'ImageAttachment',
|
|
|
- 'JobResult',
|
|
|
'JournalEntry',
|
|
|
'SavedFilter',
|
|
|
'Webhook',
|
|
|
@@ -583,193 +576,6 @@ class JournalEntry(CustomFieldsMixin, CustomLinksMixin, TagsMixin, ExportTemplat
|
|
|
return JournalEntryKindChoices.colors.get(self.kind)
|
|
|
|
|
|
|
|
|
-class JobResult(models.Model):
|
|
|
- """
|
|
|
- This model stores the results from running a user-defined report.
|
|
|
- """
|
|
|
- name = models.CharField(
|
|
|
- max_length=255
|
|
|
- )
|
|
|
- obj_type = models.ForeignKey(
|
|
|
- to=ContentType,
|
|
|
- related_name='job_results',
|
|
|
- verbose_name='Object types',
|
|
|
- limit_choices_to=FeatureQuery('jobs'),
|
|
|
- help_text=_("The object type to which this job result applies"),
|
|
|
- on_delete=models.CASCADE,
|
|
|
- )
|
|
|
- created = models.DateTimeField(
|
|
|
- auto_now_add=True
|
|
|
- )
|
|
|
- scheduled = models.DateTimeField(
|
|
|
- null=True,
|
|
|
- blank=True
|
|
|
- )
|
|
|
- interval = models.PositiveIntegerField(
|
|
|
- blank=True,
|
|
|
- null=True,
|
|
|
- validators=(
|
|
|
- MinValueValidator(1),
|
|
|
- ),
|
|
|
- help_text=_("Recurrence interval (in minutes)")
|
|
|
- )
|
|
|
- started = models.DateTimeField(
|
|
|
- null=True,
|
|
|
- blank=True
|
|
|
- )
|
|
|
- completed = models.DateTimeField(
|
|
|
- null=True,
|
|
|
- blank=True
|
|
|
- )
|
|
|
- user = models.ForeignKey(
|
|
|
- to=User,
|
|
|
- on_delete=models.SET_NULL,
|
|
|
- related_name='+',
|
|
|
- blank=True,
|
|
|
- null=True
|
|
|
- )
|
|
|
- status = models.CharField(
|
|
|
- max_length=30,
|
|
|
- choices=JobResultStatusChoices,
|
|
|
- default=JobResultStatusChoices.STATUS_PENDING
|
|
|
- )
|
|
|
- data = models.JSONField(
|
|
|
- null=True,
|
|
|
- blank=True
|
|
|
- )
|
|
|
- job_id = models.UUIDField(
|
|
|
- unique=True
|
|
|
- )
|
|
|
-
|
|
|
- objects = RestrictedQuerySet.as_manager()
|
|
|
-
|
|
|
- class Meta:
|
|
|
- ordering = ['-created']
|
|
|
-
|
|
|
- def __str__(self):
|
|
|
- return str(self.job_id)
|
|
|
-
|
|
|
- def delete(self, *args, **kwargs):
|
|
|
- super().delete(*args, **kwargs)
|
|
|
-
|
|
|
- rq_queue_name = get_config().QUEUE_MAPPINGS.get(self.obj_type.model, RQ_QUEUE_DEFAULT)
|
|
|
- queue = django_rq.get_queue(rq_queue_name)
|
|
|
- job = queue.fetch_job(str(self.job_id))
|
|
|
-
|
|
|
- if job:
|
|
|
- job.cancel()
|
|
|
-
|
|
|
- def get_absolute_url(self):
|
|
|
- try:
|
|
|
- return reverse(f'extras:{self.obj_type.model}_result', args=[self.pk])
|
|
|
- except NoReverseMatch:
|
|
|
- return None
|
|
|
-
|
|
|
- def get_status_color(self):
|
|
|
- return JobResultStatusChoices.colors.get(self.status)
|
|
|
-
|
|
|
- @property
|
|
|
- def duration(self):
|
|
|
- if not self.completed:
|
|
|
- return None
|
|
|
-
|
|
|
- start_time = self.started or self.created
|
|
|
-
|
|
|
- if not start_time:
|
|
|
- return None
|
|
|
-
|
|
|
- duration = self.completed - start_time
|
|
|
- minutes, seconds = divmod(duration.total_seconds(), 60)
|
|
|
-
|
|
|
- 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 not None:
|
|
|
- return
|
|
|
-
|
|
|
- # Start the job
|
|
|
- self.started = timezone.now()
|
|
|
- self.status = JobResultStatusChoices.STATUS_RUNNING
|
|
|
- JobResult.objects.filter(pk=self.pk).update(started=self.started, status=self.status)
|
|
|
-
|
|
|
- # Handle webhooks
|
|
|
- self.trigger_webhooks(event=EVENT_JOB_START)
|
|
|
-
|
|
|
- def terminate(self, status=JobResultStatusChoices.STATUS_COMPLETED):
|
|
|
- """
|
|
|
- Mark the job as completed, optionally specifying a particular termination status.
|
|
|
- """
|
|
|
- valid_statuses = JobResultStatusChoices.TERMINAL_STATE_CHOICES
|
|
|
- if status not in valid_statuses:
|
|
|
- raise ValueError(f"Invalid status for job termination. Choices are: {', '.join(valid_statuses)}")
|
|
|
-
|
|
|
- # Mark the job as completed
|
|
|
- self.status = status
|
|
|
- self.completed = timezone.now()
|
|
|
- JobResult.objects.filter(pk=self.pk).update(status=self.status, completed=self.completed)
|
|
|
-
|
|
|
- # Handle webhooks
|
|
|
- self.trigger_webhooks(event=EVENT_JOB_END)
|
|
|
-
|
|
|
- @classmethod
|
|
|
- def enqueue_job(cls, func, name, obj_type, user, schedule_at=None, interval=None, *args, **kwargs):
|
|
|
- """
|
|
|
- Create a JobResult instance and enqueue a job using the given callable
|
|
|
-
|
|
|
- Args:
|
|
|
- func: The callable object to be enqueued for execution
|
|
|
- name: Name for the JobResult instance
|
|
|
- obj_type: ContentType to link to the JobResult instance obj_type
|
|
|
- user: User object to link to the JobResult instance
|
|
|
- schedule_at: Schedule the job to be executed at the passed date and time
|
|
|
- interval: Recurrence interval (in minutes)
|
|
|
- """
|
|
|
- rq_queue_name = get_queue_for_model(obj_type.model)
|
|
|
- queue = django_rq.get_queue(rq_queue_name)
|
|
|
- status = JobResultStatusChoices.STATUS_SCHEDULED if schedule_at else JobResultStatusChoices.STATUS_PENDING
|
|
|
- job_result: JobResult = JobResult.objects.create(
|
|
|
- name=name,
|
|
|
- status=status,
|
|
|
- obj_type=obj_type,
|
|
|
- scheduled=schedule_at,
|
|
|
- interval=interval,
|
|
|
- user=user,
|
|
|
- job_id=uuid.uuid4()
|
|
|
- )
|
|
|
-
|
|
|
- if schedule_at:
|
|
|
- queue.enqueue_at(schedule_at, func, job_id=str(job_result.job_id), job_result=job_result, **kwargs)
|
|
|
- else:
|
|
|
- queue.enqueue(func, job_id=str(job_result.job_id), job_result=job_result, **kwargs)
|
|
|
-
|
|
|
- return job_result
|
|
|
-
|
|
|
- def trigger_webhooks(self, event):
|
|
|
- rq_queue_name = get_config().QUEUE_MAPPINGS.get('webhook', RQ_QUEUE_DEFAULT)
|
|
|
- rq_queue = django_rq.get_queue(rq_queue_name, is_async=False)
|
|
|
-
|
|
|
- # Fetch any webhooks matching this object type and action
|
|
|
- webhooks = Webhook.objects.filter(
|
|
|
- **{f'type_{event}': True},
|
|
|
- content_types=self.obj_type,
|
|
|
- enabled=True
|
|
|
- )
|
|
|
-
|
|
|
- for webhook in webhooks:
|
|
|
- rq_queue.enqueue(
|
|
|
- "extras.webhooks_worker.process_webhook",
|
|
|
- webhook=webhook,
|
|
|
- model_name=self.obj_type.model,
|
|
|
- event=event,
|
|
|
- data=self.data,
|
|
|
- timestamp=str(timezone.now()),
|
|
|
- username=self.user.username
|
|
|
- )
|
|
|
-
|
|
|
-
|
|
|
class ConfigRevision(models.Model):
|
|
|
"""
|
|
|
An atomic revision of NetBox's configuration.
|