Compare commits

...

3 Commits

Author SHA1 Message Date
Jeremy Stretch
c2d3363930 Closes #18399: Refactor logic for marking data source syncing as queued (#19960)
Some checks are pending
CI / build (20.x, 3.10) (push) Waiting to run
CI / build (20.x, 3.11) (push) Waiting to run
CI / build (20.x, 3.12) (push) Waiting to run
2025-07-28 09:04:38 -07:00
Jeremy Stretch
6e30c11017 Fixes #19956: Prevent duplicate deletion records from cascading deletions 2025-07-28 09:49:08 -04:00
github-actions
b01c75cf3a Update source translation strings 2025-07-25 05:07:26 +00:00
6 changed files with 128 additions and 58 deletions

View File

@@ -1,29 +1,28 @@
from django.http import Http404, HttpResponse from django.http import Http404, HttpResponse
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django_rq.queues import get_redis_connection
from django_rq.settings import QUEUES_LIST
from django_rq.utils import get_statistics
from drf_spectacular.types import OpenApiTypes from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import extend_schema from drf_spectacular.utils import extend_schema
from rest_framework import viewsets
from rest_framework.decorators import action from rest_framework.decorators import action
from rest_framework.exceptions import PermissionDenied from rest_framework.exceptions import PermissionDenied
from rest_framework.permissions import IsAdminUser
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.routers import APIRootView from rest_framework.routers import APIRootView
from rest_framework.viewsets import ReadOnlyModelViewSet from rest_framework.viewsets import ReadOnlyModelViewSet
from rq.job import Job as RQ_Job
from rq.worker import Worker
from core import filtersets from core import filtersets
from core.choices import DataSourceStatusChoices
from core.jobs import SyncDataSourceJob from core.jobs import SyncDataSourceJob
from core.models import * from core.models import *
from core.utils import delete_rq_job, enqueue_rq_job, get_rq_jobs, requeue_rq_job, stop_rq_job from core.utils import delete_rq_job, enqueue_rq_job, get_rq_jobs, requeue_rq_job, stop_rq_job
from django_rq.queues import get_redis_connection
from django_rq.utils import get_statistics
from django_rq.settings import QUEUES_LIST
from netbox.api.metadata import ContentTypeMetadata from netbox.api.metadata import ContentTypeMetadata
from netbox.api.pagination import LimitOffsetListPagination from netbox.api.pagination import LimitOffsetListPagination
from netbox.api.viewsets import NetBoxModelViewSet, NetBoxReadOnlyModelViewSet from netbox.api.viewsets import NetBoxModelViewSet, NetBoxReadOnlyModelViewSet
from rest_framework import viewsets
from rest_framework.permissions import IsAdminUser
from rq.job import Job as RQ_Job
from rq.worker import Worker
from . import serializers from . import serializers
@@ -50,10 +49,8 @@ class DataSourceViewSet(NetBoxModelViewSet):
if not request.user.has_perm('core.sync_datasource', obj=datasource): if not request.user.has_perm('core.sync_datasource', obj=datasource):
raise PermissionDenied(_("This user does not have permission to synchronize this data source.")) raise PermissionDenied(_("This user does not have permission to synchronize this data source."))
# Enqueue the sync job & update the DataSource's status # Enqueue the sync job
SyncDataSourceJob.enqueue(instance=datasource, user=request.user) SyncDataSourceJob.enqueue(instance=datasource, user=request.user)
datasource.status = DataSourceStatusChoices.QUEUED
DataSource.objects.filter(pk=datasource.pk).update(status=datasource.status)
serializer = serializers.DataSourceSerializer(datasource, context={'request': request}) serializer = serializers.DataSourceSerializer(datasource, context={'request': request})

View File

@@ -21,6 +21,17 @@ class SyncDataSourceJob(JobRunner):
class Meta: class Meta:
name = 'Synchronization' name = 'Synchronization'
@classmethod
def enqueue(cls, *args, **kwargs):
job = super().enqueue(*args, **kwargs)
# Update the DataSource's synchronization status to queued
if datasource := job.object:
datasource.status = DataSourceStatusChoices.QUEUED
DataSource.objects.filter(pk=datasource.pk).update(status=datasource.status)
return job
def run(self, *args, **kwargs): def run(self, *args, **kwargs):
datasource = DataSource.objects.get(pk=self.job.object_id) datasource = DataSource.objects.get(pk=self.job.object_id)

View File

