Moved API code into separate Django app.

This commit is contained in:
Chris Church
2013-11-04 15:44:43 -05:00
parent 2a58d50cfa
commit 98883e771f
61 changed files with 1553 additions and 1546 deletions

2
awx/api/__init__.py Normal file
View File

@@ -0,0 +1,2 @@
# Copyright (c) 2013 AnsibleWorks, Inc.
# All Rights Reserved.

61
awx/api/authentication.py Normal file
View File

@@ -0,0 +1,61 @@
# Copyright (c) 2013 AnsibleWorks, Inc.
# All Rights Reserved.
# Django REST Framework
from rest_framework import authentication
from rest_framework import exceptions
# AWX
from awx.main.models import Job, AuthToken
class TokenAuthentication(authentication.TokenAuthentication):
'''
Custom token authentication using tokens that expire and are associated
with parameters specific to the request.
'''
model = AuthToken
def authenticate(self, request):
self.request = request
return super(TokenAuthentication, self).authenticate(request)
def authenticate_credentials(self, key):
try:
request_hash = self.model.get_request_hash(self.request)
token = self.model.objects.get(key=key, request_hash=request_hash)
except self.model.DoesNotExist:
raise exceptions.AuthenticationFailed('Invalid token')
if token.expired:
raise exceptions.AuthenticationFailed('Token is expired')
if not token.user.is_active:
raise exceptions.AuthenticationFailed('User inactive or deleted')
token.refresh()
return (token.user, token)
class JobTaskAuthentication(authentication.BaseAuthentication):
'''
Custom authentication used for views accessed by the inventory and callback
scripts when running a job.
'''
def authenticate(self, request):
auth = authentication.get_authorization_header(request).split()
if len(auth) != 2 or auth[0].lower() != 'token' or '-' not in auth[1]:
return None
job_id, job_key = auth[1].split('-', 1)
try:
job = Job.objects.get(pk=job_id, status='running')
except Job.DoesNotExist:
return None
token = job.task_auth_token
if auth[1] != token:
raise exceptions.AuthenticationFailed('Invalid job task token')
return (None, token)
def authenticate_header(self, request):
return 'Token'

196
awx/api/filters.py Normal file
View File

@@ -0,0 +1,196 @@
# Copyright (c) 2013 AnsibleWorks, Inc.
# All Rights Reserved.
# Python
import re
# Django
from django.core.exceptions import FieldError, ValidationError
from django.db import models
from django.db.models import Q
from django.db.models.related import RelatedObject
from django.db.models.fields import FieldDoesNotExist
# Django REST Framework
from rest_framework.exceptions import ParseError
from rest_framework.filters import BaseFilterBackend
class ActiveOnlyBackend(BaseFilterBackend):
'''
Filter to show only objects where is_active/active is True.
'''
def filter_queryset(self, request, queryset, view):
for field in queryset.model._meta.fields:
if field.name == 'is_active':
queryset = queryset.filter(is_active=True)
elif field.name == 'active':
queryset = queryset.filter(active=True)
return queryset
class FieldLookupBackend(BaseFilterBackend):
'''
Filter using field lookups provided via query string parameters.
'''
RESERVED_NAMES = ('page', 'page_size', 'format', 'order', 'order_by',
'search')
SUPPORTED_LOOKUPS = ('exact', 'iexact', 'contains', 'icontains',
'startswith', 'istartswith', 'endswith', 'iendswith',
'regex', 'iregex', 'gt', 'gte', 'lt', 'lte', 'in',
'isnull')
def get_field_from_lookup(self, model, lookup):
field = None
parts = lookup.split('__')
if parts and parts[-1] not in self.SUPPORTED_LOOKUPS:
parts.append('exact')
# FIXME: Could build up a list of models used across relationships, use
# those lookups combined with request.user.get_queryset(Model) to make
# sure user cannot query using objects he could not view.
for n, name in enumerate(parts[:-1]):
if name == 'pk':
field = model._meta.pk
else:
field = model._meta.get_field_by_name(name)[0]
if n < (len(parts) - 2):
if getattr(field, 'rel', None):
model = field.rel.to
else:
model = field.model
return field
def to_python_boolean(self, value, allow_none=False):
value = unicode(value)
if value.lower() in ('true', '1'):
return True
elif value.lower() in ('false', '0'):
return False
elif allow_none and value.lower() in ('none', 'null'):
return None
else:
raise ValueError(u'Unable to convert "%s" to boolean' % unicode(value))
def to_python_related(self, value):
value = unicode(value)
if value.lower() in ('none', 'null'):
return None
else:
return int(value)
def value_to_python_for_field(self, field, value):
if isinstance(field, models.NullBooleanField):
return self.to_python_boolean(value, allow_none=True)
elif isinstance(field, models.BooleanField):
return self.to_python_boolean(value)
elif isinstance(field, RelatedObject):
return self.to_python_related(value)
else:
return field.to_python(value)
def value_to_python(self, model, lookup, value):
field = self.get_field_from_lookup(model, lookup)
if lookup.endswith('__isnull'):
value = self.to_python_boolean(value)
elif lookup.endswith('__in'):
items = []
for item in value.split(','):
items.append(self.value_to_python_for_field(field, item))
value = items
elif lookup.endswith('__regex') or lookup.endswith('__iregex'):
try:
re.compile(value)
except re.error, e:
raise ValueError(e.args[0])
return value
else:
value = self.value_to_python_for_field(field, value)
return value
def filter_queryset(self, request, queryset, view):
try:
# Apply filters specified via QUERY_PARAMS. Each entry in the lists
# below is (negate, field, value).
and_filters = []
or_filters = []
for key, values in request.QUERY_PARAMS.lists():
if key in self.RESERVED_NAMES:
continue
# Custom __int filter suffix (internal use only).
q_int = False
if key.endswith('__int'):
key = key[:-5]
q_int = True
# Custom or__ filter prefix (or__ can precede not__).
q_or = False
if key.startswith('or__'):
key = key[4:]
q_or = True
# Custom not__ filter prefix.
q_not = False
if key.startswith('not__'):
key = key[5:]
q_not = True
# Convert value(s) to python and add to the appropriate list.
for value in values:
if q_int:
value = int(value)
value = self.value_to_python(queryset.model, key, value)
if q_or:
or_filters.append((q_not, key, value))
else:
and_filters.append((q_not, key, value))
# Now build Q objects for database query filter.
if and_filters or or_filters:
args = []
for n, k, v in and_filters:
if n:
args.append(~Q(**{k:v}))
else:
args.append(Q(**{k:v}))
if or_filters:
q = Q()
for n,k,v in or_filters:
if n:
q |= ~Q(**{k:v})
else:
q |= Q(**{k:v})
args.append(q)
queryset = queryset.filter(*args)
return queryset
except (FieldError, FieldDoesNotExist, ValueError), e:
raise ParseError(e.args[0])
except ValidationError, e:
raise ParseError(e.messages)
class OrderByBackend(BaseFilterBackend):
'''
Filter to apply ordering based on query string parameters.
'''
def filter_queryset(self, request, queryset, view):
try:
order_by = None
for key, value in request.QUERY_PARAMS.items():
if key in ('order', 'order_by'):
order_by = value
if ',' in value:
order_by = value.split(',')
else:
order_by = (value,)
if order_by:
queryset = queryset.order_by(*order_by)
# Fetch the first result to run the query, otherwise we don't
# always catch the FieldError for invalid field names.
try:
queryset[0]
except IndexError:
pass
return queryset
except FieldError, e:
# Return a 400 for invalid field names.
raise ParseError(*e.args)

