diff --git a/docs/customization/custom-scripts.md b/docs/customization/custom-scripts.md index add8fafb1..81b094f24 100644 --- a/docs/customization/custom-scripts.md +++ b/docs/customization/custom-scripts.md @@ -384,6 +384,29 @@ A calendar date. Returns a `datetime.date` object. A complete date & time. Returns a `datetime.datetime` object. +## Uploading Scripts via the API + +Script modules can be uploaded to NetBox via the REST API by sending a `multipart/form-data` POST request to `/api/extras/scripts/`. The caller must have the `extras.add_scriptmodule` and `core.add_managedfile` permissions. + +```no-highlight +curl -X POST \ +-H "Authorization: Token $TOKEN" \ +-H "Accept: application/json; indent=4" \ +-F "upload_file=@/path/to/myscript.py" \ +http://netbox/api/extras/scripts/ +``` + +Alternatively, a script module can be linked to an existing data source and data file instead of uploading a file directly: + +```no-highlight +curl -X POST \ +-H "Authorization: Token $TOKEN" \ +-H "Content-Type: application/json" \ +-H "Accept: application/json; indent=4" \ +http://netbox/api/extras/scripts/ \ +--data '{"data_source": 1, "data_file": 42}' +``` + ## Running Custom Scripts !!! note diff --git a/netbox/extras/api/serializers_/scripts.py b/netbox/extras/api/serializers_/scripts.py index a0587dd25..a26b5daca 100644 --- a/netbox/extras/api/serializers_/scripts.py +++ b/netbox/extras/api/serializers_/scripts.py @@ -47,6 +47,10 @@ class ScriptModuleSerializer(ValidatedModelSerializer): raise serializers.ValidationError( _("Cannot upload a file and sync from an existing data file.") ) + if upload_file and data.get('data_source'): + raise serializers.ValidationError( + _("Cannot upload a file and sync from a data source.") + ) if self.instance is None and not upload_file and not data.get('data_file'): raise serializers.ValidationError( _("Must upload a file or select a data file to sync.") diff --git a/netbox/extras/api/views.py b/netbox/extras/api/views.py index 0c74dfbb5..8944dc207 100644 --- a/netbox/extras/api/views.py +++ b/netbox/extras/api/views.py @@ -267,8 +267,6 @@ class ConfigTemplateViewSet(SyncedDataMixin, ConfigTemplateRenderMixin, NetBoxMo @extend_schema_view( create=extend_schema(request=serializers.ScriptModuleSerializer), - update=extend_schema(request=serializers.ScriptInputSerializer), - partial_update=extend_schema(request=serializers.ScriptInputSerializer), ) class ScriptViewSet(ModelViewSet): permission_classes = [IsAuthenticatedOrLoginNotRequired] @@ -284,8 +282,15 @@ class ScriptViewSet(ModelViewSet): # Restrict the view's QuerySet to allow only the permitted objects if request.user.is_authenticated and self.action != 'create': - action = 'run' if request.method == 'POST' else 'view' - self.queryset = self.queryset.restrict(request.user, action) + if self.action == 'destroy': + perm_action = 'delete' + elif self.action in ('update', 'partial_update'): + perm_action = 'change' + elif request.method == 'POST': + perm_action = 'run' + else: + perm_action = 'view' + self.queryset = self.queryset.restrict(request.user, perm_action) def create(self, request, *args, **kwargs): """ @@ -305,6 +310,16 @@ class ScriptViewSet(ModelViewSet): return Response(serializer.data, status=status.HTTP_201_CREATED) + def update(self, request, *args, **kwargs): + if not request.user.has_perm('extras.change_script'): + raise PermissionDenied(_("This user does not have permission to modify scripts.")) + return super().update(request, *args, **kwargs) + + def destroy(self, request, *args, **kwargs): + if not request.user.has_perm('extras.delete_script'): + raise PermissionDenied(_("This user does not have permission to delete scripts.")) + return super().destroy(request, *args, **kwargs) + def _get_script(self, pk): # If pk is numeric, retrieve script by ID if pk.isnumeric():