mirror of
https://github.com/ansible/awx.git
synced 2024-10-27 00:55:06 +03:00
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:
parent
c61efc0af8
commit
57c22c20b2
6
Makefile
6
Makefile
@ -363,6 +363,12 @@ pyflakes: reports
|
||||
pylint: reports
|
||||
@(set -o pipefail && $@ | reports/$@.report)
|
||||
|
||||
swagger: reports
|
||||
@if [ "$(VENV_BASE)" ]; then \
|
||||
. $(VENV_BASE)/awx/bin/activate; \
|
||||
fi; \
|
||||
(set -o pipefail && py.test awx/main/tests/docs --release=$(RELEASE_VERSION) | tee reports/$@.report)
|
||||
|
||||
check: flake8 pep8 # pyflakes pylint
|
||||
|
||||
TEST_DIRS ?= awx/main/tests/unit awx/main/tests/functional awx/conf/tests awx/sso/tests
|
||||
|
@ -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
89
awx/api/swagger.py
Normal 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)
|
@ -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 %}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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" %}
|
||||
|
@ -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" %}
|
||||
|
1
awx/api/templates/api/host_insights.md
Normal file
1
awx/api/templates/api/host_insights.md
Normal file
@ -0,0 +1 @@
|
||||
# List Red Hat Insights for a Host
|
@ -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
|
||||
|
@ -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 }}.
|
||||
|
@ -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 }}.
|
||||
|
@ -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:
|
||||
|
@ -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 }}.
|
||||
|
||||
|
@ -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" %}
|
||||
|
@ -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" %}
|
||||
|
@ -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" %}
|
||||
|
@ -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 }}.
|
||||
|
@ -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 %}
|
||||
|
@ -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 %}
|
||||
|
44
awx/api/templates/swagger/config.yml
Normal file
44
awx/api/templates/swagger/config.yml
Normal 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
|
1
awx/api/templates/swagger/description.md
Normal file
1
awx/api/templates/swagger/description.md
Normal 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.
|
@ -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'),
|
||||
]
|
||||
|
@ -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
|
||||
|
||||
|
0
awx/main/templatetags/__init__.py
Normal file
0
awx/main/templatetags/__init__.py
Normal file
50
awx/main/templatetags/swagger.py
Normal file
50
awx/main/templatetags/swagger.py
Normal file
@ -0,0 +1,50 @@
|
||||
import re
|
||||
from django.utils.encoding import force_unicode
|
||||
from django import template
|
||||
|
||||
register = template.Library()
|
||||
|
||||
CONSONANT_SOUND = re.compile(r'''one(![ir])''', re.IGNORECASE|re.VERBOSE) # noqa
|
||||
VOWEL_SOUND = re.compile(r'''[aeio]|u([aeiou]|[^n][^aeiou]|ni[^dmnl]|nil[^l])|h(ier|onest|onou?r|ors\b|our(!i))|[fhlmnrsx]\b''', re.IGNORECASE|re.VERBOSE) # noqa
|
||||
|
||||
|
||||
@register.filter
|
||||
def anora(text):
|
||||
# https://pypi.python.org/pypi/anora
|
||||
# < 10 lines of BSD-3 code, not worth a dependency
|
||||
text = force_unicode(text)
|
||||
anora = 'an' if not CONSONANT_SOUND.match(text) and VOWEL_SOUND.match(text) else 'a'
|
||||
return anora + ' ' + text
|
||||
|
||||
|
||||
@register.tag(name='ifmeth')
|
||||
def ifmeth(parser, token):
|
||||
"""
|
||||
Used to mark template blocks for Swagger/OpenAPI output.
|
||||
If the specified method matches the *current* method in Swagger/OpenAPI
|
||||
generation, show the block. Otherwise, the block is omitted.
|
||||
|
||||
{% ifmeth GET %}
|
||||
Make a GET request to...
|
||||
{% endifmeth %}
|
||||
|
||||
{% ifmeth PUT PATCH %}
|
||||
Make a PUT or PATCH request to...
|
||||
{% endifmeth %}
|
||||
"""
|
||||
allowed_methods = [m.upper() for m in token.split_contents()[1:]]
|
||||
nodelist = parser.parse(('endifmeth',))
|
||||
parser.delete_first_token()
|
||||
return MethodFilterNode(allowed_methods, nodelist)
|
||||
|
||||
|
||||
class MethodFilterNode(template.Node):
|
||||
def __init__(self, allowed_methods, nodelist):
|
||||
self.allowed_methods = allowed_methods
|
||||
self.nodelist = nodelist
|
||||
|
||||
def render(self, context):
|
||||
swagger_method = context.get('swagger_method')
|
||||
if not swagger_method or swagger_method.upper() in self.allowed_methods:
|
||||
return self.nodelist.render(context)
|
||||
return ''
|
0
awx/main/tests/docs/__init__.py
Normal file
0
awx/main/tests/docs/__init__.py
Normal file
13
awx/main/tests/docs/conftest.py
Normal file
13
awx/main/tests/docs/conftest.py
Normal file
@ -0,0 +1,13 @@
|
||||
from awx.main.tests.functional.conftest import * # noqa
|
||||
|
||||
|
||||
def pytest_addoption(parser):
|
||||
parser.addoption("--release", action="store", help="a release version number, e.g., 3.3.0")
|
||||
|
||||
|
||||
def pytest_generate_tests(metafunc):
|
||||
# This is called for every test. Only get/set command line arguments
|
||||
# if the argument is specified in the list of test "fixturenames".
|
||||
option_value = metafunc.config.option.release
|
||||
if 'release' in metafunc.fixturenames and option_value is not None:
|
||||
metafunc.parametrize("release", [option_value])
|
95
awx/main/tests/docs/test_swagger_generation.py
Normal file
95
awx/main/tests/docs/test_swagger_generation.py
Normal file
@ -0,0 +1,95 @@
|
||||
import json
|
||||
import yaml
|
||||
import os
|
||||
|
||||
from coreapi.compat import force_bytes
|
||||
from django.conf import settings
|
||||
from openapi_codec.encode import generate_swagger_object
|
||||
import pytest
|
||||
|
||||
import awx
|
||||
from awx.api.versioning import drf_reverse
|
||||
|
||||
|
||||
config_dest = os.sep.join([
|
||||
os.path.realpath(os.path.dirname(awx.__file__)),
|
||||
'api', 'templates', 'swagger'
|
||||
])
|
||||
config_file = os.sep.join([config_dest, 'config.yml'])
|
||||
description_file = os.sep.join([config_dest, 'description.md'])
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
class TestSwaggerGeneration():
|
||||
"""
|
||||
This class is used to generate a Swagger/OpenAPI document for the awx
|
||||
API. A _prepare fixture generates a JSON blob containing OpenAPI data,
|
||||
individual tests have the ability modify the payload.
|
||||
|
||||
Finally, the JSON content is written to a file, `swagger.json`, in the
|
||||
current working directory.
|
||||
|
||||
$ py.test test_swagger_generation.py --version 3.3.0
|
||||
|
||||
To customize the `info.description` in the generated OpenAPI document,
|
||||
modify the text in `awx.api.templates.swagger.description.md`
|
||||
"""
|
||||
JSON = {}
|
||||
|
||||
@pytest.fixture(autouse=True, scope='function')
|
||||
def _prepare(self, get, admin):
|
||||
if not self.__class__.JSON:
|
||||
url = drf_reverse('api:swagger_view') + '?format=openapi'
|
||||
response = get(url, user=admin)
|
||||
data = generate_swagger_object(response.data)
|
||||
data.update(response.accepted_renderer.get_customizations() or {})
|
||||
self.__class__.JSON = data
|
||||
|
||||
def _lookup_display_name(self, method, path):
|
||||
return path
|
||||
|
||||
def test_transform_metadata(self, release):
|
||||
"""
|
||||
This test takes the JSON output from the swagger endpoint and applies
|
||||
various transformations to it.
|
||||
"""
|
||||
self.__class__.JSON['info']['version'] = release
|
||||
self.__class__.JSON['host'] = None
|
||||
self.__class__.JSON['schemes'] = ['https']
|
||||
self.__class__.JSON['produces'] = ['application/json']
|
||||
self.__class__.JSON['consumes'] = ['application/json']
|
||||
|
||||
# Inject a top-level description into the OpenAPI document
|
||||
if os.path.exists(description_file):
|
||||
with open(description_file, 'r') as f:
|
||||
self.__class__.JSON['info']['description'] = f.read()
|
||||
|
||||
# Write tags in the order we want them sorted
|
||||
if os.path.exists(config_file):
|
||||
with open(config_file, 'r') as f:
|
||||
config = yaml.load(f.read())
|
||||
for category in config.get('categories', []):
|
||||
tag = {'name': category['name']}
|
||||
if 'description' in category:
|
||||
tag['description'] = category['description']
|
||||
self.__class__.JSON.setdefault('tags', []).append(tag)
|
||||
|
||||
revised_paths = {}
|
||||
for path, node in self.__class__.JSON['paths'].items():
|
||||
# change {version} in paths to the actual default API version (e.g., v2)
|
||||
revised_paths[path.replace(
|
||||
'{version}',
|
||||
settings.REST_FRAMEWORK['DEFAULT_VERSION']
|
||||
)] = node
|
||||
for method in node:
|
||||
if 'description' in node[method]:
|
||||
# Pop off the first line and use that as the summary
|
||||
lines = node[method]['description'].splitlines()
|
||||
node[method]['summary'] = lines.pop(0).strip('#:')
|
||||
node[method]['description'] = '\n'.join(lines)
|
||||
self.__class__.JSON['paths'] = revised_paths
|
||||
|
||||
@classmethod
|
||||
def teardown_class(cls):
|
||||
with open('swagger.json', 'w') as f:
|
||||
f.write(force_bytes(json.dumps(cls.JSON)))
|
@ -1,4 +1,3 @@
|
||||
|
||||
# Python
|
||||
import pytest
|
||||
import mock
|
||||
|
@ -218,6 +218,7 @@ TEMPLATES = [
|
||||
('django.template.loaders.filesystem.Loader',
|
||||
'django.template.loaders.app_directories.Loader',),
|
||||
)],
|
||||
'builtins': ['awx.main.templatetags.swagger'],
|
||||
},
|
||||
'DIRS': [
|
||||
os.path.join(BASE_DIR, 'templates'),
|
||||
|
@ -101,6 +101,8 @@ if 'django_jenkins' in INSTALLED_APPS:
|
||||
PEP8_RCFILE = "setup.cfg"
|
||||
PYLINT_RCFILE = ".pylintrc"
|
||||
|
||||
INSTALLED_APPS += ('rest_framework_swagger',)
|
||||
|
||||
# Much faster than the default
|
||||
# https://docs.djangoproject.com/en/1.6/topics/auth/passwords/#how-django-stores-passwords
|
||||
PASSWORD_HASHERS = (
|
||||
|
@ -1,4 +1,5 @@
|
||||
django-debug-toolbar==1.5
|
||||
django-rest-swagger
|
||||
pprofile
|
||||
ipython==5.2.1
|
||||
unittest2
|
||||
|
@ -7,6 +7,7 @@ env:
|
||||
- AWX_BUILD_TARGET=test
|
||||
- AWX_BUILD_TARGET=ui-test-ci
|
||||
- AWX_BUILD_TARGET="flake8 jshint"
|
||||
- AWX_BUILD_TARGET="swagger"
|
||||
|
||||
branches:
|
||||
only:
|
||||
|
Loading…
Reference in New Issue
Block a user