411
awx/api/generics.py Normal file
View File

@@ -0,0 +1,411 @@
# Copyright (c) 2013 AnsibleWorks, Inc.
# 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.safestring import mark_safe
from django.utils.timezone import now
# Django REST Framework
from rest_framework.authentication import get_authorization_header
from rest_framework.exceptions import PermissionDenied
from rest_framework import generics
from rest_framework.response import Response
from rest_framework.request import clone_request
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
__all__ = ['APIView', 'GenericAPIView', 'ListAPIView', 'ListCreateAPIView',
'SubListAPIView', 'SubListCreateAPIView', 'RetrieveAPIView',
'RetrieveUpdateAPIView', 'RetrieveUpdateDestroyAPIView']
def get_view_name(cls, suffix=None):
'''
Wrapper around REST framework get_view_name() to support get_name() method
and view_name property on a view class.
'''
name = ''
if hasattr(cls, 'get_name') and callable(cls.get_name):
name = cls().get_name()
elif hasattr(cls, 'view_name'):
if callable(cls.view_name):
name = cls.view_name()
else:
name = cls.view_name
if name:
return ('%s %s' % (name, suffix)) if suffix else name
return views.get_view_name(cls, suffix=None)
def get_view_description(cls, html=False):
'''
Wrapper around REST framework get_view_description() to support
get_description() method and view_description property on a view class.
'''
if hasattr(cls, 'get_description') and callable(cls.get_description):
desc = cls().get_description(html=html)
cls = type(cls.__name__, (object,), {'__doc__': desc})
elif hasattr(cls, 'view_description'):
if callable(cls.view_description):
view_desc = cls.view_description()
else:
view_desc = cls.view_description
cls = type(cls.__name__, (object,), {'__doc__': view_desc})
desc = views.get_view_description(cls, html=html)
if html:
desc = '<div class="description">%s</div>' % desc
return mark_safe(desc)
class APIView(views.APIView):
def get_authenticate_header(self, request):
"""
Determine the WWW-Authenticate header to use for 401 responses. Try to
use the request header as an indication for which authentication method
was attempted.
"""
for authenticator in self.get_authenticators():
resp_hdr = authenticator.authenticate_header(request)
if not resp_hdr:
continue
req_hdr = get_authorization_header(request)
if not req_hdr:
continue
if resp_hdr.split()[0] and resp_hdr.split()[0] == req_hdr.split()[0]:
return resp_hdr
return super(APIView, self).get_authenticate_header(request)
def get_description_context(self):
return {
'docstring': type(self).__doc__ or '',
'new_in_13': getattr(self, 'new_in_13', False),
'new_in_14': getattr(self, 'new_in_14', False),
}
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('api/%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):
#if hasattr(self.request.user, 'get_queryset'):
# return self.request.user.get_queryset(self.model)
#else:
return super(GenericAPIView, self).get_queryset()
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 metadata(self, request):
'''
Add field information for GET requests (so field names/labels are
available even when we can't POST/PUT).
'''
ret = super(GenericAPIView, self).metadata(request)
actions = ret.get('actions', {})
# Remove read only fields from PUT/POST data.
for method in ('POST', 'PUT'):
fields = actions.get(method, {})
for field, meta in fields.items():
if not isinstance(meta, dict):
continue
if meta.get('read_only', False):
fields.pop(field)
if 'GET' in self.allowed_methods:
cloned_request = clone_request(request, 'GET')
try:
# Test global permissions
self.check_permissions(cloned_request)
# Test object permissions
if hasattr(self, 'retrieve'):
try:
self.get_object()
except Http404:
# Http404 should be acceptable and the serializer
# metadata should be populated. Except this so the
# outer "else" clause of the try-except-else block
# will be executed.
pass
except (exceptions.APIException, PermissionDenied):
pass
else:
# If user has appropriate permissions for the view, include
# appropriate metadata about the fields that should be supplied.
serializer = self.get_serializer()
actions['GET'] = serializer.metadata()
if actions:
ret['actions'] = actions
if getattr(self, 'search_fields', None):
ret['search_fields'] = self.search_fields
return ret
class ListAPIView(generics.ListAPIView, GenericAPIView):
# Base class for a read-only list view.
def get_queryset(self):
return self.request.user.get_queryset(self.model)
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
@property
def search_fields(self):
fields = []
for field in self.model._meta.fields:
if field.name in ('username', 'first_name', 'last_name', 'email',
'name', 'description', 'email'):
fields.append(field.name)
return fields
class ListCreateAPIView(ListAPIView, generics.ListCreateAPIView):
# Base class for a list view that allows creating new objects.
def pre_save(self, obj):
super(ListCreateAPIView, self).pre_save(obj)
if isinstance(obj, PrimordialModel):
obj.created_by = self.request.user
class SubListAPIView(ListAPIView):
# Base class for a read-only sublist view.
# Subclasses should define at least:
# model = ModelClass
# serializer_class = SerializerClass
# parent_model = ModelClass
# relationship = 'rel_name_from_parent_to_model'
# And optionally (user must have given access permission on parent object
# to view sublist):
# parent_access = 'read'
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_parent_object(self):
parent_filter = {
self.lookup_field: self.kwargs.get(self.lookup_field, None),
}
return get_object_or_404(self.parent_model, **parent_filter)
def check_parent_access(self, parent=None):
parent = parent or self.get_parent_object()
parent_access = getattr(self, 'parent_access', 'read')
if parent_access in ('read', 'delete'):
args = (self.parent_model, parent_access, parent)
else:
args = (self.parent_model, parent_access, parent, None)
if not self.request.user.can_access(*args):
raise PermissionDenied()
def get_queryset(self):
parent = self.get_parent_object()
self.check_parent_access(parent)
qs = self.request.user.get_queryset(self.model).distinct()
sublist_qs = getattr(parent, self.relationship).distinct()
return qs & sublist_qs
class SubListCreateAPIView(SubListAPIView, ListCreateAPIView):
# Base class for a sublist view that allows for creating subobjects and
# attaching/detaching them from the parent.
# In addition to SubListAPIView properties, subclasses may define (if the
# sub_obj requires a foreign key to the parent):
# parent_key = 'field_on_model_referring_to_parent'
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
# DB yet. We want to see if we can create it. The URL may choose to
# inject it's primary key into the object because we are posting to a
# subcollection. Use all the normal access control mechanisms.
# Make a copy of the data provided (since it's readonly) in order to
# inject additional data.
if hasattr(request.DATA, 'dict'):
data = request.DATA.dict()
else:
data = request.DATA
# add the parent key to the post data using the pk from the URL
parent_key = getattr(self, 'parent_key', None)
if parent_key:
data[parent_key] = self.kwargs['pk']
# attempt to deserialize the object
serializer = self.serializer_class(data=data)
if not serializer.is_valid():
return Response(serializer.errors,
status=status.HTTP_400_BAD_REQUEST)
# Verify we have permission to add the object as given.
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
obj = serializer.save()
serializer = self.serializer_class(obj)
return Response(serializer.data, status=status.HTTP_201_CREATED)
def attach(self, request, *args, **kwargs):
created = False
parent = self.get_parent_object()
relationship = getattr(parent, self.relationship)
sub_id = request.DATA.get('id', None)
data = request.DATA
# Create the sub object if an ID is not provided.
if not sub_id:
response = self.create(request, *args, **kwargs)
if response.status_code != status.HTTP_201_CREATED:
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).
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):
raise PermissionDenied()
# Attach the object to the collection.
if sub not in relationship.all():
relationship.add(sub)
if 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)
def unattach(self, request, *args, **kwargs):
sub_id = request.DATA.get('id', None)
if not sub_id:
data = dict(msg='"id" is required to disassociate')
return Response(data, status=status.HTTP_400_BAD_REQUEST)
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)
if not request.user.can_access(self.parent_model, 'unattach', parent,
sub, self.relationship):
raise PermissionDenied()
if parent_key:
# sub object has a ForeignKey to the parent, so we can't remove it
# from the set, only mark it as inactive.
sub.mark_inactive()
else:
relationship.remove(sub)
return Response(status=status.HTTP_204_NO_CONTENT)
def post(self, request, *args, **kwargs):
if not isinstance(request.DATA, dict):
return Response('invalid type for post data',
status=status.HTTP_400_BAD_REQUEST)
if 'disassociate' in request.DATA:
return self.unattach(request, *args, **kwargs)
else:
return self.attach(request, *args, **kwargs)
class RetrieveAPIView(generics.RetrieveAPIView, GenericAPIView):
pass
class RetrieveUpdateAPIView(RetrieveAPIView, generics.RetrieveUpdateAPIView):
def pre_save(self, 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.get_object()
# FIXME: Why isn't the active check being caught earlier by RBAC?
if getattr(obj, 'active', True) == False:
raise Http404()
if getattr(obj, 'is_active', True) == False:
raise Http404()
if not request.user.can_access(self.model, 'delete', obj):
raise PermissionDenied()
if hasattr(obj, 'mark_inactive'):
obj.mark_inactive()
else:
raise NotImplementedError('destroy() not implemented yet for %s' % obj)
return HttpResponse(status=204)

4
awx/api/models.py Normal file
View File

@@ -0,0 +1,4 @@
# Copyright (c) 2013 AnsibleWorks, Inc.
# All Rights Reserved.
# Empty models file.

37
awx/api/pagination.py Normal file
View File

@@ -0,0 +1,37 @@
# Copyright (c) 2013 AnsibleWorks, Inc.
# All Rights Reserved.
# Django REST Framework
from rest_framework import serializers, pagination
from rest_framework.templatetags.rest_framework import replace_query_param
class NextPageField(pagination.NextPageField):
'''Pagination field to output URL path.'''
def to_native(self, value):
if not value.has_next():
return None
page = value.next_page_number()
request = self.context.get('request')
url = request and request.get_full_path() or ''
return replace_query_param(url, self.page_field, page)
class PreviousPageField(pagination.NextPageField):
'''Pagination field to output URL path.'''
def to_native(self, value):
if not value.has_previous():
return None
page = value.previous_page_number()
request = self.context.get('request')
url = request and request.get_full_path() or ''
return replace_query_param(url, self.page_field, page)
class PaginationSerializer(pagination.BasePaginationSerializer):
'''
Custom pagination serializer to output only URL path (without host/port).
'''
count = serializers.Field(source='paginator.count')
next = NextPageField(source='*')
previous = PreviousPageField(source='*')

188
awx/api/permissions.py Normal file
View File

@@ -0,0 +1,188 @@
# Copyright (c) 2013 AnsibleWorks, Inc.
# All Rights Reserved.
# Python
import logging
# Django
from django.http import Http404
# Django REST Framework
from rest_framework.exceptions import PermissionDenied
from rest_framework import permissions
# AWX
from awx.main.access import *
from awx.main.models import *
from awx.main.utils import get_object_or_400
logger = logging.getLogger('awx.api.permissions')
__all__ = ['ModelAccessPermission', 'JobTemplateCallbackPermission',
'JobTaskPermission']
class ModelAccessPermission(permissions.BasePermission):
'''
Default permissions class to check user access based on the model and
request method, optionally verifying the request data.
'''
def check_options_permissions(self, request, view, obj=None):
return self.check_get_permissions(request, view, obj)
def check_head_permissions(self, request, view, obj=None):
return self.check_get_permissions(request, view, obj)
def check_get_permissions(self, request, view, obj=None):
if hasattr(view, 'parent_model'):
parent_obj = get_object_or_400(view.parent_model, pk=view.kwargs['pk'])
if not check_user_access(request.user, view.parent_model, 'read',
parent_obj):
return False
if not obj:
return True
return check_user_access(request.user, view.model, 'read', obj)
def check_post_permissions(self, request, view, obj=None):
if hasattr(view, 'parent_model'):
parent_obj = get_object_or_400(view.parent_model, pk=view.kwargs['pk'])
return True
elif getattr(view, 'is_job_start', False):
if not obj:
return True
return check_user_access(request.user, view.model, 'start', obj)
elif getattr(view, 'is_job_cancel', False):
if not obj:
return True
return check_user_access(request.user, view.model, 'cancel', obj)
else:
if obj:
return True
return check_user_access(request.user, view.model, 'add', request.DATA)
def check_put_permissions(self, request, view, obj=None):
if not obj:
return True # FIXME: For some reason this needs to return True
# because it is first called with obj=None?
if getattr(view, 'is_variable_data', False):
return check_user_access(request.user, view.model, 'change', obj,
dict(variables=request.DATA))
else:
return check_user_access(request.user, view.model, 'change', obj,
request.DATA)
def check_patch_permissions(self, request, view, obj=None):
return self.check_put_permissions(request, view, obj)
def check_delete_permissions(self, request, view, obj=None):
if not obj:
return True # FIXME: For some reason this needs to return True
# because it is first called with obj=None?
return check_user_access(request.user, view.model, 'delete', obj)
def check_permissions(self, request, view, obj=None):
'''
Perform basic permissions checking before delegating to the appropriate
method based on the request method.
'''
# Check that obj (if given) is active, otherwise raise a 404.
active = getattr(obj, 'active', getattr(obj, 'is_active', True))
if callable(active):
active = active()
if not active:
raise Http404()
# Don't allow anonymous users. 401, not 403, hence no raised exception.
if not request.user or request.user.is_anonymous():
return False
# Don't allow inactive users (and respond with a 403).
if not request.user.is_active:
raise PermissionDenied('your account is inactive')
# Always allow superusers (as long as they are active).
if request.user.is_superuser:
return True
# Check permissions for the given view and object, based on the request
# method used.
check_method = getattr(self, 'check_%s_permissions' % \
request.method.lower(), None)
result = check_method and check_method(request, view, obj)
if not result:
raise PermissionDenied()
return result
def has_permission(self, request, view, obj=None):
logger.debug('has_permission(user=%s method=%s data=%r, %s, %r)',
request.user, request.method, request.DATA,
view.__class__.__name__, obj)
try:
response = self.check_permissions(request, view, obj)
except Exception, e:
logger.debug('has_permission raised %r', e, exc_info=True)
raise
else:
logger.debug('has_permission returned %r', response)
return response
def has_object_permission(self, request, view, obj):
return self.has_permission(request, view, obj)
class JobTemplateCallbackPermission(ModelAccessPermission):
'''
Permission check used by job template callback view for requests from
empheral hosts.
'''
def has_permission(self, request, view, obj=None):
# If another authentication method was used and it's not a POST, return
# True to fall through to the next permission class.
if (request.user or request.auth) and request.method.lower() != 'post':
return super(JobTemplateCallbackPermission, self).has_permission(request, view, obj)
# Require method to be POST, host_config_key to be specified and match
# the requested job template, and require the job template to be
# active in order to proceed.
host_config_key = request.DATA.get('host_config_key', '')
if request.method.lower() != 'post':
raise PermissionDenied()
elif not host_config_key:
raise PermissionDenied()
elif obj and not obj.active:
raise PermissionDenied()
elif obj and obj.host_config_key != host_config_key:
raise PermissionDenied()
else:
return True
class JobTaskPermission(ModelAccessPermission):
'''
Permission checks used for API callbacks from running a task.
'''
def has_permission(self, request, view, obj=None):
# If another authentication method was used other than the one for job
# callbacks, default to the superclass permissions checking.
if request.user or not request.auth:
return super(JobTaskPermission, self).has_permission(request, view, obj)
# Verify that the job ID present in the auth token is for a valid,
# active job.
try:
job = Job.objects.get(active=True, status='running',
pk=int(request.auth.split('-')[0]))
except (Job.DoesNotExist, TypeError):
return False
# Verify that the request method is one of those allowed for the given
# view, also that the job or inventory being accessed matches the auth
# token.
if view.model == Inventory and request.method.lower() in ('head', 'get'):
return bool(not obj or obj.pk == job.inventory.pk)
elif view.model == JobEvent and request.method.lower() == 'post':
return bool(not obj or obj.pk == job.pk)
else:
return False

18
awx/api/renderers.py Normal file
View File

@@ -0,0 +1,18 @@
# Copyright (c) 2013 AnsibleWorks, Inc.
# All Rights Reserved.
# Django REST Framework
from rest_framework import renderers
class BrowsableAPIRenderer(renderers.BrowsableAPIRenderer):
'''
Customizations to the default browsable API renderer.
'''
def get_rendered_html_form(self, view, method, request):
'''Never show auto-generated form (only raw form).'''
obj = getattr(view, 'object', None)
if not self.show_form_for_method(view, method, request, obj):
return
if method in ('DELETE', 'OPTIONS'):
return True # Don't actually need to return a form

1021
awx/api/serializers.py Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,120 @@
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 "api/_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 }}
Multiple sorting fields may be specified by separating the field names with a
comma `,`:
?order_by={{ order_field }},some_other_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.
## Searching
Use the `search` query string parameter to perform a case-insensitive search
within all designated text fields of a model.
?search=findme
_New in AWX 1.4_
## Filtering
Any additional query string parameters may be used to filter the list of
results returned to those matching a given value. Only fields and relations
that exist in the database may be used for filtering. Any special characters
in the specified value should be url-encoded. For example:
?field=value%20xyz
Fields may also span relations, only for fields and relationships defined in
the database:
?other__field=value
To exclude results matching certain criteria, prefix the field parameter with
`not__`:
?not__field=value
(_New in AWX 1.4_) By default, all query string filters are AND'ed together, so
only the results matching *all* filters will be returned. To combine results
matching *any* one of multiple criteria, prefix each query string parameter
with `or__`:
?or__field=value&or__field=othervalue
?or__not__field=value&or__field=othervalue
Field lookups may also be used for more advanced queries, by appending the
lookup to the field name:
?field__lookup=value
The following field lookups are supported:
* `exact`: Exact match (default lookup if not specified).
* `iexact`: Case-insensitive version of `exact`.
* `contains`: Field contains value.
* `icontains`: Case-insensitive version of `contains`.
* `startswith`: Field starts with value.
* `istartswith`: Case-insensitive version of `startswith`.
* `endswith`: Field ends with value.
* `iendswith`: Case-insensitive version of `endswith`.
* `regex`: Field matches the given regular expression.
* `iregex`: Case-insensitive version of `regex`.
* `gt`: Greater than comparison.
* `gte`: Greater than or equal to comparison.
* `lt`: Less than comparison.
* `lte`: Less than or equal to comparison.
* `isnull`: Check whether the given field or related object is null; expects a
boolean value.
* `in`: Check whether the given field's value is present in the list provided;
expects a list of items.
Boolean values may be specified as `True` or `1` for true, `False` or `0` for
false (both case-insensitive).
Null values may be specified as `None` or `Null` (both case-insensitive),
though it is preferred to use the `isnull` lookup to explicitly check for null
values.
Lists (for the `in` lookup) may be specified as a comma-separated list of
values.

View File

@@ -0,0 +1,2 @@
{% if new_in_13 %}> _New in AWX 1.3_{% endif %}
{% if new_in_14 %}> _New in AWX 1.4_{% endif %}

View 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 %}

