diff --git a/netbox/core/models/data.py b/netbox/core/models/data.py index eaeef5f88..8d7164047 100644 --- a/netbox/core/models/data.py +++ b/netbox/core/models/data.py @@ -16,7 +16,6 @@ from django.utils.translation import gettext as _ from extras.models import JobResult from netbox.models import PrimaryModel -from netbox.models.features import ChangeLoggingMixin from netbox.registry import registry from utilities.files import sha256_hash from utilities.querysets import RestrictedQuerySet @@ -116,6 +115,7 @@ class DataSource(PrimaryModel): """ # Set the status to "syncing" self.status = DataSourceStatusChoices.QUEUED + DataSource.objects.filter(pk=self.pk).update(status=self.status) # Enqueue a sync job job_result = JobResult.enqueue_job( @@ -137,8 +137,8 @@ class DataSource(PrimaryModel): """ Create/update/delete child DataFiles as necessary to synchronize with the remote source. """ - if not self.ready_for_sync: - raise SyncError(f"Cannot initiate sync; data source not ready/enabled") + if self.status == DataSourceStatusChoices.SYNCING: + raise SyncError(f"Cannot initiate sync; syncing already in progress.") # Emit the pre_sync signal pre_sync.send(sender=self.__class__, instance=self) diff --git a/netbox/core/views.py b/netbox/core/views.py index d4baa4eaf..7a603ba1a 100644 --- a/netbox/core/views.py +++ b/netbox/core/views.py @@ -3,6 +3,7 @@ from django.shortcuts import get_object_or_404, redirect from netbox.views import generic from netbox.views.generic.base import BaseObjectView +from utilities.rqworker import get_queue_for_model, get_workers_for_queue from utilities.utils import count_related from utilities.views import register_model_view from . import filtersets, forms, tables @@ -31,7 +32,11 @@ class DataSourceView(generic.ObjectView): (DataFile.objects.restrict(request.user, 'view').filter(source=instance), 'source_id'), ) + queue_name = get_queue_for_model(DataSource) + sync_enabled = bool(get_workers_for_queue(queue_name)) + return { + 'sync_enabled': sync_enabled, 'related_models': related_models, } diff --git a/netbox/extras/models/models.py b/netbox/extras/models/models.py index 48517c39d..9b7dcce33 100644 --- a/netbox/extras/models/models.py +++ b/netbox/extras/models/models.py @@ -30,6 +30,7 @@ from netbox.models.features import ( TagsMixin, WebhooksMixin, ) from utilities.querysets import RestrictedQuerySet +from utilities.rqworker import get_queue_for_model from utilities.utils import render_jinja2 __all__ = ( @@ -730,7 +731,7 @@ class JobResult(models.Model): schedule_at: Schedule the job to be executed at the passed date and time interval: Recurrence interval (in minutes) """ - rq_queue_name = get_config().QUEUE_MAPPINGS.get(obj_type.model, RQ_QUEUE_DEFAULT) + 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( diff --git a/netbox/extras/views.py b/netbox/extras/views.py index 07829429f..daf435861 100644 --- a/netbox/extras/views.py +++ b/netbox/extras/views.py @@ -6,14 +6,13 @@ from django.http import Http404, HttpResponseBadRequest, HttpResponseForbidden, from django.shortcuts import get_object_or_404, redirect, render from django.urls import reverse from django.views.generic import View -from django_rq.queues import get_connection -from rq import Worker from extras.dashboard.forms import DashboardWidgetAddForm, DashboardWidgetForm from extras.dashboard.utils import get_widget_class from netbox.views import generic from utilities.forms import ConfirmationForm, get_field_value from utilities.htmx import is_htmx +from utilities.rqworker import get_workers_for_queue from utilities.templatetags.builtins.filters import render_markdown from utilities.utils import copy_safe_request, count_related, get_viewname, normalize_querydict, shallow_compare_dict from utilities.views import ContentTypePermissionRequiredMixin, register_model_view @@ -863,7 +862,7 @@ class ReportView(ContentTypePermissionRequiredMixin, View): if form.is_valid(): # Allow execution only if RQ worker process is running - if not Worker.count(get_connection('default')): + if not get_workers_for_queue('default'): messages.error(request, "Unable to run report: RQ worker process not running.") return render(request, 'extras/report.html', { 'report': report, @@ -994,7 +993,7 @@ class ScriptView(ContentTypePermissionRequiredMixin, GetScriptMixin, View): form = script.as_form(request.POST, request.FILES) # Allow execution only if RQ worker process is running - if not Worker.count(get_connection('default')): + if not get_workers_for_queue('default'): messages.error(request, "Unable to run script: RQ worker process not running.") elif form.is_valid(): diff --git a/netbox/templates/core/datasource.html b/netbox/templates/core/datasource.html index 061017ad7..193ceac7a 100644 --- a/netbox/templates/core/datasource.html +++ b/netbox/templates/core/datasource.html @@ -6,7 +6,7 @@ {% block extra_controls %} {% if perms.core.sync_datasource %} - {% if object.ready_for_sync %} + {% if sync_enabled and object.ready_for_sync %}
{% csrf_token %}
{% else %} - + + + {% endif %} {% endif %} {% endblock %} diff --git a/netbox/utilities/rqworker.py b/netbox/utilities/rqworker.py new file mode 100644 index 000000000..5866dfee0 --- /dev/null +++ b/netbox/utilities/rqworker.py @@ -0,0 +1,24 @@ +from django_rq.queues import get_connection +from rq import Worker + +from netbox.config import get_config +from netbox.constants import RQ_QUEUE_DEFAULT + +__all__ = ( + 'get_queue_for_model', + 'get_workers_for_queue', +) + + +def get_queue_for_model(model): + """ + Return the configured queue name for jobs associated with the given model. + """ + return get_config().QUEUE_MAPPINGS.get(model, RQ_QUEUE_DEFAULT) + + +def get_workers_for_queue(queue_name): + """ + Returns True if a worker process is currently servicing the specified queue. + """ + return Worker.count(get_connection(queue_name))