diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml index 5140a0743..e5cbc2ece 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yaml +++ b/.github/ISSUE_TEMPLATE/bug_report.yaml @@ -34,10 +34,9 @@ body: label: Python Version description: What version of Python are you currently running? options: - - "3.8" - - "3.9" - "3.10" - "3.11" + - "3.12" validations: required: true - type: textarea diff --git a/docs/release-notes/version-4.0.md b/docs/release-notes/version-4.0.md index 4f8b0f29a..a0a4fad84 100644 --- a/docs/release-notes/version-4.0.md +++ b/docs/release-notes/version-4.0.md @@ -1,5 +1,9 @@ # NetBox v4.0 +## v4.0.1 (FUTURE) + +--- + ## v4.0.0 (2024-05-06) !!! tip "Plugin Maintainers" diff --git a/netbox/circuits/api/serializers_/circuits.py b/netbox/circuits/api/serializers_/circuits.py index b59c73f09..a0d0e5e13 100644 --- a/netbox/circuits/api/serializers_/circuits.py +++ b/netbox/circuits/api/serializers_/circuits.py @@ -48,7 +48,7 @@ class CircuitCircuitTerminationSerializer(WritableNestedSerializer): class CircuitSerializer(NetBoxModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuit-detail') provider = ProviderSerializer(nested=True) - provider_account = ProviderAccountSerializer(nested=True, required=False, allow_null=True) + provider_account = ProviderAccountSerializer(nested=True, required=False, allow_null=True, default=None) status = ChoiceField(choices=CircuitStatusChoices, required=False) type = CircuitTypeSerializer(nested=True) tenant = TenantSerializer(nested=True, required=False, allow_null=True) diff --git a/netbox/circuits/api/serializers_/providers.py b/netbox/circuits/api/serializers_/providers.py index 302c2da5a..fa4489787 100644 --- a/netbox/circuits/api/serializers_/providers.py +++ b/netbox/circuits/api/serializers_/providers.py @@ -45,6 +45,7 @@ class ProviderSerializer(NetBoxModelSerializer): class ProviderAccountSerializer(NetBoxModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='circuits-api:provideraccount-detail') provider = ProviderSerializer(nested=True) + name = serializers.CharField(allow_blank=True, max_length=100, required=False, default='') class Meta: model = ProviderAccount diff --git a/netbox/circuits/tests/test_api.py b/netbox/circuits/tests/test_api.py index c8ec08943..d3745f2b1 100644 --- a/netbox/circuits/tests/test_api.py +++ b/netbox/circuits/tests/test_api.py @@ -141,7 +141,7 @@ class CircuitTest(APIViewTestCases.APIViewTestCase): { 'cid': 'Circuit 6', 'provider': providers[1].pk, - 'provider_account': provider_accounts[1].pk, + # Omit provider account to test uniqueness constraint 'type': circuit_types[1].pk, }, ] @@ -237,7 +237,7 @@ class ProviderAccountTest(APIViewTestCases.APIViewTestCase): 'account': '5678', }, { - 'name': 'Provider Account 6', + # Omit name to test uniqueness constraint 'provider': providers[0].pk, 'account': '6789', }, diff --git a/netbox/dcim/api/serializers_/devices.py b/netbox/dcim/api/serializers_/devices.py index 7d1601882..edfac3072 100644 --- a/netbox/dcim/api/serializers_/devices.py +++ b/netbox/dcim/api/serializers_/devices.py @@ -122,6 +122,7 @@ class DeviceWithConfigContextSerializer(DeviceSerializer): class VirtualDeviceContextSerializer(NetBoxModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='dcim-api:virtualdevicecontext-detail') device = DeviceSerializer(nested=True) + identifier = serializers.IntegerField(allow_null=True, max_value=32767, min_value=0, required=False, default=None) tenant = TenantSerializer(nested=True, required=False, allow_null=True, default=None) primary_ip = IPAddressSerializer(nested=True, read_only=True, allow_null=True) primary_ip4 = IPAddressSerializer(nested=True, required=False, allow_null=True) diff --git a/netbox/dcim/api/serializers_/sites.py b/netbox/dcim/api/serializers_/sites.py index 8063278a7..1e5e41069 100644 --- a/netbox/dcim/api/serializers_/sites.py +++ b/netbox/dcim/api/serializers_/sites.py @@ -51,7 +51,7 @@ class SiteSerializer(NetBoxModelSerializer): status = ChoiceField(choices=SiteStatusChoices, required=False) region = RegionSerializer(nested=True, required=False, allow_null=True) group = SiteGroupSerializer(nested=True, required=False, allow_null=True) - tenant = TenantSerializer(required=False, allow_null=True) + tenant = TenantSerializer(nested=True, required=False, allow_null=True) time_zone = TimeZoneSerializerField(required=False, allow_null=True) asns = SerializedPKRelatedField( queryset=ASN.objects.all(), @@ -83,7 +83,7 @@ class SiteSerializer(NetBoxModelSerializer): class LocationSerializer(NestedGroupModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='dcim-api:location-detail') site = SiteSerializer(nested=True) - parent = NestedLocationSerializer(required=False, allow_null=True) + parent = NestedLocationSerializer(required=False, allow_null=True, default=None) status = ChoiceField(choices=LocationStatusChoices, required=False) tenant = TenantSerializer(nested=True, required=False, allow_null=True) rack_count = serializers.IntegerField(read_only=True) diff --git a/netbox/dcim/tests/test_api.py b/netbox/dcim/tests/test_api.py index 0a3931696..52b850b24 100644 --- a/netbox/dcim/tests/test_api.py +++ b/netbox/dcim/tests/test_api.py @@ -10,6 +10,7 @@ from dcim.models import * from extras.models import ConfigTemplate from ipam.models import ASN, RIR, VLAN, VRF from netbox.api.serializers import GenericObjectSerializer +from tenancy.models import Tenant from utilities.testing import APITestCase, APIViewTestCases, create_test_device from virtualization.models import Cluster, ClusterType from wireless.choices import WirelessChannelChoices @@ -152,6 +153,7 @@ class SiteTest(APIViewTestCases.APIViewTestCase): Site.objects.bulk_create(sites) rir = RIR.objects.create(name='RFC 6996', is_private=True) + tenant = Tenant.objects.create(name='Tenant 1', slug='tenant-1') asns = [ ASN(asn=65000 + i, rir=rir) for i in range(8) @@ -166,6 +168,7 @@ class SiteTest(APIViewTestCases.APIViewTestCase): 'group': groups[1].pk, 'status': SiteStatusChoices.STATUS_ACTIVE, 'asns': [asns[0].pk, asns[1].pk], + 'tenant': tenant.pk, }, { 'name': 'Site 5', @@ -230,7 +233,7 @@ class LocationTest(APIViewTestCases.APIViewTestCase): 'name': 'Test Location 6', 'slug': 'test-location-6', 'site': sites[1].pk, - 'parent': parent_locations[1].pk, + # Omit parent to test uniqueness constraint 'status': LocationStatusChoices.STATUS_PLANNED, }, ] @@ -2307,6 +2310,6 @@ class VirtualDeviceContextTest(APIViewTestCases.APIViewTestCase): 'device': devices[1].pk, 'status': 'active', 'name': 'VDC 3', - 'identifier': 3, + # Omit identifier to test uniqueness constraint }, ] diff --git a/netbox/ipam/views.py b/netbox/ipam/views.py index 24d82d186..044474ec4 100644 --- a/netbox/ipam/views.py +++ b/netbox/ipam/views.py @@ -781,6 +781,7 @@ class IPAddressView(generic.ObjectView): class IPAddressEditView(generic.ObjectEditView): queryset = IPAddress.objects.all() form = forms.IPAddressForm + template_name = 'ipam/ipaddress_edit.html' def alter_object(self, obj, request, url_args, url_kwargs): diff --git a/netbox/netbox/navigation/__init__.py b/netbox/netbox/navigation/__init__.py index d13282f7e..b4f7dbd9f 100644 --- a/netbox/netbox/navigation/__init__.py +++ b/netbox/netbox/navigation/__init__.py @@ -32,6 +32,7 @@ class MenuItem: link: str link_text: str permissions: Optional[Sequence[str]] = () + auth_required: Optional[bool] = False staff_only: Optional[bool] = False buttons: Optional[Sequence[MenuItemButton]] = () diff --git a/netbox/netbox/navigation/menu.py b/netbox/netbox/navigation/menu.py index 4fe16e773..2a58b277e 100644 --- a/netbox/netbox/navigation/menu.py +++ b/netbox/netbox/navigation/menu.py @@ -371,6 +371,7 @@ ADMIN_MENU = Menu( MenuItem( link=f'users:user_list', link_text=_('Users'), + auth_required=True, permissions=[f'auth.view_user'], buttons=( MenuItemButton( @@ -390,6 +391,7 @@ ADMIN_MENU = Menu( MenuItem( link=f'users:group_list', link_text=_('Groups'), + auth_required=True, permissions=[f'auth.view_group'], buttons=( MenuItemButton( @@ -409,12 +411,14 @@ ADMIN_MENU = Menu( MenuItem( link=f'users:token_list', link_text=_('API Tokens'), + auth_required=True, permissions=[f'users.view_token'], buttons=get_model_buttons('users', 'token') ), MenuItem( link=f'users:objectpermission_list', link_text=_('Permissions'), + auth_required=True, permissions=[f'users.view_objectpermission'], buttons=get_model_buttons('users', 'objectpermission', actions=['add']) ), @@ -425,16 +429,19 @@ ADMIN_MENU = Menu( items=( MenuItem( link='core:system', - link_text=_('System') + link_text=_('System'), + auth_required=True ), MenuItem( link='core:configrevision_list', link_text=_('Configuration History'), + auth_required=True, permissions=['core.view_configrevision'] ), MenuItem( link='core:background_queue_list', - link_text=_('Background Tasks') + link_text=_('Background Tasks'), + auth_required=True ), ), ), diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py index 7c4c400be..38df16551 100644 --- a/netbox/netbox/settings.py +++ b/netbox/netbox/settings.py @@ -25,7 +25,7 @@ from utilities.string import trailing_slash # Environment setup # -VERSION = '4.0.0' +VERSION = '4.0.1-dev' HOSTNAME = platform.node() # Set the base directory two levels up BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) @@ -522,7 +522,6 @@ if SENTRY_ENABLED: sentry_sdk.init( dsn=SENTRY_DSN, release=VERSION, - integrations=[sentry_sdk.integrations.django.DjangoIntegration()], sample_rate=SENTRY_SAMPLE_RATE, traces_sample_rate=SENTRY_TRACES_SAMPLE_RATE, send_default_pii=True, diff --git a/netbox/templates/base/base.html b/netbox/templates/base/base.html index f7fa3fa50..7a7a4fe99 100644 --- a/netbox/templates/base/base.html +++ b/netbox/templates/base/base.html @@ -20,7 +20,7 @@ {# Initialize color mode #}