mirror of
https://github.com/netbox-community/netbox.git
synced 2026-04-10 19:27:05 +02:00
Compare commits
3 Commits
21783-bulk
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4ca688de57 | ||
|
|
ed7ebd9d98 | ||
|
|
e864dc3ae0 |
@@ -1409,16 +1409,8 @@ class CableImportForm(PrimaryModelImportForm):
|
|||||||
side_a_device = CSVModelChoiceField(
|
side_a_device = CSVModelChoiceField(
|
||||||
label=_('Side A device'),
|
label=_('Side A device'),
|
||||||
queryset=Device.objects.all(),
|
queryset=Device.objects.all(),
|
||||||
required=False,
|
|
||||||
to_field_name='name',
|
to_field_name='name',
|
||||||
help_text=_('Device name (for device component terminations)')
|
help_text=_('Device name')
|
||||||
)
|
|
||||||
side_a_power_panel = CSVModelChoiceField(
|
|
||||||
label=_('Side A power panel'),
|
|
||||||
queryset=PowerPanel.objects.all(),
|
|
||||||
required=False,
|
|
||||||
to_field_name='name',
|
|
||||||
help_text=_('Power panel name (for power feed terminations)')
|
|
||||||
)
|
)
|
||||||
side_a_type = CSVContentTypeField(
|
side_a_type = CSVContentTypeField(
|
||||||
label=_('Side A type'),
|
label=_('Side A type'),
|
||||||
@@ -1442,16 +1434,8 @@ class CableImportForm(PrimaryModelImportForm):
|
|||||||
side_b_device = CSVModelChoiceField(
|
side_b_device = CSVModelChoiceField(
|
||||||
label=_('Side B device'),
|
label=_('Side B device'),
|
||||||
queryset=Device.objects.all(),
|
queryset=Device.objects.all(),
|
||||||
required=False,
|
|
||||||
to_field_name='name',
|
to_field_name='name',
|
||||||
help_text=_('Device name (for device component terminations)')
|
help_text=_('Device name')
|
||||||
)
|
|
||||||
side_b_power_panel = CSVModelChoiceField(
|
|
||||||
label=_('Side B power panel'),
|
|
||||||
queryset=PowerPanel.objects.all(),
|
|
||||||
required=False,
|
|
||||||
to_field_name='name',
|
|
||||||
help_text=_('Power panel name (for power feed terminations)')
|
|
||||||
)
|
)
|
||||||
side_b_type = CSVContentTypeField(
|
side_b_type = CSVContentTypeField(
|
||||||
label=_('Side B type'),
|
label=_('Side B type'),
|
||||||
@@ -1506,9 +1490,8 @@ class CableImportForm(PrimaryModelImportForm):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = Cable
|
model = Cable
|
||||||
fields = [
|
fields = [
|
||||||
'side_a_site', 'side_a_device', 'side_a_power_panel', 'side_a_type', 'side_a_name',
|
'side_a_site', 'side_a_device', 'side_a_type', 'side_a_name', 'side_b_site', 'side_b_device', 'side_b_type',
|
||||||
'side_b_site', 'side_b_device', 'side_b_power_panel', 'side_b_type', 'side_b_name',
|
'side_b_name', 'type', 'status', 'profile', 'tenant', 'label', 'color', 'length', 'length_unit',
|
||||||
'type', 'status', 'profile', 'tenant', 'label', 'color', 'length', 'length_unit',
|
|
||||||
'description', 'owner', 'comments', 'tags',
|
'description', 'owner', 'comments', 'tags',
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -1518,22 +1501,16 @@ class CableImportForm(PrimaryModelImportForm):
|
|||||||
if data:
|
if data:
|
||||||
# Limit choices for side_a_device to the assigned side_a_site
|
# Limit choices for side_a_device to the assigned side_a_site
|
||||||
if side_a_site := data.get('side_a_site'):
|
if side_a_site := data.get('side_a_site'):
|
||||||
side_a_parent_params = {f'site__{self.fields['side_a_site'].to_field_name}': side_a_site}
|
side_a_device_params = {f'site__{self.fields["side_a_site"].to_field_name}': side_a_site}
|
||||||
self.fields['side_a_device'].queryset = self.fields['side_a_device'].queryset.filter(
|
self.fields['side_a_device'].queryset = self.fields['side_a_device'].queryset.filter(
|
||||||
**side_a_parent_params
|
**side_a_device_params
|
||||||
)
|
|
||||||
self.fields['side_a_power_panel'].queryset = self.fields['side_a_power_panel'].queryset.filter(
|
|
||||||
**side_a_parent_params
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Limit choices for side_b_device to the assigned side_b_site
|
# Limit choices for side_b_device to the assigned side_b_site
|
||||||
if side_b_site := data.get('side_b_site'):
|
if side_b_site := data.get('side_b_site'):
|
||||||
side_b_parent_params = {f'site__{self.fields['side_b_site'].to_field_name}': side_b_site}
|
side_b_device_params = {f'site__{self.fields["side_b_site"].to_field_name}': side_b_site}
|
||||||
self.fields['side_b_device'].queryset = self.fields['side_b_device'].queryset.filter(
|
self.fields['side_b_device'].queryset = self.fields['side_b_device'].queryset.filter(
|
||||||
**side_b_parent_params
|
**side_b_device_params
|
||||||
)
|
|
||||||
self.fields['side_b_power_panel'].queryset = self.fields['side_b_power_panel'].queryset.filter(
|
|
||||||
**side_b_parent_params
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def _clean_side(self, side):
|
def _clean_side(self, side):
|
||||||
@@ -1545,57 +1522,33 @@ class CableImportForm(PrimaryModelImportForm):
|
|||||||
assert side in 'ab', f"Invalid side designation: {side}"
|
assert side in 'ab', f"Invalid side designation: {side}"
|
||||||
|
|
||||||
device = self.cleaned_data.get(f'side_{side}_device')
|
device = self.cleaned_data.get(f'side_{side}_device')
|
||||||
power_panel = self.cleaned_data.get(f'side_{side}_power_panel')
|
|
||||||
content_type = self.cleaned_data.get(f'side_{side}_type')
|
content_type = self.cleaned_data.get(f'side_{side}_type')
|
||||||
name = self.cleaned_data.get(f'side_{side}_name')
|
name = self.cleaned_data.get(f'side_{side}_name')
|
||||||
if not content_type or not name:
|
if not device or not content_type or not name:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
model = content_type.model_class()
|
model = content_type.model_class()
|
||||||
|
try:
|
||||||
# PowerFeed terminations reference a PowerPanel, not a Device
|
if (
|
||||||
if content_type.model == 'powerfeed':
|
device.virtual_chassis and
|
||||||
if not power_panel:
|
device.virtual_chassis.master == device and
|
||||||
return None
|
not model.objects.filter(device=device, name=name).exists()
|
||||||
try:
|
):
|
||||||
termination_object = model.objects.get(power_panel=power_panel, name=name)
|
termination_object = model.objects.get(device__in=device.virtual_chassis.members.all(), name=name)
|
||||||
if termination_object.cable is not None and termination_object.cable != self.instance:
|
else:
|
||||||
raise forms.ValidationError(
|
termination_object = model.objects.get(device=device, name=name)
|
||||||
_("Side {side_upper}: {power_panel} {termination_object} is already connected").format(
|
if termination_object.cable is not None and termination_object.cable != self.instance:
|
||||||
side_upper=side.upper(), power_panel=power_panel, termination_object=termination_object
|
|
||||||
)
|
|
||||||
)
|
|
||||||
except ObjectDoesNotExist:
|
|
||||||
raise forms.ValidationError(
|
raise forms.ValidationError(
|
||||||
_("{side_upper} side termination not found: {power_panel} {name}").format(
|
_("Side {side_upper}: {device} {termination_object} is already connected").format(
|
||||||
side_upper=side.upper(), power_panel=power_panel, name=name
|
side_upper=side.upper(), device=device, termination_object=termination_object
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
else:
|
except ObjectDoesNotExist:
|
||||||
if not device:
|
raise forms.ValidationError(
|
||||||
return None
|
_("{side_upper} side termination not found: {device} {name}").format(
|
||||||
try:
|
side_upper=side.upper(), device=device, name=name
|
||||||
if (
|
|
||||||
device.virtual_chassis and
|
|
||||||
device.virtual_chassis.master == device and
|
|
||||||
not model.objects.filter(device=device, name=name).exists()
|
|
||||||
):
|
|
||||||
termination_object = model.objects.get(device__in=device.virtual_chassis.members.all(), name=name)
|
|
||||||
else:
|
|
||||||
termination_object = model.objects.get(device=device, name=name)
|
|
||||||
if termination_object.cable is not None and termination_object.cable != self.instance:
|
|
||||||
raise forms.ValidationError(
|
|
||||||
_("Side {side_upper}: {device} {termination_object} is already connected").format(
|
|
||||||
side_upper=side.upper(), device=device, termination_object=termination_object
|
|
||||||
)
|
|
||||||
)
|
|
||||||
except ObjectDoesNotExist:
|
|
||||||
raise forms.ValidationError(
|
|
||||||
_("{side_upper} side termination not found: {device} {name}").format(
|
|
||||||
side_upper=side.upper(), device=device, name=name
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
)
|
||||||
setattr(self.instance, f'{side}_terminations', [termination_object])
|
setattr(self.instance, f'{side}_terminations', [termination_object])
|
||||||
return termination_object
|
return termination_object
|
||||||
|
|
||||||
|
|||||||
@@ -3603,21 +3603,6 @@ class CableTestCase(
|
|||||||
cable3 = Cable(a_terminations=[interfaces[2]], b_terminations=[interfaces[5]], type=CableTypeChoices.TYPE_CAT6)
|
cable3 = Cable(a_terminations=[interfaces[2]], b_terminations=[interfaces[5]], type=CableTypeChoices.TYPE_CAT6)
|
||||||
cable3.save()
|
cable3.save()
|
||||||
|
|
||||||
# Power panel, power feeds, and power ports for powerfeed-to-powerport cable import tests
|
|
||||||
power_panel = PowerPanel.objects.create(site=sites[0], name='Power Panel 1')
|
|
||||||
power_feeds = (
|
|
||||||
PowerFeed(name='Power Feed 1', power_panel=power_panel),
|
|
||||||
PowerFeed(name='Power Feed 2', power_panel=power_panel),
|
|
||||||
PowerFeed(name='Power Feed 3', power_panel=power_panel),
|
|
||||||
)
|
|
||||||
PowerFeed.objects.bulk_create(power_feeds)
|
|
||||||
power_ports = (
|
|
||||||
PowerPort(device=devices[3], name='Power Port 1'),
|
|
||||||
PowerPort(device=devices[3], name='Power Port 2'),
|
|
||||||
PowerPort(device=devices[3], name='Power Port 3'),
|
|
||||||
)
|
|
||||||
PowerPort.objects.bulk_create(power_ports)
|
|
||||||
|
|
||||||
tags = create_tags('Alpha', 'Bravo', 'Charlie')
|
tags = create_tags('Alpha', 'Bravo', 'Charlie')
|
||||||
|
|
||||||
cls.form_data = {
|
cls.form_data = {
|
||||||
@@ -3655,14 +3640,7 @@ class CableTestCase(
|
|||||||
"Site 1,Device 3,dcim.interface,Interface 3,Site 2,Device 1,dcim.interface,Interface 3",
|
"Site 1,Device 3,dcim.interface,Interface 3,Site 2,Device 1,dcim.interface,Interface 3",
|
||||||
"Site 1,Device 1,dcim.interface,Device 2 Interface,Site 2,Device 1,dcim.interface,Interface 4",
|
"Site 1,Device 1,dcim.interface,Device 2 Interface,Site 2,Device 1,dcim.interface,Interface 4",
|
||||||
"Site 1,Device 1,dcim.interface,Device 3 Interface,Site 2,Device 1,dcim.interface,Interface 5",
|
"Site 1,Device 1,dcim.interface,Device 3 Interface,Site 2,Device 1,dcim.interface,Interface 5",
|
||||||
),
|
)
|
||||||
'powerfeed-to-powerport': (
|
|
||||||
# Ensure that powerfeed-to-powerport cables can be imported via CSV using side_a_power_panel
|
|
||||||
"side_a_power_panel,side_a_type,side_a_name,side_b_device,side_b_type,side_b_name",
|
|
||||||
"Power Panel 1,dcim.powerfeed,Power Feed 1,Device 4,dcim.powerport,Power Port 1",
|
|
||||||
"Power Panel 1,dcim.powerfeed,Power Feed 2,Device 4,dcim.powerport,Power Port 2",
|
|
||||||
"Power Panel 1,dcim.powerfeed,Power Feed 3,Device 4,dcim.powerport,Power Port 3",
|
|
||||||
),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
cls.csv_update_data = (
|
cls.csv_update_data = (
|
||||||
|
|||||||
@@ -1,10 +1,15 @@
|
|||||||
|
import io
|
||||||
import tempfile
|
import tempfile
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
from django.contrib.contenttypes.models import ContentType
|
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.core.files.uploadedfile import SimpleUploadedFile
|
||||||
from django.forms import ValidationError
|
from django.forms import ValidationError
|
||||||
from django.test import TestCase, tag
|
from django.test import TestCase, tag
|
||||||
|
from PIL import Image
|
||||||
|
|
||||||
from core.models import AutoSyncRecord, DataSource, ObjectType
|
from core.models import AutoSyncRecord, DataSource, ObjectType
|
||||||
from dcim.models import Device, DeviceRole, DeviceType, Location, Manufacturer, Platform, Region, Site, SiteGroup
|
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
|
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):
|
class ImageAttachmentTests(TestCase):
|
||||||
@classmethod
|
@classmethod
|
||||||
def setUpTestData(cls):
|
def setUpTestData(cls):
|
||||||
cls.ct_rack = ContentType.objects.get_by_natural_key('dcim', 'rack')
|
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''
|
cls.image_content = b''
|
||||||
|
|
||||||
def _stub_image_attachment(self, object_id, image_filename, name=None):
|
def _stub_image_attachment(self, object_id, image_filename, name=None):
|
||||||
@@ -41,6 +86,15 @@ class ImageAttachmentTests(TestCase):
|
|||||||
)
|
)
|
||||||
return ia
|
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):
|
def test_filename_strips_expected_prefix(self):
|
||||||
"""
|
"""
|
||||||
Tests that the filename of the image attachment is stripped of the expected
|
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='')
|
ia = self._stub_image_attachment(12, 'image-attachments/rack_12_file.png', name='')
|
||||||
self.assertEqual('file.png', str(ia))
|
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):
|
class TagTest(TestCase):
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
from types import SimpleNamespace
|
from types import SimpleNamespace
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
|
from django.core.files.storage import Storage
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
|
|
||||||
from extras.models import ExportTemplate
|
from extras.models import ExportTemplate, ImageAttachment
|
||||||
from extras.utils import filename_from_model, image_upload
|
from extras.utils import _build_image_attachment_path, filename_from_model, image_upload
|
||||||
from tenancy.models import ContactGroup, TenantGroup
|
from tenancy.models import ContactGroup, TenantGroup
|
||||||
from wireless.models import WirelessLANGroup
|
from wireless.models import WirelessLANGroup
|
||||||
|
|
||||||
@@ -22,6 +24,25 @@ class FilenameFromModelTests(TestCase):
|
|||||||
self.assertEqual(filename_from_model(model), expected)
|
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):
|
class ImageUploadTests(TestCase):
|
||||||
@classmethod
|
@classmethod
|
||||||
def setUpTestData(cls):
|
def setUpTestData(cls):
|
||||||
@@ -31,16 +52,18 @@ class ImageUploadTests(TestCase):
|
|||||||
|
|
||||||
def _stub_instance(self, object_id=12, name=None):
|
def _stub_instance(self, object_id=12, name=None):
|
||||||
"""
|
"""
|
||||||
Creates a minimal stub for use with the `image_upload()` function.
|
Creates a minimal stub for use with image attachment path generation.
|
||||||
|
|
||||||
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.
|
|
||||||
"""
|
"""
|
||||||
return SimpleNamespace(object_type=self.ct_rack, object_id=object_id, name=name)
|
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):
|
def _second_segment(self, path: str):
|
||||||
"""
|
"""
|
||||||
Extracts and returns the portion of the input string after the
|
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.
|
Tests handling of a Windows file path with a fake directory and extension.
|
||||||
"""
|
"""
|
||||||
inst = self._stub_instance(name=None)
|
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
|
# Base directory and single-level path
|
||||||
seg2 = self._second_segment(path)
|
seg2 = self._second_segment(path)
|
||||||
self.assertTrue(path.startswith('image-attachments/rack_12_'))
|
self.assertTrue(path.startswith('image-attachments/rack_12_'))
|
||||||
@@ -67,7 +90,7 @@ class ImageUploadTests(TestCase):
|
|||||||
create subdirectories.
|
create subdirectories.
|
||||||
"""
|
"""
|
||||||
inst = self._stub_instance(name='5/31/23')
|
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)
|
seg2 = self._second_segment(path)
|
||||||
self.assertTrue(seg2.startswith('rack_12_'))
|
self.assertTrue(seg2.startswith('rack_12_'))
|
||||||
self.assertNotIn('/', seg2)
|
self.assertNotIn('/', seg2)
|
||||||
@@ -80,7 +103,7 @@ class ImageUploadTests(TestCase):
|
|||||||
into a single directory name without creating subdirectories.
|
into a single directory name without creating subdirectories.
|
||||||
"""
|
"""
|
||||||
inst = self._stub_instance(name=r'5\31\23')
|
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)
|
seg2 = self._second_segment(path)
|
||||||
self.assertTrue(seg2.startswith('rack_12_'))
|
self.assertTrue(seg2.startswith('rack_12_'))
|
||||||
@@ -93,7 +116,7 @@ class ImageUploadTests(TestCase):
|
|||||||
Tests the output path format generated by the `image_upload` function.
|
Tests the output path format generated by the `image_upload` function.
|
||||||
"""
|
"""
|
||||||
inst = self._stub_instance(object_id=99, name='label')
|
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_"
|
# The second segment must begin with "rack_99_"
|
||||||
seg2 = self._second_segment(path)
|
seg2 = self._second_segment(path)
|
||||||
self.assertTrue(seg2.startswith('rack_99_'))
|
self.assertTrue(seg2.startswith('rack_99_'))
|
||||||
@@ -105,7 +128,7 @@ class ImageUploadTests(TestCase):
|
|||||||
is omitted.
|
is omitted.
|
||||||
"""
|
"""
|
||||||
inst = self._stub_instance(name='test')
|
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)
|
seg2 = self._second_segment(path)
|
||||||
self.assertTrue(seg2.startswith('rack_12_test'))
|
self.assertTrue(seg2.startswith('rack_12_test'))
|
||||||
@@ -121,7 +144,7 @@ class ImageUploadTests(TestCase):
|
|||||||
# Suppose the instance name has surrounding whitespace and
|
# Suppose the instance name has surrounding whitespace and
|
||||||
# extra slashes.
|
# extra slashes.
|
||||||
inst = self._stub_instance(name=' my/complex\\name ')
|
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.
|
# The output should be flattened and sanitized.
|
||||||
# We expect the name to be transformed into a valid filename without
|
# 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']:
|
for name in ['2025/09/12', r'2025\09\12']:
|
||||||
with self.subTest(name=name):
|
with self.subTest(name=name):
|
||||||
inst = self._stub_instance(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)
|
seg2 = self._second_segment(path)
|
||||||
self.assertTrue(seg2.startswith('rack_12_'))
|
self.assertTrue(seg2.startswith('rack_12_'))
|
||||||
self.assertNotIn('/', seg2)
|
self.assertNotIn('/', seg2)
|
||||||
@@ -154,7 +177,49 @@ class ImageUploadTests(TestCase):
|
|||||||
SuspiciousFileOperation, the fallback default is used.
|
SuspiciousFileOperation, the fallback default is used.
|
||||||
"""
|
"""
|
||||||
inst = self._stub_instance(name=' ')
|
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.
|
# Expect the fallback name 'unnamed' to be used.
|
||||||
self.assertIn('unnamed', path)
|
self.assertIn('unnamed', path)
|
||||||
self.assertTrue(path.startswith('image-attachments/rack_12_'))
|
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 pathlib import Path
|
||||||
|
|
||||||
from django.core.exceptions import ImproperlyConfigured, SuspiciousFileOperation
|
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.core.files.utils import validate_file_name
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
@@ -67,15 +67,13 @@ def is_taggable(obj):
|
|||||||
return False
|
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)
|
- Normalizes browser paths (e.g., C:\\fake_path\\photo.jpg)
|
||||||
- Uses the instance.name if provided (sanitized to a *basename*, no ext)
|
- Uses the instance.name if provided (sanitized to a *basename*, no ext)
|
||||||
- Prefixes with a machine-friendly identifier
|
- Prefixes with a machine-friendly identifier
|
||||||
|
|
||||||
Note: Relies on Django's default_storage utility.
|
|
||||||
"""
|
"""
|
||||||
upload_dir = 'image-attachments'
|
upload_dir = 'image-attachments'
|
||||||
default_filename = 'unnamed'
|
default_filename = 'unnamed'
|
||||||
@@ -92,22 +90,38 @@ def image_upload(instance, filename):
|
|||||||
# Rely on Django's get_valid_filename to perform sanitization.
|
# Rely on Django's get_valid_filename to perform sanitization.
|
||||||
stem = (instance.name or file_path.stem).strip()
|
stem = (instance.name or file_path.stem).strip()
|
||||||
try:
|
try:
|
||||||
safe_stem = default_storage.get_valid_name(stem)
|
safe_stem = storage.get_valid_name(stem)
|
||||||
except SuspiciousFileOperation:
|
except SuspiciousFileOperation:
|
||||||
safe_stem = default_filename
|
safe_stem = default_filename
|
||||||
|
|
||||||
# Append the uploaded extension only if it's an allowed image type
|
# 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
|
# Create a machine-friendly prefix from the instance
|
||||||
prefix = f"{instance.object_type.model}_{instance.object_id}"
|
prefix = f'{instance.object_type.model}_{instance.object_id}'
|
||||||
name_with_path = f"{upload_dir}/{prefix}_{final_name}"
|
name_with_path = f'{upload_dir}/{prefix}_{final_name}'
|
||||||
|
|
||||||
# Validate the generated relative path (blocks absolute/traversal)
|
# Validate the generated relative path (blocks absolute/traversal)
|
||||||
validate_file_name(name_with_path, allow_relative_path=True)
|
validate_file_name(name_with_path, allow_relative_path=True)
|
||||||
return name_with_path
|
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):
|
def is_script(obj):
|
||||||
"""
|
"""
|
||||||
Returns True if the object is a Script or Report.
|
Returns True if the object is a Script or Report.
|
||||||
|
|||||||
@@ -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: 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"
|
"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"
|
||||||
@@ -1247,13 +1247,13 @@ msgstr ""
|
|||||||
|
|
||||||
#: netbox/circuits/models/base.py:18 netbox/dcim/models/cables.py:81
|
#: 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:328
|
||||||
#: netbox/dcim/models/device_component_templates.py:563
|
#: netbox/dcim/models/device_component_templates.py:571
|
||||||
#: netbox/dcim/models/device_component_templates.py:636
|
#: netbox/dcim/models/device_component_templates.py:644
|
||||||
#: netbox/dcim/models/device_components.py:605
|
#: netbox/dcim/models/device_components.py:605
|
||||||
#: netbox/dcim/models/device_components.py:1188
|
#: netbox/dcim/models/device_components.py:1188
|
||||||
#: netbox/dcim/models/device_components.py:1236
|
#: netbox/dcim/models/device_components.py:1236
|
||||||
#: netbox/dcim/models/device_components.py:1387
|
#: 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
|
#: netbox/extras/models/tags.py:30
|
||||||
msgid "color"
|
msgid "color"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@@ -1281,8 +1281,8 @@ msgstr ""
|
|||||||
#: netbox/core/models/jobs.py:95 netbox/dcim/models/cables.py:57
|
#: 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:576
|
||||||
#: netbox/dcim/models/device_components.py:1426
|
#: netbox/dcim/models/device_components.py:1426
|
||||||
#: netbox/dcim/models/devices.py:589 netbox/dcim/models/devices.py:1218
|
#: netbox/dcim/models/devices.py:598 netbox/dcim/models/devices.py:1227
|
||||||
#: netbox/dcim/models/modules.py:219 netbox/dcim/models/power.py:95
|
#: 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/racks.py:301 netbox/dcim/models/racks.py:685
|
||||||
#: netbox/dcim/models/sites.py:163 netbox/dcim/models/sites.py:287
|
#: netbox/dcim/models/sites.py:163 netbox/dcim/models/sites.py:287
|
||||||
#: netbox/ipam/models/ip.py:246 netbox/ipam/models/ip.py:548
|
#: 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/circuits/models/providers.py:98 netbox/core/models/data.py:40
|
||||||
#: netbox/core/models/jobs.py:56
|
#: netbox/core/models/jobs.py:56
|
||||||
#: netbox/dcim/models/device_component_templates.py:55
|
#: netbox/dcim/models/device_component_templates.py:55
|
||||||
#: netbox/dcim/models/device_components.py:57 netbox/dcim/models/devices.py:533
|
#: netbox/dcim/models/device_components.py:57 netbox/dcim/models/devices.py:542
|
||||||
#: netbox/dcim/models/devices.py:1144 netbox/dcim/models/devices.py:1213
|
#: 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/modules.py:35 netbox/dcim/models/power.py:39
|
||||||
#: netbox/dcim/models/power.py:90 netbox/dcim/models/racks.py:270
|
#: netbox/dcim/models/power.py:90 netbox/dcim/models/racks.py:270
|
||||||
#: netbox/dcim/models/sites.py:151 netbox/extras/models/configs.py:37
|
#: 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:256
|
||||||
#: netbox/dcim/models/device_component_templates.py:321
|
#: netbox/dcim/models/device_component_templates.py:321
|
||||||
#: netbox/dcim/models/device_component_templates.py:412
|
#: netbox/dcim/models/device_component_templates.py:412
|
||||||
#: netbox/dcim/models/device_component_templates.py:558
|
#: netbox/dcim/models/device_component_templates.py:566
|
||||||
#: netbox/dcim/models/device_component_templates.py:631
|
#: netbox/dcim/models/device_component_templates.py:639
|
||||||
#: netbox/dcim/models/device_components.py:402
|
#: netbox/dcim/models/device_components.py:402
|
||||||
#: netbox/dcim/models/device_components.py:429
|
#: netbox/dcim/models/device_components.py:429
|
||||||
#: netbox/dcim/models/device_components.py:460
|
#: 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/filtersets.py:1324 netbox/dcim/forms/filtersets.py:920
|
||||||
#: netbox/dcim/forms/filtersets.py:1634 netbox/dcim/forms/filtersets.py:1979
|
#: 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/forms/model_forms.py:1941 netbox/dcim/models/devices.py:1322
|
||||||
#: netbox/dcim/models/devices.py:1336 netbox/dcim/ui/panels.py:366
|
#: netbox/dcim/models/devices.py:1345 netbox/dcim/ui/panels.py:366
|
||||||
#: netbox/dcim/ui/panels.py:513 netbox/virtualization/filtersets.py:230
|
#: netbox/dcim/ui/panels.py:513 netbox/virtualization/filtersets.py:230
|
||||||
#: netbox/virtualization/filtersets.py:318
|
#: netbox/virtualization/filtersets.py:318
|
||||||
#: netbox/virtualization/forms/filtersets.py:193
|
#: netbox/virtualization/forms/filtersets.py:193
|
||||||
@@ -4340,7 +4340,7 @@ msgstr ""
|
|||||||
msgid "Chassis"
|
msgid "Chassis"
|
||||||
msgstr ""
|
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
|
#: netbox/dcim/tables/devices.py:76 netbox/dcim/ui/panels.py:144
|
||||||
msgid "VM role"
|
msgid "VM role"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@@ -5543,7 +5543,7 @@ msgstr ""
|
|||||||
msgid "Profile & Attributes"
|
msgid "Profile & Attributes"
|
||||||
msgstr ""
|
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"
|
msgid "The lowest-numbered unit occupied by the device"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -6019,91 +6019,91 @@ msgstr ""
|
|||||||
msgid "Rear port ({rear_port}) must belong to the same device type"
|
msgid "Rear port ({rear_port}) must belong to the same device type"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/dcim/models/device_component_templates.py:567
|
#: netbox/dcim/models/device_component_templates.py:575
|
||||||
#: netbox/dcim/models/device_component_templates.py:640
|
#: netbox/dcim/models/device_component_templates.py:648
|
||||||
#: netbox/dcim/models/device_components.py:1192
|
#: netbox/dcim/models/device_components.py:1192
|
||||||
#: netbox/dcim/models/device_components.py:1240
|
#: netbox/dcim/models/device_components.py:1240
|
||||||
msgid "positions"
|
msgid "positions"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/dcim/models/device_component_templates.py:588
|
#: netbox/dcim/models/device_component_templates.py:596
|
||||||
msgid "front port template"
|
msgid "front port template"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/dcim/models/device_component_templates.py:589
|
#: netbox/dcim/models/device_component_templates.py:597
|
||||||
msgid "front port templates"
|
msgid "front port templates"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/dcim/models/device_component_templates.py:600
|
#: netbox/dcim/models/device_component_templates.py:608
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid ""
|
msgid ""
|
||||||
"The number of positions cannot be less than the number of mapped rear port "
|
"The number of positions cannot be less than the number of mapped rear port "
|
||||||
"templates ({count})"
|
"templates ({count})"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/dcim/models/device_component_templates.py:651
|
#: netbox/dcim/models/device_component_templates.py:659
|
||||||
msgid "rear port template"
|
msgid "rear port template"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/dcim/models/device_component_templates.py:652
|
#: netbox/dcim/models/device_component_templates.py:660
|
||||||
msgid "rear port templates"
|
msgid "rear port templates"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/dcim/models/device_component_templates.py:663
|
#: netbox/dcim/models/device_component_templates.py:671
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid ""
|
msgid ""
|
||||||
"The number of positions cannot be less than the number of mapped front port "
|
"The number of positions cannot be less than the number of mapped front port "
|
||||||
"templates ({count})"
|
"templates ({count})"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/dcim/models/device_component_templates.py:695
|
#: netbox/dcim/models/device_component_templates.py:703
|
||||||
#: netbox/dcim/models/device_components.py:1287
|
#: netbox/dcim/models/device_components.py:1287
|
||||||
msgid "position"
|
msgid "position"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/dcim/models/device_component_templates.py:698
|
#: netbox/dcim/models/device_component_templates.py:706
|
||||||
#: netbox/dcim/models/device_components.py:1290
|
#: netbox/dcim/models/device_components.py:1290
|
||||||
msgid "Identifier to reference when renaming installed components"
|
msgid "Identifier to reference when renaming installed components"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/dcim/models/device_component_templates.py:704
|
#: netbox/dcim/models/device_component_templates.py:712
|
||||||
msgid "module bay template"
|
msgid "module bay template"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/dcim/models/device_component_templates.py:705
|
#: netbox/dcim/models/device_component_templates.py:713
|
||||||
msgid "module bay templates"
|
msgid "module bay templates"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/dcim/models/device_component_templates.py:737
|
#: netbox/dcim/models/device_component_templates.py:745
|
||||||
msgid "device bay template"
|
msgid "device bay template"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/dcim/models/device_component_templates.py:738
|
#: netbox/dcim/models/device_component_templates.py:746
|
||||||
msgid "device bay templates"
|
msgid "device bay templates"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/dcim/models/device_component_templates.py:752
|
#: netbox/dcim/models/device_component_templates.py:760
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid ""
|
msgid ""
|
||||||
"Subdevice role of device type ({device_type}) must be set to \"parent\" to "
|
"Subdevice role of device type ({device_type}) must be set to \"parent\" to "
|
||||||
"allow device bays."
|
"allow device bays."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/dcim/models/device_component_templates.py:807
|
#: netbox/dcim/models/device_component_templates.py:815
|
||||||
#: netbox/dcim/models/device_components.py:1447
|
#: netbox/dcim/models/device_components.py:1447
|
||||||
msgid "part ID"
|
msgid "part ID"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/dcim/models/device_component_templates.py:809
|
#: netbox/dcim/models/device_component_templates.py:817
|
||||||
#: netbox/dcim/models/device_components.py:1449
|
#: netbox/dcim/models/device_components.py:1449
|
||||||
msgid "Manufacturer-assigned part identifier"
|
msgid "Manufacturer-assigned part identifier"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/dcim/models/device_component_templates.py:826
|
#: netbox/dcim/models/device_component_templates.py:834
|
||||||
msgid "inventory item template"
|
msgid "inventory item template"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/dcim/models/device_component_templates.py:827
|
#: netbox/dcim/models/device_component_templates.py:835
|
||||||
msgid "inventory item templates"
|
msgid "inventory item templates"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -6448,7 +6448,7 @@ msgid "module bays"
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/dcim/models/device_components.py:1322
|
#: 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."
|
msgid "A module bay cannot belong to a module installed within it."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -6484,14 +6484,14 @@ msgid "inventory item roles"
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/dcim/models/device_components.py:1453
|
#: 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/dcim/models/racks.py:317
|
||||||
#: netbox/virtualization/models/virtualmachines.py:132
|
#: netbox/virtualization/models/virtualmachines.py:132
|
||||||
msgid "serial number"
|
msgid "serial number"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/dcim/models/device_components.py:1461
|
#: 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
|
#: netbox/dcim/models/racks.py:324
|
||||||
msgid "asset tag"
|
msgid "asset tag"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@@ -6587,7 +6587,7 @@ msgid ""
|
|||||||
"device type is neither a parent nor a child."
|
"device type is neither a parent nor a child."
|
||||||
msgstr ""
|
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
|
#: netbox/dcim/models/modules.py:87 netbox/dcim/models/racks.py:328
|
||||||
msgid "airflow"
|
msgid "airflow"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@@ -6600,310 +6600,310 @@ msgstr ""
|
|||||||
msgid "device types"
|
msgid "device types"
|
||||||
msgstr ""
|
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."
|
msgid "U height must be in increments of 0.5 rack units."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/dcim/models/devices.py:312
|
#: netbox/dcim/models/devices.py:321
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid ""
|
msgid ""
|
||||||
"Device {device} in rack {rack} does not have sufficient space to accommodate "
|
"Device {device} in rack {rack} does not have sufficient space to accommodate "
|
||||||
"a height of {height}U"
|
"a height of {height}U"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/dcim/models/devices.py:327
|
#: netbox/dcim/models/devices.py:336
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid ""
|
msgid ""
|
||||||
"Unable to set 0U height: Found <a href=\"{url}\">{racked_instance_count} "
|
"Unable to set 0U height: Found <a href=\"{url}\">{racked_instance_count} "
|
||||||
"instances</a> already mounted within racks."
|
"instances</a> already mounted within racks."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/dcim/models/devices.py:336
|
#: netbox/dcim/models/devices.py:345
|
||||||
msgid ""
|
msgid ""
|
||||||
"Must delete all device bay templates associated with this device before "
|
"Must delete all device bay templates associated with this device before "
|
||||||
"declassifying it as a parent device."
|
"declassifying it as a parent device."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/dcim/models/devices.py:342
|
#: netbox/dcim/models/devices.py:351
|
||||||
msgid "Child device types must be 0U."
|
msgid "Child device types must be 0U."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/dcim/models/devices.py:391
|
#: netbox/dcim/models/devices.py:400
|
||||||
msgid "Virtual machines may be assigned to this role"
|
msgid "Virtual machines may be assigned to this role"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/dcim/models/devices.py:417
|
#: netbox/dcim/models/devices.py:426
|
||||||
msgid "A top-level device role with this name already exists."
|
msgid "A top-level device role with this name already exists."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/dcim/models/devices.py:427
|
#: netbox/dcim/models/devices.py:436
|
||||||
msgid "A top-level device role with this slug already exists."
|
msgid "A top-level device role with this slug already exists."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/dcim/models/devices.py:430
|
#: netbox/dcim/models/devices.py:439
|
||||||
msgid "device role"
|
msgid "device role"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/dcim/models/devices.py:431
|
#: netbox/dcim/models/devices.py:440
|
||||||
msgid "device roles"
|
msgid "device roles"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/dcim/models/devices.py:445
|
#: netbox/dcim/models/devices.py:454
|
||||||
msgid "Optionally limit this platform to devices of a certain manufacturer"
|
msgid "Optionally limit this platform to devices of a certain manufacturer"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/dcim/models/devices.py:462
|
#: netbox/dcim/models/devices.py:471
|
||||||
msgid "platform"
|
msgid "platform"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/dcim/models/devices.py:463
|
#: netbox/dcim/models/devices.py:472
|
||||||
msgid "platforms"
|
msgid "platforms"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/dcim/models/devices.py:473
|
#: netbox/dcim/models/devices.py:482
|
||||||
msgid "Platform name must be unique."
|
msgid "Platform name must be unique."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/dcim/models/devices.py:483
|
#: netbox/dcim/models/devices.py:492
|
||||||
msgid "Platform slug must be unique."
|
msgid "Platform slug must be unique."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/dcim/models/devices.py:516
|
#: netbox/dcim/models/devices.py:525
|
||||||
msgid "The function this device serves"
|
msgid "The function this device serves"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/dcim/models/devices.py:543
|
#: netbox/dcim/models/devices.py:552
|
||||||
msgid "Chassis serial number, assigned by the manufacturer"
|
msgid "Chassis serial number, assigned by the manufacturer"
|
||||||
msgstr ""
|
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"
|
msgid "A unique tag used to identify this device"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/dcim/models/devices.py:578
|
#: netbox/dcim/models/devices.py:587
|
||||||
msgid "position (U)"
|
msgid "position (U)"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/dcim/models/devices.py:586
|
#: netbox/dcim/models/devices.py:595
|
||||||
msgid "rack face"
|
msgid "rack face"
|
||||||
msgstr ""
|
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
|
#: netbox/virtualization/models/virtualmachines.py:101
|
||||||
msgid "primary IPv4"
|
msgid "primary IPv4"
|
||||||
msgstr ""
|
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
|
#: netbox/virtualization/models/virtualmachines.py:109
|
||||||
msgid "primary IPv6"
|
msgid "primary IPv6"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/dcim/models/devices.py:623
|
#: netbox/dcim/models/devices.py:632
|
||||||
msgid "out-of-band IP"
|
msgid "out-of-band IP"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/dcim/models/devices.py:640
|
#: netbox/dcim/models/devices.py:649
|
||||||
msgid "VC position"
|
msgid "VC position"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/dcim/models/devices.py:643
|
#: netbox/dcim/models/devices.py:652
|
||||||
msgid "Virtual chassis position"
|
msgid "Virtual chassis position"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/dcim/models/devices.py:646
|
#: netbox/dcim/models/devices.py:655
|
||||||
msgid "VC priority"
|
msgid "VC priority"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/dcim/models/devices.py:650
|
#: netbox/dcim/models/devices.py:659
|
||||||
msgid "Virtual chassis master election priority"
|
msgid "Virtual chassis master election priority"
|
||||||
msgstr ""
|
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"
|
msgid "latitude"
|
||||||
msgstr ""
|
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
|
#: netbox/dcim/models/sites.py:226 netbox/dcim/models/sites.py:238
|
||||||
msgid "GPS coordinate in decimal format (xx.yyyyyy)"
|
msgid "GPS coordinate in decimal format (xx.yyyyyy)"
|
||||||
msgstr ""
|
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"
|
msgid "longitude"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/dcim/models/devices.py:748
|
#: netbox/dcim/models/devices.py:757
|
||||||
msgid "Device name must be unique per site."
|
msgid "Device name must be unique per site."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/dcim/models/devices.py:759
|
#: netbox/dcim/models/devices.py:768
|
||||||
msgid "device"
|
msgid "device"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/dcim/models/devices.py:760
|
#: netbox/dcim/models/devices.py:769
|
||||||
msgid "devices"
|
msgid "devices"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/dcim/models/devices.py:779
|
#: netbox/dcim/models/devices.py:788
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "Rack {rack} does not belong to site {site}."
|
msgid "Rack {rack} does not belong to site {site}."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/dcim/models/devices.py:784
|
#: netbox/dcim/models/devices.py:793
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "Location {location} does not belong to site {site}."
|
msgid "Location {location} does not belong to site {site}."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/dcim/models/devices.py:790
|
#: netbox/dcim/models/devices.py:799
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "Rack {rack} does not belong to location {location}."
|
msgid "Rack {rack} does not belong to location {location}."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/dcim/models/devices.py:797
|
#: netbox/dcim/models/devices.py:806
|
||||||
msgid "Cannot select a rack face without assigning a rack."
|
msgid "Cannot select a rack face without assigning a rack."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/dcim/models/devices.py:801
|
#: netbox/dcim/models/devices.py:810
|
||||||
msgid "Cannot select a rack position without assigning a rack."
|
msgid "Cannot select a rack position without assigning a rack."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/dcim/models/devices.py:807
|
#: netbox/dcim/models/devices.py:816
|
||||||
msgid "Position must be in increments of 0.5 rack units."
|
msgid "Position must be in increments of 0.5 rack units."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/dcim/models/devices.py:811
|
#: netbox/dcim/models/devices.py:820
|
||||||
msgid "Must specify rack face when defining rack position."
|
msgid "Must specify rack face when defining rack position."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/dcim/models/devices.py:819
|
#: netbox/dcim/models/devices.py:828
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "A 0U device type ({device_type}) cannot be assigned to a rack position."
|
msgid "A 0U device type ({device_type}) cannot be assigned to a rack position."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/dcim/models/devices.py:830
|
#: netbox/dcim/models/devices.py:839
|
||||||
msgid ""
|
msgid ""
|
||||||
"Child device types cannot be assigned to a rack face. This is an attribute "
|
"Child device types cannot be assigned to a rack face. This is an attribute "
|
||||||
"of the parent device."
|
"of the parent device."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/dcim/models/devices.py:837
|
#: netbox/dcim/models/devices.py:846
|
||||||
msgid ""
|
msgid ""
|
||||||
"Child device types cannot be assigned to a rack position. This is an "
|
"Child device types cannot be assigned to a rack position. This is an "
|
||||||
"attribute of the parent device."
|
"attribute of the parent device."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/dcim/models/devices.py:851
|
#: netbox/dcim/models/devices.py:860
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid ""
|
msgid ""
|
||||||
"U{position} is already occupied or does not have sufficient space to "
|
"U{position} is already occupied or does not have sufficient space to "
|
||||||
"accommodate this device type: {device_type} ({u_height}U)"
|
"accommodate this device type: {device_type} ({u_height}U)"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/dcim/models/devices.py:866
|
#: netbox/dcim/models/devices.py:875
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "{ip} is not an IPv4 address."
|
msgid "{ip} is not an IPv4 address."
|
||||||
msgstr ""
|
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
|
#, python-brace-format
|
||||||
msgid "The specified IP address ({ip}) is not assigned to this device."
|
msgid "The specified IP address ({ip}) is not assigned to this device."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/dcim/models/devices.py:884
|
#: netbox/dcim/models/devices.py:893
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "{ip} is not an IPv6 address."
|
msgid "{ip} is not an IPv6 address."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/dcim/models/devices.py:914
|
#: netbox/dcim/models/devices.py:923
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid ""
|
msgid ""
|
||||||
"The assigned platform is limited to {platform_manufacturer} device types, "
|
"The assigned platform is limited to {platform_manufacturer} device types, "
|
||||||
"but this device's type belongs to {devicetype_manufacturer}."
|
"but this device's type belongs to {devicetype_manufacturer}."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/dcim/models/devices.py:925
|
#: netbox/dcim/models/devices.py:934
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "The assigned cluster belongs to a different site ({site})"
|
msgid "The assigned cluster belongs to a different site ({site})"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/dcim/models/devices.py:932
|
#: netbox/dcim/models/devices.py:941
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "The assigned cluster belongs to a different location ({location})"
|
msgid "The assigned cluster belongs to a different location ({location})"
|
||||||
msgstr ""
|
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."
|
msgid "A device assigned to a virtual chassis must have its position defined."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/dcim/models/devices.py:946
|
#: netbox/dcim/models/devices.py:955
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid ""
|
msgid ""
|
||||||
"Device cannot be removed from virtual chassis {virtual_chassis} because it "
|
"Device cannot be removed from virtual chassis {virtual_chassis} because it "
|
||||||
"is currently designated as its master."
|
"is currently designated as its master."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/dcim/models/devices.py:1149
|
#: netbox/dcim/models/devices.py:1158
|
||||||
msgid "domain"
|
msgid "domain"
|
||||||
msgstr ""
|
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"
|
msgid "virtual chassis"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/dcim/models/devices.py:1175
|
#: netbox/dcim/models/devices.py:1184
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "The selected master ({master}) is not assigned to this virtual chassis."
|
msgid "The selected master ({master}) is not assigned to this virtual chassis."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/dcim/models/devices.py:1190
|
#: netbox/dcim/models/devices.py:1199
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid ""
|
msgid ""
|
||||||
"Unable to delete virtual chassis {self}. There are member interfaces which "
|
"Unable to delete virtual chassis {self}. There are member interfaces which "
|
||||||
"form a cross-chassis LAG interfaces."
|
"form a cross-chassis LAG interfaces."
|
||||||
msgstr ""
|
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"
|
msgid "identifier"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/dcim/models/devices.py:1224
|
#: netbox/dcim/models/devices.py:1233
|
||||||
msgid "Numeric identifier unique to the parent device"
|
msgid "Numeric identifier unique to the parent device"
|
||||||
msgstr ""
|
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/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:134 netbox/netbox/models/__init__.py:173
|
||||||
#: netbox/netbox/models/__init__.py:223
|
#: netbox/netbox/models/__init__.py:223
|
||||||
msgid "comments"
|
msgid "comments"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/dcim/models/devices.py:1268
|
#: netbox/dcim/models/devices.py:1277
|
||||||
msgid "virtual device context"
|
msgid "virtual device context"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/dcim/models/devices.py:1269
|
#: netbox/dcim/models/devices.py:1278
|
||||||
msgid "virtual device contexts"
|
msgid "virtual device contexts"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/dcim/models/devices.py:1297
|
#: netbox/dcim/models/devices.py:1306
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "{ip} is not an IPv{family} address."
|
msgid "{ip} is not an IPv{family} address."
|
||||||
msgstr ""
|
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."
|
msgid "Primary IP address must belong to an interface on the assigned device."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/dcim/models/devices.py:1337
|
#: netbox/dcim/models/devices.py:1346
|
||||||
msgid "MAC addresses"
|
msgid "MAC addresses"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/dcim/models/devices.py:1369
|
#: netbox/dcim/models/devices.py:1378
|
||||||
msgid ""
|
msgid ""
|
||||||
"Cannot unassign MAC Address while it is designated as the primary MAC for an "
|
"Cannot unassign MAC Address while it is designated as the primary MAC for an "
|
||||||
"object"
|
"object"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/dcim/models/devices.py:1373
|
#: netbox/dcim/models/devices.py:1382
|
||||||
msgid ""
|
msgid ""
|
||||||
"Cannot reassign MAC Address while it is designated as the primary MAC for an "
|
"Cannot reassign MAC Address while it is designated as the primary MAC for an "
|
||||||
"object"
|
"object"
|
||||||
@@ -6959,15 +6959,15 @@ msgstr ""
|
|||||||
msgid "Invalid schema: {error}"
|
msgid "Invalid schema: {error}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/dcim/models/modules.py:242
|
#: netbox/dcim/models/modules.py:250
|
||||||
msgid "module"
|
msgid "module"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/dcim/models/modules.py:243
|
#: netbox/dcim/models/modules.py:251
|
||||||
msgid "modules"
|
msgid "modules"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: netbox/dcim/models/modules.py:256
|
#: netbox/dcim/models/modules.py:264
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid ""
|
msgid ""
|
||||||
"Module must be installed within a module bay belonging to the assigned "
|
"Module must be installed within a module bay belonging to the assigned "
|
||||||
@@ -8005,7 +8005,12 @@ msgstr ""
|
|||||||
msgid "Removed {device} from virtual chassis {chassis}"
|
msgid "Removed {device} from virtual chassis {chassis}"
|
||||||
msgstr ""
|
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
|
#, python-brace-format
|
||||||
msgid "Unknown related object(s): {name}"
|
msgid "Unknown related object(s): {name}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|||||||
Reference in New Issue
Block a user