diff --git a/netbox/extras/api/serializers_/scripts.py b/netbox/extras/api/serializers_/scripts.py index c5ebce3f2..56b6bc607 100644 --- a/netbox/extras/api/serializers_/scripts.py +++ b/netbox/extras/api/serializers_/scripts.py @@ -1,6 +1,10 @@ +import logging + from django.core.files.storage import storages from django.db import IntegrityError from django.utils.translation import gettext_lazy as _ + +logger = logging.getLogger(__name__) from drf_spectacular.utils import extend_schema_field from rest_framework import serializers @@ -58,7 +62,7 @@ class ScriptModuleSerializer(ValidatedModelSerializer): try: storage.delete(file_path) except Exception: - pass + logger.warning(f"Failed to delete orphaned script file '{file_path}' from storage.") class ScriptSerializer(ValidatedModelSerializer): diff --git a/netbox/extras/api/views.py b/netbox/extras/api/views.py index 8aed9f151..5a2c03212 100644 --- a/netbox/extras/api/views.py +++ b/netbox/extras/api/views.py @@ -21,6 +21,7 @@ from netbox.api.features import SyncedDataMixin from netbox.api.metadata import ContentTypeMetadata from netbox.api.renderers import TextRenderer from netbox.api.viewsets import BaseViewSet, NetBoxModelViewSet +from netbox.api.viewsets.mixins import ObjectValidationMixin from utilities.exceptions import RQWorkerNotRunningException from utilities.request import copy_safe_request @@ -264,7 +265,7 @@ class ConfigTemplateViewSet(SyncedDataMixin, ConfigTemplateRenderMixin, NetBoxMo # Scripts # -class ScriptModuleViewSet(CreateModelMixin, BaseViewSet): +class ScriptModuleViewSet(ObjectValidationMixin, CreateModelMixin, BaseViewSet): queryset = ScriptModule.objects.all() serializer_class = serializers.ScriptModuleSerializer diff --git a/netbox/extras/tests/test_api.py b/netbox/extras/tests/test_api.py index 92e6f93ff..c85433982 100644 --- a/netbox/extras/tests/test_api.py +++ b/netbox/extras/tests/test_api.py @@ -1433,27 +1433,6 @@ class ScriptModuleTest(APITestCase): mock_storage.save.assert_called_once() self.assertTrue(ScriptModule.objects.filter(file_path='test_upload.py').exists()) - def test_upload_script_module_duplicate_fails(self): - self.add_permissions('extras.add_scriptmodule', 'core.add_managedfile') - script_content = b"from extras.scripts import Script\nclass TestScript(Script):\n pass\n" - mock_storage = MagicMock() - mock_storage.save.return_value = 'test_upload.py' - with patch('extras.api.serializers_.scripts.storages') as mock_storages: - mock_storages.create_storage.return_value = mock_storage - mock_storages.backends = {'scripts': {}} - # First upload succeeds - upload_file = SimpleUploadedFile('test_upload.py', script_content, content_type='text/plain') - self.client.post(self.url, {'file': upload_file}, format='multipart', **self.header) - # Second upload with same name should fail - upload_file = SimpleUploadedFile('test_upload.py', script_content, content_type='text/plain') - response = self.client.post( - self.url, - {'file': upload_file}, - format='multipart', - **self.header, - ) - self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST) - def test_upload_script_module_without_file_fails(self): self.add_permissions('extras.add_scriptmodule', 'core.add_managedfile') response = self.client.post(self.url, {}, format='json', **self.header)