mirror of
https://github.com/netbox-community/netbox.git
synced 2026-04-10 11:23:58 +02:00
Compare commits
3 Commits
21783-bulk
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4ca688de57 | ||
|
|
ed7ebd9d98 | ||
|
|
e864dc3ae0 |
@@ -1,10 +1,15 @@
|
||||
import io
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.core.files.base import ContentFile
|
||||
from django.core.files.storage import Storage
|
||||
from django.core.files.uploadedfile import SimpleUploadedFile
|
||||
from django.forms import ValidationError
|
||||
from django.test import TestCase, tag
|
||||
from PIL import Image
|
||||
|
||||
from core.models import AutoSyncRecord, DataSource, ObjectType
|
||||
from dcim.models import Device, DeviceRole, DeviceType, Location, Manufacturer, Platform, Region, Site, SiteGroup
|
||||
@@ -14,10 +19,50 @@ from utilities.exceptions import AbortRequest
|
||||
from virtualization.models import Cluster, ClusterGroup, ClusterType, VirtualMachine
|
||||
|
||||
|
||||
class OverwriteStyleMemoryStorage(Storage):
|
||||
"""
|
||||
In-memory storage that mimics overwrite-style backends by returning the
|
||||
incoming name unchanged from get_available_name().
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.files = {}
|
||||
|
||||
def _open(self, name, mode='rb'):
|
||||
return ContentFile(self.files[name], name=name)
|
||||
|
||||
def _save(self, name, content):
|
||||
self.files[name] = content.read()
|
||||
return name
|
||||
|
||||
def delete(self, name):
|
||||
self.files.pop(name, None)
|
||||
|
||||
def exists(self, name):
|
||||
return name in self.files
|
||||
|
||||
def get_available_name(self, name, max_length=None):
|
||||
return name
|
||||
|
||||
def get_alternative_name(self, file_root, file_ext):
|
||||
return f'{file_root}_sdmmer4{file_ext}'
|
||||
|
||||
def listdir(self, path):
|
||||
return [], list(self.files)
|
||||
|
||||
def size(self, name):
|
||||
return len(self.files[name])
|
||||
|
||||
def url(self, name):
|
||||
return f'https://example.invalid/{name}'
|
||||
|
||||
|
||||
class ImageAttachmentTests(TestCase):
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
cls.ct_rack = ContentType.objects.get_by_natural_key('dcim', 'rack')
|
||||
cls.ct_site = ContentType.objects.get_by_natural_key('dcim', 'site')
|
||||
cls.site = Site.objects.create(name='Site 1')
|
||||
cls.image_content = b''
|
||||
|
||||
def _stub_image_attachment(self, object_id, image_filename, name=None):
|
||||
@@ -41,6 +86,15 @@ class ImageAttachmentTests(TestCase):
|
||||
)
|
||||
return ia
|
||||
|
||||
def _uploaded_png(self, filename):
|
||||
image = io.BytesIO()
|
||||
Image.new('RGB', (1, 1)).save(image, format='PNG')
|
||||
return SimpleUploadedFile(
|
||||
name=filename,
|
||||
content=image.getvalue(),
|
||||
content_type='image/png',
|
||||
)
|
||||
|
||||
def test_filename_strips_expected_prefix(self):
|
||||
"""
|
||||
Tests that the filename of the image attachment is stripped of the expected
|
||||
@@ -89,6 +143,37 @@ class ImageAttachmentTests(TestCase):
|
||||
ia = self._stub_image_attachment(12, 'image-attachments/rack_12_file.png', name='')
|
||||
self.assertEqual('file.png', str(ia))
|
||||
|
||||
def test_duplicate_uploaded_names_get_suffixed_with_overwrite_style_storage(self):
|
||||
storage = OverwriteStyleMemoryStorage()
|
||||
field = ImageAttachment._meta.get_field('image')
|
||||
|
||||
with patch.object(field, 'storage', storage):
|
||||
first = ImageAttachment(
|
||||
object_type=self.ct_site,
|
||||
object_id=self.site.pk,
|
||||
image=self._uploaded_png('action-buttons.png'),
|
||||
)
|
||||
first.save()
|
||||
|
||||
second = ImageAttachment(
|
||||
object_type=self.ct_site,
|
||||
object_id=self.site.pk,
|
||||
image=self._uploaded_png('action-buttons.png'),
|
||||
)
|
||||
second.save()
|
||||
|
||||
base_name = f'image-attachments/site_{self.site.pk}_action-buttons.png'
|
||||
suffixed_name = f'image-attachments/site_{self.site.pk}_action-buttons_sdmmer4.png'
|
||||
|
||||
self.assertEqual(first.image.name, base_name)
|
||||
self.assertEqual(second.image.name, suffixed_name)
|
||||
self.assertNotEqual(first.image.name, second.image.name)
|
||||
|
||||
self.assertEqual(first.filename, 'action-buttons.png')
|
||||
self.assertEqual(second.filename, 'action-buttons_sdmmer4.png')
|
||||
|
||||
self.assertCountEqual(storage.files.keys(), {base_name, suffixed_name})
|
||||
|
||||
|
||||
class TagTest(TestCase):
|
||||
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import patch
|
||||
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.core.files.storage import Storage
|
||||
from django.test import TestCase
|
||||
|
||||
from extras.models import ExportTemplate
|
||||
from extras.utils import filename_from_model, image_upload
|
||||
from extras.models import ExportTemplate, ImageAttachment
|
||||
from extras.utils import _build_image_attachment_path, filename_from_model, image_upload
|
||||
from tenancy.models import ContactGroup, TenantGroup
|
||||
from wireless.models import WirelessLANGroup
|
||||
|
||||
@@ -22,6 +24,25 @@ class FilenameFromModelTests(TestCase):
|
||||
self.assertEqual(filename_from_model(model), expected)
|
||||
|
||||
|
||||
class OverwriteStyleStorage(Storage):
|
||||
"""
|
||||
Mimic an overwrite-style backend (for example, S3 with file_overwrite=True),
|
||||
where get_available_name() returns the incoming name unchanged.
|
||||
"""
|
||||
|
||||
def __init__(self, existing_names=None):
|
||||
self.existing_names = set(existing_names or [])
|
||||
|
||||
def exists(self, name):
|
||||
return name in self.existing_names
|
||||
|
||||
def get_available_name(self, name, max_length=None):
|
||||
return name
|
||||
|
||||
def get_alternative_name(self, file_root, file_ext):
|
||||
return f'{file_root}_sdmmer4{file_ext}'
|
||||
|
||||
|
||||
class ImageUploadTests(TestCase):
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
@@ -31,16 +52,18 @@ class ImageUploadTests(TestCase):
|
||||
|
||||
def _stub_instance(self, object_id=12, name=None):
|
||||
"""
|
||||
Creates a minimal stub for use with the `image_upload()` function.
|
||||
|
||||
This method generates an instance of `SimpleNamespace` containing a set
|
||||
of attributes required to simulate the expected input for the
|
||||
`image_upload()` method.
|
||||
It is designed to simplify testing or processing by providing a
|
||||
lightweight representation of an object.
|
||||
Creates a minimal stub for use with image attachment path generation.
|
||||
"""
|
||||
return SimpleNamespace(object_type=self.ct_rack, object_id=object_id, name=name)
|
||||
|
||||
def _bound_instance(self, *, storage, object_id=12, name=None, max_length=100):
|
||||
return SimpleNamespace(
|
||||
object_type=self.ct_rack,
|
||||
object_id=object_id,
|
||||
name=name,
|
||||
image=SimpleNamespace(field=SimpleNamespace(storage=storage, max_length=max_length)),
|
||||
)
|
||||
|
||||
def _second_segment(self, path: str):
|
||||
"""
|
||||
Extracts and returns the portion of the input string after the
|
||||
@@ -53,7 +76,7 @@ class ImageUploadTests(TestCase):
|
||||
Tests handling of a Windows file path with a fake directory and extension.
|
||||
"""
|
||||
inst = self._stub_instance(name=None)
|
||||
path = image_upload(inst, r'C:\fake_path\MyPhoto.JPG')
|
||||
path = _build_image_attachment_path(inst, r'C:\fake_path\MyPhoto.JPG')
|
||||
# Base directory and single-level path
|
||||
seg2 = self._second_segment(path)
|
||||
self.assertTrue(path.startswith('image-attachments/rack_12_'))
|
||||
@@ -67,7 +90,7 @@ class ImageUploadTests(TestCase):
|
||||
create subdirectories.
|
||||
"""
|
||||
inst = self._stub_instance(name='5/31/23')
|
||||
path = image_upload(inst, 'image.png')
|
||||
path = _build_image_attachment_path(inst, 'image.png')
|
||||
seg2 = self._second_segment(path)
|
||||
self.assertTrue(seg2.startswith('rack_12_'))
|
||||
self.assertNotIn('/', seg2)
|
||||
@@ -80,7 +103,7 @@ class ImageUploadTests(TestCase):
|
||||
into a single directory name without creating subdirectories.
|
||||
"""
|
||||
inst = self._stub_instance(name=r'5\31\23')
|
||||
path = image_upload(inst, 'image_name.png')
|
||||
path = _build_image_attachment_path(inst, 'image_name.png')
|
||||
|
||||
seg2 = self._second_segment(path)
|
||||
self.assertTrue(seg2.startswith('rack_12_'))
|
||||
@@ -93,7 +116,7 @@ class ImageUploadTests(TestCase):
|
||||
Tests the output path format generated by the `image_upload` function.
|
||||
"""
|
||||
inst = self._stub_instance(object_id=99, name='label')
|
||||
path = image_upload(inst, 'a.webp')
|
||||
path = _build_image_attachment_path(inst, 'a.webp')
|
||||
# The second segment must begin with "rack_99_"
|
||||
seg2 = self._second_segment(path)
|
||||
self.assertTrue(seg2.startswith('rack_99_'))
|
||||
@@ -105,7 +128,7 @@ class ImageUploadTests(TestCase):
|
||||
is omitted.
|
||||
"""
|
||||
inst = self._stub_instance(name='test')
|
||||
path = image_upload(inst, 'document.txt')
|
||||
path = _build_image_attachment_path(inst, 'document.txt')
|
||||
|
||||
seg2 = self._second_segment(path)
|
||||
self.assertTrue(seg2.startswith('rack_12_test'))
|
||||
@@ -121,7 +144,7 @@ class ImageUploadTests(TestCase):
|
||||
# Suppose the instance name has surrounding whitespace and
|
||||
# extra slashes.
|
||||
inst = self._stub_instance(name=' my/complex\\name ')
|
||||
path = image_upload(inst, 'irrelevant.png')
|
||||
path = _build_image_attachment_path(inst, 'irrelevant.png')
|
||||
|
||||
# The output should be flattened and sanitized.
|
||||
# We expect the name to be transformed into a valid filename without
|
||||
@@ -141,7 +164,7 @@ class ImageUploadTests(TestCase):
|
||||
for name in ['2025/09/12', r'2025\09\12']:
|
||||
with self.subTest(name=name):
|
||||
inst = self._stub_instance(name=name)
|
||||
path = image_upload(inst, 'x.jpeg')
|
||||
path = _build_image_attachment_path(inst, 'x.jpeg')
|
||||
seg2 = self._second_segment(path)
|
||||
self.assertTrue(seg2.startswith('rack_12_'))
|
||||
self.assertNotIn('/', seg2)
|
||||
@@ -154,7 +177,49 @@ class ImageUploadTests(TestCase):
|
||||
SuspiciousFileOperation, the fallback default is used.
|
||||
"""
|
||||
inst = self._stub_instance(name=' ')
|
||||
path = image_upload(inst, 'sample.png')
|
||||
path = _build_image_attachment_path(inst, 'sample.png')
|
||||
# Expect the fallback name 'unnamed' to be used.
|
||||
self.assertIn('unnamed', path)
|
||||
self.assertTrue(path.startswith('image-attachments/rack_12_'))
|
||||
|
||||
def test_image_upload_preserves_original_name_when_available(self):
|
||||
inst = self._bound_instance(
|
||||
storage=OverwriteStyleStorage(),
|
||||
name='action-buttons',
|
||||
)
|
||||
|
||||
path = image_upload(inst, 'action-buttons.png')
|
||||
|
||||
self.assertEqual(path, 'image-attachments/rack_12_action-buttons.png')
|
||||
|
||||
def test_image_upload_uses_base_collision_handling_with_overwrite_style_storage(self):
|
||||
inst = self._bound_instance(
|
||||
storage=OverwriteStyleStorage(existing_names={'image-attachments/rack_12_action-buttons.png'}),
|
||||
name='action-buttons',
|
||||
)
|
||||
|
||||
path = image_upload(inst, 'action-buttons.png')
|
||||
|
||||
self.assertEqual(
|
||||
path,
|
||||
'image-attachments/rack_12_action-buttons_sdmmer4.png',
|
||||
)
|
||||
|
||||
def test_image_field_generate_filename_uses_image_upload_collision_handling(self):
|
||||
field = ImageAttachment._meta.get_field('image')
|
||||
instance = ImageAttachment(
|
||||
object_type=self.ct_rack,
|
||||
object_id=12,
|
||||
)
|
||||
|
||||
with patch.object(
|
||||
field,
|
||||
'storage',
|
||||
OverwriteStyleStorage(existing_names={'image-attachments/rack_12_action-buttons.png'}),
|
||||
):
|
||||
path = field.generate_filename(instance, 'action-buttons.png')
|
||||
|
||||
self.assertEqual(
|
||||
path,
|
||||
'image-attachments/rack_12_action-buttons_sdmmer4.png',
|
||||
)
|
||||
|
||||
@@ -2,7 +2,7 @@ import importlib
|
||||
from pathlib import Path
|
||||
|
||||
from django.core.exceptions import ImproperlyConfigured, SuspiciousFileOperation
|
||||
from django.core.files.storage import default_storage
|
||||
from django.core.files.storage import Storage, default_storage
|
||||
from django.core.files.utils import validate_file_name
|
||||
from django.db import models
|
||||
from django.db.models import Q
|
||||
@@ -67,15 +67,13 @@ def is_taggable(obj):
|
||||
return False
|
||||
|
||||
|
||||
def image_upload(instance, filename):
|
||||
def _build_image_attachment_path(instance, filename, *, storage=default_storage):
|
||||
"""
|
||||
Return a path for uploading image attachments.
|
||||
Build a deterministic relative path for an image attachment.
|
||||
|
||||
- Normalizes browser paths (e.g., C:\\fake_path\\photo.jpg)
|
||||
- Uses the instance.name if provided (sanitized to a *basename*, no ext)
|
||||
- Prefixes with a machine-friendly identifier
|
||||
|
||||
Note: Relies on Django's default_storage utility.
|
||||
"""
|
||||
upload_dir = 'image-attachments'
|
||||
default_filename = 'unnamed'
|
||||
@@ -92,22 +90,38 @@ def image_upload(instance, filename):
|
||||
# Rely on Django's get_valid_filename to perform sanitization.
|
||||
stem = (instance.name or file_path.stem).strip()
|
||||
try:
|
||||
safe_stem = default_storage.get_valid_name(stem)
|
||||
safe_stem = storage.get_valid_name(stem)
|
||||
except SuspiciousFileOperation:
|
||||
safe_stem = default_filename
|
||||
|
||||
# Append the uploaded extension only if it's an allowed image type
|
||||
final_name = f"{safe_stem}.{ext}" if ext in allowed_img_extensions else safe_stem
|
||||
final_name = f'{safe_stem}.{ext}' if ext in allowed_img_extensions else safe_stem
|
||||
|
||||
# Create a machine-friendly prefix from the instance
|
||||
prefix = f"{instance.object_type.model}_{instance.object_id}"
|
||||
name_with_path = f"{upload_dir}/{prefix}_{final_name}"
|
||||
prefix = f'{instance.object_type.model}_{instance.object_id}'
|
||||
name_with_path = f'{upload_dir}/{prefix}_{final_name}'
|
||||
|
||||
# Validate the generated relative path (blocks absolute/traversal)
|
||||
validate_file_name(name_with_path, allow_relative_path=True)
|
||||
return name_with_path
|
||||
|
||||
|
||||
def image_upload(instance, filename):
|
||||
"""
|
||||
Return a relative upload path for an image attachment, applying Django's
|
||||
usual suffix-on-collision behavior regardless of storage backend.
|
||||
"""
|
||||
field = instance.image.field
|
||||
name_with_path = _build_image_attachment_path(instance, filename, storage=field.storage)
|
||||
|
||||
# Intentionally call Django's base Storage implementation here. Some
|
||||
# backends override get_available_name() to reuse the incoming name
|
||||
# unchanged, but we want Django's normal suffix-on-collision behavior
|
||||
# while still dispatching exists() / get_alternative_name() to the
|
||||
# configured storage instance.
|
||||
return Storage.get_available_name(field.storage, name_with_path, max_length=field.max_length)
|
||||
|
||||
|
||||
def is_script(obj):
|
||||
"""
|
||||
Returns True if the object is a Script or Report.
|
||||
|
||||
@@ -8,7 +8,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-04-08 05:31+0000\n"
|
||||
"POT-Creation-Date: 2026-04-10 05:39+0000\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
@@ -1247,13 +1247,13 @@ msgstr ""
|
||||
|
||||
#: netbox/circuits/models/base.py:18 netbox/dcim/models/cables.py:81
|
||||
#: netbox/dcim/models/device_component_templates.py:328
|
||||
#: netbox/dcim/models/device_component_templates.py:563
|
||||
#: netbox/dcim/models/device_component_templates.py:636
|
||||
#: netbox/dcim/models/device_component_templates.py:571
|
||||
#: netbox/dcim/models/device_component_templates.py:644
|
||||
#: netbox/dcim/models/device_components.py:605
|
||||
#: netbox/dcim/models/device_components.py:1188
|
||||
#: netbox/dcim/models/device_components.py:1236
|
||||
#: netbox/dcim/models/device_components.py:1387
|
||||
#: netbox/dcim/models/devices.py:385 netbox/dcim/models/racks.py:234
|
||||
#: netbox/dcim/models/devices.py:394 netbox/dcim/models/racks.py:234
|
||||
#: netbox/extras/models/tags.py:30
|
||||
msgid "color"
|
||||
msgstr ""
|
||||
@@ -1281,8 +1281,8 @@ msgstr ""
|
||||
#: netbox/core/models/jobs.py:95 netbox/dcim/models/cables.py:57
|
||||
#: netbox/dcim/models/device_components.py:576
|
||||
#: netbox/dcim/models/device_components.py:1426
|
||||
#: netbox/dcim/models/devices.py:589 netbox/dcim/models/devices.py:1218
|
||||
#: netbox/dcim/models/modules.py:219 netbox/dcim/models/power.py:95
|
||||
#: netbox/dcim/models/devices.py:598 netbox/dcim/models/devices.py:1227
|
||||
#: netbox/dcim/models/modules.py:227 netbox/dcim/models/power.py:95
|
||||
#: netbox/dcim/models/racks.py:301 netbox/dcim/models/racks.py:685
|
||||
#: netbox/dcim/models/sites.py:163 netbox/dcim/models/sites.py:287
|
||||
#: netbox/ipam/models/ip.py:246 netbox/ipam/models/ip.py:548
|
||||
@@ -1414,8 +1414,8 @@ msgstr ""
|
||||
#: netbox/circuits/models/providers.py:98 netbox/core/models/data.py:40
|
||||
#: netbox/core/models/jobs.py:56
|
||||
#: netbox/dcim/models/device_component_templates.py:55
|
||||
#: netbox/dcim/models/device_components.py:57 netbox/dcim/models/devices.py:533
|
||||
#: netbox/dcim/models/devices.py:1144 netbox/dcim/models/devices.py:1213
|
||||
#: netbox/dcim/models/device_components.py:57 netbox/dcim/models/devices.py:542
|
||||
#: netbox/dcim/models/devices.py:1153 netbox/dcim/models/devices.py:1222
|
||||
#: netbox/dcim/models/modules.py:35 netbox/dcim/models/power.py:39
|
||||
#: netbox/dcim/models/power.py:90 netbox/dcim/models/racks.py:270
|
||||
#: netbox/dcim/models/sites.py:151 netbox/extras/models/configs.py:37
|
||||
@@ -2281,8 +2281,8 @@ msgstr ""
|
||||
#: netbox/dcim/models/device_component_templates.py:256
|
||||
#: netbox/dcim/models/device_component_templates.py:321
|
||||
#: netbox/dcim/models/device_component_templates.py:412
|
||||
#: netbox/dcim/models/device_component_templates.py:558
|
||||
#: netbox/dcim/models/device_component_templates.py:631
|
||||
#: netbox/dcim/models/device_component_templates.py:566
|
||||
#: netbox/dcim/models/device_component_templates.py:639
|
||||
#: netbox/dcim/models/device_components.py:402
|
||||
#: netbox/dcim/models/device_components.py:429
|
||||
#: netbox/dcim/models/device_components.py:460
|
||||
@@ -3710,8 +3710,8 @@ msgstr ""
|
||||
|
||||
#: netbox/dcim/filtersets.py:1324 netbox/dcim/forms/filtersets.py:920
|
||||
#: netbox/dcim/forms/filtersets.py:1634 netbox/dcim/forms/filtersets.py:1979
|
||||
#: netbox/dcim/forms/model_forms.py:1941 netbox/dcim/models/devices.py:1313
|
||||
#: netbox/dcim/models/devices.py:1336 netbox/dcim/ui/panels.py:366
|
||||
#: netbox/dcim/forms/model_forms.py:1941 netbox/dcim/models/devices.py:1322
|
||||
#: netbox/dcim/models/devices.py:1345 netbox/dcim/ui/panels.py:366
|
||||
#: netbox/dcim/ui/panels.py:513 netbox/virtualization/filtersets.py:230
|
||||
#: netbox/virtualization/filtersets.py:318
|
||||
#: netbox/virtualization/forms/filtersets.py:193
|
||||
@@ -4340,7 +4340,7 @@ msgstr ""
|
||||
msgid "Chassis"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/forms/bulk_edit.py:617 netbox/dcim/models/devices.py:390
|
||||
#: netbox/dcim/forms/bulk_edit.py:617 netbox/dcim/models/devices.py:399
|
||||
#: netbox/dcim/tables/devices.py:76 netbox/dcim/ui/panels.py:144
|
||||
msgid "VM role"
|
||||
msgstr ""
|
||||
@@ -5543,7 +5543,7 @@ msgstr ""
|
||||
msgid "Profile & Attributes"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/forms/model_forms.py:623 netbox/dcim/models/devices.py:579
|
||||
#: netbox/dcim/forms/model_forms.py:623 netbox/dcim/models/devices.py:588
|
||||
msgid "The lowest-numbered unit occupied by the device"
|
||||
msgstr ""
|
||||
|
||||
@@ -6019,91 +6019,91 @@ msgstr ""
|
||||
msgid "Rear port ({rear_port}) must belong to the same device type"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/models/device_component_templates.py:567
|
||||
#: netbox/dcim/models/device_component_templates.py:640
|
||||
#: netbox/dcim/models/device_component_templates.py:575
|
||||
#: netbox/dcim/models/device_component_templates.py:648
|
||||
#: netbox/dcim/models/device_components.py:1192
|
||||
#: netbox/dcim/models/device_components.py:1240
|
||||
msgid "positions"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/models/device_component_templates.py:588
|
||||
#: netbox/dcim/models/device_component_templates.py:596
|
||||
msgid "front port template"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/models/device_component_templates.py:589
|
||||
#: netbox/dcim/models/device_component_templates.py:597
|
||||
msgid "front port templates"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/models/device_component_templates.py:600
|
||||
#: netbox/dcim/models/device_component_templates.py:608
|
||||
#, python-brace-format
|
||||
msgid ""
|
||||
"The number of positions cannot be less than the number of mapped rear port "
|
||||
"templates ({count})"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/models/device_component_templates.py:651
|
||||
#: netbox/dcim/models/device_component_templates.py:659
|
||||
msgid "rear port template"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/models/device_component_templates.py:652
|
||||
#: netbox/dcim/models/device_component_templates.py:660
|
||||
msgid "rear port templates"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/models/device_component_templates.py:663
|
||||
#: netbox/dcim/models/device_component_templates.py:671
|
||||
#, python-brace-format
|
||||
msgid ""
|
||||
"The number of positions cannot be less than the number of mapped front port "
|
||||
"templates ({count})"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/models/device_component_templates.py:695
|
||||
#: netbox/dcim/models/device_component_templates.py:703
|
||||
#: netbox/dcim/models/device_components.py:1287
|
||||
msgid "position"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/models/device_component_templates.py:698
|
||||
#: netbox/dcim/models/device_component_templates.py:706
|
||||
#: netbox/dcim/models/device_components.py:1290
|
||||
msgid "Identifier to reference when renaming installed components"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/models/device_component_templates.py:704
|
||||
#: netbox/dcim/models/device_component_templates.py:712
|
||||
msgid "module bay template"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/models/device_component_templates.py:705
|
||||
#: netbox/dcim/models/device_component_templates.py:713
|
||||
msgid "module bay templates"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/models/device_component_templates.py:737
|
||||
#: netbox/dcim/models/device_component_templates.py:745
|
||||
msgid "device bay template"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/models/device_component_templates.py:738
|
||||
#: netbox/dcim/models/device_component_templates.py:746
|
||||
msgid "device bay templates"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/models/device_component_templates.py:752
|
||||
#: netbox/dcim/models/device_component_templates.py:760
|
||||
#, python-brace-format
|
||||
msgid ""
|
||||
"Subdevice role of device type ({device_type}) must be set to \"parent\" to "
|
||||
"allow device bays."
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/models/device_component_templates.py:807
|
||||
#: netbox/dcim/models/device_component_templates.py:815
|
||||
#: netbox/dcim/models/device_components.py:1447
|
||||
msgid "part ID"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/models/device_component_templates.py:809
|
||||
#: netbox/dcim/models/device_component_templates.py:817
|
||||
#: netbox/dcim/models/device_components.py:1449
|
||||
msgid "Manufacturer-assigned part identifier"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/models/device_component_templates.py:826
|
||||
#: netbox/dcim/models/device_component_templates.py:834
|
||||
msgid "inventory item template"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/models/device_component_templates.py:827
|
||||
#: netbox/dcim/models/device_component_templates.py:835
|
||||
msgid "inventory item templates"
|
||||
msgstr ""
|
||||
|
||||
@@ -6448,7 +6448,7 @@ msgid "module bays"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/models/device_components.py:1322
|
||||
#: netbox/dcim/models/modules.py:268
|
||||
#: netbox/dcim/models/modules.py:276
|
||||
msgid "A module bay cannot belong to a module installed within it."
|
||||
msgstr ""
|
||||
|
||||
@@ -6484,14 +6484,14 @@ msgid "inventory item roles"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/models/device_components.py:1453
|
||||
#: netbox/dcim/models/devices.py:542 netbox/dcim/models/modules.py:227
|
||||
#: netbox/dcim/models/devices.py:551 netbox/dcim/models/modules.py:235
|
||||
#: netbox/dcim/models/racks.py:317
|
||||
#: netbox/virtualization/models/virtualmachines.py:132
|
||||
msgid "serial number"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/models/device_components.py:1461
|
||||
#: netbox/dcim/models/devices.py:550 netbox/dcim/models/modules.py:234
|
||||
#: netbox/dcim/models/devices.py:559 netbox/dcim/models/modules.py:242
|
||||
#: netbox/dcim/models/racks.py:324
|
||||
msgid "asset tag"
|
||||
msgstr ""
|
||||
@@ -6587,7 +6587,7 @@ msgid ""
|
||||
"device type is neither a parent nor a child."
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/models/devices.py:131 netbox/dcim/models/devices.py:595
|
||||
#: netbox/dcim/models/devices.py:131 netbox/dcim/models/devices.py:604
|
||||
#: netbox/dcim/models/modules.py:87 netbox/dcim/models/racks.py:328
|
||||
msgid "airflow"
|
||||
msgstr ""
|
||||
@@ -6600,310 +6600,310 @@ msgstr ""
|
||||
msgid "device types"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/models/devices.py:295
|
||||
#: netbox/dcim/models/devices.py:304
|
||||
msgid "U height must be in increments of 0.5 rack units."
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/models/devices.py:312
|
||||
#: netbox/dcim/models/devices.py:321
|
||||
#, python-brace-format
|
||||
msgid ""
|
||||
"Device {device} in rack {rack} does not have sufficient space to accommodate "
|
||||
"a height of {height}U"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/models/devices.py:327
|
||||
#: netbox/dcim/models/devices.py:336
|
||||
#, python-brace-format
|
||||
msgid ""
|
||||
"Unable to set 0U height: Found <a href=\"{url}\">{racked_instance_count} "
|
||||
"instances</a> already mounted within racks."
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/models/devices.py:336
|
||||
#: netbox/dcim/models/devices.py:345
|
||||
msgid ""
|
||||
"Must delete all device bay templates associated with this device before "
|
||||
"declassifying it as a parent device."
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/models/devices.py:342
|
||||
#: netbox/dcim/models/devices.py:351
|
||||
msgid "Child device types must be 0U."
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/models/devices.py:391
|
||||
#: netbox/dcim/models/devices.py:400
|
||||
msgid "Virtual machines may be assigned to this role"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/models/devices.py:417
|
||||
#: netbox/dcim/models/devices.py:426
|
||||
msgid "A top-level device role with this name already exists."
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/models/devices.py:427
|
||||
#: netbox/dcim/models/devices.py:436
|
||||
msgid "A top-level device role with this slug already exists."
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/models/devices.py:430
|
||||
#: netbox/dcim/models/devices.py:439
|
||||
msgid "device role"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/models/devices.py:431
|
||||
#: netbox/dcim/models/devices.py:440
|
||||
msgid "device roles"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/models/devices.py:445
|
||||
#: netbox/dcim/models/devices.py:454
|
||||
msgid "Optionally limit this platform to devices of a certain manufacturer"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/models/devices.py:462
|
||||
#: netbox/dcim/models/devices.py:471
|
||||
msgid "platform"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/models/devices.py:463
|
||||
#: netbox/dcim/models/devices.py:472
|
||||
msgid "platforms"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/models/devices.py:473
|
||||
#: netbox/dcim/models/devices.py:482
|
||||
msgid "Platform name must be unique."
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/models/devices.py:483
|
||||
#: netbox/dcim/models/devices.py:492
|
||||
msgid "Platform slug must be unique."
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/models/devices.py:516
|
||||
#: netbox/dcim/models/devices.py:525
|
||||
msgid "The function this device serves"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/models/devices.py:543
|
||||
#: netbox/dcim/models/devices.py:552
|
||||
msgid "Chassis serial number, assigned by the manufacturer"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/models/devices.py:551 netbox/dcim/models/modules.py:235
|
||||
#: netbox/dcim/models/devices.py:560 netbox/dcim/models/modules.py:243
|
||||
msgid "A unique tag used to identify this device"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/models/devices.py:578
|
||||
#: netbox/dcim/models/devices.py:587
|
||||
msgid "position (U)"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/models/devices.py:586
|
||||
#: netbox/dcim/models/devices.py:595
|
||||
msgid "rack face"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/models/devices.py:607 netbox/dcim/models/devices.py:1234
|
||||
#: netbox/dcim/models/devices.py:616 netbox/dcim/models/devices.py:1243
|
||||
#: netbox/virtualization/models/virtualmachines.py:101
|
||||
msgid "primary IPv4"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/models/devices.py:615 netbox/dcim/models/devices.py:1242
|
||||
#: netbox/dcim/models/devices.py:624 netbox/dcim/models/devices.py:1251
|
||||
#: netbox/virtualization/models/virtualmachines.py:109
|
||||
msgid "primary IPv6"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/models/devices.py:623
|
||||
#: netbox/dcim/models/devices.py:632
|
||||
msgid "out-of-band IP"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/models/devices.py:640
|
||||
#: netbox/dcim/models/devices.py:649
|
||||
msgid "VC position"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/models/devices.py:643
|
||||
#: netbox/dcim/models/devices.py:652
|
||||
msgid "Virtual chassis position"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/models/devices.py:646
|
||||
#: netbox/dcim/models/devices.py:655
|
||||
msgid "VC priority"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/models/devices.py:650
|
||||
#: netbox/dcim/models/devices.py:659
|
||||
msgid "Virtual chassis master election priority"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/models/devices.py:653 netbox/dcim/models/sites.py:217
|
||||
#: netbox/dcim/models/devices.py:662 netbox/dcim/models/sites.py:217
|
||||
msgid "latitude"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/models/devices.py:662 netbox/dcim/models/devices.py:674
|
||||
#: netbox/dcim/models/devices.py:671 netbox/dcim/models/devices.py:683
|
||||
#: netbox/dcim/models/sites.py:226 netbox/dcim/models/sites.py:238
|
||||
msgid "GPS coordinate in decimal format (xx.yyyyyy)"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/models/devices.py:665 netbox/dcim/models/sites.py:229
|
||||
#: netbox/dcim/models/devices.py:674 netbox/dcim/models/sites.py:229
|
||||
msgid "longitude"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/models/devices.py:748
|
||||
#: netbox/dcim/models/devices.py:757
|
||||
msgid "Device name must be unique per site."
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/models/devices.py:759
|
||||
#: netbox/dcim/models/devices.py:768
|
||||
msgid "device"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/models/devices.py:760
|
||||
#: netbox/dcim/models/devices.py:769
|
||||
msgid "devices"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/models/devices.py:779
|
||||
#: netbox/dcim/models/devices.py:788
|
||||
#, python-brace-format
|
||||
msgid "Rack {rack} does not belong to site {site}."
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/models/devices.py:784
|
||||
#: netbox/dcim/models/devices.py:793
|
||||
#, python-brace-format
|
||||
msgid "Location {location} does not belong to site {site}."
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/models/devices.py:790
|
||||
#: netbox/dcim/models/devices.py:799
|
||||
#, python-brace-format
|
||||
msgid "Rack {rack} does not belong to location {location}."
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/models/devices.py:797
|
||||
#: netbox/dcim/models/devices.py:806
|
||||
msgid "Cannot select a rack face without assigning a rack."
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/models/devices.py:801
|
||||
#: netbox/dcim/models/devices.py:810
|
||||
msgid "Cannot select a rack position without assigning a rack."
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/models/devices.py:807
|
||||
#: netbox/dcim/models/devices.py:816
|
||||
msgid "Position must be in increments of 0.5 rack units."
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/models/devices.py:811
|
||||
#: netbox/dcim/models/devices.py:820
|
||||
msgid "Must specify rack face when defining rack position."
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/models/devices.py:819
|
||||
#: netbox/dcim/models/devices.py:828
|
||||
#, python-brace-format
|
||||
msgid "A 0U device type ({device_type}) cannot be assigned to a rack position."
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/models/devices.py:830
|
||||
#: netbox/dcim/models/devices.py:839
|
||||
msgid ""
|
||||
"Child device types cannot be assigned to a rack face. This is an attribute "
|
||||
"of the parent device."
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/models/devices.py:837
|
||||
#: netbox/dcim/models/devices.py:846
|
||||
msgid ""
|
||||
"Child device types cannot be assigned to a rack position. This is an "
|
||||
"attribute of the parent device."
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/models/devices.py:851
|
||||
#: netbox/dcim/models/devices.py:860
|
||||
#, python-brace-format
|
||||
msgid ""
|
||||
"U{position} is already occupied or does not have sufficient space to "
|
||||
"accommodate this device type: {device_type} ({u_height}U)"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/models/devices.py:866
|
||||
#: netbox/dcim/models/devices.py:875
|
||||
#, python-brace-format
|
||||
msgid "{ip} is not an IPv4 address."
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/models/devices.py:878 netbox/dcim/models/devices.py:896
|
||||
#: netbox/dcim/models/devices.py:887 netbox/dcim/models/devices.py:905
|
||||
#, python-brace-format
|
||||
msgid "The specified IP address ({ip}) is not assigned to this device."
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/models/devices.py:884
|
||||
#: netbox/dcim/models/devices.py:893
|
||||
#, python-brace-format
|
||||
msgid "{ip} is not an IPv6 address."
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/models/devices.py:914
|
||||
#: netbox/dcim/models/devices.py:923
|
||||
#, python-brace-format
|
||||
msgid ""
|
||||
"The assigned platform is limited to {platform_manufacturer} device types, "
|
||||
"but this device's type belongs to {devicetype_manufacturer}."
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/models/devices.py:925
|
||||
#: netbox/dcim/models/devices.py:934
|
||||
#, python-brace-format
|
||||
msgid "The assigned cluster belongs to a different site ({site})"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/models/devices.py:932
|
||||
#: netbox/dcim/models/devices.py:941
|
||||
#, python-brace-format
|
||||
msgid "The assigned cluster belongs to a different location ({location})"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/models/devices.py:940
|
||||
#: netbox/dcim/models/devices.py:949
|
||||
msgid "A device assigned to a virtual chassis must have its position defined."
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/models/devices.py:946
|
||||
#: netbox/dcim/models/devices.py:955
|
||||
#, python-brace-format
|
||||
msgid ""
|
||||
"Device cannot be removed from virtual chassis {virtual_chassis} because it "
|
||||
"is currently designated as its master."
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/models/devices.py:1149
|
||||
#: netbox/dcim/models/devices.py:1158
|
||||
msgid "domain"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/models/devices.py:1162 netbox/dcim/models/devices.py:1163
|
||||
#: netbox/dcim/models/devices.py:1171 netbox/dcim/models/devices.py:1172
|
||||
msgid "virtual chassis"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/models/devices.py:1175
|
||||
#: netbox/dcim/models/devices.py:1184
|
||||
#, python-brace-format
|
||||
msgid "The selected master ({master}) is not assigned to this virtual chassis."
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/models/devices.py:1190
|
||||
#: netbox/dcim/models/devices.py:1199
|
||||
#, python-brace-format
|
||||
msgid ""
|
||||
"Unable to delete virtual chassis {self}. There are member interfaces which "
|
||||
"form a cross-chassis LAG interfaces."
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/models/devices.py:1223 netbox/vpn/models/l2vpn.py:42
|
||||
#: netbox/dcim/models/devices.py:1232 netbox/vpn/models/l2vpn.py:42
|
||||
msgid "identifier"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/models/devices.py:1224
|
||||
#: netbox/dcim/models/devices.py:1233
|
||||
msgid "Numeric identifier unique to the parent device"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/models/devices.py:1252 netbox/extras/models/customfields.py:253
|
||||
#: netbox/dcim/models/devices.py:1261 netbox/extras/models/customfields.py:253
|
||||
#: netbox/extras/models/models.py:118 netbox/extras/models/models.py:813
|
||||
#: netbox/netbox/models/__init__.py:134 netbox/netbox/models/__init__.py:173
|
||||
#: netbox/netbox/models/__init__.py:223
|
||||
msgid "comments"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/models/devices.py:1268
|
||||
#: netbox/dcim/models/devices.py:1277
|
||||
msgid "virtual device context"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/models/devices.py:1269
|
||||
#: netbox/dcim/models/devices.py:1278
|
||||
msgid "virtual device contexts"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/models/devices.py:1297
|
||||
#: netbox/dcim/models/devices.py:1306
|
||||
#, python-brace-format
|
||||
msgid "{ip} is not an IPv{family} address."
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/models/devices.py:1303
|
||||
#: netbox/dcim/models/devices.py:1312
|
||||
msgid "Primary IP address must belong to an interface on the assigned device."
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/models/devices.py:1337
|
||||
#: netbox/dcim/models/devices.py:1346
|
||||
msgid "MAC addresses"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/models/devices.py:1369
|
||||
#: netbox/dcim/models/devices.py:1378
|
||||
msgid ""
|
||||
"Cannot unassign MAC Address while it is designated as the primary MAC for an "
|
||||
"object"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/models/devices.py:1373
|
||||
#: netbox/dcim/models/devices.py:1382
|
||||
msgid ""
|
||||
"Cannot reassign MAC Address while it is designated as the primary MAC for an "
|
||||
"object"
|
||||
@@ -6959,15 +6959,15 @@ msgstr ""
|
||||
msgid "Invalid schema: {error}"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/models/modules.py:242
|
||||
#: netbox/dcim/models/modules.py:250
|
||||
msgid "module"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/models/modules.py:243
|
||||
#: netbox/dcim/models/modules.py:251
|
||||
msgid "modules"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/dcim/models/modules.py:256
|
||||
#: netbox/dcim/models/modules.py:264
|
||||
#, python-brace-format
|
||||
msgid ""
|
||||
"Module must be installed within a module bay belonging to the assigned "
|
||||
@@ -8005,7 +8005,12 @@ msgstr ""
|
||||
msgid "Removed {device} from virtual chassis {chassis}"
|
||||
msgstr ""
|
||||
|
||||
#: netbox/extras/api/customfields.py:100
|
||||
#: netbox/extras/api/customfields.py:94
|
||||
#, python-brace-format
|
||||
msgid "Custom field '{name}' does not exist for this object type."
|
||||
msgstr ""
|
||||
|
||||
#: netbox/extras/api/customfields.py:110
|
||||
#, python-brace-format
|
||||
msgid "Unknown related object(s): {name}"
|
||||
msgstr ""
|
||||
|
||||
Reference in New Issue
Block a user