diff --git a/.github/ISSUE_TEMPLATE/01-feature_request.yaml b/.github/ISSUE_TEMPLATE/01-feature_request.yaml index 6212af3b8..48a3a859c 100644 --- a/.github/ISSUE_TEMPLATE/01-feature_request.yaml +++ b/.github/ISSUE_TEMPLATE/01-feature_request.yaml @@ -1,5 +1,6 @@ --- name: ✨ Feature Request +type: Feature description: Propose a new NetBox feature or enhancement labels: ["type: feature", "status: needs triage"] body: diff --git a/.github/ISSUE_TEMPLATE/02-bug_report.yaml b/.github/ISSUE_TEMPLATE/02-bug_report.yaml index 4382a9b76..83397944c 100644 --- a/.github/ISSUE_TEMPLATE/02-bug_report.yaml +++ b/.github/ISSUE_TEMPLATE/02-bug_report.yaml @@ -1,5 +1,6 @@ --- name: 🐛 Bug Report +type: Bug description: Report a reproducible bug in the current release of NetBox labels: ["type: bug", "status: needs triage"] body: diff --git a/.github/ISSUE_TEMPLATE/03-documentation_change.yaml b/.github/ISSUE_TEMPLATE/03-documentation_change.yaml index b5a970782..2dea61acc 100644 --- a/.github/ISSUE_TEMPLATE/03-documentation_change.yaml +++ b/.github/ISSUE_TEMPLATE/03-documentation_change.yaml @@ -1,5 +1,6 @@ --- name: 📖 Documentation Change +type: Documentation description: Suggest an addition or modification to the NetBox documentation labels: ["type: documentation", "status: needs triage"] body: diff --git a/.github/ISSUE_TEMPLATE/04-translation.yaml b/.github/ISSUE_TEMPLATE/04-translation.yaml index d07bc399d..72130ae47 100644 --- a/.github/ISSUE_TEMPLATE/04-translation.yaml +++ b/.github/ISSUE_TEMPLATE/04-translation.yaml @@ -1,5 +1,6 @@ --- name: 🌍 Translation +type: Translation description: Request support for a new language in the user interface labels: ["type: translation"] body: diff --git a/.github/ISSUE_TEMPLATE/05-housekeeping.yaml b/.github/ISSUE_TEMPLATE/05-housekeeping.yaml index 777871395..65b983e18 100644 --- a/.github/ISSUE_TEMPLATE/05-housekeeping.yaml +++ b/.github/ISSUE_TEMPLATE/05-housekeeping.yaml @@ -1,5 +1,6 @@ --- name: 🏡 Housekeeping +type: Housekeeping description: A change pertaining to the codebase itself (developers only) labels: ["type: housekeeping"] body: diff --git a/.github/ISSUE_TEMPLATE/06-deprecation.yaml b/.github/ISSUE_TEMPLATE/06-deprecation.yaml index 27e13e5c0..83905a39a 100644 --- a/.github/ISSUE_TEMPLATE/06-deprecation.yaml +++ b/.github/ISSUE_TEMPLATE/06-deprecation.yaml @@ -1,5 +1,6 @@ --- name: 🗑️ Deprecation +type: Deprecation description: The removal of an existing feature or resource labels: ["type: deprecation"] body: diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index aab8bc34f..85070d98e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -3,11 +3,15 @@ name: CI on: push: paths-ignore: + - '.github/ISSUE_TEMPLATE/**' + - '.github/PULL_REQUEST_TEMPLATE.md' - 'contrib/**' - 'docs/**' - 'netbox/translations/**' pull_request: paths-ignore: + - '.github/ISSUE_TEMPLATE/**' + - '.github/PULL_REQUEST_TEMPLATE.md' - 'contrib/**' - 'docs/**' - 'netbox/translations/**' diff --git a/docs/features/facilities.md b/docs/features/facilities.md index 4c8dfe265..1421281eb 100644 --- a/docs/features/facilities.md +++ b/docs/features/facilities.md @@ -46,7 +46,7 @@ Regions will always be listed alphabetically by name within each parent, and the Like regions, site groups can be arranged in a recursive hierarchy for grouping sites. However, whereas regions are intended for geographic organization, site groups may be used for functional grouping. For example, you might classify sites as corporate, branch, or customer sites in addition to where they are physically located. -The use of both regions and site groups affords to independent but complementary dimensions across which sites can be organized. +The use of both regions and site groups affords two independent but complementary dimensions across which sites can be organized. ## Sites diff --git a/docs/installation/1-postgresql.md b/docs/installation/1-postgresql.md index 3f826fa8a..8ba302909 100644 --- a/docs/installation/1-postgresql.md +++ b/docs/installation/1-postgresql.md @@ -62,6 +62,9 @@ GRANT CREATE ON SCHEMA public TO netbox; !!! danger "Use a strong password" **Do not use the password from the example.** Choose a strong, random password to ensure secure database authentication for your NetBox installation. +!!! danger "Use UTF8 encoding" + Make sure that your database uses `UTF8` encoding (the default for new installations). Especially do not use `SQL_ASCII` encoding, as it can lead to unpredictable and unrecoverable errors. Enter `\l` to check your encoding. + Once complete, enter `\q` to exit the PostgreSQL shell. ## Verify Service Status diff --git a/netbox/circuits/migrations/0048_circuitterminations_cached_relations.py b/netbox/circuits/migrations/0048_circuitterminations_cached_relations.py index fc1cef0e5..9be254d54 100644 --- a/netbox/circuits/migrations/0048_circuitterminations_cached_relations.py +++ b/netbox/circuits/migrations/0048_circuitterminations_cached_relations.py @@ -1,4 +1,3 @@ -# Generated by Django 5.0.9 on 2024-10-21 17:34 import django.db.models.deletion from django.db import migrations, models @@ -16,7 +15,7 @@ def populate_denormalized_fields(apps, schema_editor): termination._site_id = termination.site_id # Note: Location cannot be set prior to migration - CircuitTermination.objects.bulk_update(terminations, ['_region', '_site_group', '_site']) + CircuitTermination.objects.bulk_update(terminations, ['_region', '_site_group', '_site'], batch_size=100) class Migration(migrations.Migration): diff --git a/netbox/dcim/base_filtersets.py b/netbox/dcim/base_filtersets.py index c007c0120..df2a6b650 100644 --- a/netbox/dcim/base_filtersets.py +++ b/netbox/dcim/base_filtersets.py @@ -53,10 +53,10 @@ class ScopedFilterSet(BaseFilterSet): label=_('Site (slug)'), ) location_id = TreeNodeMultipleChoiceFilter( - queryset=Location.objects.all(), - field_name='_location', - lookup_expr='in', - label=_('Location (ID)'), + queryset=Location.objects.all(), + field_name='_location', + lookup_expr='in', + label=_('Location (ID)'), ) location = TreeNodeMultipleChoiceFilter( queryset=Location.objects.all(), diff --git a/netbox/dcim/filtersets.py b/netbox/dcim/filtersets.py index 90a9993c2..60c3c4d38 100644 --- a/netbox/dcim/filtersets.py +++ b/netbox/dcim/filtersets.py @@ -1652,8 +1652,8 @@ class MACAddressFilterSet(NetBoxModelFilterSet): if not value.strip(): return queryset qs_filter = ( - Q(mac_address__icontains=value) | - Q(description__icontains=value) + Q(mac_address__icontains=value) | + Q(description__icontains=value) ) return queryset.filter(qs_filter) diff --git a/netbox/dcim/forms/model_forms.py b/netbox/dcim/forms/model_forms.py index 9bc69e991..5a3a27d25 100644 --- a/netbox/dcim/forms/model_forms.py +++ b/netbox/dcim/forms/model_forms.py @@ -1810,6 +1810,11 @@ class MACAddressForm(NetBoxModelForm): super().__init__(*args, **kwargs) + if instance and instance.assigned_object and instance.assigned_object.primary_mac_address: + if instance.assigned_object.primary_mac_address.pk == instance.pk: + self.fields['interface'].disabled = True + self.fields['vminterface'].disabled = True + def clean(self): super().clean() diff --git a/netbox/dcim/models/devices.py b/netbox/dcim/models/devices.py index 87a87c90b..2acd98801 100644 --- a/netbox/dcim/models/devices.py +++ b/netbox/dcim/models/devices.py @@ -15,6 +15,7 @@ from django.urls import reverse from django.utils.safestring import mark_safe from django.utils.translation import gettext_lazy as _ +from core.models import ObjectType from dcim.choices import * from dcim.constants import * from dcim.fields import MACAddressField @@ -1523,9 +1524,33 @@ class MACAddress(PrimaryModel): def __str__(self): return str(self.mac_address) + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + # Denote the original assigned object (if any) for validation in clean() + self._original_assigned_object_id = self.__dict__.get('assigned_object_id') + self._original_assigned_object_type_id = self.__dict__.get('assigned_object_type_id') + @cached_property def is_primary(self): if self.assigned_object and hasattr(self.assigned_object, 'primary_mac_address'): if self.assigned_object.primary_mac_address and self.assigned_object.primary_mac_address.pk == self.pk: return True return False + + def clean(self, *args, **kwargs): + super().clean() + if self._original_assigned_object_id and self._original_assigned_object_type_id: + assigned_object = self.assigned_object + ct = ObjectType.objects.get_for_id(self._original_assigned_object_type_id) + original_assigned_object = ct.get_object_for_this_type(pk=self._original_assigned_object_id) + + if original_assigned_object.primary_mac_address: + if not assigned_object: + raise ValidationError( + _("Cannot unassign MAC Address while it is designated as the primary MAC for an object") + ) + elif original_assigned_object != assigned_object: + raise ValidationError( + _("Cannot reassign MAC Address while it is designated as the primary MAC for an object") + ) diff --git a/netbox/dcim/tables/devices.py b/netbox/dcim/tables/devices.py index 087132331..d4f2f74b3 100644 --- a/netbox/dcim/tables/devices.py +++ b/netbox/dcim/tables/devices.py @@ -705,7 +705,7 @@ class DeviceInterfaceTable(InterfaceTable): model = models.Interface fields = ( 'pk', 'id', 'name', 'module_bay', 'module', 'label', 'enabled', 'type', 'parent', 'bridge', 'lag', - 'mgmt_only', 'mtu', 'mode', 'mac_address', 'wwn', 'rf_role', 'rf_channel', 'rf_channel_frequency', + 'mgmt_only', 'mtu', 'mode', 'primary_mac_address', 'wwn', 'rf_role', 'rf_channel', 'rf_channel_frequency', 'rf_channel_width', 'tx_power', 'description', 'mark_connected', 'cable', 'cable_color', 'wireless_link', 'wireless_lans', 'link_peer', 'connection', 'tags', 'vdcs', 'vrf', 'l2vpn', 'tunnel', 'ip_addresses', 'fhrp_groups', 'untagged_vlan', 'tagged_vlans', 'qinq_svlan', 'actions', diff --git a/netbox/extras/tests/test_customfields.py b/netbox/extras/tests/test_customfields.py index d36477da8..8bae8dfc9 100644 --- a/netbox/extras/tests/test_customfields.py +++ b/netbox/extras/tests/test_customfields.py @@ -660,8 +660,8 @@ class CustomFieldAPITest(APITestCase): CustomField( type=CustomFieldTypeChoices.TYPE_BOOLEAN, name='boolean_field', - default=False) - , + default=False + ), CustomField( type=CustomFieldTypeChoices.TYPE_DATE, name='date_field', diff --git a/netbox/ipam/migrations/0072_prefix_cached_relations.py b/netbox/ipam/migrations/0072_prefix_cached_relations.py index e4a789704..58cefb12d 100644 --- a/netbox/ipam/migrations/0072_prefix_cached_relations.py +++ b/netbox/ipam/migrations/0072_prefix_cached_relations.py @@ -15,7 +15,7 @@ def populate_denormalized_fields(apps, schema_editor): prefix._site_id = prefix.site_id # Note: Location cannot be set prior to migration - Prefix.objects.bulk_update(prefixes, ['_region', '_site_group', '_site']) + Prefix.objects.bulk_update(prefixes, ['_region', '_site_group', '_site'], batch_size=100) class Migration(migrations.Migration): diff --git a/netbox/ipam/models/vlans.py b/netbox/ipam/models/vlans.py index 4c7f191c9..91e39c6d3 100644 --- a/netbox/ipam/models/vlans.py +++ b/netbox/ipam/models/vlans.py @@ -361,7 +361,7 @@ class VLANTranslationRule(NetBoxModel): ) local_vid = models.PositiveSmallIntegerField( verbose_name=_('Local VLAN ID'), - validators=( + validators=( MinValueValidator(VLAN_VID_MIN), MaxValueValidator(VLAN_VID_MAX) ), @@ -369,7 +369,7 @@ class VLANTranslationRule(NetBoxModel): ) remote_vid = models.PositiveSmallIntegerField( verbose_name=_('Remote VLAN ID'), - validators=( + validators=( MinValueValidator(VLAN_VID_MIN), MaxValueValidator(VLAN_VID_MAX) ), diff --git a/netbox/translations/en/LC_MESSAGES/django.po b/netbox/translations/en/LC_MESSAGES/django.po index 6b0b5e1d0..d63e1dd34 100644 --- a/netbox/translations/en/LC_MESSAGES/django.po +++ b/netbox/translations/en/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2025-01-18 05:01+0000\n" +"POT-Creation-Date: 2025-01-24 05:02+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -1402,7 +1402,7 @@ msgstr "" #: netbox/dcim/models/device_components.py:1024 #: netbox/dcim/models/device_components.py:1095 #: netbox/dcim/models/device_components.py:1241 -#: netbox/dcim/models/devices.py:477 netbox/dcim/models/racks.py:221 +#: netbox/dcim/models/devices.py:478 netbox/dcim/models/racks.py:221 #: netbox/extras/models/tags.py:28 msgid "color" msgstr "" @@ -1429,8 +1429,8 @@ msgstr "" #: netbox/circuits/models/virtual_circuits.py:59 netbox/core/models/data.py:52 #: netbox/core/models/jobs.py:85 netbox/dcim/models/cables.py:49 #: netbox/dcim/models/device_components.py:1281 -#: netbox/dcim/models/devices.py:644 netbox/dcim/models/devices.py:1176 -#: netbox/dcim/models/devices.py:1403 netbox/dcim/models/power.py:94 +#: netbox/dcim/models/devices.py:645 netbox/dcim/models/devices.py:1177 +#: netbox/dcim/models/devices.py:1404 netbox/dcim/models/power.py:94 #: netbox/dcim/models/racks.py:288 netbox/dcim/models/sites.py:154 #: netbox/dcim/models/sites.py:270 netbox/ipam/models/ip.py:237 #: netbox/ipam/models/ip.py:508 netbox/ipam/models/ip.py:729 @@ -1560,8 +1560,8 @@ msgstr "" #: netbox/circuits/models/providers.py:98 netbox/core/models/data.py:39 #: netbox/core/models/jobs.py:46 #: netbox/dcim/models/device_component_templates.py:43 -#: netbox/dcim/models/device_components.py:52 netbox/dcim/models/devices.py:588 -#: netbox/dcim/models/devices.py:1335 netbox/dcim/models/devices.py:1398 +#: netbox/dcim/models/device_components.py:52 netbox/dcim/models/devices.py:589 +#: netbox/dcim/models/devices.py:1336 netbox/dcim/models/devices.py:1399 #: netbox/dcim/models/power.py:38 netbox/dcim/models/power.py:89 #: netbox/dcim/models/racks.py:257 netbox/dcim/models/sites.py:142 #: netbox/extras/models/configs.py:36 netbox/extras/models/configs.py:215 @@ -1593,7 +1593,7 @@ msgstr "" msgid "Full name of the provider" msgstr "" -#: netbox/circuits/models/providers.py:28 netbox/dcim/models/devices.py:87 +#: netbox/circuits/models/providers.py:28 netbox/dcim/models/devices.py:88 #: netbox/dcim/models/racks.py:137 netbox/dcim/models/sites.py:149 #: netbox/extras/models/models.py:506 netbox/ipam/models/asns.py:23 #: netbox/ipam/models/vlans.py:42 netbox/netbox/models/__init__.py:145 @@ -3532,7 +3532,7 @@ msgstr "" #: netbox/dcim/filtersets.py:1104 netbox/dcim/forms/filtersets.py:819 #: netbox/dcim/forms/filtersets.py:1390 netbox/dcim/forms/filtersets.py:1586 #: netbox/dcim/forms/filtersets.py:1591 netbox/dcim/forms/model_forms.py:1762 -#: netbox/dcim/models/devices.py:1499 netbox/dcim/models/devices.py:1520 +#: netbox/dcim/models/devices.py:1500 netbox/dcim/models/devices.py:1521 #: netbox/virtualization/filtersets.py:196 #: netbox/virtualization/filtersets.py:268 #: netbox/virtualization/forms/filtersets.py:177 @@ -4125,7 +4125,7 @@ msgstr "" msgid "Chassis" msgstr "" -#: netbox/dcim/forms/bulk_edit.py:619 netbox/dcim/models/devices.py:482 +#: netbox/dcim/forms/bulk_edit.py:619 netbox/dcim/models/devices.py:483 #: netbox/dcim/tables/devices.py:78 msgid "VM role" msgstr "" @@ -5234,7 +5234,7 @@ msgstr "" msgid "Device Role" msgstr "" -#: netbox/dcim/forms/model_forms.py:500 netbox/dcim/models/devices.py:634 +#: netbox/dcim/forms/model_forms.py:500 netbox/dcim/models/devices.py:635 msgid "The lowest-numbered unit occupied by the device" msgstr "" @@ -5417,7 +5417,7 @@ msgstr "" msgid "Virtual Machine" msgstr "" -#: netbox/dcim/forms/model_forms.py:1822 +#: netbox/dcim/forms/model_forms.py:1827 msgid "A MAC address can only be assigned to a single object." msgstr "" @@ -6139,7 +6139,7 @@ msgid "module bays" msgstr "" #: netbox/dcim/models/device_components.py:1178 -#: netbox/dcim/models/devices.py:1224 +#: netbox/dcim/models/devices.py:1225 msgid "A module bay cannot belong to a module installed within it." msgstr "" @@ -6175,14 +6175,14 @@ msgid "inventory item roles" msgstr "" #: netbox/dcim/models/device_components.py:1308 -#: netbox/dcim/models/devices.py:597 netbox/dcim/models/devices.py:1184 +#: netbox/dcim/models/devices.py:598 netbox/dcim/models/devices.py:1185 #: netbox/dcim/models/racks.py:304 #: netbox/virtualization/models/virtualmachines.py:126 msgid "serial number" msgstr "" #: netbox/dcim/models/device_components.py:1316 -#: netbox/dcim/models/devices.py:605 netbox/dcim/models/devices.py:1191 +#: netbox/dcim/models/devices.py:606 netbox/dcim/models/devices.py:1192 #: netbox/dcim/models/racks.py:311 msgid "asset tag" msgstr "" @@ -6223,377 +6223,389 @@ msgstr "" msgid "Cannot assign inventory item to component on another device" msgstr "" -#: netbox/dcim/models/devices.py:58 +#: netbox/dcim/models/devices.py:59 msgid "manufacturer" msgstr "" -#: netbox/dcim/models/devices.py:59 +#: netbox/dcim/models/devices.py:60 msgid "manufacturers" msgstr "" -#: netbox/dcim/models/devices.py:83 netbox/dcim/models/devices.py:382 +#: netbox/dcim/models/devices.py:84 netbox/dcim/models/devices.py:383 #: netbox/dcim/models/racks.py:133 msgid "model" msgstr "" -#: netbox/dcim/models/devices.py:96 +#: netbox/dcim/models/devices.py:97 msgid "default platform" msgstr "" -#: netbox/dcim/models/devices.py:99 netbox/dcim/models/devices.py:386 +#: netbox/dcim/models/devices.py:100 netbox/dcim/models/devices.py:387 msgid "part number" msgstr "" -#: netbox/dcim/models/devices.py:102 netbox/dcim/models/devices.py:389 +#: netbox/dcim/models/devices.py:103 netbox/dcim/models/devices.py:390 msgid "Discrete part number (optional)" msgstr "" -#: netbox/dcim/models/devices.py:108 netbox/dcim/models/racks.py:53 +#: netbox/dcim/models/devices.py:109 netbox/dcim/models/racks.py:53 msgid "height (U)" msgstr "" -#: netbox/dcim/models/devices.py:112 +#: netbox/dcim/models/devices.py:113 msgid "exclude from utilization" msgstr "" -#: netbox/dcim/models/devices.py:113 +#: netbox/dcim/models/devices.py:114 msgid "Devices of this type are excluded when calculating rack utilization." msgstr "" -#: netbox/dcim/models/devices.py:117 +#: netbox/dcim/models/devices.py:118 msgid "is full depth" msgstr "" -#: netbox/dcim/models/devices.py:118 +#: netbox/dcim/models/devices.py:119 msgid "Device consumes both front and rear rack faces." msgstr "" -#: netbox/dcim/models/devices.py:125 +#: netbox/dcim/models/devices.py:126 msgid "parent/child status" msgstr "" -#: netbox/dcim/models/devices.py:126 +#: netbox/dcim/models/devices.py:127 msgid "" "Parent devices house child devices in device bays. Leave blank if this " "device type is neither a parent nor a child." msgstr "" -#: netbox/dcim/models/devices.py:130 netbox/dcim/models/devices.py:392 -#: netbox/dcim/models/devices.py:650 netbox/dcim/models/racks.py:315 +#: netbox/dcim/models/devices.py:131 netbox/dcim/models/devices.py:393 +#: netbox/dcim/models/devices.py:651 netbox/dcim/models/racks.py:315 msgid "airflow" msgstr "" -#: netbox/dcim/models/devices.py:207 +#: netbox/dcim/models/devices.py:208 msgid "device type" msgstr "" -#: netbox/dcim/models/devices.py:208 +#: netbox/dcim/models/devices.py:209 msgid "device types" msgstr "" -#: netbox/dcim/models/devices.py:290 +#: netbox/dcim/models/devices.py:291 msgid "U height must be in increments of 0.5 rack units." msgstr "" -#: netbox/dcim/models/devices.py:307 +#: netbox/dcim/models/devices.py:308 #, 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:322 +#: netbox/dcim/models/devices.py:323 #, python-brace-format msgid "" "Unable to set 0U height: Found {racked_instance_count} " "instances already mounted within racks." msgstr "" -#: netbox/dcim/models/devices.py:331 +#: netbox/dcim/models/devices.py:332 msgid "" "Must delete all device bay templates associated with this device before " "declassifying it as a parent device." msgstr "" -#: netbox/dcim/models/devices.py:337 +#: netbox/dcim/models/devices.py:338 msgid "Child device types must be 0U." msgstr "" -#: netbox/dcim/models/devices.py:412 +#: netbox/dcim/models/devices.py:413 msgid "module type" msgstr "" -#: netbox/dcim/models/devices.py:413 +#: netbox/dcim/models/devices.py:414 msgid "module types" msgstr "" -#: netbox/dcim/models/devices.py:483 +#: netbox/dcim/models/devices.py:484 msgid "Virtual machines may be assigned to this role" msgstr "" -#: netbox/dcim/models/devices.py:495 +#: netbox/dcim/models/devices.py:496 msgid "device role" msgstr "" -#: netbox/dcim/models/devices.py:496 +#: netbox/dcim/models/devices.py:497 msgid "device roles" msgstr "" -#: netbox/dcim/models/devices.py:510 +#: netbox/dcim/models/devices.py:511 msgid "Optionally limit this platform to devices of a certain manufacturer" msgstr "" -#: netbox/dcim/models/devices.py:522 +#: netbox/dcim/models/devices.py:523 msgid "platform" msgstr "" -#: netbox/dcim/models/devices.py:523 +#: netbox/dcim/models/devices.py:524 msgid "platforms" msgstr "" -#: netbox/dcim/models/devices.py:571 +#: netbox/dcim/models/devices.py:572 msgid "The function this device serves" msgstr "" -#: netbox/dcim/models/devices.py:598 +#: netbox/dcim/models/devices.py:599 msgid "Chassis serial number, assigned by the manufacturer" msgstr "" -#: netbox/dcim/models/devices.py:606 netbox/dcim/models/devices.py:1192 +#: netbox/dcim/models/devices.py:607 netbox/dcim/models/devices.py:1193 msgid "A unique tag used to identify this device" msgstr "" -#: netbox/dcim/models/devices.py:633 +#: netbox/dcim/models/devices.py:634 msgid "position (U)" msgstr "" -#: netbox/dcim/models/devices.py:641 +#: netbox/dcim/models/devices.py:642 msgid "rack face" msgstr "" -#: netbox/dcim/models/devices.py:662 netbox/dcim/models/devices.py:1419 +#: netbox/dcim/models/devices.py:663 netbox/dcim/models/devices.py:1420 #: netbox/virtualization/models/virtualmachines.py:95 msgid "primary IPv4" msgstr "" -#: netbox/dcim/models/devices.py:670 netbox/dcim/models/devices.py:1427 +#: netbox/dcim/models/devices.py:671 netbox/dcim/models/devices.py:1428 #: netbox/virtualization/models/virtualmachines.py:103 msgid "primary IPv6" msgstr "" -#: netbox/dcim/models/devices.py:678 +#: netbox/dcim/models/devices.py:679 msgid "out-of-band IP" msgstr "" -#: netbox/dcim/models/devices.py:695 +#: netbox/dcim/models/devices.py:696 msgid "VC position" msgstr "" -#: netbox/dcim/models/devices.py:698 +#: netbox/dcim/models/devices.py:699 msgid "Virtual chassis position" msgstr "" -#: netbox/dcim/models/devices.py:701 +#: netbox/dcim/models/devices.py:702 msgid "VC priority" msgstr "" -#: netbox/dcim/models/devices.py:705 +#: netbox/dcim/models/devices.py:706 msgid "Virtual chassis master election priority" msgstr "" -#: netbox/dcim/models/devices.py:708 netbox/dcim/models/sites.py:208 +#: netbox/dcim/models/devices.py:709 netbox/dcim/models/sites.py:208 msgid "latitude" msgstr "" -#: netbox/dcim/models/devices.py:713 netbox/dcim/models/devices.py:721 +#: netbox/dcim/models/devices.py:714 netbox/dcim/models/devices.py:722 #: netbox/dcim/models/sites.py:213 netbox/dcim/models/sites.py:221 msgid "GPS coordinate in decimal format (xx.yyyyyy)" msgstr "" -#: netbox/dcim/models/devices.py:716 netbox/dcim/models/sites.py:216 +#: netbox/dcim/models/devices.py:717 netbox/dcim/models/sites.py:216 msgid "longitude" msgstr "" -#: netbox/dcim/models/devices.py:789 +#: netbox/dcim/models/devices.py:790 msgid "Device name must be unique per site." msgstr "" -#: netbox/dcim/models/devices.py:800 netbox/ipam/models/services.py:71 +#: netbox/dcim/models/devices.py:801 netbox/ipam/models/services.py:71 msgid "device" msgstr "" -#: netbox/dcim/models/devices.py:801 +#: netbox/dcim/models/devices.py:802 msgid "devices" msgstr "" -#: netbox/dcim/models/devices.py:824 +#: netbox/dcim/models/devices.py:825 #, python-brace-format msgid "Rack {rack} does not belong to site {site}." msgstr "" -#: netbox/dcim/models/devices.py:829 +#: netbox/dcim/models/devices.py:830 #, python-brace-format msgid "Location {location} does not belong to site {site}." msgstr "" -#: netbox/dcim/models/devices.py:835 +#: netbox/dcim/models/devices.py:836 #, python-brace-format msgid "Rack {rack} does not belong to location {location}." msgstr "" -#: netbox/dcim/models/devices.py:842 +#: netbox/dcim/models/devices.py:843 msgid "Cannot select a rack face without assigning a rack." msgstr "" -#: netbox/dcim/models/devices.py:846 +#: netbox/dcim/models/devices.py:847 msgid "Cannot select a rack position without assigning a rack." msgstr "" -#: netbox/dcim/models/devices.py:852 +#: netbox/dcim/models/devices.py:853 msgid "Position must be in increments of 0.5 rack units." msgstr "" -#: netbox/dcim/models/devices.py:856 +#: netbox/dcim/models/devices.py:857 msgid "Must specify rack face when defining rack position." msgstr "" -#: netbox/dcim/models/devices.py:864 +#: netbox/dcim/models/devices.py:865 #, python-brace-format msgid "A 0U device type ({device_type}) cannot be assigned to a rack position." msgstr "" -#: netbox/dcim/models/devices.py:875 +#: netbox/dcim/models/devices.py:876 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:882 +#: netbox/dcim/models/devices.py:883 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:896 +#: netbox/dcim/models/devices.py:897 #, 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:911 +#: netbox/dcim/models/devices.py:912 #, python-brace-format msgid "{ip} is not an IPv4 address." msgstr "" -#: netbox/dcim/models/devices.py:923 netbox/dcim/models/devices.py:941 +#: netbox/dcim/models/devices.py:924 netbox/dcim/models/devices.py:942 #, python-brace-format msgid "The specified IP address ({ip}) is not assigned to this device." msgstr "" -#: netbox/dcim/models/devices.py:929 +#: netbox/dcim/models/devices.py:930 #, python-brace-format msgid "{ip} is not an IPv6 address." msgstr "" -#: netbox/dcim/models/devices.py:959 +#: netbox/dcim/models/devices.py:960 #, 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:970 +#: netbox/dcim/models/devices.py:971 #, python-brace-format msgid "The assigned cluster belongs to a different site ({site})" msgstr "" -#: netbox/dcim/models/devices.py:977 +#: netbox/dcim/models/devices.py:978 #, python-brace-format msgid "The assigned cluster belongs to a different location ({location})" msgstr "" -#: netbox/dcim/models/devices.py:985 +#: netbox/dcim/models/devices.py:986 msgid "A device assigned to a virtual chassis must have its position defined." msgstr "" -#: netbox/dcim/models/devices.py:991 +#: netbox/dcim/models/devices.py:992 #, 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:1199 +#: netbox/dcim/models/devices.py:1200 msgid "module" msgstr "" -#: netbox/dcim/models/devices.py:1200 +#: netbox/dcim/models/devices.py:1201 msgid "modules" msgstr "" -#: netbox/dcim/models/devices.py:1213 +#: netbox/dcim/models/devices.py:1214 #, python-brace-format msgid "" "Module must be installed within a module bay belonging to the assigned " "device ({device})." msgstr "" -#: netbox/dcim/models/devices.py:1340 +#: netbox/dcim/models/devices.py:1341 msgid "domain" msgstr "" -#: netbox/dcim/models/devices.py:1353 netbox/dcim/models/devices.py:1354 +#: netbox/dcim/models/devices.py:1354 netbox/dcim/models/devices.py:1355 msgid "virtual chassis" msgstr "" -#: netbox/dcim/models/devices.py:1366 +#: netbox/dcim/models/devices.py:1367 #, python-brace-format msgid "The selected master ({master}) is not assigned to this virtual chassis." msgstr "" -#: netbox/dcim/models/devices.py:1382 +#: netbox/dcim/models/devices.py:1383 #, 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:1408 netbox/vpn/models/l2vpn.py:37 +#: netbox/dcim/models/devices.py:1409 netbox/vpn/models/l2vpn.py:37 msgid "identifier" msgstr "" -#: netbox/dcim/models/devices.py:1409 +#: netbox/dcim/models/devices.py:1410 msgid "Numeric identifier unique to the parent device" msgstr "" -#: netbox/dcim/models/devices.py:1437 netbox/extras/models/customfields.py:225 +#: netbox/dcim/models/devices.py:1438 netbox/extras/models/customfields.py:225 #: netbox/extras/models/models.py:107 netbox/extras/models/models.py:694 #: netbox/netbox/models/__init__.py:119 msgid "comments" msgstr "" -#: netbox/dcim/models/devices.py:1453 +#: netbox/dcim/models/devices.py:1454 msgid "virtual device context" msgstr "" -#: netbox/dcim/models/devices.py:1454 +#: netbox/dcim/models/devices.py:1455 msgid "virtual device contexts" msgstr "" -#: netbox/dcim/models/devices.py:1483 +#: netbox/dcim/models/devices.py:1484 #, python-brace-format msgid "{ip} is not an IPv{family} address." msgstr "" -#: netbox/dcim/models/devices.py:1489 +#: netbox/dcim/models/devices.py:1490 msgid "Primary IP address must belong to an interface on the assigned device." msgstr "" -#: netbox/dcim/models/devices.py:1521 +#: netbox/dcim/models/devices.py:1522 msgid "MAC addresses" msgstr "" +#: netbox/dcim/models/devices.py:1544 +msgid "" +"Cannot unassign MAC Address while it is designated as the primary MAC for an " +"object" +msgstr "" + +#: netbox/dcim/models/devices.py:1548 +msgid "" +"Cannot reassign MAC Address while it is designated as the primary MAC for an " +"object" +msgstr "" + #: netbox/dcim/models/mixins.py:94 #, python-brace-format msgid "Please select a {scope_type}." diff --git a/netbox/virtualization/forms/model_forms.py b/netbox/virtualization/forms/model_forms.py index 9d53c9382..291d6dbe8 100644 --- a/netbox/virtualization/forms/model_forms.py +++ b/netbox/virtualization/forms/model_forms.py @@ -150,8 +150,8 @@ class ClusterAddDevicesForm(forms.Form): for scope_field in ['site', 'location']: device_scope = getattr(device, scope_field) if ( - self.cluster.scope_type.model_class() == apps.get_model('dcim', scope_field) - and device_scope != self.cluster.scope + self.cluster.scope_type.model_class() == apps.get_model('dcim', scope_field) and + device_scope != self.cluster.scope ): raise ValidationError({ 'devices': _( diff --git a/netbox/virtualization/migrations/0045_clusters_cached_relations.py b/netbox/virtualization/migrations/0045_clusters_cached_relations.py index 6d0c8ff33..9918bf594 100644 --- a/netbox/virtualization/migrations/0045_clusters_cached_relations.py +++ b/netbox/virtualization/migrations/0045_clusters_cached_relations.py @@ -15,7 +15,7 @@ def populate_denormalized_fields(apps, schema_editor): cluster._site_id = cluster.site_id # Note: Location cannot be set prior to migration - Cluster.objects.bulk_update(clusters, ['_region', '_site_group', '_site']) + Cluster.objects.bulk_update(clusters, ['_region', '_site_group', '_site'], batch_size=100) class Migration(migrations.Migration): diff --git a/netbox/virtualization/tables/virtualmachines.py b/netbox/virtualization/tables/virtualmachines.py index 116051037..335d1de7d 100644 --- a/netbox/virtualization/tables/virtualmachines.py +++ b/netbox/virtualization/tables/virtualmachines.py @@ -120,9 +120,9 @@ class VMInterfaceTable(BaseInterfaceTable): class Meta(NetBoxTable.Meta): model = VMInterface fields = ( - 'pk', 'id', 'name', 'virtual_machine', 'enabled', 'mac_address', 'mtu', 'mode', 'description', 'tags', - 'vrf', 'primary_mac_address', 'l2vpn', 'tunnel', 'ip_addresses', 'fhrp_groups', 'untagged_vlan', - 'tagged_vlans', 'qinq_svlan', 'created', 'last_updated', + 'pk', 'id', 'name', 'virtual_machine', 'enabled', 'mtu', 'mode', 'description', 'tags', 'vrf', + 'primary_mac_address', 'l2vpn', 'tunnel', 'ip_addresses', 'fhrp_groups', 'untagged_vlan', 'tagged_vlans', + 'qinq_svlan', 'created', 'last_updated', ) default_columns = ('pk', 'name', 'virtual_machine', 'enabled', 'description') @@ -144,11 +144,11 @@ class VirtualMachineVMInterfaceTable(VMInterfaceTable): class Meta(NetBoxTable.Meta): model = VMInterface fields = ( - 'pk', 'id', 'name', 'enabled', 'parent', 'bridge', 'mac_address', 'mtu', 'mode', 'description', 'tags', - 'vrf', 'l2vpn', 'tunnel', 'ip_addresses', 'fhrp_groups', 'untagged_vlan', 'tagged_vlans', 'qinq_svlan', - 'actions', + 'pk', 'id', 'name', 'enabled', 'parent', 'bridge', 'primary_mac_address', 'mtu', 'mode', 'description', + 'tags', 'vrf', 'l2vpn', 'tunnel', 'ip_addresses', 'fhrp_groups', 'untagged_vlan', 'tagged_vlans', + 'qinq_svlan', 'actions', ) - default_columns = ('pk', 'name', 'enabled', 'mac_address', 'mtu', 'mode', 'description', 'ip_addresses') + default_columns = ('pk', 'name', 'enabled', 'primary_mac_address', 'mtu', 'mode', 'description', 'ip_addresses') row_attrs = { 'data-name': lambda record: record.name, 'data-virtual': lambda record: "true",