View 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.

View 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.

View 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.

View File

@@ -0,0 +1,3 @@
{{ docstring }}
{% include "api/_new_in_awx.md" %}

View File

@@ -0,0 +1,31 @@
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 and an `expires` field with
the timestamp when the token will expire:
{
"token": "8f17825cf08a7efea124f2638f3896f6637f8745",
"expires": "2013-09-05T21:46:35.729Z"
}
Otherwise, the response will indicate the error that occurred and return a 4xx
status code.
For subsequent requests, pass the token via the HTTP `Authorization` request
header:
Authorization: Token 8f17825cf08a7efea124f2638f3896f6637f8745
Each request that uses the token for authentication will refresh its expiration
timestamp and keep it from expiring. A token only expires when it is not used
for the configured timeout interval (default 1800 seconds).

View 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 }}.

View 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 "api/_list_common.md" %}

View File

@@ -0,0 +1,9 @@
# List Potential Child Groups for this {{ parent_model_verbose_name|title }}:
Make a GET request to this resource to retrieve a list of
{{ model_verbose_name_plural }} available to be added as children of the
current {{ parent_model_verbose_name }}.
{% include "api/_list_common.md" %}
{% include "api/_new_in_awx.md" %}

View 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 "api/_list_common.md" %}

View 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 "api/_list_common.md" %}