@@ -1,10 +1,12 @@
import logging import logging
from threading import local
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.db.models.fields.reverse_related import ManyToManyRel, ManyToOneRel from django.db.models.fields.reverse_related import ManyToManyRel, ManyToOneRel
from django.db.models.signals import m2m_changed, post_save, pre_delete from django.db.models.signals import m2m_changed, post_save, pre_delete
from django.dispatch import receiver, Signal from django.dispatch import receiver, Signal
from django.core.signals import request_finished
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django_prometheus.models import model_deletes, model_inserts, model_updates from django_prometheus.models import model_deletes, model_inserts, model_updates
@@ -42,6 +44,10 @@ clear_events = Signal()
# Change logging & event handling # Change logging & event handling
# #
# Used to track received signals per object
_signals_received = local()
@receiver((post_save, m2m_changed)) @receiver((post_save, m2m_changed))
def handle_changed_object(sender, instance, **kwargs): def handle_changed_object(sender, instance, **kwargs):
""" """
@@ -130,6 +136,16 @@ def handle_deleted_object(sender, instance, **kwargs):
if request is None: if request is None:
return return
# Check whether we've already processed a pre_delete signal for this object. (This can
# happen e.g. when both a parent object and its child are deleted simultaneously, due
# to cascading deletion.)
if not hasattr(_signals_received, 'pre_delete'):
_signals_received.pre_delete = set()
signature = (ContentType.objects.get_for_model(instance), instance.pk)
if signature in _signals_received.pre_delete:
return
_signals_received.pre_delete.add(signature)
# Record an ObjectChange if applicable # Record an ObjectChange if applicable
if hasattr(instance, 'to_objectchange'): if hasattr(instance, 'to_objectchange'):
if hasattr(instance, 'snapshot') and not getattr(instance, '_prechange_snapshot', None): if hasattr(instance, 'snapshot') and not getattr(instance, '_prechange_snapshot', None):
@@ -179,6 +195,14 @@ def handle_deleted_object(sender, instance, **kwargs):
model_deletes.labels(instance._meta.model_name).inc() model_deletes.labels(instance._meta.model_name).inc()
@receiver(request_finished)
def clear_signal_history(sender, **kwargs):
"""
Clear out the signals history once the request is finished.
"""
_signals_received.pre_delete = set()
@receiver(clear_events) @receiver(clear_events)
def clear_events_queue(sender, **kwargs): def clear_events_queue(sender, **kwargs):
""" """

View File

@@ -346,6 +346,38 @@ class ChangeLogViewTest(ModelViewTestCase):
self.assertEqual(changes[1].changed_object_type, ContentType.objects.get_for_model(Interface)) self.assertEqual(changes[1].changed_object_type, ContentType.objects.get_for_model(Interface))
self.assertEqual(changes[2].changed_object_type, ContentType.objects.get_for_model(Device)) self.assertEqual(changes[2].changed_object_type, ContentType.objects.get_for_model(Device))
def test_duplicate_deletions(self):
"""
Check that a cascading deletion event does not generate multiple "deleted" ObjectChange records for
the same object.
"""
role1 = DeviceRole(name='Role 1', slug='role-1')
role1.save()
role2 = DeviceRole(name='Role 2', slug='role-2', parent=role1)
role2.save()
pk_list = [role1.pk, role2.pk]
# Delete both objects simultaneously
form_data = {
'pk': pk_list,
'confirm': True,
'_confirm': True,
}
request = {
'path': reverse('dcim:devicerole_bulk_delete'),
'data': post_data(form_data),
}
self.add_permissions('dcim.delete_devicerole')
self.assertHttpStatus(self.client.post(**request), 302)
# This should result in exactly one change record per object
objectchanges = ObjectChange.objects.filter(
changed_object_type=ContentType.objects.get_for_model(DeviceRole),
changed_object_id__in=pk_list,
action=ObjectChangeActionChoices.ACTION_DELETE
)
self.assertEqual(objectchanges.count(), 2)
class ChangeLogAPITest(APITestCase): class ChangeLogAPITest(APITestCase):

View File

