mirror of
https://github.com/ansible/awx.git
synced 2026-01-12 02:19:58 -03:30
Update browsable API built-in documentation to use templates.
This commit is contained in:
parent
538097ed8e
commit
057e7bad59
@ -2,12 +2,14 @@
|
||||
# All Rights Reserved.
|
||||
|
||||
# Python
|
||||
import inspect
|
||||
import json
|
||||
|
||||
# Django
|
||||
from django.http import HttpResponse, Http404
|
||||
from django.contrib.auth.models import User
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.template.loader import render_to_string
|
||||
from django.utils.timezone import now
|
||||
|
||||
# Django REST Framework
|
||||
@ -15,31 +17,71 @@ from rest_framework.exceptions import PermissionDenied
|
||||
from rest_framework import generics
|
||||
from rest_framework.response import Response
|
||||
from rest_framework import status
|
||||
from rest_framework import views
|
||||
|
||||
# AWX
|
||||
from awx.main.models import *
|
||||
from awx.main.utils import *
|
||||
|
||||
# FIXME: machinery for auto-adding audit trail logs to all CREATE/EDITS
|
||||
|
||||
class ListAPIView(generics.ListAPIView):
|
||||
# Base class for a read-only list view.
|
||||
__all__ = ['APIView', 'GenericAPIView', 'ListAPIView', 'ListCreateAPIView',
|
||||
'SubListAPIView', 'SubListCreateAPIView', 'RetrieveAPIView',
|
||||
'RetrieveUpdateAPIView', 'RetrieveUpdateDestroyAPIView']
|
||||
|
||||
class APIView(views.APIView):
|
||||
|
||||
def get_description_context(self):
|
||||
return {
|
||||
'docstring': type(self).__doc__ or '',
|
||||
}
|
||||
|
||||
def get_description(self, html=False):
|
||||
template_list = []
|
||||
for klass in inspect.getmro(type(self)):
|
||||
template_basename = camelcase_to_underscore(klass.__name__)
|
||||
template_list.append('main/%s.md' % template_basename)
|
||||
context = self.get_description_context()
|
||||
return render_to_string(template_list, context)
|
||||
|
||||
class GenericAPIView(generics.GenericAPIView, APIView):
|
||||
# Base class for all model-based views.
|
||||
|
||||
# Subclasses should define:
|
||||
# model = ModelClass
|
||||
# serializer_class = SerializerClass
|
||||
|
||||
def get_queryset(self):
|
||||
return self.request.user.get_queryset(self.model)
|
||||
|
||||
def get_description_vars(self):
|
||||
return {
|
||||
def get_description_context(self):
|
||||
# Set instance attributes needed to get serializer metadata.
|
||||
if not hasattr(self, 'request'):
|
||||
self.request = None
|
||||
if not hasattr(self, 'format_kwarg'):
|
||||
self.format_kwarg = 'format'
|
||||
d = super(GenericAPIView, self).get_description_context()
|
||||
d.update({
|
||||
'model_verbose_name': unicode(self.model._meta.verbose_name),
|
||||
'model_verbose_name_plural': unicode(self.model._meta.verbose_name_plural),
|
||||
}
|
||||
'serializer_fields': self.get_serializer().metadata(),
|
||||
})
|
||||
return d
|
||||
|
||||
def get_description(self, html=False):
|
||||
s = 'Use a GET request to retrieve a list of %(model_verbose_name_plural)s.'
|
||||
return s % self.get_description_vars()
|
||||
class ListAPIView(generics.ListAPIView, GenericAPIView):
|
||||
# Base class for a read-only list view.
|
||||
|
||||
def get_description_context(self):
|
||||
opts = self.model._meta
|
||||
if 'username' in opts.get_all_field_names():
|
||||
order_field = 'username'
|
||||
else:
|
||||
order_field = 'name'
|
||||
d = super(ListAPIView, self).get_description_context()
|
||||
d.update({
|
||||
'order_field': order_field,
|
||||
})
|
||||
return d
|
||||
|
||||
def get_queryset(self):
|
||||
return self.request.user.get_queryset(self.model)
|
||||
|
||||
class ListCreateAPIView(ListAPIView, generics.ListCreateAPIView):
|
||||
# Base class for a list view that allows creating new objects.
|
||||
@ -49,11 +91,6 @@ class ListCreateAPIView(ListAPIView, generics.ListCreateAPIView):
|
||||
if isinstance(obj, PrimordialModel):
|
||||
obj.created_by = self.request.user
|
||||
|
||||
def get_description(self, html=False):
|
||||
s = 'Use a GET request to retrieve a list of %(model_verbose_name_plural)s.'
|
||||
s2 = 'Use a POST request with required %(model_verbose_name)s fields to create a new %(model_verbose_name)s.'
|
||||
return '\n\n'.join([s, s2]) % self.get_description_vars()
|
||||
|
||||
class SubListAPIView(ListAPIView):
|
||||
# Base class for a read-only sublist view.
|
||||
|
||||
@ -66,18 +103,14 @@ class SubListAPIView(ListAPIView):
|
||||
# to view sublist):
|
||||
# parent_access = 'read'
|
||||
|
||||
def get_description_vars(self):
|
||||
d = super(SubListAPIView, self).get_description_vars()
|
||||
def get_description_context(self):
|
||||
d = super(SubListAPIView, self).get_description_context()
|
||||
d.update({
|
||||
'parent_model_verbose_name': unicode(self.parent_model._meta.verbose_name),
|
||||
'parent_model_verbose_name_plural': unicode(self.parent_model._meta.verbose_name_plural),
|
||||
})
|
||||
return d
|
||||
|
||||
def get_description(self, html=False):
|
||||
s = 'Use a GET request to retrieve a list of %(model_verbose_name_plural)s associated with the selected %(parent_model_verbose_name)s.'
|
||||
return s % self.get_description_vars()
|
||||
|
||||
def get_parent_object(self):
|
||||
parent_filter = {
|
||||
self.lookup_field: self.kwargs.get(self.lookup_field, None),
|
||||
@ -109,16 +142,12 @@ class SubListCreateAPIView(SubListAPIView, ListCreateAPIView):
|
||||
# sub_obj requires a foreign key to the parent):
|
||||
# parent_key = 'field_on_model_referring_to_parent'
|
||||
|
||||
def get_description(self, html=False):
|
||||
s = 'Use a GET request to retrieve a list of %(model_verbose_name_plural)s associated with the selected %(parent_model_verbose_name)s.'
|
||||
s2 = 'Use a POST request with required %(model_verbose_name)s fields to create a new %(model_verbose_name)s.'
|
||||
if getattr(self, 'parent_key', None):
|
||||
s3 = 'Use a POST request with an `id` field and `disassociate` set to delete the associated %(model_verbose_name)s.'
|
||||
s4 = ''
|
||||
else:
|
||||
s3 = 'Use a POST request with only an `id` field to associate an existing %(model_verbose_name)s with this %(parent_model_verbose_name)s.'
|
||||
s4 = 'Use a POST request with an `id` field and `disassociate` set to remove the %(model_verbose_name)s from this %(parent_model_verbose_name)s without deleting the %(model_verbose_name)s.'
|
||||
return '\n\n'.join(filter(None, [s, s2, s3, s4])) % self.get_description_vars()
|
||||
def get_description_context(self):
|
||||
d = super(SubListCreateAPIView, self).get_description_context()
|
||||
d.update({
|
||||
'parent_key': getattr(self, 'parent_key', None),
|
||||
})
|
||||
return d
|
||||
|
||||
def create(self, request, *args, **kwargs):
|
||||
# If the object ID was not specified, it probably doesn't exist in the
|
||||
@ -148,7 +177,8 @@ class SubListCreateAPIView(SubListAPIView, ListCreateAPIView):
|
||||
if not request.user.can_access(self.model, 'add', serializer.init_data):
|
||||
raise PermissionDenied()
|
||||
|
||||
# save the object through the serializer, reload and returned the saved object deserialized
|
||||
# save the object through the serializer, reload and returned the saved
|
||||
# object deserialized
|
||||
obj = serializer.save()
|
||||
serializer = self.serializer_class(obj)
|
||||
|
||||
@ -168,17 +198,19 @@ class SubListCreateAPIView(SubListAPIView, ListCreateAPIView):
|
||||
return response
|
||||
sub_id = response.data['id']
|
||||
data = response.data
|
||||
try:
|
||||
location = response['Location']
|
||||
except KeyError:
|
||||
location = None
|
||||
created = True
|
||||
|
||||
# Retrive the sub object (whether created or by ID).
|
||||
try:
|
||||
sub = self.model.objects.get(pk=sub_id)
|
||||
except self.model.DoesNotExist:
|
||||
data = dict(msg='Object with id %s cannot be found' % sub_id)
|
||||
return Response(data, status=status.HTTP_400_BAD_REQUEST)
|
||||
sub = get_object_or_400(self.model, pk=sub_id)
|
||||
|
||||
# Verify we have permission to attach.
|
||||
if not request.user.can_access(self.parent_model, 'attach', parent, sub, self.relationship, data, skip_sub_obj_read_check=created):
|
||||
if not request.user.can_access(self.parent_model, 'attach', parent, sub,
|
||||
self.relationship, data,
|
||||
skip_sub_obj_read_check=created):
|
||||
raise PermissionDenied()
|
||||
|
||||
# Attach the object to the collection.
|
||||
@ -186,7 +218,10 @@ class SubListCreateAPIView(SubListAPIView, ListCreateAPIView):
|
||||
relationship.add(sub)
|
||||
|
||||
if created:
|
||||
return Response(data, status=status.HTTP_201_CREATED)
|
||||
headers = {}
|
||||
if location:
|
||||
headers['Location'] = location
|
||||
return Response(data, status=status.HTTP_201_CREATED, headers=headers)
|
||||
else:
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
@ -199,14 +234,10 @@ class SubListCreateAPIView(SubListAPIView, ListCreateAPIView):
|
||||
parent = self.get_parent_object()
|
||||
parent_key = getattr(self, 'parent_key', None)
|
||||
relationship = getattr(parent, self.relationship)
|
||||
sub = get_object_or_400(self.model, pk=sub_id)
|
||||
|
||||
try:
|
||||
sub = self.model.objects.get(pk=sub_id)
|
||||
except self.model.DoesNotExist:
|
||||
data = dict(msg='Object with id %s cannot be found' % sub_id)
|
||||
return Response(data, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
if not request.user.can_access(self.parent_model, 'unattach', parent, sub, self.relationship):
|
||||
if not request.user.can_access(self.parent_model, 'unattach', parent,
|
||||
sub, self.relationship):
|
||||
raise PermissionDenied()
|
||||
|
||||
if parent_key:
|
||||
@ -224,19 +255,29 @@ class SubListCreateAPIView(SubListAPIView, ListCreateAPIView):
|
||||
else:
|
||||
return self.attach(request, *args, **kwargs)
|
||||
|
||||
class RetrieveAPIView(generics.RetrieveAPIView):
|
||||
class RetrieveAPIView(generics.RetrieveAPIView, GenericAPIView):
|
||||
pass
|
||||
|
||||
class RetrieveUpdateDestroyAPIView(RetrieveAPIView, generics.RetrieveUpdateDestroyAPIView):
|
||||
class RetrieveUpdateAPIView(RetrieveAPIView, generics.RetrieveUpdateAPIView):
|
||||
|
||||
def pre_save(self, obj):
|
||||
super(RetrieveUpdateDestroyAPIView, self).pre_save(obj)
|
||||
super(RetrieveUpdateAPIView, self).pre_save(obj)
|
||||
if isinstance(obj, PrimordialModel):
|
||||
obj.created_by = self.request.user
|
||||
|
||||
def update(self, request, *args, **kwargs):
|
||||
self.update_filter(request, *args, **kwargs)
|
||||
return super(RetrieveUpdateAPIView, self).update(request, *args, **kwargs)
|
||||
|
||||
def update_filter(self, request, *args, **kwargs):
|
||||
''' scrub any fields the user cannot/should not put/patch, based on user context. This runs after read-only serialization filtering '''
|
||||
pass
|
||||
|
||||
class RetrieveUpdateDestroyAPIView(RetrieveUpdateAPIView, generics.RetrieveUpdateDestroyAPIView):
|
||||
|
||||
def destroy(self, request, *args, **kwargs):
|
||||
# somewhat lame that delete has to call it's own permissions check
|
||||
obj = self.model.objects.get(pk=kwargs['pk'])
|
||||
obj = self.get_object()
|
||||
# FIXME: Why isn't the active check being caught earlier by RBAC?
|
||||
if getattr(obj, 'active', True) == False:
|
||||
raise Http404()
|
||||
@ -247,13 +288,5 @@ class RetrieveUpdateDestroyAPIView(RetrieveAPIView, generics.RetrieveUpdateDestr
|
||||
if hasattr(obj, 'mark_inactive'):
|
||||
obj.mark_inactive()
|
||||
else:
|
||||
raise Exception("InternalError: destroy() not implemented yet for %s" % obj)
|
||||
raise NotImplementedError('destroy() not implemented yet for %s' % obj)
|
||||
return HttpResponse(status=204)
|
||||
|
||||
def update(self, request, *args, **kwargs):
|
||||
self.update_filter(request, *args, **kwargs)
|
||||
return super(RetrieveUpdateDestroyAPIView, self).update(request, *args, **kwargs)
|
||||
|
||||
def update_filter(self, request, *args, **kwargs):
|
||||
''' scrub any fields the user cannot/should not put/patch, based on user context. This runs after read-only serialization filtering '''
|
||||
pass
|
||||
|
||||
@ -147,14 +147,23 @@ class Inventory(CommonModel):
|
||||
verbose_name_plural = _('inventories')
|
||||
unique_together = (("name", "organization"),)
|
||||
|
||||
organization = models.ForeignKey(Organization, null=False, related_name='inventories')
|
||||
organization = models.ForeignKey(
|
||||
Organization,
|
||||
null=False,
|
||||
related_name='inventories',
|
||||
help_text=_('Organization containing this inventory.'),
|
||||
)
|
||||
variables = models.TextField(
|
||||
blank=True,
|
||||
default='',
|
||||
null=True,
|
||||
help_text=_('Variables in JSON or YAML format.'),
|
||||
)
|
||||
has_active_failures = models.BooleanField(default=False, editable=False)
|
||||
has_active_failures = models.BooleanField(
|
||||
default=False,
|
||||
editable=False,
|
||||
help_text=_('Flag indicating whether any hosts in this inventory have failed.'),
|
||||
)
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('main:inventory_detail', args=(self.pk,))
|
||||
|
||||
@ -11,8 +11,11 @@ import yaml
|
||||
from django.contrib.auth.models import User
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from django.utils.datastructures import SortedDict
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
# Django REST Framework
|
||||
from rest_framework.compat import get_concrete_model
|
||||
from rest_framework import serializers
|
||||
|
||||
# AWX
|
||||
@ -34,7 +37,7 @@ SUMMARIZABLE_FIELDS = (
|
||||
class BaseSerializer(serializers.ModelSerializer):
|
||||
|
||||
# add the URL and related resources
|
||||
url = serializers.SerializerMethodField('get_absolute_url')
|
||||
url = serializers.SerializerMethodField('get_url')
|
||||
related = serializers.SerializerMethodField('get_related')
|
||||
summary_fields = serializers.SerializerMethodField('get_summary_fields')
|
||||
|
||||
@ -42,14 +45,34 @@ class BaseSerializer(serializers.ModelSerializer):
|
||||
created = serializers.SerializerMethodField('get_created')
|
||||
active = serializers.SerializerMethodField('get_active')
|
||||
|
||||
def get_absolute_url(self, obj):
|
||||
def get_fields(self):
|
||||
opts = get_concrete_model(self.opts.model)._meta
|
||||
ret = super(BaseSerializer, self).get_fields()
|
||||
for key, field in ret.items():
|
||||
if key == 'id' and not getattr(field, 'help_text', None):
|
||||
field.help_text = 'Database ID for this %s.' % unicode(opts.verbose_name)
|
||||
elif key == 'url':
|
||||
field.help_text = 'URL for this %s.' % unicode(opts.verbose_name)
|
||||
field.type_label = 'string'
|
||||
elif key == 'related':
|
||||
field.help_text = 'Data structure with URLs of related resources.'
|
||||
field.type_label = 'object'
|
||||
elif key == 'summary_fields':
|
||||
field.help_text = 'Data structure with name/description for related resources.'
|
||||
field.type_label = 'object'
|
||||
elif key == 'created':
|
||||
field.help_text = 'Timestamp when this %s was created.' % unicode(opts.verbose_name)
|
||||
field.type_label = 'datetime'
|
||||
return ret
|
||||
|
||||
def get_url(self, obj):
|
||||
if isinstance(obj, User):
|
||||
return reverse('main:user_detail', args=(obj.pk,))
|
||||
else:
|
||||
return obj.get_absolute_url()
|
||||
|
||||
def get_related(self, obj):
|
||||
res = dict()
|
||||
res = SortedDict()
|
||||
if getattr(obj, 'created_by', None):
|
||||
res['created_by'] = reverse('main:user_detail', args=(obj.created_by.pk,))
|
||||
return res
|
||||
@ -57,12 +80,12 @@ class BaseSerializer(serializers.ModelSerializer):
|
||||
def get_summary_fields(self, obj):
|
||||
# return the names (at least) for various fields, so we don't have to write this
|
||||
# method for each object.
|
||||
summary_fields = {}
|
||||
summary_fields = SortedDict()
|
||||
for fk in SUMMARIZABLE_FKS:
|
||||
try:
|
||||
fkval = getattr(obj, fk, None)
|
||||
if fkval is not None:
|
||||
summary_fields[fk] = {}
|
||||
summary_fields[fk] = SortedDict()
|
||||
for field in SUMMARIZABLE_FIELDS:
|
||||
fval = getattr(fkval, field, None)
|
||||
if fval is not None:
|
||||
@ -86,13 +109,13 @@ class BaseSerializer(serializers.ModelSerializer):
|
||||
|
||||
class UserSerializer(BaseSerializer):
|
||||
|
||||
password = serializers.WritableField(required=False, default='')
|
||||
password = serializers.WritableField(required=False, default='',
|
||||
help_text='Write-only field used to change the password.')
|
||||
|
||||
class Meta:
|
||||
model = User
|
||||
fields = ('id', 'url', 'related', 'created', 'username', 'first_name',
|
||||
'last_name', 'email', 'is_active', 'is_superuser',
|
||||
'password')
|
||||
'last_name', 'email', 'is_superuser', 'password')
|
||||
|
||||
def to_native(self, obj):
|
||||
ret = super(UserSerializer, self).to_native(obj)
|
||||
@ -152,7 +175,7 @@ class OrganizationSerializer(BaseSerializer):
|
||||
|
||||
class ProjectSerializer(BaseSerializer):
|
||||
|
||||
playbooks = serializers.Field(source='playbooks')
|
||||
playbooks = serializers.Field(source='playbooks', help_text='Array ')
|
||||
|
||||
class Meta:
|
||||
model = Project
|
||||
@ -163,7 +186,7 @@ class ProjectSerializer(BaseSerializer):
|
||||
res.update(dict(
|
||||
organizations = reverse('main:project_organizations_list', args=(obj.pk,)),
|
||||
teams = reverse('main:project_teams_list', args=(obj.pk,)),
|
||||
playbooks = reverse('main:project_detail_playbooks', args=(obj.pk,)),
|
||||
playbooks = reverse('main:project_playbooks', args=(obj.pk,)),
|
||||
))
|
||||
return res
|
||||
|
||||
@ -190,7 +213,7 @@ class BaseSerializerWithVariables(BaseSerializer):
|
||||
|
||||
def validate_variables(self, attrs, source):
|
||||
try:
|
||||
json.loads(attrs[source].strip() or '{}')
|
||||
json.loads(attrs.get(source, '').strip() or '{}')
|
||||
except ValueError:
|
||||
try:
|
||||
yaml.safe_load(attrs[source])
|
||||
@ -211,8 +234,9 @@ class InventorySerializer(BaseSerializerWithVariables):
|
||||
hosts = reverse('main:inventory_hosts_list', args=(obj.pk,)),
|
||||
groups = reverse('main:inventory_groups_list', args=(obj.pk,)),
|
||||
root_groups = reverse('main:inventory_root_groups_list', args=(obj.pk,)),
|
||||
variable_data = reverse('main:inventory_variable_detail', args=(obj.pk,)),
|
||||
organization = reverse('main:organization_detail', args=(obj.organization.pk,)),
|
||||
variable_data = reverse('main:inventory_variable_data', args=(obj.pk,)),
|
||||
script = reverse('main:inventory_script_view', args=(obj.pk,)),
|
||||
organization = reverse('main:organization_detail', args=(obj.organization.pk,)),
|
||||
))
|
||||
return res
|
||||
|
||||
@ -225,7 +249,7 @@ class HostSerializer(BaseSerializerWithVariables):
|
||||
def get_related(self, obj):
|
||||
res = super(HostSerializer, self).get_related(obj)
|
||||
res.update(dict(
|
||||
variable_data = reverse('main:host_variable_detail', args=(obj.pk,)),
|
||||
variable_data = reverse('main:host_variable_data', args=(obj.pk,)),
|
||||
inventory = reverse('main:inventory_detail', args=(obj.inventory.pk,)),
|
||||
groups = reverse('main:host_groups_list', args=(obj.pk,)),
|
||||
all_groups = reverse('main:host_all_groups_list', args=(obj.pk,)),
|
||||
@ -247,7 +271,7 @@ class GroupSerializer(BaseSerializerWithVariables):
|
||||
def get_related(self, obj):
|
||||
res = super(GroupSerializer, self).get_related(obj)
|
||||
res.update(dict(
|
||||
variable_data = reverse('main:group_variable_detail', args=(obj.pk,)),
|
||||
variable_data = reverse('main:group_variable_data', args=(obj.pk,)),
|
||||
hosts = reverse('main:group_hosts_list', args=(obj.pk,)),
|
||||
children = reverse('main:group_children_list', args=(obj.pk,)),
|
||||
all_hosts = reverse('main:group_all_hosts_list', args=(obj.pk,)),
|
||||
|
||||
57
awx/main/templates/main/_list_common.md
Normal file
57
awx/main/templates/main/_list_common.md
Normal file
@ -0,0 +1,57 @@
|
||||
The resulting data structure contains:
|
||||
|
||||
{
|
||||
"count": 99,
|
||||
"next": null,
|
||||
"previous": null,
|
||||
"results": [
|
||||
...
|
||||
]
|
||||
}
|
||||
|
||||
The `count` field indicates the total number of {{ model_verbose_name_plural }}
|
||||
found for the given query. The `next` and `previous` fields provides links to
|
||||
additional results if there are more than will fit on a single page. The
|
||||
`results` list contains zero or more {{ model_verbose_name }} records.
|
||||
|
||||
## Results
|
||||
|
||||
Each {{ model_verbose_name }} data structure includes the following fields:
|
||||
|
||||
{% include "main/_result_fields_common.md" %}
|
||||
|
||||
## Sorting
|
||||
|
||||
To specify that {{ model_verbose_name_plural }} are returned in a particular
|
||||
order, use the `order_by` query string parameter on the GET request.
|
||||
|
||||
?order_by={{ order_field }}
|
||||
|
||||
Prefix the field name with a dash `-` to sort in reverse:
|
||||
|
||||
?order_by=-{{ order_field }}
|
||||
|
||||
## Pagination
|
||||
|
||||
Use the `page_size` query string parameter to change the number of results
|
||||
returned for each request. Use the `page` query string parameter to retrieve
|
||||
a particular page of results.
|
||||
|
||||
?page_size=100&page=2
|
||||
|
||||
The `previous` and `next` links returned with the results will set these query
|
||||
string parameters automatically.
|
||||
|
||||
## Filtering
|
||||
|
||||
Any additional query string parameters may be used to filter the list of
|
||||
results returned to those matching a given value. Only fields that exist in
|
||||
the database can be used for filtering.
|
||||
|
||||
?{{ order_field }}=value
|
||||
|
||||
Field lookups may also be used for slightly more advanced queries, for example:
|
||||
|
||||
?{{ order_field }}__startswith=A
|
||||
?{{ order_field }}__endsswith=C
|
||||
?{{ order_field }}__contains=ABC
|
||||
6
awx/main/templates/main/_result_fields_common.md
Normal file
6
awx/main/templates/main/_result_fields_common.md
Normal file
@ -0,0 +1,6 @@
|
||||
{% for fn, fm in serializer_fields.items %}{% spaceless %}
|
||||
{% if not write_only or not fm.read_only %}
|
||||
* `{{ fn }}`: {{ fm.help_text|capfirst }} ({{ fm.type }}{% if fm.required %}, required{% endif %}{% if fm.read_only %}, read-only{% endif %})
|
||||
{% endif %}
|
||||
{% endspaceless %}
|
||||
{% endfor %}
|
||||
4
awx/main/templates/main/api_root_view.md
Normal file
4
awx/main/templates/main/api_root_view.md
Normal file
@ -0,0 +1,4 @@
|
||||
The root of the AWX REST API.
|
||||
|
||||
Make a GET request to this resource to obtain information about the available
|
||||
API versions.
|
||||
12
awx/main/templates/main/api_v1_config_view.md
Normal file
12
awx/main/templates/main/api_v1_config_view.md
Normal file
@ -0,0 +1,12 @@
|
||||
Site configuration settings and general information.
|
||||
|
||||
Make a GET request to this resource to retrieve the configuration containing
|
||||
the following fields (some fields may not be visible to all users):
|
||||
|
||||
* `project_base_dir`: Path on the server where projects and playbooks are \
|
||||
stored.
|
||||
* `project_local_paths`: List of directories beneath `project_base_dir` to
|
||||
use when creating/editing a project.
|
||||
* `time_zone`: The configured time zone for the server.
|
||||
* `license_info`: Information about the current license.
|
||||
* `version`: Version of AWX package installed.
|
||||
4
awx/main/templates/main/api_v1_root_view.md
Normal file
4
awx/main/templates/main/api_v1_root_view.md
Normal file
@ -0,0 +1,4 @@
|
||||
Version 1 of the AWX REST API.
|
||||
|
||||
Make a GET request to this resource to obtain a list of all child resources
|
||||
available via the API.
|
||||
1
awx/main/templates/main/api_view.md
Normal file
1
awx/main/templates/main/api_view.md
Normal file
@ -0,0 +1 @@
|
||||
{{ docstring }}
|
||||
23
awx/main/templates/main/auth_token_view.md
Normal file
23
awx/main/templates/main/auth_token_view.md
Normal file
@ -0,0 +1,23 @@
|
||||
Make a POST request to this resource with `username` and `password` fields to
|
||||
obtain an authentication token to use for subsequent requests.
|
||||
|
||||
Example JSON to POST (content type is `application/json`):
|
||||
|
||||
{"username": "user", "password": "my pass"}
|
||||
|
||||
Example form data to post (content type is `application/x-www-form-urlencoded`):
|
||||
|
||||
username=user&password=my%20pass
|
||||
|
||||
If the username and password provided are valid, the response will contain a
|
||||
`token` field with the authentication token to use:
|
||||
|
||||
{"token": "8f17825cf08a7efea124f2638f3896f6637f8745"}
|
||||
|
||||
Otherwise, the response will indicate the error that occurred and return a 4xx
|
||||
status code.
|
||||
|
||||
For subsequent requests, pass the token via the HTTP `Authenticate` request
|
||||
header:
|
||||
|
||||
Authenticate: Token 8f17825cf08a7efea124f2638f3896f6637f8745
|
||||
9
awx/main/templates/main/base_variable_data.md
Normal file
9
awx/main/templates/main/base_variable_data.md
Normal file
@ -0,0 +1,9 @@
|
||||
# Retrieve {{ model_verbose_name|title }} Variable Data:
|
||||
|
||||
Make a GET request to this resource to retrieve all variables defined for this
|
||||
{{ model_verbose_name }}.
|
||||
|
||||
# Update {{ model_verbose_name|title }} Variable Data:
|
||||
|
||||
Make a PUT request to this resource to update variables defined for this
|
||||
{{ model_verbose_name }}.
|
||||
7
awx/main/templates/main/group_all_hosts_list.md
Normal file
7
awx/main/templates/main/group_all_hosts_list.md
Normal file
@ -0,0 +1,7 @@
|
||||
# List All {{ model_verbose_name_plural|title }} for this {{ parent_model_verbose_name|title }}:
|
||||
|
||||
Make a GET request to this resource to retrieve a list of all
|
||||
{{ model_verbose_name_plural }} directly or indirectly belonging to this
|
||||
{{ parent_model_verbose_name }}.
|
||||
|
||||
{% include "main/_list_common.md" %}
|
||||
7
awx/main/templates/main/host_all_groups_list.md
Normal file
7
awx/main/templates/main/host_all_groups_list.md
Normal file
@ -0,0 +1,7 @@
|
||||
# List All {{ model_verbose_name_plural|title }} for this {{ parent_model_verbose_name|title }}:
|
||||
|
||||
Make a GET request to this resource to retrieve a list of all
|
||||
{{ model_verbose_name_plural }} of which the selected
|
||||
{{ parent_model_verbose_name }} is directly or indirectly a member.
|
||||
|
||||
{% include "main/_list_common.md" %}
|
||||
7
awx/main/templates/main/inventory_root_groups_list.md
Normal file
7
awx/main/templates/main/inventory_root_groups_list.md
Normal file
@ -0,0 +1,7 @@
|
||||
# List Root {{ model_verbose_name_plural|title }} for this {{ parent_model_verbose_name|title }}:
|
||||
|
||||
Make a GET request to this resource to retrieve a list of root (top-level)
|
||||
{{ model_verbose_name_plural }} associated with this
|
||||
{{ parent_model_verbose_name }}.
|
||||
|
||||
{% include "main/_list_common.md" %}
|
||||
11
awx/main/templates/main/inventory_script_view.md
Normal file
11
awx/main/templates/main/inventory_script_view.md
Normal file
@ -0,0 +1,11 @@
|
||||
Generate inventory group and host data as needed for an inventory script.
|
||||
|
||||
Make a GET request to this resource without query parameters to retrieve a JSON
|
||||
object containing groups, including the hosts, children and variables for each
|
||||
group. The response data is equivalent to that returned by passing the
|
||||
`--list` argument to an inventory script.
|
||||
|
||||
Make a GET request to this resource with a query string similar to
|
||||
`?host=HOSTNAME` to retrieve a JSON object containing host variables for the
|
||||
specified host. The response data is equivalent to that returned by passing
|
||||
the `--host HOSTNAME` argument to an inventory script.
|
||||
10
awx/main/templates/main/job_cancel.md
Normal file
10
awx/main/templates/main/job_cancel.md
Normal file
@ -0,0 +1,10 @@
|
||||
# Cancel Job
|
||||
|
||||
Make a GET request to this resource to determine if the job can be cancelled.
|
||||
The response will include the following field:
|
||||
|
||||
* `can_cancel`: Indicates whether this job can be canceled (boolean, read-only)
|
||||
|
||||
Make a POST request to this resource to cancel a pending or running job. The
|
||||
response status code will be 202 if successful, or 405 if the job cannot be
|
||||
canceled.
|
||||
5
awx/main/templates/main/job_list.md
Normal file
5
awx/main/templates/main/job_list.md
Normal file
@ -0,0 +1,5 @@
|
||||
{% include "main/list_create_api_view.md" %}
|
||||
|
||||
If the `job_template` field is specified, any fields not explicitly provided
|
||||
for the new job (except `name` and `description`) will use the default values
|
||||
from the job template.
|
||||
15
awx/main/templates/main/job_start.md
Normal file
15
awx/main/templates/main/job_start.md
Normal file
@ -0,0 +1,15 @@
|
||||
# Start Job
|
||||
|
||||
Make a GET request to this resource to determine if the job can be started and
|
||||
whether any passwords are required to start the job. The response will include
|
||||
the following fields:
|
||||
|
||||
* `can_start`: Flag indicating if this job can be started (boolean, read-only)
|
||||
* `passwords_needed_to_start`: Password names required to start the job (array, read-only)
|
||||
|
||||
Make a POST request to this resource to start the job. If any passwords are
|
||||
required, they must be passed via POST data.
|
||||
|
||||
If successful, the response status code will be 202. If any required passwords
|
||||
are not provided, a 400 status code will be returned. If the job cannot be
|
||||
started, a 405 status code will be returned.
|
||||
33
awx/main/templates/main/job_template_callback.md
Normal file
33
awx/main/templates/main/job_template_callback.md
Normal file
@ -0,0 +1,33 @@
|
||||
The job template callback allows for empheral hosts to launch a new job.
|
||||
|
||||
Configure a host to POST to this resource, passing the `host_config_key`
|
||||
parameter, to start a new job limited to only the requesting host. In the
|
||||
examples below, replace the `N` parameter with the `id` of the job template
|
||||
and the `HOST_CONFIG_KEY` with the `host_config_key` associated with the
|
||||
job template.
|
||||
|
||||
For example, using curl:
|
||||
|
||||
curl --data-urlencode host_config_key=HOST_CONFIG_KEY http://server/api/v1/job_templates/N/callback/
|
||||
|
||||
Or using wget:
|
||||
|
||||
wget -O /dev/null --post-data="host_config_key=HOST_CONFIG_KEY" http://server/api/v1/job_templates/N/callback/
|
||||
|
||||
The response will return status 202 if the request is valid, 403 for an
|
||||
invalid host config key, or 400 if the host cannot be determined from the
|
||||
address making the request.
|
||||
|
||||
A GET request may be used to verify that the correct host will be selected.
|
||||
This request must authenticate as a valid user with permission to edit the
|
||||
job template. For example:
|
||||
|
||||
curl http://user:password@server/api/v1/job_templates/N/callback/
|
||||
|
||||
The response will include the host config key as well as the host name(s)
|
||||
that would match the request:
|
||||
|
||||
{
|
||||
"host_config_key": "HOST_CONFIG_KEY",
|
||||
"matching_hosts": ["hostname"]
|
||||
}
|
||||
6
awx/main/templates/main/job_template_jobs_list.md
Normal file
6
awx/main/templates/main/job_template_jobs_list.md
Normal file
@ -0,0 +1,6 @@
|
||||
{% extends "main/sub_list_create_api_view.md" %}
|
||||
|
||||
{% block post_create %}
|
||||
Any fields not explicitly provided for the new job (except `name` and
|
||||
`description`) will use the default values from the job template.
|
||||
{% endblock %}
|
||||
6
awx/main/templates/main/list_api_view.md
Normal file
6
awx/main/templates/main/list_api_view.md
Normal file
@ -0,0 +1,6 @@
|
||||
# List {{ model_verbose_name_plural|title }}:
|
||||
|
||||
Make a GET request to this resource to retrieve the list of
|
||||
{{ model_verbose_name_plural }}.
|
||||
|
||||
{% include "main/_list_common.md" %}
|
||||
10
awx/main/templates/main/list_create_api_view.md
Normal file
10
awx/main/templates/main/list_create_api_view.md
Normal file
@ -0,0 +1,10 @@
|
||||
{% include "main/list_api_view.md" %}
|
||||
|
||||
# Create {{ model_verbose_name_plural|title }}:
|
||||
|
||||
Make a POST request to this resource with the following {{ model_verbose_name }}
|
||||
fields to create a new {{ model_verbose_name }}:
|
||||
|
||||
{% with write_only=1 %}
|
||||
{% include "main/_result_fields_common.md" %}
|
||||
{% endwith %}
|
||||
3
awx/main/templates/main/organization_admins_list.md
Normal file
3
awx/main/templates/main/organization_admins_list.md
Normal file
@ -0,0 +1,3 @@
|
||||
{% with model_verbose_name="admin user" model_verbose_name_plural="admin users" %}
|
||||
{% include "main/sub_list_create_api_view.md" %}
|
||||
{% endwith %}
|
||||
4
awx/main/templates/main/project_playbooks.md
Normal file
4
awx/main/templates/main/project_playbooks.md
Normal file
@ -0,0 +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 }}.
|
||||
6
awx/main/templates/main/retrieve_api_view.md
Normal file
6
awx/main/templates/main/retrieve_api_view.md
Normal file
@ -0,0 +1,6 @@
|
||||
# Retrieve {{ model_verbose_name|title }}:
|
||||
|
||||
Make GET request to this resource to retrieve a single {{ model_verbose_name }}
|
||||
record containing the following fields:
|
||||
|
||||
{% include "main/_result_fields_common.md" %}
|
||||
18
awx/main/templates/main/retrieve_update_destroy_api_view.md
Normal file
18
awx/main/templates/main/retrieve_update_destroy_api_view.md
Normal file
@ -0,0 +1,18 @@
|
||||
{% include "main/retrieve_api_view.md" %}
|
||||
|
||||
# Update {{ model_verbose_name|title }}:
|
||||
|
||||
Make a PUT or PATCH request to this resource to update this
|
||||
{{ model_verbose_name }}. The following fields may be modified:
|
||||
|
||||
{% with write_only=1 %}
|
||||
{% include "main/_result_fields_common.md" %}
|
||||
{% endwith %}
|
||||
|
||||
For a PUT request, include **all** fields in the request.
|
||||
|
||||
For a PATCH request, include only the fields that are being modified.
|
||||
|
||||
# Delete {{ model_verbose_name|title }}:
|
||||
|
||||
Make a DELETE request to this resource to delete this {{ model_verbose_name }}.
|
||||
7
awx/main/templates/main/sub_list_api_view.md
Normal file
7
awx/main/templates/main/sub_list_api_view.md
Normal file
@ -0,0 +1,7 @@
|
||||
# List {{ model_verbose_name_plural|title }} for this {{ parent_model_verbose_name|title }}:
|
||||
|
||||
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 "main/_list_common.md" %}
|
||||
37
awx/main/templates/main/sub_list_create_api_view.md
Normal file
37
awx/main/templates/main/sub_list_create_api_view.md
Normal file
@ -0,0 +1,37 @@
|
||||
{% include "main/sub_list_api_view.md" %}
|
||||
|
||||
# Create {{ model_verbose_name_plural|title }} for this {{ parent_model_verbose_name|title }}:
|
||||
|
||||
Make a POST request to this resource with the following {{ model_verbose_name }}
|
||||
fields to create a new {{ model_verbose_name }} associated with this
|
||||
{{ parent_model_verbose_name }}.
|
||||
|
||||
{% with write_only=1 %}
|
||||
{% include "main/_result_fields_common.md" %}
|
||||
{% endwith %}
|
||||
|
||||
{% block post_create %}{% endblock %}
|
||||
|
||||
{% if parent_key %}
|
||||
# Remove {{ parent_model_verbose_name|title }} {{ model_verbose_name_plural|title }}:
|
||||
|
||||
Make a POST request to this resource with `id` and `disassociate` fields to
|
||||
delete the associated {{ model_verbose_name }}.
|
||||
|
||||
{
|
||||
"id": 123,
|
||||
"disassociate": true
|
||||
}
|
||||
|
||||
{% else %}
|
||||
# Add {{ model_verbose_name_plural|title }} for this {{ parent_model_verbose_name|title }}:
|
||||
|
||||
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 }}.
|
||||
|
||||
# Remove {{ model_verbose_name_plural|title }} from this {{ parent_model_verbose_name|title }}:
|
||||
|
||||
Make a POST request to this resource with `id` and `disassociate` fields to
|
||||
remove the {{ model_verbose_name }} from this {{ parent_model_verbose_name }}
|
||||
without deleting the {{ model_verbose_name }}.
|
||||
{% endif %}
|
||||
@ -0,0 +1,7 @@
|
||||
# List {{ model_verbose_name_plural|title }} Administered by this {{ parent_model_verbose_name|title }}:
|
||||
|
||||
Make a GET request to this resource to retrieve a list of
|
||||
{{ model_verbose_name_plural }} of which the selected
|
||||
{{ parent_model_verbose_name }} is an admin.
|
||||
|
||||
{% include "main/_list_common.md" %}
|
||||
7
awx/main/templates/main/user_me_list.md
Normal file
7
awx/main/templates/main/user_me_list.md
Normal file
@ -0,0 +1,7 @@
|
||||
Make a GET request to retrieve user information about the current user.
|
||||
|
||||
One result should be returned containing the following fields:
|
||||
|
||||
{% include "main/_result_fields_common.md" %}
|
||||
|
||||
Use the primary URL for the user (/api/v1/users/N/) to modify the user.
|
||||
@ -342,7 +342,7 @@ class InventoryTest(BaseTest):
|
||||
vars_c = dict(asdf=5555, dog='mouse', cat='mogwai', unstructured=dict(a=[3,0,3],b=dict(z=2600)))
|
||||
|
||||
# attempting to get a variable object creates it, even though it does not already exist
|
||||
vdata_url = reverse('main:host_variable_detail', args=(added_by_collection_a['id'],))
|
||||
vdata_url = reverse('main:host_variable_data', args=(added_by_collection_a['id'],))
|
||||
|
||||
got = self.get(vdata_url, expect=200, auth=self.get_super_credentials())
|
||||
self.assertEquals(got, {})
|
||||
@ -373,8 +373,8 @@ class InventoryTest(BaseTest):
|
||||
vars_c = dict(asdf=9999, dog='pluto', cat='five', unstructured=dict(a=[3,3,3],b=dict(z=5)))
|
||||
groups = Group.objects.all()
|
||||
|
||||
vdata1_url = reverse('main:group_variable_detail', args=(groups[0].pk,))
|
||||
vdata2_url = reverse('main:group_variable_detail', args=(groups[1].pk,))
|
||||
vdata1_url = reverse('main:group_variable_data', args=(groups[0].pk,))
|
||||
vdata2_url = reverse('main:group_variable_data', args=(groups[1].pk,))
|
||||
|
||||
# a super user can associate variable objects with groups
|
||||
got = self.get(vdata1_url, expect=200, auth=self.get_super_credentials())
|
||||
@ -399,7 +399,7 @@ class InventoryTest(BaseTest):
|
||||
vars_b = dict(asdf=2736, dog='benji', cat='garfield', unstructured=dict(a=[2,2,2],b=dict(x=3,y=4)))
|
||||
vars_c = dict(asdf=7692, dog='buck', cat='sylvester', unstructured=dict(a=[3,3,3],b=dict(z=5)))
|
||||
|
||||
vdata_url = reverse('main:inventory_variable_detail', args=(self.inventory_a.pk,))
|
||||
vdata_url = reverse('main:inventory_variable_data', args=(self.inventory_a.pk,))
|
||||
|
||||
# a super user can associate variable objects with inventory
|
||||
got = self.get(vdata_url, expect=200, auth=self.get_super_credentials())
|
||||
|
||||
@ -235,7 +235,7 @@ class ProjectsTest(BaseTest):
|
||||
self.get(project, expect=404, auth=self.get_normal_credentials())
|
||||
|
||||
# can list playbooks for projects
|
||||
proj_playbooks = reverse('main:project_detail_playbooks', args=(self.projects[2].pk,))
|
||||
proj_playbooks = reverse('main:project_playbooks', args=(self.projects[2].pk,))
|
||||
got = self.get(proj_playbooks, expect=200, auth=self.get_super_credentials())
|
||||
self.assertEqual(got, self.projects[2].playbooks)
|
||||
|
||||
|
||||
@ -33,7 +33,7 @@ user_urls = patterns('awx.main.views',
|
||||
project_urls = patterns('awx.main.views',
|
||||
url(r'^$', 'project_list'),
|
||||
url(r'^(?P<pk>[0-9]+)/$', 'project_detail'),
|
||||
url(r'^(?P<pk>[0-9]+)/playbooks/$', 'project_detail_playbooks'),
|
||||
url(r'^(?P<pk>[0-9]+)/playbooks/$', 'project_playbooks'),
|
||||
url(r'^(?P<pk>[0-9]+)/organizations/$', 'project_organizations_list'),
|
||||
url(r'^(?P<pk>[0-9]+)/teams/$', 'project_teams_list'),
|
||||
)
|
||||
@ -53,14 +53,14 @@ inventory_urls = patterns('awx.main.views',
|
||||
url(r'^(?P<pk>[0-9]+)/hosts/$', 'inventory_hosts_list'),
|
||||
url(r'^(?P<pk>[0-9]+)/groups/$', 'inventory_groups_list'),
|
||||
url(r'^(?P<pk>[0-9]+)/root_groups/$', 'inventory_root_groups_list'),
|
||||
url(r'^(?P<pk>[0-9]+)/variable_data/$', 'inventory_variable_detail'),
|
||||
url(r'^(?P<pk>[0-9]+)/variable_data/$', 'inventory_variable_data'),
|
||||
url(r'^(?P<pk>[0-9]+)/script/$', 'inventory_script_view'),
|
||||
)
|
||||
|
||||
host_urls = patterns('awx.main.views',
|
||||
url(r'^$', 'host_list'),
|
||||
url(r'^(?P<pk>[0-9]+)/$', 'host_detail'),
|
||||
url(r'^(?P<pk>[0-9]+)/variable_data/$', 'host_variable_detail'),
|
||||
url(r'^(?P<pk>[0-9]+)/variable_data/$', 'host_variable_data'),
|
||||
url(r'^(?P<pk>[0-9]+)/groups/$', 'host_groups_list'),
|
||||
url(r'^(?P<pk>[0-9]+)/all_groups/$', 'host_all_groups_list'),
|
||||
url(r'^(?P<pk>[0-9]+)/job_events/', 'host_job_events_list'),
|
||||
@ -73,7 +73,7 @@ group_urls = patterns('awx.main.views',
|
||||
url(r'^(?P<pk>[0-9]+)/children/$', 'group_children_list'),
|
||||
url(r'^(?P<pk>[0-9]+)/hosts/$', 'group_hosts_list'),
|
||||
url(r'^(?P<pk>[0-9]+)/all_hosts/$', 'group_all_hosts_list'),
|
||||
url(r'^(?P<pk>[0-9]+)/variable_data/$', 'group_variable_detail'),
|
||||
url(r'^(?P<pk>[0-9]+)/variable_data/$', 'group_variable_data'),
|
||||
url(r'^(?P<pk>[0-9]+)/job_events/$', 'group_job_events_list'),
|
||||
url(r'^(?P<pk>[0-9]+)/job_host_summaries/$', 'group_job_host_summaries_list'),
|
||||
)
|
||||
|
||||
@ -6,7 +6,8 @@ import sys
|
||||
# Django REST Framework
|
||||
from rest_framework.exceptions import ParseError, PermissionDenied
|
||||
|
||||
__all__ = ['get_object_or_400', 'get_object_or_403', 'camelcase_to_underscore']
|
||||
__all__ = ['get_object_or_400', 'get_object_or_403', 'camelcase_to_underscore',
|
||||
'get_awx_version']
|
||||
|
||||
def get_object_or_400(klass, *args, **kwargs):
|
||||
'''
|
||||
@ -51,3 +52,14 @@ class RequireDebugTrueOrTest(logging.Filter):
|
||||
def filter(self, record):
|
||||
from django.conf import settings
|
||||
return settings.DEBUG or 'test' in sys.argv
|
||||
|
||||
def get_awx_version():
|
||||
'''
|
||||
Return AWX version as reported by setuptools.
|
||||
'''
|
||||
from awx import __version__
|
||||
try:
|
||||
import pkg_resources
|
||||
return pkg_resources.require('awx')[0].version
|
||||
except:
|
||||
return __version__
|
||||
|
||||
@ -13,17 +13,17 @@ from django.contrib.auth.models import User
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.shortcuts import get_object_or_404, render_to_response
|
||||
from django.template import RequestContext
|
||||
from django.utils.datastructures import SortedDict
|
||||
|
||||
# Django REST Framework
|
||||
from rest_framework.authtoken.views import ObtainAuthToken
|
||||
from rest_framework.exceptions import PermissionDenied
|
||||
from rest_framework import generics
|
||||
from rest_framework.parsers import YAMLParser
|
||||
from rest_framework.permissions import AllowAny, IsAuthenticated
|
||||
from rest_framework.renderers import YAMLRenderer
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.settings import api_settings
|
||||
from rest_framework.views import APIView
|
||||
from rest_framework import status
|
||||
|
||||
# AWX
|
||||
from awx.main.authentication import JobTaskAuthentication
|
||||
@ -54,10 +54,6 @@ def handle_500(request):
|
||||
return handle_error(request, 500)
|
||||
|
||||
class ApiRootView(APIView):
|
||||
'''
|
||||
This resource is the root of the AWX REST API and provides
|
||||
information about the available API versions.
|
||||
'''
|
||||
|
||||
permission_classes = (AllowAny,)
|
||||
view_name = 'REST API'
|
||||
@ -76,11 +72,6 @@ class ApiRootView(APIView):
|
||||
return Response(data)
|
||||
|
||||
class ApiV1RootView(APIView):
|
||||
'''
|
||||
Version 1 of the REST API.
|
||||
|
||||
Subject to change until the final 1.2 release.
|
||||
'''
|
||||
|
||||
permission_classes = (AllowAny,)
|
||||
view_name = 'Version 1'
|
||||
@ -106,17 +97,6 @@ class ApiV1RootView(APIView):
|
||||
return Response(data)
|
||||
|
||||
class ApiV1ConfigView(APIView):
|
||||
'''
|
||||
Various sitewide configuration settings (some may only be visible to
|
||||
superusers or organization admins):
|
||||
|
||||
* `project_base_dir`: Path on the server where projects and playbooks are \
|
||||
stored.
|
||||
* `project_local_paths`: List of directories beneath `project_base_dir` to
|
||||
use when creating/editing a project.
|
||||
* `time_zone`: The configured time zone for the server.
|
||||
* `license_info`: Information about the current license.
|
||||
'''
|
||||
|
||||
permission_classes = (IsAuthenticated,)
|
||||
view_name = 'Configuration'
|
||||
@ -130,6 +110,7 @@ class ApiV1ConfigView(APIView):
|
||||
data = dict(
|
||||
time_zone=settings.TIME_ZONE,
|
||||
license_info=license_data,
|
||||
version=get_awx_version(),
|
||||
)
|
||||
if request.user.is_superuser or request.user.admin_of_organizations.filter(active=True).count():
|
||||
data.update(dict(
|
||||
@ -139,29 +120,7 @@ class ApiV1ConfigView(APIView):
|
||||
|
||||
return Response(data)
|
||||
|
||||
class AuthTokenView(ObtainAuthToken):
|
||||
'''
|
||||
POST username and password to this resource to obtain an authentication
|
||||
token for subsequent requests.
|
||||
|
||||
Example JSON to post (application/json):
|
||||
|
||||
{"username": "user", "password": "my pass"}
|
||||
|
||||
Example form data to post (application/x-www-form-urlencoded):
|
||||
|
||||
username=user&password=my%20pass
|
||||
|
||||
If the username and password are valid, the response should be:
|
||||
|
||||
{"token": "8f17825cf08a7efea124f2638f3896f6637f8745"}
|
||||
|
||||
Otherwise, the response will indicate the error that occurred.
|
||||
|
||||
For subsequent requests, pass the token via the HTTP request headers:
|
||||
|
||||
Authenticate: Token 8f17825cf08a7efea124f2638f3896f6637f8745
|
||||
'''
|
||||
class AuthTokenView(ObtainAuthToken, APIView):
|
||||
|
||||
permission_classes = (AllowAny,)
|
||||
renderer_classes = api_settings.DEFAULT_RENDERER_CLASSES
|
||||
@ -273,7 +232,7 @@ class ProjectDetail(RetrieveUpdateDestroyAPIView):
|
||||
model = Project
|
||||
serializer_class = ProjectSerializer
|
||||
|
||||
class ProjectDetailPlaybooks(RetrieveAPIView):
|
||||
class ProjectPlaybooks(RetrieveAPIView):
|
||||
|
||||
model = Project
|
||||
serializer_class = ProjectPlaybooksSerializer
|
||||
@ -301,8 +260,7 @@ class UserMeList(ListAPIView):
|
||||
|
||||
model = User
|
||||
serializer_class = UserSerializer
|
||||
|
||||
view_name = 'Me!'
|
||||
view_name = 'Me'
|
||||
|
||||
def get_queryset(self):
|
||||
return self.model.objects.filter(pk=self.request.user.pk)
|
||||
@ -522,37 +480,28 @@ class InventoryRootGroupsList(SubListCreateAPIView):
|
||||
sublist_qs = parent.groups.exclude(parents__pk__in=all_pks).distinct()
|
||||
return qs & sublist_qs
|
||||
|
||||
class BaseVariableDetail(RetrieveUpdateDestroyAPIView):
|
||||
class BaseVariableData(RetrieveUpdateAPIView):
|
||||
|
||||
parser_classes = api_settings.DEFAULT_PARSER_CLASSES + [YAMLParser]
|
||||
renderer_classes = api_settings.DEFAULT_RENDERER_CLASSES + [YAMLRenderer]
|
||||
is_variable_data = True # Special flag for permissions check.
|
||||
|
||||
class InventoryVariableDetail(BaseVariableDetail):
|
||||
class InventoryVariableData(BaseVariableData):
|
||||
|
||||
model = Inventory
|
||||
serializer_class = InventoryVariableDataSerializer
|
||||
|
||||
class HostVariableDetail(BaseVariableDetail):
|
||||
class HostVariableData(BaseVariableData):
|
||||
|
||||
model = Host
|
||||
serializer_class = HostVariableDataSerializer
|
||||
|
||||
class GroupVariableDetail(BaseVariableDetail):
|
||||
class GroupVariableData(BaseVariableData):
|
||||
|
||||
model = Group
|
||||
serializer_class = GroupVariableDataSerializer
|
||||
|
||||
class InventoryScriptView(RetrieveAPIView):
|
||||
'''
|
||||
Return inventory group and host data as needed for an inventory script.
|
||||
|
||||
Without query parameters, return groups with hosts, children and vars
|
||||
(equivalent to the --list parameter to an inventory script).
|
||||
|
||||
With ?host=HOSTNAME, return host vars for the given host (equivalent to the
|
||||
--host HOSTNAME parameter to an inventory script).
|
||||
'''
|
||||
|
||||
model = Inventory
|
||||
authentication_classes = [JobTaskAuthentication] + \
|
||||
@ -568,48 +517,30 @@ class InventoryScriptView(RetrieveAPIView):
|
||||
name=hostname)
|
||||
data = host.variables_dict
|
||||
else:
|
||||
data = {}
|
||||
data = SortedDict()
|
||||
if self.object.variables_dict:
|
||||
data['all'] = SortedDict()
|
||||
data['all']['vars'] = self.object.variables_dict
|
||||
|
||||
for group in self.object.groups.filter(active=True):
|
||||
hosts = group.hosts.filter(active=True)
|
||||
children = group.children.filter(active=True)
|
||||
group_info = {
|
||||
'hosts': list(hosts.values_list('name', flat=True)),
|
||||
'children': list(children.values_list('name', flat=True)),
|
||||
'vars': group.variables_dict,
|
||||
}
|
||||
group_info = SortedDict()
|
||||
group_info['hosts'] = list(hosts.values_list('name', flat=True))
|
||||
group_info['children'] = list(children.values_list('name', flat=True))
|
||||
group_info['vars'] = group.variables_dict
|
||||
data[group.name] = group_info
|
||||
|
||||
# this confuses the inventory script if the group
|
||||
# has children set and no variables or hosts.
|
||||
# no other reason to do this right?
|
||||
#
|
||||
# group_info = dict(filter(lambda x: bool(x[1]),
|
||||
# group_info.items()))
|
||||
|
||||
if group_info.keys() in ([], ['hosts']):
|
||||
data[group.name] = group_info.get('hosts', [])
|
||||
else:
|
||||
data[group.name] = group_info
|
||||
|
||||
if self.object.variables_dict:
|
||||
data['all'] = {
|
||||
'vars': self.object.variables_dict,
|
||||
}
|
||||
|
||||
|
||||
# workaround for Ansible inventory bug (github #3687), localhost must
|
||||
# be explicitly listed in the all group for dynamic inventory
|
||||
# scripts to pick it up
|
||||
|
||||
localhost = Host.objects.filter(inventory=self.object, name='localhost').count()
|
||||
localhost2 = Host.objects.filter(inventory=self.object, name='127.0.0.1').count()
|
||||
if localhost or localhost2:
|
||||
if not 'all' in data:
|
||||
data['all'] = {}
|
||||
data['all']['hosts'] = []
|
||||
if localhost:
|
||||
data['all']['hosts'].append('localhost')
|
||||
if localhost2:
|
||||
data['all']['hosts'].append('127.0.0.1')
|
||||
# workaround for Ansible inventory bug (github #3687), localhost
|
||||
# must be explicitly listed in the all group for dynamic inventory
|
||||
# scripts to pick it up.
|
||||
localhost_names = ('localhost', '127.0.0.1', '::1')
|
||||
localhosts_qs = self.object.hosts.filter(active=True,
|
||||
name__in=localhost_names)
|
||||
localhosts = list(localhosts_qs.values_list('name', flat=True))
|
||||
if localhosts:
|
||||
data.setdefault('all', SortedDict())
|
||||
data['all']['hosts'] = localhosts
|
||||
|
||||
return Response(data)
|
||||
|
||||
@ -618,51 +549,12 @@ class JobTemplateList(ListCreateAPIView):
|
||||
model = JobTemplate
|
||||
serializer_class = JobTemplateSerializer
|
||||
|
||||
def _get_queryset(self):
|
||||
return self.request.user.get_queryset(self.model)
|
||||
|
||||
class JobTemplateDetail(RetrieveUpdateDestroyAPIView):
|
||||
|
||||
model = JobTemplate
|
||||
serializer_class = JobTemplateSerializer
|
||||
|
||||
class JobTemplateCallback(generics.GenericAPIView):
|
||||
'''
|
||||
The job template callback allows for empheral hosts to launch a new job.
|
||||
|
||||
Configure a host to POST to this resource, passing the `host_config_key`
|
||||
parameter, to start a new job limited to only the requesting host. In the
|
||||
examples below, replace the `N` parameter with the `id` of the job template
|
||||
and the `HOST_CONFIG_KEY` with the `host_config_key` associated with the
|
||||
job template.
|
||||
|
||||
For example, using curl:
|
||||
|
||||
curl --data-urlencode host_config_key=HOST_CONFIG_KEY http://server/api/v1/job_templates/N/callback/
|
||||
|
||||
Or using wget:
|
||||
|
||||
wget -O /dev/null --post-data="host_config_key=HOST_CONFIG_KEY" http://server/api/v1/job_templates/N/callback/
|
||||
|
||||
The response will return status 202 if the request is valid, 403 for an
|
||||
invalid host config key, or 400 if the host cannot be determined from the
|
||||
address making the request.
|
||||
|
||||
A GET request may be used to verify that the correct host will be selected.
|
||||
This request must authenticate as a valid user with permission to edit the
|
||||
job template. For example:
|
||||
|
||||
curl http://user:password@server/api/v1/job_templates/N/callback/
|
||||
|
||||
The response will include the host config key as well as the host name(s)
|
||||
that would match the request:
|
||||
|
||||
{
|
||||
"host_config_key": "HOST_CONFIG_KEY",
|
||||
"matching_hosts": ["hostname"]
|
||||
}
|
||||
|
||||
'''
|
||||
class JobTemplateCallback(GenericAPIView):
|
||||
|
||||
model = JobTemplate
|
||||
permission_classes = (JobTemplateCallbackPermission,)
|
||||
@ -782,9 +674,6 @@ class JobList(ListCreateAPIView):
|
||||
model = Job
|
||||
serializer_class = JobSerializer
|
||||
|
||||
def _get_queryset(self):
|
||||
return self.model.objects.all() # FIXME
|
||||
|
||||
class JobDetail(RetrieveUpdateDestroyAPIView):
|
||||
|
||||
model = Job
|
||||
@ -797,7 +686,7 @@ class JobDetail(RetrieveUpdateDestroyAPIView):
|
||||
return self.http_method_not_allowed(request, *args, **kwargs)
|
||||
return super(JobDetail, self).update(request, *args, **kwargs)
|
||||
|
||||
class JobStart(generics.GenericAPIView):
|
||||
class JobStart(GenericAPIView):
|
||||
|
||||
model = Job
|
||||
is_job_start = True
|
||||
@ -823,7 +712,7 @@ class JobStart(generics.GenericAPIView):
|
||||
else:
|
||||
return self.http_method_not_allowed(request, *args, **kwargs)
|
||||
|
||||
class JobCancel(generics.GenericAPIView):
|
||||
class JobCancel(GenericAPIView):
|
||||
|
||||
model = Job
|
||||
is_job_cancel = True
|
||||
@ -849,8 +738,7 @@ class BaseJobHostSummariesList(SubListAPIView):
|
||||
serializer_class = JobHostSummarySerializer
|
||||
parent_model = None # Subclasses must define this attribute.
|
||||
relationship = 'job_host_summaries'
|
||||
|
||||
view_name = 'Job Host Summary List'
|
||||
view_name = 'Job Host Summaries List'
|
||||
|
||||
class HostJobHostSummariesList(BaseJobHostSummariesList):
|
||||
|
||||
@ -885,7 +773,6 @@ class JobEventChildrenList(SubListAPIView):
|
||||
serializer_class = JobEventSerializer
|
||||
parent_model = JobEvent
|
||||
relationship = 'children'
|
||||
|
||||
view_name = 'Job Event Children List'
|
||||
|
||||
class JobEventHostsList(SubListAPIView):
|
||||
@ -894,7 +781,6 @@ class JobEventHostsList(SubListAPIView):
|
||||
serializer_class = HostSerializer
|
||||
parent_model = JobEvent
|
||||
relationship = 'hosts'
|
||||
|
||||
view_name = 'Job Event Hosts List'
|
||||
|
||||
class BaseJobEventsList(SubListAPIView):
|
||||
@ -903,6 +789,7 @@ class BaseJobEventsList(SubListAPIView):
|
||||
serializer_class = JobEventSerializer
|
||||
parent_model = None # Subclasses must define this attribute.
|
||||
relationship = 'job_events'
|
||||
view_name = 'Job Events List'
|
||||
|
||||
class HostJobEventsList(BaseJobEventsList):
|
||||
|
||||
|
||||
@ -12,18 +12,18 @@ html body {
|
||||
}
|
||||
html body .navbar .navbar-inner {
|
||||
border-top: none;
|
||||
height: 20px;
|
||||
height: 54px;
|
||||
}
|
||||
html body .navbar-inverse .navbar-inner {
|
||||
background-color: #36454F;
|
||||
background-image: -moz-linear-gradient(top, #36454F, #36454F);
|
||||
background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#36454F), to(#36454F));
|
||||
background-image: -webkit-linear-gradient(top, #36454F, #36454F);
|
||||
background-image: -o-linear-gradient(top, #36454F, #36454F);
|
||||
background-image: linear-gradient(to bottom, #36454F, #36454F);
|
||||
background-color: #171717;
|
||||
background-image: -moz-linear-gradient(top, #171717, #171717);
|
||||
background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#171717), to(#171717));
|
||||
background-image: -webkit-linear-gradient(top, #171717, #171717);
|
||||
background-image: -o-linear-gradient(top, #171717, #171717);
|
||||
background-image: linear-gradient(to bottom, #171717, #171717);
|
||||
background-repeat: repeat-x;
|
||||
border-color: #36454F;
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#36454F', endColorstr='#36454F', GradientType=0);
|
||||
border-color: #171717;
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#171717', endColorstr='#171717', GradientType=0);
|
||||
}
|
||||
html body .navbar-inverse .nav > li > a {
|
||||
color: #A9A9A9;
|
||||
@ -33,7 +33,7 @@ html body .navbar-inverse .nav > li > a:focus {
|
||||
color: #2078be;
|
||||
}
|
||||
html body .navbar .brand img {
|
||||
width: 130px;
|
||||
width: 200px;
|
||||
margin-top: -6px;
|
||||
margin-right: 0.5em;
|
||||
}
|
||||
@ -45,6 +45,15 @@ html body .navbar-inverse .nav li.dropdown.open.active > .dropdown-toggle {
|
||||
html body .navbar-inverse .brand {
|
||||
font-size: 1.2em;
|
||||
color: #fff;
|
||||
padding-top: 6px;
|
||||
padding-left: 4px;
|
||||
}
|
||||
html body .navbar-inverse .nav.pull-right {
|
||||
margin-top: 8px;
|
||||
margin-right: -8px;
|
||||
}
|
||||
html body ul.breadcrumb {
|
||||
margin-top: 72px;
|
||||
}
|
||||
span.powered-by .version {
|
||||
color: #ddd;
|
||||
@ -88,20 +97,35 @@ html body .description {
|
||||
padding-bottom: 0;
|
||||
display: none;
|
||||
}
|
||||
.footer {
|
||||
margin-top: 0.5em;
|
||||
#footer {
|
||||
font-size: 0.8em;
|
||||
text-align: center;
|
||||
padding-bottom: 1em;
|
||||
}
|
||||
.footer a,
|
||||
.footer a:hover {
|
||||
#footer a,
|
||||
#footer a:hover {
|
||||
color: #333;
|
||||
}
|
||||
img.awxlogo {
|
||||
width: 125px;
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
html body .wrapper {
|
||||
min-height: 1%;
|
||||
height: auto !important;
|
||||
margin: 0 auto 0;
|
||||
}
|
||||
html body #footer {
|
||||
height: auto !important;
|
||||
}
|
||||
html body #push {
|
||||
height: 0;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block branding %}
|
||||
<a class="brand" href="/api/"><img class="logo" src="{{ STATIC_URL }}img/ansibleworks-logo.png">{% trans 'REST API' %}</a>
|
||||
<a class="brand" href="/api/"><img class="logo" src="{{ STATIC_URL }}img/logo.png">{% trans 'REST API' %}</a>
|
||||
{% endblock %}
|
||||
|
||||
{% block userlinks %}
|
||||
@ -113,7 +137,9 @@ html body .description {
|
||||
{% endblock %}
|
||||
|
||||
{% block footer %}
|
||||
<div class="footer">Copyright © 2013 <a href="http://www.ansibleworks.com/">AnsibleWorks, Inc.</a> All rights reserved.<br />
|
||||
<div id="footer">
|
||||
<img class="awxlogo" src="{{ STATIC_URL }}img/AWX_logo.png" /><br/>
|
||||
Copyright © 2013 <a href="http://www.ansibleworks.com/">AnsibleWorks, Inc.</a> All rights reserved.<br />
|
||||
1482 East Valley Road, Suite 888 · Montecito, California 9308 · <a href="tel:18008250212">+1-800-825-0212<a/></div>
|
||||
{% endblock %}
|
||||
|
||||
@ -121,13 +147,51 @@ html body .description {
|
||||
{{ block.super }}
|
||||
<script type="text/javascript">
|
||||
$(function() {
|
||||
// Make linkes from relative URLs to resources.
|
||||
// Make links from relative URLs to resources.
|
||||
$('span.str').each(function() {
|
||||
// Remove REST API links within data.
|
||||
if ($(this).parent('a').size()) {
|
||||
$(this).unwrap();
|
||||
}
|
||||
var s = $(this).html();
|
||||
if (s.match(/^\"\/.+\/\"$/) || s.match(/^\"\/.+\/\?.*\"$/)) {
|
||||
$(this).html('"<a href=' + s + '>' + s.replace(/\"/g, '') + '</a>"');
|
||||
}
|
||||
});
|
||||
// Make links for all inventory script hosts.
|
||||
$('.request-info .pln:contains("script")').each(function() {
|
||||
$('.response-info span.str:contains("hosts")').each(function() {
|
||||
if ($(this).text() != '"hosts"') {
|
||||
return;
|
||||
}
|
||||
var hosts_state = 0;
|
||||
$(this).find('~ span').each(function() {
|
||||
if (hosts_state == 0) {
|
||||
if ($(this).is('span.pun') && $(this).text() == '[') {
|
||||
hosts_state = 1;
|
||||
}
|
||||
}
|
||||
else if (hosts_state == 1) {
|
||||
if ($(this).is('span.pun') && ($(this).text() == ']' || $(this).text() == '],')) {
|
||||
hosts_state = 2;
|
||||
}
|
||||
else if ($(this).is('span.str')) {
|
||||
if ($(this).text() == '"') {
|
||||
}
|
||||
else if ($(this).text().match(/^\".+\"$/)) {
|
||||
var s = $(this).text().replace(/\"/g, '');
|
||||
$(this).html('"<a href="' + '?host=' + s + '">' + s + '</a>"');
|
||||
}
|
||||
else {
|
||||
var s = $(this).text();
|
||||
$(this).html('<a href="' + '?host=' + s + '">' + s + '</a>');
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
// Add classes/icons for dynamically showing/hiding help.
|
||||
if ($('.description').html()) {
|
||||
$('.description').addClass('well').addClass('well-small').addClass('prettyprint');
|
||||
$('.description').prepend('<a class="hide-description pull-right" href="#" title="Hide Description"><i class="icon-remove"></i></a>');
|
||||
@ -140,6 +204,9 @@ $(function() {
|
||||
$('.description').slideToggle('fast');
|
||||
return false;
|
||||
});
|
||||
if (window.location.hash == '#showhelp') {
|
||||
$('.description').slideDown('fast');
|
||||
}
|
||||
}
|
||||
$('.btn-primary').removeClass('btn-primary').addClass('btn-success');
|
||||
});
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user