add support for building swagger/OpenAPI JSON

to build, run `make swagger`; a file named `swagger.json` will be
written to the current working directory
This commit is contained in:
Ryan Petrello
2018-02-01 10:52:14 -05:00
parent c61efc0af8
commit 57c22c20b2
35 changed files with 379 additions and 29 deletions

View File

@@ -28,6 +28,7 @@ from rest_framework import status
from rest_framework import views
# AWX
from awx.api.swagger import AutoSchema
from awx.api.filters import FieldLookupBackend
from awx.main.models import * # noqa
from awx.main.access import access_registry
@@ -93,6 +94,7 @@ def get_view_description(cls, request, html=False):
class APIView(views.APIView):
schema = AutoSchema()
versioning_class = URLPathVersioning
def initialize_request(self, request, *args, **kwargs):
@@ -176,7 +178,7 @@ class APIView(views.APIView):
and in the browsable API.
"""
func = self.settings.VIEW_DESCRIPTION_FUNCTION
return func(self.__class__, self._request, html)
return func(self.__class__, getattr(self, '_request', None), html)
def get_description_context(self):
return {
@@ -197,6 +199,7 @@ class APIView(views.APIView):
'new_in_330': getattr(self, 'new_in_330', False),
'new_in_api_v2': getattr(self, 'new_in_api_v2', False),
'deprecated': getattr(self, 'deprecated', False),
'swagger_method': getattr(self.request, 'swagger_method', None),
}
def get_description(self, request, html=False):

89
awx/api/swagger.py Normal file
View File

@@ -0,0 +1,89 @@
import warnings
from coreapi.document import Object, Link
from rest_framework import exceptions
from rest_framework.permissions import AllowAny
from rest_framework.renderers import CoreJSONRenderer
from rest_framework.response import Response
from rest_framework.schemas import SchemaGenerator, AutoSchema as DRFAuthSchema
from rest_framework.views import APIView
from rest_framework_swagger import renderers
class AutoSchema(DRFAuthSchema):
def get_link(self, path, method, base_url):
link = super(AutoSchema, self).get_link(path, method, base_url)
try:
serializer = self.view.get_serializer()
except Exception:
serializer = None
warnings.warn('{}.get_serializer() raised an exception during '
'schema generation. Serializer fields will not be '
'generated for {} {}.'
.format(self.view.__class__.__name__, method, path))
# auto-generate a topic/tag for the serializer based on its model
if hasattr(self.view, 'swagger_topic'):
link.__dict__['topic'] = str(self.view.swagger_topic).title()
elif serializer and hasattr(serializer, 'Meta'):
link.__dict__['topic'] = str(
serializer.Meta.model._meta.verbose_name_plural
).title()
elif hasattr(self.view, 'model'):
link.__dict__['topic'] = str(self.view.model._meta.verbose_name_plural).title()
else:
warnings.warn('Could not determine a Swagger tag for path {}'.format(path))
return link
def get_description(self, path, method):
self.view._request = self.view.request
setattr(self.view.request, 'swagger_method', method)
description = super(AutoSchema, self).get_description(path, method)
return description
class SwaggerSchemaView(APIView):
_ignore_model_permissions = True
exclude_from_schema = True
permission_classes = [AllowAny]
renderer_classes = [
CoreJSONRenderer,
renderers.OpenAPIRenderer,
renderers.SwaggerUIRenderer
]
def get(self, request):
generator = SchemaGenerator(
title='Ansible Tower API',
patterns=None,
urlconf=None
)
schema = generator.get_schema(request=request)
# By default, DRF OpenAPI serialization places all endpoints in
# a single node based on their root path (/api). Instead, we want to
# group them by topic/tag so that they're categorized in the rendered
# output
document = schema._data.pop('api')
for path, node in document.items():
if isinstance(node, Object):
for action in node.values():
topic = getattr(action, 'topic', None)
if topic:
schema._data.setdefault(topic, Object())
schema._data[topic]._data[path] = node
elif isinstance(node, Link):
topic = getattr(node, 'topic', None)
if topic:
schema._data.setdefault(topic, Object())
schema._data[topic]._data[path] = node
if not schema:
raise exceptions.ValidationError(
'The schema generator did not return a schema Document'
)
return Response(schema)

View File

@@ -1,9 +1,13 @@
{% ifmeth GET %}
# Retrieve {{ model_verbose_name|title }} Variable Data:
Make a GET request to this resource to retrieve all variables defined for this
Make a GET request to this resource to retrieve all variables defined for a
{{ model_verbose_name }}.
{% endifmeth %}
{% ifmeth PUT PATCH %}
# Update {{ model_verbose_name|title }} Variable Data:
Make a PUT request to this resource to update variables defined for this
Make a PUT or PATCH request to this resource to update variables defined for a
{{ model_verbose_name }}.
{% endifmeth %}

View File

@@ -1,4 +1,4 @@
# List All {{ model_verbose_name_plural|title }} for this {{ parent_model_verbose_name|title }}:
# List All {{ model_verbose_name_plural|title }} for {{ parent_model_verbose_name|title|anora }}:
Make a GET request to this resource to retrieve a list of all
{{ model_verbose_name_plural }} directly or indirectly belonging to this

View File

@@ -1,4 +1,4 @@
# List Potential Child Groups for this {{ parent_model_verbose_name|title }}:
# List Potential Child Groups for {{ parent_model_verbose_name|title|anora }}:
Make a GET request to this resource to retrieve a list of
{{ model_verbose_name_plural }} available to be added as children of the

View File

@@ -1,4 +1,4 @@
# List All {{ model_verbose_name_plural|title }} for this {{ parent_model_verbose_name|title }}:
# List All {{ model_verbose_name_plural|title }} for {{ parent_model_verbose_name|title|anora }}:
Make a GET request to this resource to retrieve a list of all
{{ model_verbose_name_plural }} of which the selected

View File

@@ -1,3 +1,5 @@
# List Fact Scans for a Host Specific Host Scan
Make a GET request to this resource to retrieve system tracking data for a particular scan
You may filter by datetime:
@@ -8,4 +10,4 @@ and module
`?datetime=2015-06-01&module=ansible`
{% include "api/_new_in_awx.md" %}
{% include "api/_new_in_awx.md" %}

View File

@@ -1,3 +1,5 @@
# List Fact Scans for a Host by Module and Date
Make a GET request to this resource to retrieve system tracking scans by module and date/time
You may filter scan runs using the `from` and `to` properties:
@@ -8,4 +10,4 @@ You may also filter by module
`?module=packages`
{% include "api/_new_in_awx.md" %}
{% include "api/_new_in_awx.md" %}

View File

@@ -0,0 +1 @@
# List Red Hat Insights for a Host

View File

@@ -1,4 +1,4 @@
# List Root {{ model_verbose_name_plural|title }} for this {{ parent_model_verbose_name|title }}:
# List Root {{ model_verbose_name_plural|title }} for {{ parent_model_verbose_name|title|anora }}:
Make a GET request to this resource to retrieve a list of root (top-level)
{{ model_verbose_name_plural }} associated with this

View File

@@ -1,4 +1,4 @@
# Group Tree for this {{ model_verbose_name|title }}:
# Group Tree for {{ model_verbose_name|title|anora }}:
Make a GET request to this resource to retrieve a hierarchical view of groups
associated with the selected {{ model_verbose_name }}.

View File

@@ -1,4 +1,4 @@
# Retrieve {{ model_verbose_name|title }} Playbooks:
Make GET request to this resource to retrieve a list of playbooks available
for this {{ model_verbose_name }}.
for {{ model_verbose_name|anora }}.

View File

@@ -2,7 +2,7 @@
### Note: starting from api v2, this resource object can be accessed via its named URL.
{% endif %}
# Retrieve {{ model_verbose_name|title }}:
# Retrieve {{ model_verbose_name|title|anora }}:
Make GET request to this resource to retrieve a single {{ model_verbose_name }}
record containing the following fields:

View File

@@ -2,14 +2,14 @@
### Note: starting from api v2, this resource object can be accessed via its named URL.
{% endif %}
# Retrieve {{ model_verbose_name|title }}:
# Retrieve {{ model_verbose_name|title|anora }}:
Make GET request to this resource to retrieve a single {{ model_verbose_name }}
record containing the following fields:
{% include "api/_result_fields_common.md" %}
# Delete {{ model_verbose_name|title }}:
# Delete {{ model_verbose_name|title|anora }}:
Make a DELETE request to this resource to delete this {{ model_verbose_name }}.

View File

@@ -2,14 +2,17 @@
### Note: starting from api v2, this resource object can be accessed via its named URL.
{% endif %}
# Retrieve {{ model_verbose_name|title }}:
{% ifmeth GET %}
# Retrieve {{ model_verbose_name|title|anora }}:
Make GET request to this resource to retrieve a single {{ model_verbose_name }}
record containing the following fields:
{% include "api/_result_fields_common.md" %}
{% endifmeth %}
# Update {{ model_verbose_name|title }}:
{% ifmeth PUT PATCH %}
# Update {{ model_verbose_name|title|anora }}:
Make a PUT or PATCH request to this resource to update this
{{ model_verbose_name }}. The following fields may be modified:
@@ -21,5 +24,6 @@ Make a PUT or PATCH request to this resource to update this
For a PUT request, include **all** fields in the request.
For a PATCH request, include only the fields that are being modified.
{% endifmeth %}
{% include "api/_new_in_awx.md" %}

View File

@@ -2,14 +2,17 @@
### Note: starting from api v2, this resource object can be accessed via its named URL.
{% endif %}
# Retrieve {{ model_verbose_name|title }}:
{% ifmeth GET %}
# Retrieve {{ model_verbose_name|title|anora }}:
Make GET request to this resource to retrieve a single {{ model_verbose_name }}
record containing the following fields:
{% include "api/_result_fields_common.md" %}
{% endifmeth %}
# Update {{ model_verbose_name|title }}:
{% ifmeth PUT PATCH %}
# Update {{ model_verbose_name|title|anora }}:
Make a PUT or PATCH request to this resource to update this
{{ model_verbose_name }}. The following fields may be modified:
@@ -21,9 +24,12 @@ Make a PUT or PATCH request to this resource to update this
For a PUT request, include **all** fields in the request.
For a PATCH request, include only the fields that are being modified.
{% endifmeth %}
# Delete {{ model_verbose_name|title }}:
{% ifmeth DELETE %}
# Delete {{ model_verbose_name|title|anora }}:
Make a DELETE request to this resource to delete this {{ model_verbose_name }}.
{% endifmeth %}
{% include "api/_new_in_awx.md" %}

View File

@@ -1,9 +1,11 @@
# List {{ model_verbose_name_plural|title }} for this {{ parent_model_verbose_name|title }}:
{% ifmeth GET %}
# List {{ model_verbose_name_plural|title }} for {{ parent_model_verbose_name|title|anora }}:
Make a GET request to this resource to retrieve a list of
{{ model_verbose_name_plural }} associated with the selected
{{ parent_model_verbose_name }}.
{% include "api/_list_common.md" %}
{% endifmeth %}
{% include "api/_new_in_awx.md" %}

View File

@@ -1,6 +1,6 @@
{% include "api/sub_list_api_view.md" %}
# Create {{ model_verbose_name_plural|title }} for this {{ parent_model_verbose_name|title }}:
# Create {{ model_verbose_name_plural|title }} for {{ parent_model_verbose_name|title|anora }}:
Make a POST request to this resource with the following {{ model_verbose_name }}
fields to create a new {{ model_verbose_name }} associated with this
@@ -25,7 +25,7 @@ delete the associated {{ model_verbose_name }}.
}
{% else %}
# Add {{ model_verbose_name_plural|title }} for this {{ parent_model_verbose_name|title }}:
# Add {{ model_verbose_name_plural|title }} for {{ parent_model_verbose_name|title|anora }}:
Make a POST request to this resource with only an `id` field to associate an
existing {{ model_verbose_name }} with this {{ parent_model_verbose_name }}.

View File

@@ -1,12 +1,16 @@
# List Roles for this Team:
# List Roles for a Team:
{% ifmeth GET %}
Make a GET request to this resource to retrieve a list of roles associated with the selected team.
{% include "api/_list_common.md" %}
{% endifmeth %}
{% ifmeth POST %}
# Associate Roles with this Team:
Make a POST request to this resource to add or remove a role from this team. The following fields may be modified:
* `id`: The Role ID to add to the team. (int, required)
* `disassociate`: Provide if you want to remove the role. (any value, optional)
{% endifmeth %}

View File

@@ -1,12 +1,16 @@
# List Roles for this User:
# List Roles for a User:
{% ifmeth GET %}
Make a GET request to this resource to retrieve a list of roles associated with the selected user.
{% include "api/_list_common.md" %}
{% endifmeth %}
{% ifmeth POST %}
# Associate Roles with this User:
Make a POST request to this resource to add or remove a role from this user. The following fields may be modified:
* `id`: The Role ID to add to the user. (int, required)
* `disassociate`: Provide if you want to remove the role. (any value, optional)
{% endifmeth %}

View File

@@ -0,0 +1,44 @@
---
# Add categories here for generated Swagger docs; order will be respected
# in the generated document.
categories:
- name: Versioning
- name: Authentication
- name: Instances
- name: Instance Groups
- name: System Configuration
- name: Settings
- name: Dashboard
- name: Organizations
- name: Users
- name: Projects
- name: Project Updates
- name: Teams
- name: Credentials
- name: Credential Types
- name: Inventories
- name: Custom Inventory Scripts
- name: Inventory Sources
- name: Inventory Updates
- name: Groups
- name: Hosts
- name: Job Templates
- name: Jobs
- name: Job Events
- name: Job Host Summaries
- name: Ad Hoc Commands
- name: Ad Hoc Command Events
- name: System Job Templates
- name: System Jobs
- name: Schedules
- name: Roles
- name: Notification Templates
- name: Notifications
- name: Labels
- name: Unified Job Templates
- name: Unified Jobs
- name: Activity Streams
- name: Workflow Job Templates
- name: Workflow Jobs
- name: Workflow Job Template Nodes
- name: Workflow Job Nodes

View File

@@ -0,0 +1 @@
The Ansible Tower API Reference Manual provides in-depth documentation for Tower's REST API, including examples on how to integrate with it.

View File

@@ -2,7 +2,9 @@
# All Rights Reserved.
from __future__ import absolute_import, unicode_literals
from django.conf import settings
from django.conf.urls import include, url
from awx.api.swagger import SwaggerSchemaView
from awx.api.views import (
ApiRootView,
@@ -123,5 +125,9 @@ app_name = 'api'
urlpatterns = [
url(r'^$', ApiRootView.as_view(), name='api_root_view'),
url(r'^(?P<version>(v2))/', include(v2_urls)),
url(r'^(?P<version>(v1|v2))/', include(v1_urls))
url(r'^(?P<version>(v1|v2))/', include(v1_urls)),
]
if settings.SETTINGS_MODULE == 'awx.settings.development':
urlpatterns += [
url(r'^swagger/$', SwaggerSchemaView.as_view(), name='swagger_view'),
]

View File

@@ -189,6 +189,7 @@ class ApiRootView(APIView):
permission_classes = (AllowAny,)
view_name = _('REST API')
versioning_class = None
swagger_topic = 'Versioning'
def get(self, request, format=None):
''' list supported API versions '''
@@ -210,6 +211,7 @@ class ApiVersionRootView(APIView):
authentication_classes = []
permission_classes = (AllowAny,)
swagger_topic = 'Versioning'
def get(self, request, format=None):
''' list top level resources '''
@@ -275,6 +277,7 @@ class ApiV1PingView(APIView):
authentication_classes = ()
view_name = _('Ping')
new_in_210 = True
swagger_topic = 'System Configuration'
def get(self, request, format=None):
"""Return some basic information about this instance.
@@ -305,6 +308,7 @@ class ApiV1ConfigView(APIView):
permission_classes = (IsAuthenticated,)
view_name = _('Configuration')
swagger_topic = 'System Configuration'
def check_permissions(self, request):
super(ApiV1ConfigView, self).check_permissions(request)
@@ -407,6 +411,7 @@ class DashboardView(APIView):
view_name = _("Dashboard")
new_in_14 = True
swagger_topic = 'Dashboard'
def get(self, request, format=None):
''' Show Dashboard Details '''
@@ -506,6 +511,7 @@ class DashboardJobsGraphView(APIView):
view_name = _("Dashboard Jobs Graphs")
new_in_200 = True
swagger_topic = 'Jobs'
def get(self, request, format=None):
period = request.query_params.get('period', 'month')
@@ -690,6 +696,8 @@ class SchedulePreview(GenericAPIView):
class ScheduleZoneInfo(APIView):
swagger_topic = 'System Configuration'
def get(self, request):
from dateutil.zoneinfo import get_zonefile_instance
return Response(sorted(get_zonefile_instance().zones.keys()))
@@ -750,6 +758,7 @@ class AuthView(APIView):
authentication_classes = []
permission_classes = (AllowAny,)
new_in_240 = True
swagger_topic = 'Authentication'
def get(self, request):
from rest_framework.reverse import reverse
@@ -793,6 +802,7 @@ class AuthTokenView(APIView):
permission_classes = (AllowAny,)
serializer_class = AuthTokenSerializer
model = AuthToken
swagger_topic = 'Authentication'
def get_serializer(self, *args, **kwargs):
serializer = self.serializer_class(*args, **kwargs)
@@ -982,7 +992,7 @@ class OrganizationDetail(RetrieveUpdateDestroyAPIView):
def get_serializer_context(self, *args, **kwargs):
full_context = super(OrganizationDetail, self).get_serializer_context(*args, **kwargs)
if not hasattr(self, 'kwargs'):
if not hasattr(self, 'kwargs') or 'pk' not in self.kwargs:
return full_context
org_id = int(self.kwargs['pk'])
@@ -2680,7 +2690,7 @@ class InventorySourceList(ListCreateAPIView):
@property
def allowed_methods(self):
methods = super(InventorySourceList, self).allowed_methods
if get_request_version(self.request) == 1:
if get_request_version(getattr(self, 'request', None)) == 1:
methods.remove('POST')
return methods
@@ -3994,7 +4004,7 @@ class JobList(ListCreateAPIView):
@property
def allowed_methods(self):
methods = super(JobList, self).allowed_methods
if get_request_version(self.request) > 1:
if get_request_version(getattr(self, 'request', None)) > 1:
methods.remove('POST')
return methods