diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml index b0b8c02ad..612d01d89 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yaml +++ b/.github/ISSUE_TEMPLATE/bug_report.yaml @@ -17,15 +17,16 @@ body: How are you running NetBox? (For issues with the Docker image, please go to the [netbox-docker](https://github.com/netbox-community/netbox-docker) repo.) options: - - Self-hosted - NetBox Cloud + - NetBox Enterprise + - Self-hosted validations: required: true - type: input attributes: label: NetBox Version description: What version of NetBox are you currently running? - placeholder: v3.7.3 + placeholder: v3.7.4 validations: required: true - type: dropdown diff --git a/.github/ISSUE_TEMPLATE/feature_request.yaml b/.github/ISSUE_TEMPLATE/feature_request.yaml index bc99999c0..8eb47180d 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yaml +++ b/.github/ISSUE_TEMPLATE/feature_request.yaml @@ -14,7 +14,7 @@ body: attributes: label: NetBox version description: What version of NetBox are you currently running? - placeholder: v3.7.3 + placeholder: v3.7.4 validations: required: true - type: dropdown diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index af3d303b2..d794786f1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -84,4 +84,4 @@ jobs: run: coverage run --source="netbox/" netbox/manage.py test netbox/ --parallel - name: Show coverage report - run: coverage report --skip-covered --omit *migrations* + run: coverage report --skip-covered --omit '*/migrations/*,*/tests/*' diff --git a/base_requirements.txt b/base_requirements.txt index 336607e5c..1d6a2e97d 100644 --- a/base_requirements.txt +++ b/base_requirements.txt @@ -96,7 +96,7 @@ markdown-include mkdocs-material # Introspection for embedded code -# https://github.com/mkdocstrings/mkdocstrings/blob/master/CHANGELOG.md +# https://github.com/mkdocstrings/mkdocstrings/blob/main/CHANGELOG.md mkdocstrings[python-legacy] # Library for manipulating IP prefixes and addresses diff --git a/contrib/generated_schema.json b/contrib/generated_schema.json index 5e8507798..1164f2e48 100644 --- a/contrib/generated_schema.json +++ b/contrib/generated_schema.json @@ -384,7 +384,10 @@ "8gfc-sfpp", "16gfc-sfpp", "32gfc-sfp28", + "32gfc-sfpp", "64gfc-qsfpp", + "64gfc-sfpdd", + "64gfc-sfpp", "128gfc-qsfp28", "infiniband-sdr", "infiniband-ddr", diff --git a/docs/installation/1-postgresql.md b/docs/installation/1-postgresql.md index 184fc26d2..9d30f4514 100644 --- a/docs/installation/1-postgresql.md +++ b/docs/installation/1-postgresql.md @@ -31,8 +31,7 @@ This section entails the installation and configuration of a local PostgreSQL da Once PostgreSQL has been installed, start the service and enable it to run at boot: ```no-highlight - sudo systemctl start postgresql - sudo systemctl enable postgresql + sudo systemctl enable --now postgresql ``` Before continuing, verify that you have installed PostgreSQL 12 or later: diff --git a/docs/installation/2-redis.md b/docs/installation/2-redis.md index 7c364947e..2756a1ab0 100644 --- a/docs/installation/2-redis.md +++ b/docs/installation/2-redis.md @@ -14,8 +14,7 @@ ```no-highlight sudo yum install -y redis - sudo systemctl start redis - sudo systemctl enable redis + sudo systemctl enable --now redis ``` Before continuing, verify that your installed version of Redis is at least v4.0: diff --git a/docs/installation/4-gunicorn.md b/docs/installation/4-gunicorn.md index e31c48466..1e8d49453 100644 --- a/docs/installation/4-gunicorn.md +++ b/docs/installation/4-gunicorn.md @@ -27,8 +27,7 @@ sudo systemctl daemon-reload Then, start the `netbox` and `netbox-rq` services and enable them to initiate at boot time: ```no-highlight -sudo systemctl start netbox netbox-rq -sudo systemctl enable netbox netbox-rq +sudo systemctl enable --now netbox netbox-rq ``` You can use the command `systemctl status netbox` to verify that the WSGI service is running: diff --git a/docs/release-notes/version-3.7.md b/docs/release-notes/version-3.7.md index 21e7489c3..9724c4488 100644 --- a/docs/release-notes/version-3.7.md +++ b/docs/release-notes/version-3.7.md @@ -1,6 +1,31 @@ # NetBox v3.7 -## v3.7.4 (FUTURE) +## v3.7.5 (FUTURE) + +--- + +## v3.7.4 (2024-03-13) + +### Enhancements + +* [#14206](https://github.com/netbox-community/netbox/issues/14206) - Add additional FibreChannel SFP+ interface types +* [#14366](https://github.com/netbox-community/netbox/issues/14366) - Enable custom links for config contexts & templates +* [#15291](https://github.com/netbox-community/netbox/issues/15291) - Add tunnel termination buttons to VM interfaces table +* [#15297](https://github.com/netbox-community/netbox/issues/15297) - Linkify platform column in device & virtual machine tables + +### Bug Fixes + +* [#13722](https://github.com/netbox-community/netbox/issues/13722) - Fix range expansion for comma-separated numerical values +* [#14832](https://github.com/netbox-community/netbox/issues/14832) - Enable querying IP addresses for an FHRP group via GraphQL +* [#15220](https://github.com/netbox-community/netbox/issues/15220) - Fix validation check when bulk editing the mask length of IP addresses +* [#15232](https://github.com/netbox-community/netbox/issues/15232) - Permit user with sufficient permissions to assign an inventory item to a device type +* [#15241](https://github.com/netbox-community/netbox/issues/15241) - Restore missing `display` field on VirtualDisk serialization in REST API +* [#15243](https://github.com/netbox-community/netbox/issues/15243) - Correct representation of installed module when listing module bays using REST API brief mode +* [#15316](https://github.com/netbox-community/netbox/issues/15316) - Fix selection of 3DES encryption for IKE & IPSec proposals +* [#15322](https://github.com/netbox-community/netbox/issues/15322) - Add description field to YAML export for device & module types +* [#15336](https://github.com/netbox-community/netbox/issues/15336) - Correct label for recurring scheduled jobs +* [#15347](https://github.com/netbox-community/netbox/issues/15347) - Fix querying virtual machine contacts via GraphQL +* [#15356](https://github.com/netbox-community/netbox/issues/15356) - Fix assignment of front & rear images to device types via REST API --- diff --git a/netbox/circuits/graphql/schema.py b/netbox/circuits/graphql/schema.py index 48eff5413..9aa7a0228 100644 --- a/netbox/circuits/graphql/schema.py +++ b/netbox/circuits/graphql/schema.py @@ -1,4 +1,5 @@ from typing import List + import strawberry import strawberry_django diff --git a/netbox/circuits/graphql/types.py b/netbox/circuits/graphql/types.py index 6a96a54b9..21422cf41 100644 --- a/netbox/circuits/graphql/types.py +++ b/netbox/circuits/graphql/types.py @@ -2,13 +2,12 @@ from typing import Annotated, List import strawberry import strawberry_django + from circuits import models from dcim.graphql.mixins import CabledObjectMixin from extras.graphql.mixins import ContactsMixin, CustomFieldsMixin, TagsMixin -from tenancy.graphql.types import TenantType - from netbox.graphql.types import NetBoxObjectType, ObjectType, OrganizationalObjectType - +from tenancy.graphql.types import TenantType from .filters import * __all__ = ( diff --git a/netbox/core/graphql/filters.py b/netbox/core/graphql/filters.py index fd3f9e459..64b4d0de2 100644 --- a/netbox/core/graphql/filters.py +++ b/netbox/core/graphql/filters.py @@ -1,7 +1,6 @@ -import strawberry import strawberry_django -from core import filtersets, models +from core import filtersets, models from netbox.graphql.filter_mixins import autotype_decorator, BaseFilterMixin __all__ = ( diff --git a/netbox/core/graphql/schema.py b/netbox/core/graphql/schema.py index 3dc4c7806..64ed87985 100644 --- a/netbox/core/graphql/schema.py +++ b/netbox/core/graphql/schema.py @@ -1,4 +1,5 @@ from typing import List + import strawberry import strawberry_django diff --git a/netbox/dcim/api/nested_serializers.py b/netbox/dcim/api/nested_serializers.py index 1d9828ee3..bcb141a71 100644 --- a/netbox/dcim/api/nested_serializers.py +++ b/netbox/dcim/api/nested_serializers.py @@ -309,6 +309,14 @@ class ModuleNestedModuleBaySerializer(WritableNestedSerializer): fields = ['id', 'url', 'display', 'name'] +class ModuleBayNestedModuleSerializer(WritableNestedSerializer): + url = serializers.HyperlinkedIdentityField(view_name='dcim-api:module-detail') + + class Meta: + model = models.Module + fields = ['id', 'url', 'display', 'serial'] + + class NestedModuleSerializer(WritableNestedSerializer): url = serializers.HyperlinkedIdentityField(view_name='dcim-api:module-detail') device = NestedDeviceSerializer(read_only=True) @@ -392,11 +400,11 @@ class NestedFrontPortSerializer(WritableNestedSerializer): class NestedModuleBaySerializer(WritableNestedSerializer): url = serializers.HyperlinkedIdentityField(view_name='dcim-api:modulebay-detail') - module = NestedModuleSerializer(required=False, read_only=True, allow_null=True) + installed_module = ModuleBayNestedModuleSerializer(required=False, allow_null=True) class Meta: model = models.ModuleBay - fields = ['id', 'url', 'display', 'module', 'name'] + fields = ['id', 'url', 'display', 'installed_module', 'name'] class NestedDeviceBaySerializer(WritableNestedSerializer): diff --git a/netbox/dcim/api/serializers_/devicetypes.py b/netbox/dcim/api/serializers_/devicetypes.py index 2384f7b02..0bd8ba824 100644 --- a/netbox/dcim/api/serializers_/devicetypes.py +++ b/netbox/dcim/api/serializers_/devicetypes.py @@ -28,8 +28,8 @@ class DeviceTypeSerializer(NetBoxModelSerializer): subdevice_role = ChoiceField(choices=SubdeviceRoleChoices, allow_blank=True, required=False, allow_null=True) airflow = ChoiceField(choices=DeviceAirflowChoices, allow_blank=True, required=False, allow_null=True) weight_unit = ChoiceField(choices=WeightUnitChoices, allow_blank=True, required=False, allow_null=True) - front_image = serializers.URLField(allow_null=True, required=False) - rear_image = serializers.URLField(allow_null=True, required=False) + front_image = serializers.ImageField(required=False, allow_null=True) + rear_image = serializers.ImageField(required=False, allow_null=True) # Counter fields console_port_template_count = serializers.IntegerField(read_only=True) diff --git a/netbox/dcim/api/serializers_/virtualchassis.py b/netbox/dcim/api/serializers_/virtualchassis.py index 570abfc7d..5a5917119 100644 --- a/netbox/dcim/api/serializers_/virtualchassis.py +++ b/netbox/dcim/api/serializers_/virtualchassis.py @@ -12,6 +12,7 @@ __all__ = ( class VirtualChassisSerializer(NetBoxModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='dcim-api:virtualchassis-detail') master = NestedDeviceSerializer(required=False, allow_null=True, default=None) + members = NestedDeviceSerializer(many=True, read_only=True) # Counter fields member_count = serializers.IntegerField(read_only=True) @@ -20,6 +21,6 @@ class VirtualChassisSerializer(NetBoxModelSerializer): model = VirtualChassis fields = [ 'id', 'url', 'display', 'name', 'domain', 'master', 'description', 'comments', 'tags', 'custom_fields', - 'created', 'last_updated', 'member_count', + 'created', 'last_updated', 'member_count', 'members', ] brief_fields = ('id', 'url', 'display', 'name', 'master', 'description', 'member_count') diff --git a/netbox/dcim/api/views.py b/netbox/dcim/api/views.py index 668af28da..d6ddd466b 100644 --- a/netbox/dcim/api/views.py +++ b/netbox/dcim/api/views.py @@ -511,7 +511,10 @@ class CableTerminationViewSet(NetBoxModelViewSet): # class VirtualChassisViewSet(NetBoxModelViewSet): - queryset = VirtualChassis.objects.all() + queryset = VirtualChassis.objects.prefetch_related( + # Prefetch related object for the display of unnamed devices + 'master__virtual_chassis', + ) serializer_class = serializers.VirtualChassisSerializer filterset_class = filtersets.VirtualChassisFilterSet diff --git a/netbox/dcim/choices.py b/netbox/dcim/choices.py index 2ba24e0aa..b00784265 100644 --- a/netbox/dcim/choices.py +++ b/netbox/dcim/choices.py @@ -889,7 +889,10 @@ class InterfaceTypeChoices(ChoiceSet): TYPE_8GFC_SFP_PLUS = '8gfc-sfpp' TYPE_16GFC_SFP_PLUS = '16gfc-sfpp' TYPE_32GFC_SFP28 = '32gfc-sfp28' + TYPE_32GFC_SFP_PLUS = '32gfc-sfpp' TYPE_64GFC_QSFP_PLUS = '64gfc-qsfpp' + TYPE_64GFC_SFP_DD = '64gfc-sfpdd' + TYPE_64GFC_SFP_PLUS = '64gfc-sfpp' TYPE_128GFC_QSFP28 = '128gfc-qsfp28' # InfiniBand @@ -1058,7 +1061,10 @@ class InterfaceTypeChoices(ChoiceSet): (TYPE_8GFC_SFP_PLUS, 'SFP+ (8GFC)'), (TYPE_16GFC_SFP_PLUS, 'SFP+ (16GFC)'), (TYPE_32GFC_SFP28, 'SFP28 (32GFC)'), + (TYPE_32GFC_SFP_PLUS, 'SFP+ (32GFC)'), (TYPE_64GFC_QSFP_PLUS, 'QSFP+ (64GFC)'), + (TYPE_64GFC_SFP_DD, 'SFP-DD (64GFC)'), + (TYPE_64GFC_SFP_PLUS, 'SFP+ (64GFC)'), (TYPE_128GFC_QSFP28, 'QSFP28 (128GFC)'), ) ), diff --git a/netbox/dcim/graphql/filters.py b/netbox/dcim/graphql/filters.py index 2874b4418..2ae5e7771 100644 --- a/netbox/dcim/graphql/filters.py +++ b/netbox/dcim/graphql/filters.py @@ -1,7 +1,6 @@ -import strawberry import strawberry_django -from dcim import filtersets, models +from dcim import filtersets, models from netbox.graphql.filter_mixins import autotype_decorator, BaseFilterMixin __all__ = ( diff --git a/netbox/dcim/graphql/gfk_mixins.py b/netbox/dcim/graphql/gfk_mixins.py index 8bd435284..86ca88774 100644 --- a/netbox/dcim/graphql/gfk_mixins.py +++ b/netbox/dcim/graphql/gfk_mixins.py @@ -1,7 +1,3 @@ -from typing import TYPE_CHECKING, Annotated, List, Union - -import strawberry -import strawberry_django from circuits.graphql.types import CircuitTerminationType, ProviderNetworkType from circuits.models import CircuitTermination, ProviderNetwork from dcim.graphql.types import ( diff --git a/netbox/dcim/graphql/mixins.py b/netbox/dcim/graphql/mixins.py index 19fd62b14..0b85fedd6 100644 --- a/netbox/dcim/graphql/mixins.py +++ b/netbox/dcim/graphql/mixins.py @@ -1,7 +1,7 @@ +from typing import Annotated, List, Union + import strawberry import strawberry_django -from typing import TYPE_CHECKING, Annotated, List, Union - __all__ = ( 'CabledObjectMixin', diff --git a/netbox/dcim/graphql/schema.py b/netbox/dcim/graphql/schema.py index 4a895c3a7..c8c0ee777 100644 --- a/netbox/dcim/graphql/schema.py +++ b/netbox/dcim/graphql/schema.py @@ -1,4 +1,5 @@ from typing import List + import strawberry import strawberry_django diff --git a/netbox/dcim/graphql/types.py b/netbox/dcim/graphql/types.py index abbd87528..72fe7ae35 100644 --- a/netbox/dcim/graphql/types.py +++ b/netbox/dcim/graphql/types.py @@ -2,6 +2,7 @@ from typing import Annotated, List, Union import strawberry import strawberry_django + from dcim import models from extras.graphql.mixins import ( ChangelogMixin, @@ -12,14 +13,8 @@ from extras.graphql.mixins import ( TagsMixin, ) from ipam.graphql.mixins import IPAddressesMixin, VLANGroupsMixin - from netbox.graphql.scalars import BigInt -from netbox.graphql.types import ( - BaseObjectType, - NetBoxObjectType, - OrganizationalObjectType, -) - +from netbox.graphql.types import BaseObjectType, NetBoxObjectType, OrganizationalObjectType from .filters import * from .mixins import CabledObjectMixin, PathEndpointMixin diff --git a/netbox/dcim/models/devices.py b/netbox/dcim/models/devices.py index c75757fa7..4f221fe16 100644 --- a/netbox/dcim/models/devices.py +++ b/netbox/dcim/models/devices.py @@ -229,15 +229,16 @@ class DeviceType(ImageAttachmentsMixin, PrimaryModel, WeightMixin): 'manufacturer': self.manufacturer.name, 'model': self.model, 'slug': self.slug, + 'description': self.description, 'default_platform': self.default_platform.name if self.default_platform else None, 'part_number': self.part_number, 'u_height': float(self.u_height), 'is_full_depth': self.is_full_depth, 'subdevice_role': self.subdevice_role, 'airflow': self.airflow, - 'comments': self.comments, 'weight': float(self.weight) if self.weight is not None else None, 'weight_unit': self.weight_unit, + 'comments': self.comments, } # Component templates @@ -415,9 +416,10 @@ class ModuleType(ImageAttachmentsMixin, PrimaryModel, WeightMixin): 'manufacturer': self.manufacturer.name, 'model': self.model, 'part_number': self.part_number, - 'comments': self.comments, + 'description': self.description, 'weight': float(self.weight) if self.weight is not None else None, 'weight_unit': self.weight_unit, + 'comments': self.comments, } # Component templates diff --git a/netbox/dcim/tables/devices.py b/netbox/dcim/tables/devices.py index d4c9641b6..98dcfcb3c 100644 --- a/netbox/dcim/tables/devices.py +++ b/netbox/dcim/tables/devices.py @@ -210,6 +210,10 @@ class DeviceTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable): linkify=True, verbose_name=_('Type') ) + platform = tables.Column( + linkify=True, + verbose_name=_('Platform') + ) primary_ip = tables.Column( linkify=True, order_by=('primary_ip4', 'primary_ip6'), @@ -294,7 +298,7 @@ class DeviceTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable): model = models.Device fields = ( 'pk', 'id', 'name', 'status', 'tenant', 'tenant_group', 'role', 'manufacturer', 'device_type', - 'platform', 'serial', 'asset_tag', 'region', 'site_group', 'site', 'location', 'rack', 'parent_device', + 'serial', 'asset_tag', 'region', 'site_group', 'site', 'location', 'rack', 'parent_device', 'device_bay_position', 'position', 'face', 'latitude', 'longitude', 'airflow', 'primary_ip', 'primary_ip4', 'primary_ip6', 'oob_ip', 'cluster', 'virtual_chassis', 'vc_position', 'vc_priority', 'description', 'config_template', 'comments', 'contacts', 'tags', 'created', 'last_updated', diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index b5a90ced3..93e5f04dc 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -1079,7 +1079,7 @@ class DeviceTypeInventoryItemsView(DeviceTypeComponentsView): tab = ViewTab( label=_('Inventory Items'), badge=lambda obj: obj.inventory_item_template_count, - permission='dcim.view_invenotryitemtemplate', + permission='dcim.view_inventoryitemtemplate', weight=590, hide_if_empty=True ) diff --git a/netbox/extras/graphql/filters.py b/netbox/extras/graphql/filters.py index c20509fa3..af3a93588 100644 --- a/netbox/extras/graphql/filters.py +++ b/netbox/extras/graphql/filters.py @@ -1,7 +1,6 @@ -import strawberry import strawberry_django -from extras import filtersets, models +from extras import filtersets, models from netbox.graphql.filter_mixins import autotype_decorator, BaseFilterMixin __all__ = ( diff --git a/netbox/extras/graphql/mixins.py b/netbox/extras/graphql/mixins.py index df8d06b3b..79b1c783d 100644 --- a/netbox/extras/graphql/mixins.py +++ b/netbox/extras/graphql/mixins.py @@ -1,7 +1,7 @@ -import strawberry -import strawberry_django from typing import TYPE_CHECKING, Annotated, List +import strawberry +import strawberry_django from django.contrib.contenttypes.models import ContentType from extras.models import ObjectChange @@ -9,6 +9,7 @@ from extras.models import ObjectChange __all__ = ( 'ChangelogMixin', 'ConfigContextMixin', + 'ContactsMixin', 'CustomFieldsMixin', 'ImageAttachmentsMixin', 'JournalEntriesMixin', diff --git a/netbox/extras/graphql/schema.py b/netbox/extras/graphql/schema.py index 0f1e99c47..a607882b2 100644 --- a/netbox/extras/graphql/schema.py +++ b/netbox/extras/graphql/schema.py @@ -1,4 +1,5 @@ from typing import List + import strawberry import strawberry_django diff --git a/netbox/extras/graphql/types.py b/netbox/extras/graphql/types.py index c301b26b2..28777557b 100644 --- a/netbox/extras/graphql/types.py +++ b/netbox/extras/graphql/types.py @@ -3,10 +3,6 @@ from typing import Annotated, List import strawberry import strawberry_django -import strawberry -from strawberry import auto -import strawberry_django - from extras import models from extras.graphql.mixins import CustomFieldsMixin, TagsMixin from netbox.graphql.types import BaseObjectType, ContentTypeType, ObjectType, OrganizationalObjectType diff --git a/netbox/extras/models/configs.py b/netbox/extras/models/configs.py index 6ea2167c2..ff39c3e8b 100644 --- a/netbox/extras/models/configs.py +++ b/netbox/extras/models/configs.py @@ -11,7 +11,7 @@ from extras.querysets import ConfigContextQuerySet from netbox.config import get_config from netbox.registry import registry from netbox.models import ChangeLoggedModel -from netbox.models.features import CloningMixin, ExportTemplatesMixin, SyncedDataMixin, TagsMixin +from netbox.models.features import CloningMixin, CustomLinksMixin, ExportTemplatesMixin, SyncedDataMixin, TagsMixin from utilities.jinja2 import ConfigTemplateLoader from utilities.utils import deepmerge @@ -26,7 +26,7 @@ __all__ = ( # Config contexts # -class ConfigContext(SyncedDataMixin, CloningMixin, ChangeLoggedModel): +class ConfigContext(SyncedDataMixin, CloningMixin, CustomLinksMixin, ChangeLoggedModel): """ A ConfigContext represents a set of arbitrary data available to any Device or VirtualMachine matching its assigned qualifiers (region, site, etc.). For example, the data stored in a ConfigContext assigned to site A and tenant B @@ -210,7 +210,7 @@ class ConfigContextModel(models.Model): # Config templates # -class ConfigTemplate(SyncedDataMixin, ExportTemplatesMixin, TagsMixin, ChangeLoggedModel): +class ConfigTemplate(SyncedDataMixin, CustomLinksMixin, ExportTemplatesMixin, TagsMixin, ChangeLoggedModel): name = models.CharField( verbose_name=_('name'), max_length=100 diff --git a/netbox/ipam/forms/model_forms.py b/netbox/ipam/forms/model_forms.py index 689e18940..47087139a 100644 --- a/netbox/ipam/forms/model_forms.py +++ b/netbox/ipam/forms/model_forms.py @@ -373,20 +373,6 @@ class IPAddressForm(TenancyForm, NetBoxModelForm): 'primary_for_parent', _("Only IP addresses assigned to an interface can be designated as primary IPs.") ) - # Do not allow assigning a network ID or broadcast address to an interface. - if interface and (address := self.cleaned_data.get('address')): - if address.ip == address.network: - msg = _("{ip} is a network ID, which may not be assigned to an interface.").format(ip=address.ip) - if address.version == 4 and address.prefixlen not in (31, 32): - raise ValidationError(msg) - if address.version == 6 and address.prefixlen not in (127, 128): - raise ValidationError(msg) - if address.version == 4 and address.ip == address.broadcast and address.prefixlen not in (31, 32): - msg = _("{ip} is a broadcast address, which may not be assigned to an interface.").format( - ip=address.ip - ) - raise ValidationError(msg) - def save(self, *args, **kwargs): ipaddress = super().save(*args, **kwargs) diff --git a/netbox/ipam/graphql/filters.py b/netbox/ipam/graphql/filters.py index a47f5adf5..5f6602416 100644 --- a/netbox/ipam/graphql/filters.py +++ b/netbox/ipam/graphql/filters.py @@ -1,10 +1,8 @@ -import strawberry import strawberry_django + from ipam import filtersets, models - from netbox.graphql.filter_mixins import autotype_decorator, BaseFilterMixin - __all__ = ( 'ASNFilter', 'ASNRangeFilter', diff --git a/netbox/ipam/graphql/mixins.py b/netbox/ipam/graphql/mixins.py index 4fe41adde..db76b98a3 100644 --- a/netbox/ipam/graphql/mixins.py +++ b/netbox/ipam/graphql/mixins.py @@ -1,6 +1,7 @@ +from typing import Annotated, List + import strawberry import strawberry_django -from typing import TYPE_CHECKING, Annotated, List, Union __all__ = ( 'IPAddressesMixin', diff --git a/netbox/ipam/graphql/schema.py b/netbox/ipam/graphql/schema.py index 29fff5a7b..4a977d07d 100644 --- a/netbox/ipam/graphql/schema.py +++ b/netbox/ipam/graphql/schema.py @@ -1,4 +1,5 @@ from typing import List + import strawberry import strawberry_django diff --git a/netbox/ipam/graphql/types.py b/netbox/ipam/graphql/types.py index 9fd646562..05ea239a3 100644 --- a/netbox/ipam/graphql/types.py +++ b/netbox/ipam/graphql/types.py @@ -1,19 +1,15 @@ -from typing import TYPE_CHECKING, Annotated, List, Union +from typing import Annotated, List, Union import strawberry import strawberry_django + from circuits.graphql.types import ProviderType from dcim.graphql.types import SiteType from ipam import models - from netbox.graphql.scalars import BigInt -from netbox.graphql.types import ( - BaseObjectType, - NetBoxObjectType, - OrganizationalObjectType, -) - +from netbox.graphql.types import BaseObjectType, NetBoxObjectType, OrganizationalObjectType from .filters import * +from .mixins import IPAddressesMixin __all__ = ( 'ASNType', @@ -101,7 +97,7 @@ class AggregateType(NetBoxObjectType, BaseIPAddressFamilyType): fields='__all__', filters=FHRPGroupFilter ) -class FHRPGroupType(NetBoxObjectType): +class FHRPGroupType(NetBoxObjectType, IPAddressesMixin): @strawberry_django.field def fhrpgroupassignment_set(self) -> List[Annotated["FHRPGroupAssignmentType", strawberry.lazy('ipam.graphql.types')]]: diff --git a/netbox/ipam/models/ip.py b/netbox/ipam/models/ip.py index cce5b6b68..422c5ba37 100644 --- a/netbox/ipam/models/ip.py +++ b/netbox/ipam/models/ip.py @@ -844,6 +844,25 @@ class IPAddress(PrimaryModel): 'address': _("Cannot create IP address with /0 mask.") }) + # Do not allow assigning a network ID or broadcast address to an interface. + if self.assigned_object: + if self.address.ip == self.address.network: + msg = _("{ip} is a network ID, which may not be assigned to an interface.").format( + ip=self.address.ip + ) + if self.address.version == 4 and self.address.prefixlen not in (31, 32): + raise ValidationError(msg) + if self.address.version == 6 and self.address.prefixlen not in (127, 128): + raise ValidationError(msg) + if ( + self.address.version == 4 and self.address.ip == self.address.broadcast and + self.address.prefixlen not in (31, 32) + ): + msg = _("{ip} is a broadcast address, which may not be assigned to an interface.").format( + ip=self.address.ip + ) + raise ValidationError(msg) + # Enforce unique IP space (if applicable) if (self.vrf is None and get_config().ENFORCE_GLOBAL_UNIQUE) or (self.vrf and self.vrf.enforce_unique): duplicate_ips = self.get_duplicates() diff --git a/netbox/netbox/search/__init__.py b/netbox/netbox/search/__init__.py index 590188f21..76898be13 100644 --- a/netbox/netbox/search/__init__.py +++ b/netbox/netbox/search/__init__.py @@ -1,6 +1,9 @@ from collections import namedtuple +from decimal import Decimal +from django.core.exceptions import FieldDoesNotExist from django.db import models +from netaddr import IPAddress, IPNetwork from ipam.fields import IPAddressField, IPNetworkField from netbox.registry import registry @@ -56,6 +59,24 @@ class SearchIndex: return FieldTypes.INTEGER return FieldTypes.STRING + @staticmethod + def get_attr_type(instance, field_name): + """ + Return the data type of the specified object attribute. + """ + value = getattr(instance, field_name) + if type(value) is str: + return FieldTypes.STRING + if type(value) is int: + return FieldTypes.INTEGER + if type(value) in (float, Decimal): + return FieldTypes.FLOAT + if type(value) is IPNetwork: + return FieldTypes.CIDR + if type(value) is IPAddress: + return FieldTypes.INET + return FieldTypes.STRING + @staticmethod def get_field_value(instance, field_name): """ @@ -82,7 +103,11 @@ class SearchIndex: # Capture built-in fields for name, weight in cls.fields: - type_ = cls.get_field_type(instance, name) + try: + type_ = cls.get_field_type(instance, name) + except FieldDoesNotExist: + # Not a concrete field; handle as an object attribute + type_ = cls.get_attr_type(instance, name) value = cls.get_field_value(instance, name) if type_ and value: values.append( diff --git a/netbox/netbox/tables/tables.py b/netbox/netbox/tables/tables.py index afef74752..31502f6c5 100644 --- a/netbox/netbox/tables/tables.py +++ b/netbox/netbox/tables/tables.py @@ -263,9 +263,11 @@ class SearchTable(tables.Table): super().__init__(data, **kwargs) def render_field(self, value, record): - if hasattr(record.object, value): - return title(record.object._meta.get_field(value).verbose_name) - return value + try: + model_field = record.object._meta.get_field(value) + return title(model_field.verbose_name) + except FieldDoesNotExist: + return value def render_value(self, value): if not self.highlight: diff --git a/netbox/templates/core/job.html b/netbox/templates/core/job.html index a2170db20..0851fa114 100644 --- a/netbox/templates/core/job.html +++ b/netbox/templates/core/job.html @@ -56,7 +56,7 @@