Merge branch 'develop' into feature

This commit is contained in:
Jeremy Stretch 2024-09-03 11:59:59 -04:00
commit a777850702
47 changed files with 4929 additions and 4783 deletions

View File

@ -26,7 +26,7 @@ body:
attributes:
label: NetBox Version
description: What version of NetBox are you currently running?
placeholder: v4.0.10
placeholder: v4.0.11
validations:
required: true
- type: dropdown

View File

@ -14,7 +14,7 @@ body:
attributes:
label: NetBox version
description: What version of NetBox are you currently running?
placeholder: v4.0.10
placeholder: v4.0.11
validations:
required: true
- type: dropdown

View File

@ -1,5 +1,15 @@
# NetBox v4.0
## v4.0.11 (2024-09-03)
### Bug Fixes
* [#17310](https://github.com/netbox-community/netbox/issues/17310) - Enforce restricted queryset for related objects in GraphQL API requests
* [#17321](https://github.com/netbox-community/netbox/issues/17321) - Ensure the job is attributed to the specified user when using the `runscript` management command
* [#17323](https://github.com/netbox-community/netbox/issues/17323) - Associate job with script object when executed using the `runscript` management command
* [#17337](https://github.com/netbox-community/netbox/issues/17337) - Fix ordering of virtual device contexts by device name
* [#17341](https://github.com/netbox-community/netbox/issues/17341) - Avoid `NoReverseMatch` exceptions with specific dashboard widget configurations
## v4.0.10 (2024-08-29)
### Enhancements

View File

@ -96,6 +96,7 @@ class CircuitTest(APIViewTestCases.APIViewTestCase):
bulk_update_data = {
'status': 'planned',
}
user_permissions = ('circuits.view_provider', 'circuits.view_circuittype')
@classmethod
def setUpTestData(cls):
@ -150,6 +151,7 @@ class CircuitTest(APIViewTestCases.APIViewTestCase):
class CircuitTerminationTest(APIViewTestCases.APIViewTestCase):
model = CircuitTermination
brief_fields = ['_occupied', 'cable', 'circuit', 'description', 'display', 'id', 'term_side', 'url']
user_permissions = ('circuits.view_circuit', )
@classmethod
def setUpTestData(cls):
@ -241,6 +243,7 @@ class CircuitGroupTest(APIViewTestCases.APIViewTestCase):
class ProviderAccountTest(APIViewTestCases.APIViewTestCase):
model = ProviderAccount
brief_fields = ['account', 'description', 'display', 'id', 'name', 'url']
user_permissions = ('circuits.view_provider',)
@classmethod
def setUpTestData(cls):
@ -287,6 +290,7 @@ class CircuitGroupAssignmentTest(APIViewTestCases.APIViewTestCase):
bulk_update_data = {
'priority': CircuitPriorityChoices.PRIORITY_INACTIVE,
}
user_permissions = ('circuits.view_circuit', 'circuits.view_circuitgroup')
@classmethod
def setUpTestData(cls):
@ -355,6 +359,7 @@ class CircuitGroupAssignmentTest(APIViewTestCases.APIViewTestCase):
class ProviderNetworkTest(APIViewTestCases.APIViewTestCase):
model = ProviderNetwork
brief_fields = ['description', 'display', 'id', 'name', 'url']
user_permissions = ('circuits.view_provider', )
@classmethod
def setUpTestData(cls):

View File

@ -57,6 +57,7 @@ class DataFileTest(
):
model = DataFile
brief_fields = ['display', 'id', 'path', 'url']
user_permissions = ('core.view_datasource', )
@classmethod
def setUpTestData(cls):

View File

@ -1051,7 +1051,7 @@ class VirtualDeviceContextTable(TenancyColumnsMixin, NetBoxTable):
)
device = tables.TemplateColumn(
verbose_name=_('Device'),
order_by=('_name',),
order_by=('device___name',),
template_code=DEVICE_LINK,
linkify=True
)

View File

@ -192,6 +192,7 @@ class LocationTest(APIViewTestCases.APIViewTestCase):
bulk_update_data = {
'description': 'New description',
}
user_permissions = ('dcim.view_site',)
@classmethod
def setUpTestData(cls):
@ -277,6 +278,7 @@ class RackTypeTest(APIViewTestCases.APIViewTestCase):
bulk_update_data = {
'description': 'new description',
}
user_permissions = ('dcim.view_manufacturer',)
@classmethod
def setUpTestData(cls):
@ -321,6 +323,7 @@ class RackTest(APIViewTestCases.APIViewTestCase):
bulk_update_data = {
'status': 'planned',
}
user_permissions = ('dcim.view_site', )
@classmethod
def setUpTestData(cls):
@ -409,6 +412,7 @@ class RackReservationTest(APIViewTestCases.APIViewTestCase):
bulk_update_data = {
'description': 'New description',
}
user_permissions = ('dcim.view_rack', 'users.view_user')
@classmethod
def setUpTestData(cls):
@ -488,6 +492,7 @@ class DeviceTypeTest(APIViewTestCases.APIViewTestCase):
bulk_update_data = {
'part_number': 'ABC123',
}
user_permissions = ('dcim.view_manufacturer', )
@classmethod
def setUpTestData(cls):
@ -533,6 +538,7 @@ class ModuleTypeTest(APIViewTestCases.APIViewTestCase):
bulk_update_data = {
'part_number': 'ABC123',
}
user_permissions = ('dcim.view_manufacturer', )
@classmethod
def setUpTestData(cls):
@ -704,6 +710,7 @@ class PowerOutletTemplateTest(APIViewTestCases.APIViewTestCase):
bulk_update_data = {
'description': 'New description',
}
user_permissions = ('dcim.view_devicetype', )
@classmethod
def setUpTestData(cls):
@ -809,6 +816,7 @@ class FrontPortTemplateTest(APIViewTestCases.APIViewTestCase):
bulk_update_data = {
'description': 'New description',
}
user_permissions = ('dcim.view_rearporttemplate', )
@classmethod
def setUpTestData(cls):
@ -946,6 +954,7 @@ class ModuleBayTemplateTest(APIViewTestCases.APIViewTestCase):
bulk_update_data = {
'description': 'New description',
}
user_permissions = ('dcim.view_devicetype', )
@classmethod
def setUpTestData(cls):
@ -986,6 +995,7 @@ class DeviceBayTemplateTest(APIViewTestCases.APIViewTestCase):
bulk_update_data = {
'description': 'New description',
}
user_permissions = ('dcim.view_devicetype', )
@classmethod
def setUpTestData(cls):
@ -1026,6 +1036,7 @@ class InventoryItemTemplateTest(APIViewTestCases.APIViewTestCase):
bulk_update_data = {
'description': 'New description',
}
user_permissions = ('dcim.view_devicetype', 'dcim.view_manufacturer',)
@classmethod
def setUpTestData(cls):
@ -1144,6 +1155,10 @@ class DeviceTest(APIViewTestCases.APIViewTestCase):
bulk_update_data = {
'status': 'failed',
}
user_permissions = (
'dcim.view_site', 'dcim.view_rack', 'dcim.view_location', 'dcim.view_devicerole', 'dcim.view_devicetype',
'extras.view_configtemplate',
)
@classmethod
def setUpTestData(cls):
@ -1334,6 +1349,7 @@ class ModuleTest(APIViewTestCases.APIViewTestCase):
bulk_update_data = {
'serial': '1234ABCD',
}
user_permissions = ('dcim.view_modulebay', 'dcim.view_moduletype', 'dcim.view_device')
@classmethod
def setUpTestData(cls):
@ -1400,6 +1416,7 @@ class ConsolePortTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCa
'description': 'New description',
}
peer_termination_type = ConsoleServerPort
user_permissions = ('dcim.view_device', )
@classmethod
def setUpTestData(cls):
@ -1442,6 +1459,7 @@ class ConsoleServerPortTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIView
'description': 'New description',
}
peer_termination_type = ConsolePort
user_permissions = ('dcim.view_device', )
@classmethod
def setUpTestData(cls):
@ -1484,6 +1502,7 @@ class PowerPortTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCase
'description': 'New description',
}
peer_termination_type = PowerOutlet
user_permissions = ('dcim.view_device', )
@classmethod
def setUpTestData(cls):
@ -1523,6 +1542,7 @@ class PowerOutletTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCa
'description': 'New description',
}
peer_termination_type = PowerPort
user_permissions = ('dcim.view_device', )
@classmethod
def setUpTestData(cls):
@ -1571,6 +1591,7 @@ class InterfaceTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCase
'description': 'New description',
}
peer_termination_type = Interface
user_permissions = ('dcim.view_device', )
@classmethod
def setUpTestData(cls):
@ -1705,6 +1726,7 @@ class FrontPortTest(APIViewTestCases.APIViewTestCase):
'description': 'New description',
}
peer_termination_type = Interface
user_permissions = ('dcim.view_device', 'dcim.view_rearport')
@classmethod
def setUpTestData(cls):
@ -1763,6 +1785,7 @@ class RearPortTest(APIViewTestCases.APIViewTestCase):
'description': 'New description',
}
peer_termination_type = Interface
user_permissions = ('dcim.view_device', )
@classmethod
def setUpTestData(cls):
@ -1804,6 +1827,7 @@ class ModuleBayTest(APIViewTestCases.APIViewTestCase):
bulk_update_data = {
'description': 'New description',
}
user_permissions = ('dcim.view_device', )
@classmethod
def setUpTestData(cls):
@ -1844,6 +1868,7 @@ class DeviceBayTest(APIViewTestCases.APIViewTestCase):
bulk_update_data = {
'description': 'New description',
}
user_permissions = ('dcim.view_device', )
@classmethod
def setUpTestData(cls):
@ -1907,6 +1932,7 @@ class InventoryItemTest(APIViewTestCases.APIViewTestCase):
bulk_update_data = {
'description': 'New description',
}
user_permissions = ('dcim.view_device', 'dcim.view_manufacturer')
@classmethod
def setUpTestData(cls):
@ -2203,6 +2229,7 @@ class VirtualChassisTest(APIViewTestCases.APIViewTestCase):
class PowerPanelTest(APIViewTestCases.APIViewTestCase):
model = PowerPanel
brief_fields = ['description', 'display', 'id', 'name', 'powerfeed_count', 'url']
user_permissions = ('dcim.view_site', )
@classmethod
def setUpTestData(cls):
@ -2255,6 +2282,7 @@ class PowerFeedTest(APIViewTestCases.APIViewTestCase):
bulk_update_data = {
'status': 'planned',
}
user_permissions = ('dcim.view_powerpanel', )
@classmethod
def setUpTestData(cls):