@@ -33,7 +33,6 @@ from utilities.json import ConfigJSONEncoder
from utilities.query import count_related from utilities.query import count_related
from utilities.views import ContentTypePermissionRequiredMixin, GetRelatedModelsMixin, register_model_view from utilities.views import ContentTypePermissionRequiredMixin, GetRelatedModelsMixin, register_model_view
from . import filtersets, forms, tables from . import filtersets, forms, tables
from .choices import DataSourceStatusChoices
from .jobs import SyncDataSourceJob from .jobs import SyncDataSourceJob
from .models import * from .models import *
from .plugins import get_catalog_plugins, get_local_plugins from .plugins import get_catalog_plugins, get_local_plugins
@@ -78,12 +77,8 @@ class DataSourceSyncView(BaseObjectView):
def post(self, request, pk): def post(self, request, pk):
datasource = get_object_or_404(self.queryset, pk=pk) datasource = get_object_or_404(self.queryset, pk=pk)
# Enqueue the sync job
# Enqueue the sync job & update the DataSource's status
job = SyncDataSourceJob.enqueue(instance=datasource, user=request.user) job = SyncDataSourceJob.enqueue(instance=datasource, user=request.user)
datasource.status = DataSourceStatusChoices.QUEUED
DataSource.objects.filter(pk=datasource.pk).update(status=datasource.status)
messages.success( messages.success(
request, request,
_("Queued job #{id} to sync {datasource}").format(id=job.pk, datasource=datasource) _("Queued job #{id} to sync {datasource}").format(id=job.pk, datasource=datasource)

View File

@@ -8,7 +8,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-07-24 05:05+0000\n" "POT-Creation-Date: 2025-07-25 05:07+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
@@ -215,8 +215,8 @@ msgstr ""
#: netbox/dcim/forms/bulk_edit.py:344 netbox/dcim/forms/bulk_edit.py:730 #: netbox/dcim/forms/bulk_edit.py:344 netbox/dcim/forms/bulk_edit.py:730
#: netbox/dcim/forms/bulk_edit.py:935 netbox/dcim/forms/bulk_import.py:134 #: netbox/dcim/forms/bulk_edit.py:935 netbox/dcim/forms/bulk_import.py:134
#: netbox/dcim/forms/bulk_import.py:236 netbox/dcim/forms/bulk_import.py:337 #: netbox/dcim/forms/bulk_import.py:236 netbox/dcim/forms/bulk_import.py:337
#: netbox/dcim/forms/bulk_import.py:598 netbox/dcim/forms/bulk_import.py:1512 #: netbox/dcim/forms/bulk_import.py:598 netbox/dcim/forms/bulk_import.py:1539
#: netbox/dcim/forms/bulk_import.py:1540 netbox/dcim/forms/filtersets.py:89 #: netbox/dcim/forms/bulk_import.py:1567 netbox/dcim/forms/filtersets.py:89
#: netbox/dcim/forms/filtersets.py:227 netbox/dcim/forms/filtersets.py:344 #: netbox/dcim/forms/filtersets.py:227 netbox/dcim/forms/filtersets.py:344
#: netbox/dcim/forms/filtersets.py:441 netbox/dcim/forms/filtersets.py:773 #: netbox/dcim/forms/filtersets.py:441 netbox/dcim/forms/filtersets.py:773
#: netbox/dcim/forms/filtersets.py:992 netbox/dcim/forms/filtersets.py:1065 #: netbox/dcim/forms/filtersets.py:992 netbox/dcim/forms/filtersets.py:1065
@@ -656,13 +656,13 @@ msgstr ""
#: netbox/circuits/forms/filtersets.py:321 netbox/dcim/forms/bulk_edit.py:216 #: netbox/circuits/forms/filtersets.py:321 netbox/dcim/forms/bulk_edit.py:216
#: netbox/dcim/forms/bulk_edit.py:656 netbox/dcim/forms/bulk_edit.py:866 #: netbox/dcim/forms/bulk_edit.py:656 netbox/dcim/forms/bulk_edit.py:866
#: netbox/dcim/forms/bulk_edit.py:1235 netbox/dcim/forms/bulk_edit.py:1262 #: netbox/dcim/forms/bulk_edit.py:1235 netbox/dcim/forms/bulk_edit.py:1262
#: netbox/dcim/forms/bulk_edit.py:1796 netbox/dcim/forms/filtersets.py:1132 #: netbox/dcim/forms/bulk_edit.py:1796 netbox/dcim/forms/bulk_import.py:1414
#: netbox/dcim/forms/filtersets.py:1390 netbox/dcim/forms/filtersets.py:1543 #: netbox/dcim/forms/filtersets.py:1132 netbox/dcim/forms/filtersets.py:1390
#: netbox/dcim/forms/filtersets.py:1567 netbox/dcim/tables/devices.py:748 #: netbox/dcim/forms/filtersets.py:1543 netbox/dcim/forms/filtersets.py:1567
#: netbox/dcim/tables/devices.py:804 netbox/dcim/tables/devices.py:1045 #: netbox/dcim/tables/devices.py:748 netbox/dcim/tables/devices.py:804
#: netbox/dcim/tables/devicetypes.py:256 netbox/dcim/tables/devicetypes.py:271 #: netbox/dcim/tables/devices.py:1045 netbox/dcim/tables/devicetypes.py:256
#: netbox/dcim/tables/racks.py:33 netbox/extras/forms/bulk_edit.py:303 #: netbox/dcim/tables/devicetypes.py:271 netbox/dcim/tables/racks.py:33
#: netbox/extras/tables/tables.py:487 #: netbox/extras/forms/bulk_edit.py:303 netbox/extras/tables/tables.py:487
#: netbox/templates/circuits/circuittype.html:30 #: netbox/templates/circuits/circuittype.html:30
#: netbox/templates/circuits/virtualcircuittype.html:30 #: netbox/templates/circuits/virtualcircuittype.html:30
#: netbox/templates/dcim/cable.html:40 netbox/templates/dcim/devicerole.html:38 #: netbox/templates/dcim/cable.html:40 netbox/templates/dcim/devicerole.html:38
@@ -694,7 +694,7 @@ msgstr ""
#: netbox/dcim/forms/bulk_import.py:818 netbox/dcim/forms/bulk_import.py:838 #: netbox/dcim/forms/bulk_import.py:818 netbox/dcim/forms/bulk_import.py:838
#: netbox/dcim/forms/bulk_import.py:924 netbox/dcim/forms/bulk_import.py:1018 #: netbox/dcim/forms/bulk_import.py:924 netbox/dcim/forms/bulk_import.py:1018
#: netbox/dcim/forms/bulk_import.py:1060 netbox/dcim/forms/bulk_import.py:1395 #: netbox/dcim/forms/bulk_import.py:1060 netbox/dcim/forms/bulk_import.py:1395
#: netbox/dcim/forms/bulk_import.py:1577 netbox/dcim/forms/filtersets.py:1023 #: netbox/dcim/forms/bulk_import.py:1604 netbox/dcim/forms/filtersets.py:1023
#: netbox/dcim/forms/filtersets.py:1122 netbox/dcim/forms/filtersets.py:1243 #: netbox/dcim/forms/filtersets.py:1122 netbox/dcim/forms/filtersets.py:1243
#: netbox/dcim/forms/filtersets.py:1315 netbox/dcim/forms/filtersets.py:1340 #: netbox/dcim/forms/filtersets.py:1315 netbox/dcim/forms/filtersets.py:1340
#: netbox/dcim/forms/filtersets.py:1364 netbox/dcim/forms/filtersets.py:1384 #: netbox/dcim/forms/filtersets.py:1364 netbox/dcim/forms/filtersets.py:1384
@@ -765,7 +765,7 @@ msgstr ""
#: netbox/dcim/forms/bulk_import.py:150 netbox/dcim/forms/bulk_import.py:254 #: netbox/dcim/forms/bulk_import.py:150 netbox/dcim/forms/bulk_import.py:254
#: netbox/dcim/forms/bulk_import.py:563 netbox/dcim/forms/bulk_import.py:717 #: netbox/dcim/forms/bulk_import.py:563 netbox/dcim/forms/bulk_import.py:717
#: netbox/dcim/forms/bulk_import.py:1168 netbox/dcim/forms/bulk_import.py:1389 #: netbox/dcim/forms/bulk_import.py:1168 netbox/dcim/forms/bulk_import.py:1389
#: netbox/dcim/forms/bulk_import.py:1572 netbox/dcim/forms/bulk_import.py:1636 #: netbox/dcim/forms/bulk_import.py:1599 netbox/dcim/forms/bulk_import.py:1663
#: netbox/dcim/forms/filtersets.py:180 netbox/dcim/forms/filtersets.py:239 #: netbox/dcim/forms/filtersets.py:180 netbox/dcim/forms/filtersets.py:239
#: netbox/dcim/forms/filtersets.py:361 netbox/dcim/forms/filtersets.py:819 #: netbox/dcim/forms/filtersets.py:361 netbox/dcim/forms/filtersets.py:819
#: netbox/dcim/forms/filtersets.py:944 netbox/dcim/forms/filtersets.py:1026 #: netbox/dcim/forms/filtersets.py:944 netbox/dcim/forms/filtersets.py:1026
@@ -843,7 +843,7 @@ msgstr ""
#: netbox/dcim/forms/bulk_import.py:110 netbox/dcim/forms/bulk_import.py:155 #: netbox/dcim/forms/bulk_import.py:110 netbox/dcim/forms/bulk_import.py:155
#: netbox/dcim/forms/bulk_import.py:247 netbox/dcim/forms/bulk_import.py:362 #: netbox/dcim/forms/bulk_import.py:247 netbox/dcim/forms/bulk_import.py:362
#: netbox/dcim/forms/bulk_import.py:537 netbox/dcim/forms/bulk_import.py:1401 #: netbox/dcim/forms/bulk_import.py:537 netbox/dcim/forms/bulk_import.py:1401
#: netbox/dcim/forms/bulk_import.py:1629 netbox/dcim/forms/filtersets.py:175 #: netbox/dcim/forms/bulk_import.py:1656 netbox/dcim/forms/filtersets.py:175
#: netbox/dcim/forms/filtersets.py:207 netbox/dcim/forms/filtersets.py:325 #: netbox/dcim/forms/filtersets.py:207 netbox/dcim/forms/filtersets.py:325
#: netbox/dcim/forms/filtersets.py:401 netbox/dcim/forms/filtersets.py:422 #: netbox/dcim/forms/filtersets.py:401 netbox/dcim/forms/filtersets.py:422
#: netbox/dcim/forms/filtersets.py:742 netbox/dcim/forms/filtersets.py:936 #: netbox/dcim/forms/filtersets.py:742 netbox/dcim/forms/filtersets.py:936
@@ -1149,7 +1149,7 @@ msgstr ""
#: netbox/circuits/forms/bulk_import.py:229 netbox/dcim/forms/bulk_import.py:93 #: netbox/circuits/forms/bulk_import.py:229 netbox/dcim/forms/bulk_import.py:93
#: netbox/dcim/forms/bulk_import.py:152 netbox/dcim/forms/bulk_import.py:256 #: netbox/dcim/forms/bulk_import.py:152 netbox/dcim/forms/bulk_import.py:256
#: netbox/dcim/forms/bulk_import.py:565 netbox/dcim/forms/bulk_import.py:719 #: netbox/dcim/forms/bulk_import.py:565 netbox/dcim/forms/bulk_import.py:719
#: netbox/dcim/forms/bulk_import.py:1170 netbox/dcim/forms/bulk_import.py:1574 #: netbox/dcim/forms/bulk_import.py:1170 netbox/dcim/forms/bulk_import.py:1601
#: netbox/ipam/forms/bulk_import.py:197 netbox/ipam/forms/bulk_import.py:265 #: netbox/ipam/forms/bulk_import.py:197 netbox/ipam/forms/bulk_import.py:265
#: netbox/ipam/forms/bulk_import.py:301 netbox/ipam/forms/bulk_import.py:498 #: netbox/ipam/forms/bulk_import.py:301 netbox/ipam/forms/bulk_import.py:498
#: netbox/ipam/forms/bulk_import.py:511 #: netbox/ipam/forms/bulk_import.py:511
@@ -1165,8 +1165,8 @@ msgstr ""
#: netbox/circuits/forms/bulk_import.py:236 #: netbox/circuits/forms/bulk_import.py:236
#: netbox/dcim/forms/bulk_import.py:114 netbox/dcim/forms/bulk_import.py:159 #: netbox/dcim/forms/bulk_import.py:114 netbox/dcim/forms/bulk_import.py:159
#: netbox/dcim/forms/bulk_import.py:366 netbox/dcim/forms/bulk_import.py:541 #: netbox/dcim/forms/bulk_import.py:366 netbox/dcim/forms/bulk_import.py:541
#: netbox/dcim/forms/bulk_import.py:1405 netbox/dcim/forms/bulk_import.py:1569 #: netbox/dcim/forms/bulk_import.py:1405 netbox/dcim/forms/bulk_import.py:1596
#: netbox/dcim/forms/bulk_import.py:1633 netbox/ipam/forms/bulk_import.py:45 #: netbox/dcim/forms/bulk_import.py:1660 netbox/ipam/forms/bulk_import.py:45
#: netbox/ipam/forms/bulk_import.py:74 netbox/ipam/forms/bulk_import.py:102 #: netbox/ipam/forms/bulk_import.py:74 netbox/ipam/forms/bulk_import.py:102
#: netbox/ipam/forms/bulk_import.py:122 netbox/ipam/forms/bulk_import.py:142 #: netbox/ipam/forms/bulk_import.py:122 netbox/ipam/forms/bulk_import.py:142
#: netbox/ipam/forms/bulk_import.py:171 netbox/ipam/forms/bulk_import.py:260 #: netbox/ipam/forms/bulk_import.py:171 netbox/ipam/forms/bulk_import.py:260
@@ -1246,8 +1246,8 @@ msgstr ""
#: netbox/dcim/forms/bulk_edit.py:466 netbox/dcim/forms/bulk_edit.py:735 #: netbox/dcim/forms/bulk_edit.py:466 netbox/dcim/forms/bulk_edit.py:735
#: netbox/dcim/forms/bulk_edit.py:790 netbox/dcim/forms/bulk_edit.py:944 #: netbox/dcim/forms/bulk_edit.py:790 netbox/dcim/forms/bulk_edit.py:944
#: netbox/dcim/forms/bulk_import.py:241 netbox/dcim/forms/bulk_import.py:343 #: netbox/dcim/forms/bulk_import.py:241 netbox/dcim/forms/bulk_import.py:343
#: netbox/dcim/forms/bulk_import.py:604 netbox/dcim/forms/bulk_import.py:1518 #: netbox/dcim/forms/bulk_import.py:604 netbox/dcim/forms/bulk_import.py:1545
#: netbox/dcim/forms/bulk_import.py:1552 netbox/dcim/forms/filtersets.py:97 #: netbox/dcim/forms/bulk_import.py:1579 netbox/dcim/forms/filtersets.py:97
#: netbox/dcim/forms/filtersets.py:324 netbox/dcim/forms/filtersets.py:358 #: netbox/dcim/forms/filtersets.py:324 netbox/dcim/forms/filtersets.py:358
#: netbox/dcim/forms/filtersets.py:398 netbox/dcim/forms/filtersets.py:449 #: netbox/dcim/forms/filtersets.py:398 netbox/dcim/forms/filtersets.py:449
#: netbox/dcim/forms/filtersets.py:739 netbox/dcim/forms/filtersets.py:782 #: netbox/dcim/forms/filtersets.py:739 netbox/dcim/forms/filtersets.py:782
@@ -1949,7 +1949,7 @@ msgstr ""
#: netbox/dcim/forms/bulk_import.py:1007 netbox/dcim/forms/bulk_import.py:1055 #: netbox/dcim/forms/bulk_import.py:1007 netbox/dcim/forms/bulk_import.py:1055
#: netbox/dcim/forms/bulk_import.py:1072 netbox/dcim/forms/bulk_import.py:1084 #: netbox/dcim/forms/bulk_import.py:1072 netbox/dcim/forms/bulk_import.py:1084
#: netbox/dcim/forms/bulk_import.py:1132 netbox/dcim/forms/bulk_import.py:1254 #: netbox/dcim/forms/bulk_import.py:1132 netbox/dcim/forms/bulk_import.py:1254
#: netbox/dcim/forms/bulk_import.py:1623 netbox/dcim/forms/connections.py:24 #: netbox/dcim/forms/bulk_import.py:1650 netbox/dcim/forms/connections.py:24
#: netbox/dcim/forms/filtersets.py:133 netbox/dcim/forms/filtersets.py:941 #: netbox/dcim/forms/filtersets.py:133 netbox/dcim/forms/filtersets.py:941
#: netbox/dcim/forms/filtersets.py:973 netbox/dcim/forms/filtersets.py:1119 #: netbox/dcim/forms/filtersets.py:973 netbox/dcim/forms/filtersets.py:1119
#: netbox/dcim/forms/filtersets.py:1310 netbox/dcim/forms/filtersets.py:1335 #: netbox/dcim/forms/filtersets.py:1310 netbox/dcim/forms/filtersets.py:1335
@@ -4194,8 +4194,8 @@ msgstr ""
#: netbox/dcim/forms/bulk_edit.py:465 netbox/dcim/forms/bulk_edit.py:972 #: netbox/dcim/forms/bulk_edit.py:465 netbox/dcim/forms/bulk_edit.py:972
#: netbox/dcim/forms/bulk_import.py:350 netbox/dcim/forms/bulk_import.py:353 #: netbox/dcim/forms/bulk_import.py:350 netbox/dcim/forms/bulk_import.py:353
#: netbox/dcim/forms/bulk_import.py:611 netbox/dcim/forms/bulk_import.py:1559 #: netbox/dcim/forms/bulk_import.py:611 netbox/dcim/forms/bulk_import.py:1586
#: netbox/dcim/forms/bulk_import.py:1563 netbox/dcim/forms/filtersets.py:106 #: netbox/dcim/forms/bulk_import.py:1590 netbox/dcim/forms/filtersets.py:106
#: netbox/dcim/forms/filtersets.py:326 netbox/dcim/forms/filtersets.py:407 #: netbox/dcim/forms/filtersets.py:326 netbox/dcim/forms/filtersets.py:407
#: netbox/dcim/forms/filtersets.py:421 netbox/dcim/forms/filtersets.py:459 #: netbox/dcim/forms/filtersets.py:421 netbox/dcim/forms/filtersets.py:459
#: netbox/dcim/forms/filtersets.py:792 netbox/dcim/forms/filtersets.py:1005 #: netbox/dcim/forms/filtersets.py:792 netbox/dcim/forms/filtersets.py:1005
@@ -4405,17 +4405,17 @@ msgstr ""
msgid "Domain" msgid "Domain"
msgstr "" msgstr ""
#: netbox/dcim/forms/bulk_edit.py:967 netbox/dcim/forms/bulk_import.py:1546 #: netbox/dcim/forms/bulk_edit.py:967 netbox/dcim/forms/bulk_import.py:1573
#: netbox/dcim/forms/filtersets.py:1226 netbox/dcim/forms/model_forms.py:855 #: netbox/dcim/forms/filtersets.py:1226 netbox/dcim/forms/model_forms.py:855
msgid "Power panel" msgid "Power panel"
msgstr "" msgstr ""
#: netbox/dcim/forms/bulk_edit.py:989 netbox/dcim/forms/bulk_import.py:1582 #: netbox/dcim/forms/bulk_edit.py:989 netbox/dcim/forms/bulk_import.py:1609
#: netbox/dcim/forms/filtersets.py:1248 netbox/templates/dcim/powerfeed.html:83 #: netbox/dcim/forms/filtersets.py:1248 netbox/templates/dcim/powerfeed.html:83
msgid "Supply" msgid "Supply"
msgstr "" msgstr ""
#: netbox/dcim/forms/bulk_edit.py:995 netbox/dcim/forms/bulk_import.py:1587 #: netbox/dcim/forms/bulk_edit.py:995 netbox/dcim/forms/bulk_import.py:1614
#: netbox/dcim/forms/filtersets.py:1253 netbox/templates/dcim/powerfeed.html:95 #: netbox/dcim/forms/filtersets.py:1253 netbox/templates/dcim/powerfeed.html:95
msgid "Phase" msgid "Phase"
msgstr "" msgstr ""
@@ -4653,7 +4653,7 @@ msgid "available options"
msgstr "" msgstr ""
#: netbox/dcim/forms/bulk_import.py:137 netbox/dcim/forms/bulk_import.py:601 #: netbox/dcim/forms/bulk_import.py:137 netbox/dcim/forms/bulk_import.py:601
#: netbox/dcim/forms/bulk_import.py:1543 netbox/ipam/forms/bulk_import.py:479 #: netbox/dcim/forms/bulk_import.py:1570 netbox/ipam/forms/bulk_import.py:479
#: netbox/virtualization/forms/bulk_import.py:64 #: netbox/virtualization/forms/bulk_import.py:64
#: netbox/virtualization/forms/bulk_import.py:95 #: netbox/virtualization/forms/bulk_import.py:95
msgid "Assigned site" msgid "Assigned site"
@@ -4716,7 +4716,7 @@ msgstr ""
msgid "Parent site" msgid "Parent site"
msgstr "" msgstr ""
#: netbox/dcim/forms/bulk_import.py:347 netbox/dcim/forms/bulk_import.py:1556 #: netbox/dcim/forms/bulk_import.py:347 netbox/dcim/forms/bulk_import.py:1583
msgid "Rack's location (if any)" msgid "Rack's location (if any)"
msgstr "" msgstr ""
@@ -4767,7 +4767,7 @@ msgstr ""
msgid "Limit platform assignments to this manufacturer" msgid "Limit platform assignments to this manufacturer"
msgstr "" msgstr ""
#: netbox/dcim/forms/bulk_import.py:534 netbox/dcim/forms/bulk_import.py:1626 #: netbox/dcim/forms/bulk_import.py:534 netbox/dcim/forms/bulk_import.py:1653
#: netbox/tenancy/forms/bulk_import.py:105 #: netbox/tenancy/forms/bulk_import.py:105
msgid "Assigned role" msgid "Assigned role"
msgstr "" msgstr ""
@@ -5100,66 +5100,77 @@ msgstr ""
msgid "Connection status" msgid "Connection status"
msgstr "" msgstr ""
#: netbox/dcim/forms/bulk_import.py:1463 #: netbox/dcim/forms/bulk_import.py:1417
#, python-brace-format msgid "Color name (e.g. \"Red\") or hex code (e.g. \"f44336\")"
msgid "Side {side_upper}: {device} {termination_object} is already connected"
msgstr "" msgstr ""
#: netbox/dcim/forms/bulk_import.py:1469 #: netbox/dcim/forms/bulk_import.py:1469
#, python-brace-format #, python-brace-format
msgid "Side {side_upper}: {device} {termination_object} is already connected"
msgstr ""
#: netbox/dcim/forms/bulk_import.py:1475
#, python-brace-format
msgid "{side_upper} side termination not found: {device} {name}" msgid "{side_upper} side termination not found: {device} {name}"
msgstr "" msgstr ""
#: netbox/dcim/forms/bulk_import.py:1494 netbox/dcim/forms/model_forms.py:891 #: netbox/dcim/forms/bulk_import.py:1496
#, python-brace-format
msgid ""
"{color} did not match any used color name and was longer than six "
"characters: invalid hex."
msgstr ""
#: netbox/dcim/forms/bulk_import.py:1521 netbox/dcim/forms/model_forms.py:891
#: netbox/dcim/tables/devices.py:1069 netbox/templates/dcim/device.html:138 #: netbox/dcim/tables/devices.py:1069 netbox/templates/dcim/device.html:138
#: netbox/templates/dcim/virtualchassis.html:27 #: netbox/templates/dcim/virtualchassis.html:27
#: netbox/templates/dcim/virtualchassis.html:67 #: netbox/templates/dcim/virtualchassis.html:67
msgid "Master" msgid "Master"
msgstr "" msgstr ""
#: netbox/dcim/forms/bulk_import.py:1498 #: netbox/dcim/forms/bulk_import.py:1525
msgid "Master device" msgid "Master device"
msgstr "" msgstr ""
#: netbox/dcim/forms/bulk_import.py:1515 #: netbox/dcim/forms/bulk_import.py:1542
msgid "Name of parent site" msgid "Name of parent site"
msgstr "" msgstr ""
#: netbox/dcim/forms/bulk_import.py:1549 #: netbox/dcim/forms/bulk_import.py:1576
msgid "Upstream power panel" msgid "Upstream power panel"
msgstr "" msgstr ""
#: netbox/dcim/forms/bulk_import.py:1579 #: netbox/dcim/forms/bulk_import.py:1606
msgid "Primary or redundant" msgid "Primary or redundant"
msgstr "" msgstr ""
#: netbox/dcim/forms/bulk_import.py:1584 #: netbox/dcim/forms/bulk_import.py:1611
msgid "Supply type (AC/DC)" msgid "Supply type (AC/DC)"
msgstr "" msgstr ""
#: netbox/dcim/forms/bulk_import.py:1589 #: netbox/dcim/forms/bulk_import.py:1616
msgid "Single or three-phase" msgid "Single or three-phase"
msgstr "" msgstr ""
#: netbox/dcim/forms/bulk_import.py:1640 netbox/dcim/forms/model_forms.py:1847 #: netbox/dcim/forms/bulk_import.py:1667 netbox/dcim/forms/model_forms.py:1847
#: netbox/templates/dcim/device.html:196 #: netbox/templates/dcim/device.html:196
#: netbox/templates/dcim/virtualdevicecontext.html:30 #: netbox/templates/dcim/virtualdevicecontext.html:30
#: netbox/templates/virtualization/virtualmachine.html:52 #: netbox/templates/virtualization/virtualmachine.html:52
msgid "Primary IPv4" msgid "Primary IPv4"
msgstr "" msgstr ""
#: netbox/dcim/forms/bulk_import.py:1644 #: netbox/dcim/forms/bulk_import.py:1671
msgid "IPv4 address with mask, e.g. 1.2.3.4/24" msgid "IPv4 address with mask, e.g. 1.2.3.4/24"
msgstr "" msgstr ""
#: netbox/dcim/forms/bulk_import.py:1647 netbox/dcim/forms/model_forms.py:1856 #: netbox/dcim/forms/bulk_import.py:1674 netbox/dcim/forms/model_forms.py:1856
#: netbox/templates/dcim/device.html:212 #: netbox/templates/dcim/device.html:212
#: netbox/templates/dcim/virtualdevicecontext.html:41 #: netbox/templates/dcim/virtualdevicecontext.html:41
#: netbox/templates/virtualization/virtualmachine.html:68 #: netbox/templates/virtualization/virtualmachine.html:68
msgid "Primary IPv6" msgid "Primary IPv6"
msgstr "" msgstr ""
#: netbox/dcim/forms/bulk_import.py:1651 #: netbox/dcim/forms/bulk_import.py:1678
msgid "IPv6 address with prefix length, e.g. 2001:db8::1/64" msgid "IPv6 address with prefix length, e.g. 2001:db8::1/64"
msgstr "" msgstr ""