View File

@@ -0,0 +1,31 @@
Generate inventory group and host data as needed for an inventory script.
Refer to [External Inventory Scripts](http://www.ansibleworks.com/docs/api.html#external-inventory-scripts)
for more information on inventory scripts.
## List Response
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.
_(New in AWX 1.3)_ Specify a query string of `?hostvars=1` to retrieve the JSON
object above including all host variables. The `['_meta']['hostvars']` object
in the response contains an entry for each host with its variables. This
response format can be used with Ansible 1.3 and later to avoid making a
separate API request for each host. Refer to
[Tuning the External Inventory Script](http://www.ansibleworks.com/docs/api.html#tuning-the-external-inventory-script)
for more information on this feature.
_(New in AWX 1.4)_ By default, the inventory script will only return hosts that
are enabled in the inventory. This feature allows disabled hosts to be skipped
when running jobs without removing them from the inventory. Specify a query
string of `?all=1` to return all hosts, including disabled ones.
## Host Response
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.

View File

@@ -0,0 +1,13 @@
# Cancel Inventory Update
Make a GET request to this resource to determine if the inventory update can be
cancelled. The response will include the following field:
* `can_cancel`: Indicates whether this update can be canceled (boolean,
read-only)
Make a POST request to this resource to cancel a pending or running inventory
update. The response status code will be 202 if successful, or 405 if the
update cannot be canceled.
{% include "api/_new_in_awx.md" %}

View File

@@ -0,0 +1,18 @@
# Update Inventory Source
Make a GET request to this resource to determine if the group can be updated
from its inventory source and whether any passwords are required for the
update. The response will include the following fields:
* `can_start`: Flag indicating if this job can be started (boolean, read-only)
* `passwords_needed_to_update`: Password names required to update from the
inventory source (array, read-only)
Make a POST request to this resource to update the inventory source. 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 inventory source
is not defined or cannot be updated, a 405 status code will be returned.
{% include "api/_new_in_awx.md" %}

View File

@@ -0,0 +1,15 @@
# Group Tree for this {{ model_verbose_name|title }}:
Make a GET request to this resource to retrieve a hierarchical view of groups
associated with the selected {{ model_verbose_name }}.
The resulting data structure contains a list of root groups, with each group
also containing a list of its children.
## Results
Each group data structure includes the following fields:
{% include "api/_result_fields_common.md" %}
{% include "api/_new_in_awx.md" %}

View 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.

View File

@@ -0,0 +1,5 @@
{% include "api/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.

View 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.

View 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"]
}

View File

@@ -0,0 +1,6 @@
{% extends "api/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 %}

View File

@@ -0,0 +1,8 @@
# List {{ model_verbose_name_plural|title }}:
Make a GET request to this resource to retrieve the list of
{{ model_verbose_name_plural }}.
{% include "api/_list_common.md" %}
{% include "api/_new_in_awx.md" %}

View File

@@ -0,0 +1,12 @@
{% include "api/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 "api/_result_fields_common.md" %}
{% endwith %}
{% include "api/_new_in_awx.md" %}

View File

@@ -0,0 +1,3 @@
{% with model_verbose_name="admin user" model_verbose_name_plural="admin users" %}
{% include "api/sub_list_create_api_view.md" %}
{% endwith %}

View 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 }}.

View File

@@ -0,0 +1,13 @@
# Cancel Project Update
Make a GET request to this resource to determine if the project update can be
cancelled. The response will include the following field:
* `can_cancel`: Indicates whether this update can be canceled (boolean,
read-only)
Make a POST request to this resource to cancel a pending or running project
update. The response status code will be 202 if successful, or 405 if the
update cannot be canceled.
{% include "api/_new_in_awx.md" %}

View File

@@ -0,0 +1,18 @@
# Update Project
Make a GET request to this resource to determine if the project can be updated
from its SCM source and whether any passwords are required for the update. The
response will include the following fields:
* `can_start`: Flag indicating if this job can be started (boolean, read-only)
* `passwords_needed_to_update`: Password names required to update the project
(array, read-only)
Make a POST request to this resource to update the project. 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 project cannot be
updated, a 405 status code will be returned.
{% include "api/_new_in_awx.md" %}

View File

@@ -0,0 +1,9 @@
# Retrieve {{ model_verbose_name|title }}:
Make GET request to this resource to retrieve a single {{ model_verbose_name }}
record containing the following fields:
{% include "api/_result_fields_common.md" %}
{% include "api/_new_in_awx.md" %}

View File

@@ -0,0 +1,16 @@
{% include "api/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 "api/_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.
{% include "api/_new_in_awx.md" %}

View File

@@ -0,0 +1,20 @@
{% include "api/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 "api/_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 }}.
{% include "api/_new_in_awx.md" %}

View File

@@ -0,0 +1,9 @@
# 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 "api/_list_common.md" %}
{% include "api/_new_in_awx.md" %}

View File

@@ -0,0 +1,39 @@
{% include "api/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 "api/_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 %}
{% include "api/_new_in_awx.md" %}

View File

@@ -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 "api/_list_common.md" %}

View 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 "api/_result_fields_common.md" %}
Use the primary URL for the user (/api/v1/users/N/) to modify the user.