View File

@ -183,10 +183,13 @@ class ObjectCountsWidget(DashboardWidget):
for model in get_models_from_content_types(self.config['models']):
permission = get_permission_for_model(model, 'view')
if request.user.has_perm(permission):
url = reverse(get_viewname(model, 'list'))
try:
url = reverse(get_viewname(model, 'list'))
except NoReverseMatch:
url = None
qs = model.objects.restrict(request.user, 'view')
# Apply any specified filters
if filters := self.config.get('filters'):
if url and (filters := self.config.get('filters')):
params = dict_to_querydict(filters)
filterset = getattr(resolve(url).func.view_class, 'filterset', None)
qs = filterset(params, qs).qs

View File

@ -768,6 +768,7 @@ class FHRPGroupAssignmentTest(APIViewTestCases.APIViewTestCase):
bulk_update_data = {
'priority': 100,
}
user_permissions = ('ipam.view_fhrpgroup', )
@classmethod
def setUpTestData(cls):

View File

@ -38,7 +38,7 @@ schema = strawberry.Schema(
query=Query,
config=StrawberryConfig(auto_camel_case=False),
extensions=[
DjangoOptimizerExtension,
DjangoOptimizerExtension(prefetch_custom_queryset=True),
MaxAliasesLimiter(max_alias_count=settings.GRAPHQL_MAX_ALIASES),
]
)

