Repository: netbox-community/netbox
Pull request: Fixes #1982: Swagger NAPALM documentation
<!--
    Thank you for your interest in contributing to NetBox! Please note
    that our contribution policy requires that a feature request or bug
    report be opened for approval prior to filing a pull request. This
    helps avoid wasting time and effort on something that we might not
    be able to accept.

    Please indicate the relevant feature request or bug report below.
    IF YOUR PULL REQUEST DOES NOT REFERENCE AN ACCEPTED BUG REPORT OR
    FEATURE REQUEST, IT WILL BE MARKED AS INVALID AND CLOSED.
-->
### Fixes: #1982 <ISSUE NUMBER GOES HERE>

The NAPALM endpoint in Swagger is now documented. In addition, a call with no methods will raise an exception.

Problem statement:
Enhance SwaggerUI to support NAPALM methods
### Issue type
[X] Feature request

### Environment
* Python version:  3.6
* NetBox version:  2.3.1

### Description
I know how to use the NetBox API directly from the CLI to get info from devices using `NAPALM` (and the works great!).  However, I can't do the same thing in the SwaggerUI and I'm hoping a small change could enable me to do that.

What I'm trying to accomplish is to programatically query the devices I have in NetBox (via `NAPALM`) to see if their `model` matches the `device_type` (or specifically `part_number`) that I have defined in NetBox.  

From what I can tell, `pynetbox` doesn't have this functionalty so I'm trying to use `netbox-pyswagger` (from @jeremyschulman) to accomplish this.  However, that "client" is solely based on the output gathered from `/api/docs/?format=openapi`.

To clarify, under the `dcim` section of Swagger, there is this option:
```
GET /api/dcim/devices/{id}/napalm/
```
And when you expand it, you can only pass the `id` field.

After entering an `id` there and clicking the "Try It Out!" button, I can confirm (with command logging on the device in question) that `NAPALM` **does** connect just fine, but doesn't run anything else (due to not be told otherwise) and then disconnects.

The, the response seen in Swagger is the following:
```
Response Body
{}

Response Code
200
```

I suppose what I'm looking for is the option to pass other `method`s (via Swagger) to get the same information as I can using the CLI.  I hope that makes sense.  If not, let me know and I can try to explain further.

Hints from issue discussion:

Commit message: Fixes #1982: Swagger NAPALM documentation

Patch to review:
diff --git a/docs/release-notes/version-2.6.md b/docs/release-notes/version-2.6.md
index f3ff8798adf..90eba732768 100644
--- a/docs/release-notes/version-2.6.md
+++ b/docs/release-notes/version-2.6.md
@@ -2,6 +2,7 @@
 
 ## Enhancements
 
+* [#1982](https://github.com/netbox-community/netbox/issues/1982) - Improved NAPALM method documentation in Swagger
 * [#2050](https://github.com/netbox-community/netbox/issues/2050) - Preview image attachments when hovering the link
 * [#3187](https://github.com/netbox-community/netbox/issues/3187) - Add rack selection field to rack elevations
 
diff --git a/netbox/dcim/api/serializers.py b/netbox/dcim/api/serializers.py
index 4957092684a..db5fe992fc4 100644
--- a/netbox/dcim/api/serializers.py
+++ b/netbox/dcim/api/serializers.py
@@ -370,6 +370,10 @@ def get_config_context(self, obj):
         return obj.get_config_context()
 
 
+class DeviceNAPALMSerializer(serializers.Serializer):
+    method = serializers.DictField()
+
+
 class ConsoleServerPortSerializer(TaggitSerializer, ConnectedEndpointSerializer):
     device = NestedDeviceSerializer()
     cable = NestedCableSerializer(read_only=True)
diff --git a/netbox/dcim/api/views.py b/netbox/dcim/api/views.py
index 12774e4beed..9bfe0f4213e 100644
--- a/netbox/dcim/api/views.py
+++ b/netbox/dcim/api/views.py
@@ -327,6 +327,13 @@ class DeviceViewSet(CustomFieldModelViewSet):
     )
     filterset_class = filters.DeviceFilter
 
+    _method = Parameter(
+        name='method',
+        in_='query',
+        required=True,
+        type=openapi.TYPE_STRING
+    )
+
     def get_serializer_class(self):
         """
         Select the specific serializer based on the request context.
@@ -358,11 +365,15 @@ def graphs(self, request, pk):
 
         return Response(serializer.data)
 
+    @swagger_auto_schema(manual_parameters=[_method], responses={'200': serializers.DeviceNAPALMSerializer})
     @action(detail=True, url_path='napalm')
     def napalm(self, request, pk):
         """
         Execute a NAPALM method on a Device
         """
+        if not request.GET.get('method'):
+            raise ServiceUnavailable('No NAPALM methods were specified.')
+
         device = get_object_or_404(Device, pk=pk)
         if not device.primary_ip:
             raise ServiceUnavailable("This device does not have a primary IP address configured.")


Code review comments:
diff_hunk: @@ -358,11 +365,15 @@ def graphs(self, request, pk):
 
         return Response(serializer.data)
 
+    @swagger_auto_schema(manual_parameters=[_method], responses={'200': serializers.DeviceNAPALMSerializer})
     @action(detail=True, url_path='napalm')
     def napalm(self, request, pk):
         """
         Execute a NAPALM method on a Device
         """
+        if not request.GET.get('method'):
path: netbox/dcim/api/views.py
text: @user1:
I see why you did this, but this is technically out of scope for #1982 as it modifies the endpoint behavior. It needs a separate issue; I'd argue for a bug report (rather than a FR).

I say this because in a scenario where a user has a script making malformed calls to the NAPALM endpoint, this change might cause it to start throwing errors. We want to make sure the user can quickly discern the suspect reason from reviewing the release notes.

@author:
Took it out. To be honest, I'm not at all bothered by the fact that it doesn't do anything. I just misunderstood the original ticket when the person was referring to the call not "running anything".

diff_hunk: @@ -327,6 +327,13 @@ class DeviceViewSet(CustomFieldModelViewSet):
     )
     filterset_class = filters.DeviceFilter
 
+    _method = Parameter(
path: netbox/dcim/api/views.py
text: @user2:
Let's avoid using a generic variable name on DeviceView for something that's specific to the NAPALM endpoint. Honestly, I'd be okay with just defining the Parameter list directly inside the `@user1()` decorator.

@author:
Moved parameter to decorator