170
awx/api/urls.py Normal file
View File

@@ -0,0 +1,170 @@
# Copyright (c) 2013 AnsibleWorks, Inc.
# All Rights Reserved.
from django.conf.urls import include, patterns, url as original_url
def url(regex, view, kwargs=None, name=None, prefix=''):
# Set default name from view name (if a string).
if isinstance(view, basestring) and name is None:
name = view
return original_url(regex, view, kwargs, name, prefix)
organization_urls = patterns('awx.api.views',
url(r'^$', 'organization_list'),
url(r'^(?P<pk>[0-9]+)/$', 'organization_detail'),
url(r'^(?P<pk>[0-9]+)/users/$', 'organization_users_list'),
url(r'^(?P<pk>[0-9]+)/admins/$', 'organization_admins_list'),
url(r'^(?P<pk>[0-9]+)/inventories/$', 'organization_inventories_list'),
url(r'^(?P<pk>[0-9]+)/projects/$', 'organization_projects_list'),
url(r'^(?P<pk>[0-9]+)/teams/$', 'organization_teams_list'),
)
user_urls = patterns('awx.api.views',
url(r'^$', 'user_list'),
url(r'^(?P<pk>[0-9]+)/$', 'user_detail'),
url(r'^(?P<pk>[0-9]+)/teams/$', 'user_teams_list'),
url(r'^(?P<pk>[0-9]+)/organizations/$', 'user_organizations_list'),
url(r'^(?P<pk>[0-9]+)/admin_of_organizations/$', 'user_admin_of_organizations_list'),
url(r'^(?P<pk>[0-9]+)/projects/$', 'user_projects_list'),
url(r'^(?P<pk>[0-9]+)/credentials/$', 'user_credentials_list'),
url(r'^(?P<pk>[0-9]+)/permissions/$', 'user_permissions_list'),
)
project_urls = patterns('awx.api.views',
url(r'^$', 'project_list'),
url(r'^(?P<pk>[0-9]+)/$', 'project_detail'),
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'),
url(r'^(?P<pk>[0-9]+)/update/$', 'project_update_view'),
url(r'^(?P<pk>[0-9]+)/project_updates/$', 'project_updates_list'),
)
project_update_urls = patterns('awx.api.views',
url(r'^(?P<pk>[0-9]+)/$', 'project_update_detail'),
url(r'^(?P<pk>[0-9]+)/cancel/$', 'project_update_cancel'),
)
team_urls = patterns('awx.api.views',
url(r'^$', 'team_list'),
url(r'^(?P<pk>[0-9]+)/$', 'team_detail'),
url(r'^(?P<pk>[0-9]+)/projects/$', 'team_projects_list'),
url(r'^(?P<pk>[0-9]+)/users/$', 'team_users_list'),
url(r'^(?P<pk>[0-9]+)/credentials/$', 'team_credentials_list'),
url(r'^(?P<pk>[0-9]+)/permissions/$', 'team_permissions_list'),
)
inventory_urls = patterns('awx.api.views',
url(r'^$', 'inventory_list'),
url(r'^(?P<pk>[0-9]+)/$', 'inventory_detail'),
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_data'),
url(r'^(?P<pk>[0-9]+)/script/$', 'inventory_script_view'),
url(r'^(?P<pk>[0-9]+)/tree/$', 'inventory_tree_view'),
url(r'^(?P<pk>[0-9]+)/inventory_sources/$', 'inventory_inventory_sources_list'),
)
host_urls = patterns('awx.api.views',
url(r'^$', 'host_list'),
url(r'^(?P<pk>[0-9]+)/$', 'host_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'),
url(r'^(?P<pk>[0-9]+)/job_host_summaries/$', 'host_job_host_summaries_list'),
#url(r'^(?P<pk>[0-9]+)/inventory_sources/$', 'host_inventory_sources_list'),
)
group_urls = patterns('awx.api.views',
url(r'^$', 'group_list'),
url(r'^(?P<pk>[0-9]+)/$', 'group_detail'),
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_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'),
url(r'^(?P<pk>[0-9]+)/potential_children/$', 'group_potential_children_list'),
#url(r'^(?P<pk>[0-9]+)/inventory_sources/$', 'group_inventory_sources_list'),
)
inventory_source_urls = patterns('awx.api.views',
url(r'^$', 'inventory_source_list'),
url(r'^(?P<pk>[0-9]+)/$', 'inventory_source_detail'),
url(r'^(?P<pk>[0-9]+)/update/$', 'inventory_source_update_view'),
url(r'^(?P<pk>[0-9]+)/inventory_updates/$', 'inventory_source_updates_list'),
#url(r'^(?P<pk>[0-9]+)/groups/$', 'inventory_source_groups_list'),
#url(r'^(?P<pk>[0-9]+)/hosts/$', 'inventory_source_hosts_list'),
)
inventory_update_urls = patterns('awx.api.views',
url(r'^(?P<pk>[0-9]+)/$', 'inventory_update_detail'),
url(r'^(?P<pk>[0-9]+)/cancel/$', 'inventory_update_cancel'),
)
credential_urls = patterns('awx.api.views',
url(r'^$', 'credential_list'),
url(r'^(?P<pk>[0-9]+)/$', 'credential_detail'),
# See also credentials resources on users/teams.
)
permission_urls = patterns('awx.api.views',
url(r'^(?P<pk>[0-9]+)/$', 'permission_detail'),
)
job_template_urls = patterns('awx.api.views',
url(r'^$', 'job_template_list'),
url(r'^(?P<pk>[0-9]+)/$', 'job_template_detail'),
url(r'^(?P<pk>[0-9]+)/jobs/$', 'job_template_jobs_list'),
url(r'^(?P<pk>[0-9]+)/callback/$', 'job_template_callback'),
)
job_urls = patterns('awx.api.views',
url(r'^$', 'job_list'),
url(r'^(?P<pk>[0-9]+)/$', 'job_detail'),
url(r'^(?P<pk>[0-9]+)/start/$', 'job_start'),
url(r'^(?P<pk>[0-9]+)/cancel/$', 'job_cancel'),
url(r'^(?P<pk>[0-9]+)/job_host_summaries/$', 'job_job_host_summaries_list'),
url(r'^(?P<pk>[0-9]+)/job_events/$', 'job_job_events_list'),
)
job_host_summary_urls = patterns('awx.api.views',
url(r'^(?P<pk>[0-9]+)/$', 'job_host_summary_detail'),
)
job_event_urls = patterns('awx.api.views',
url(r'^$', 'job_event_list'),
url(r'^(?P<pk>[0-9]+)/$', 'job_event_detail'),
url(r'^(?P<pk>[0-9]+)/children/$', 'job_event_children_list'),
url(r'^(?P<pk>[0-9]+)/hosts/$', 'job_event_hosts_list'),
)
v1_urls = patterns('awx.api.views',
url(r'^$', 'api_v1_root_view'),
url(r'^config/$', 'api_v1_config_view'),
url(r'^authtoken/$', 'auth_token_view'),
url(r'^me/$', 'user_me_list'),
url(r'^organizations/', include(organization_urls)),
url(r'^users/', include(user_urls)),
url(r'^projects/', include(project_urls)),
url(r'^project_updates/', include(project_update_urls)),
url(r'^teams/', include(team_urls)),
url(r'^inventories/', include(inventory_urls)),
url(r'^hosts/', include(host_urls)),
url(r'^groups/', include(group_urls)),
url(r'^inventory_sources/', include(inventory_source_urls)),
url(r'^inventory_updates/', include(inventory_update_urls)),
url(r'^credentials/', include(credential_urls)),
url(r'^permissions/', include(permission_urls)),
url(r'^job_templates/', include(job_template_urls)),
url(r'^jobs/', include(job_urls)),
url(r'^job_host_summaries/', include(job_host_summary_urls)),
url(r'^job_events/', include(job_event_urls)),
)
urlpatterns = patterns('awx.api.views',
url(r'^$', 'api_root_view'),
url(r'^v1/', include(v1_urls)),
)

1059
awx/api/views.py Normal file

File diff suppressed because it is too large Load Diff