View File

@ -6,6 +6,7 @@ from rest_framework import status
from core.models import ObjectType
from dcim.models import Site, Location
from ipam.models import ASN, RIR
from users.models import ObjectPermission
from utilities.testing import disable_warnings, APITestCase, TestCase
@ -45,7 +46,6 @@ class GraphQLTestCase(TestCase):
class GraphQLAPITestCase(APITestCase):
@override_settings(LOGIN_REQUIRED=True)
@override_settings(EXEMPT_VIEW_PERMISSIONS=['*', 'auth.user'])
def test_graphql_filter_objects(self):
"""
Test the operation of filters for GraphQL API requests.
@ -66,6 +66,7 @@ class GraphQLAPITestCase(APITestCase):
obj_perm.save()
obj_perm.users.add(self.user)
obj_perm.object_types.add(ObjectType.objects.get_for_model(Location))
obj_perm.object_types.add(ObjectType.objects.get_for_model(Site))
# A valid request should return the filtered list
url = reverse('graphql')
@ -75,6 +76,7 @@ class GraphQLAPITestCase(APITestCase):
data = json.loads(response.content)
self.assertNotIn('errors', data)
self.assertEqual(len(data['data']['location_list']), 1)
self.assertIsNotNone(data['data']['location_list'][0]['site'])
# An invalid request should return an empty list
query = '{location_list(filters: {site_id: "99999"}) {id site {id}}}' # Invalid site ID
@ -82,3 +84,12 @@ class GraphQLAPITestCase(APITestCase):
self.assertHttpStatus(response, status.HTTP_200_OK)
data = json.loads(response.content)
self.assertEqual(len(data['data']['location_list']), 0)
# Removing the permissions from location should result in an empty locations list
obj_perm.object_types.remove(ObjectType.objects.get_for_model(Location))
query = '{site(id: ' + str(sites[0].pk) + ') {id locations {id}}}'
response = self.client.post(url, data={'query': query}, format="json", **self.header)
self.assertHttpStatus(response, status.HTTP_200_OK)
data = json.loads(response.content)
self.assertNotIn('errors', data)
self.assertEqual(len(data['data']['site']['locations']), 0)

View File

@ -992,14 +992,7 @@ brace-expansion@^2.0.1:
dependencies:
balanced-match "^1.0.0"
braces@^3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107"
integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==
dependencies:
fill-range "^7.0.1"
braces@~3.0.2:
braces@^3.0.3, braces@~3.0.2:
version "3.0.3"
resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789"
integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==
@ -1575,7 +1568,7 @@ file-entry-cache@^6.0.1:
dependencies:
flat-cache "^3.0.4"
fill-range@^7.0.1, fill-range@^7.1.1:
fill-range@^7.1.1:
version "7.1.1"
resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292"
integrity sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==
@ -2190,11 +2183,11 @@ meros@^1.1.4:
integrity sha512-2BNGOimxEz5hmjUG2FwoxCt5HN7BXdaWyFqEwxPTrJzVdABtrL4TiHTcsWSFAxPQ/tOnEaQEJh3qWq71QRMY+w==
micromatch@^4.0.4:
version "4.0.5"
resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.5.tgz#bc8999a7cbbf77cdc89f132f6e467051b49090c6"
integrity sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==
version "4.0.8"
resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.8.tgz#d66fa18f3a47076789320b9b1af32bd86d9fa202"
integrity sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==
dependencies:
braces "^3.0.2"
braces "^3.0.3"
picomatch "^2.3.1"
minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2:

View File

@ -3,7 +3,7 @@
{% if counts %}
<div class="list-group list-group-flush">
{% for model, count, url in counts %}
<a href="{{ url }}" class="list-group-item list-group-item-action px-1 py-2">
<a {% if url %}href="{{ url }}" {% endif %}class="list-group-item list-group-item-action px-1 py-2">
<div class="d-flex w-100 justify-content-between align-items-center">
{{ model|meta:"verbose_name_plural"|bettertitle }}
{% if count is None %}

View File

@ -210,6 +210,7 @@ class ContactAssignmentTest(APIViewTestCases.APIViewTestCase):
bulk_update_data = {
'priority': ContactPriorityChoices.PRIORITY_INACTIVE,
}
user_permissions = ('tenancy.view_contact', )
@classmethod
def setUpTestData(cls):

Binary file not shown.

File diff suppressed because it is too large Load Diff

Binary file not shown.

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

Binary file not shown.

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

Binary file not shown.

File diff suppressed because it is too large Load Diff

Binary file not shown.

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -253,6 +253,7 @@ class VMInterfaceTest(APIViewTestCases.APIViewTestCase):
'description': 'New description',
}
graphql_base_name = 'vm_interface'
user_permissions = ('virtualization.view_virtualmachine', )
@classmethod
def setUpTestData(cls):
@ -342,6 +343,7 @@ class VirtualDiskTest(APIViewTestCases.APIViewTestCase):
'size': 888,
}
graphql_base_name = 'virtual_disk'
user_permissions = ('virtualization.view_virtualmachine', )
@classmethod
def setUpTestData(cls):

View File

@ -116,6 +116,7 @@ class TunnelTerminationTest(APIViewTestCases.APIViewTestCase):
bulk_update_data = {
'role': TunnelTerminationRoleChoices.ROLE_PEER,
}
user_permissions = ('vpn.view_tunnel', )
@classmethod
def setUpTestData(cls):
@ -430,6 +431,7 @@ class IPSecPolicyTest(APIViewTestCases.APIViewTestCase):
class IPSecProfileTest(APIViewTestCases.APIViewTestCase):
model = IPSecProfile
brief_fields = ['description', 'display', 'id', 'name', 'url']
user_permissions = ('vpn.view_ikepolicy', 'vpn.view_ipsecpolicy')
@classmethod
def setUpTestData(cls):
@ -558,6 +560,7 @@ class L2VPNTest(APIViewTestCases.APIViewTestCase):
class L2VPNTerminationTest(APIViewTestCases.APIViewTestCase):
model = L2VPNTermination
brief_fields = ['display', 'id', 'l2vpn', 'url']
user_permissions = ('dcim.view_location', 'vpn.view_l2vpn')
@classmethod
def setUpTestData(cls):

View File

@ -116,6 +116,7 @@ class WirelessLinkTest(APIViewTestCases.APIViewTestCase):
'distance': 100,
'distance_unit': 'm',
}
user_permissions = ('dcim.view_interface', )
@classmethod
def setUpTestData(cls):

View File

@ -1,4 +1,4 @@
Django==5.0.8
Django==5.0.9
django-cors-headers==4.4.0
django-debug-toolbar==4.4.6
django-filter==24.3
@ -8,7 +8,7 @@ django-mptt==0.16.0
django-pglocks==1.0.4
django-prometheus==2.3.1
django-redis==5.4.0
django-rich==1.10.0
django-rich==1.11.0
django-rq==2.10.2
django-taggit==6.0.0
django-tables2==2.7.0
@ -20,8 +20,8 @@ feedparser==6.0.11
gunicorn==23.0.0
Jinja2==3.1.4
Markdown==3.7
mkdocs-material==9.5.33
mkdocstrings[python-legacy]==0.25.2
mkdocs-material==9.5.34
mkdocstrings[python-legacy]==0.26.0
netaddr==1.3.0
nh3==0.2.18
Pillow==10.4.0
@ -30,7 +30,7 @@ PyYAML==6.0.2
requests==2.32.3
social-auth-app-django==5.4.2
social-auth-core==4.5.4
strawberry-graphql==0.237.3
strawberry-graphql==0.239.2
strawberry-graphql-django==0.47.1
svgwrite==1.4.3
tablib==3.6.1