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 #}