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

View File

@@ -15,7 +15,7 @@ from django.utils.translation import ugettext_lazy as _
from django.contrib.auth.models import User
from django.contrib.auth.admin import UserAdmin
from awx.main.compat import format_html
from awx.lib.compat import format_html
from awx.main.models import *
from awx.main.forms import *

View File

@@ -1,61 +0,0 @@
# 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'

View File

@@ -1,411 +0,0 @@
# 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('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):
#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)

View File

@@ -1,31 +0,0 @@
# Copyright (c) 2013 AnsibleWorks, Inc.
# All Rights Reserved.
'''
Compability library for support of both Django 1.4.x and Django 1.5.x.
'''
try:
from django.utils.html import format_html
except ImportError:
from django.utils.html import conditional_escape
from django.utils.safestring import mark_safe
def format_html(format_string, *args, **kwargs):
args_safe = map(conditional_escape, args)
kwargs_safe = dict([(k, conditional_escape(v)) for (k, v) in
kwargs.items()])
return mark_safe(format_string.format(*args_safe, **kwargs_safe))
try:
from django.utils.log import RequireDebugTrue
except ImportError:
import logging
from django.conf import settings
class RequireDebugTrue(logging.Filter):
def filter(self, record):
return settings.DEBUG
try:
from django.utils.text import slugify
except ImportError:
from django.template.defaultfilters import slugify

View File

@@ -1,196 +0,0 @@
# 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)

View File

@@ -35,7 +35,7 @@ from taggit.managers import TaggableManager
from djcelery.models import TaskMeta
# AWX
from awx.main.compat import slugify
from awx.lib.compat import slugify
from awx.main.fields import AutoOneToOneField
from awx.main.utils import encrypt_field, decrypt_field
@@ -343,7 +343,7 @@ class Organization(CommonModel):
projects = models.ManyToManyField('Project', blank=True, related_name='organizations')
def get_absolute_url(self):
return reverse('main:organization_detail', args=(self.pk,))
return reverse('api:organization_detail', args=(self.pk,))
def __unicode__(self):
return self.name
@@ -412,7 +412,7 @@ class Inventory(CommonModel):
)
def get_absolute_url(self):
return reverse('main:inventory_detail', args=(self.pk,))
return reverse('api:inventory_detail', args=(self.pk,))
def mark_inactive(self, save=True):
'''
@@ -537,7 +537,7 @@ class Host(CommonModelNameNotUnique):
return self.name
def get_absolute_url(self):
return reverse('main:host_detail', args=(self.pk,))
return reverse('api:host_detail', args=(self.pk,))
def mark_inactive(self, save=True):
'''
@@ -667,7 +667,7 @@ class Group(CommonModelNameNotUnique):
return self.name
def get_absolute_url(self):
return reverse('main:group_detail', args=(self.pk,))
return reverse('api:group_detail', args=(self.pk,))
def mark_inactive(self, save=True):
'''
@@ -938,7 +938,7 @@ class InventorySource(PrimordialModel):
return inventory_update
def get_absolute_url(self):
return reverse('main:inventory_source_detail', args=(self.pk,))
return reverse('api:inventory_source_detail', args=(self.pk,))
class InventoryUpdate(CommonTask):
'''
@@ -972,7 +972,7 @@ class InventoryUpdate(CommonTask):
return self.inventory_source
def get_absolute_url(self):
return reverse('main:inventory_update_detail', args=(self.pk,))
return reverse('api:inventory_update_detail', args=(self.pk,))
def _get_task_class(self):
from awx.main.tasks import RunInventoryUpdate
@@ -1087,7 +1087,7 @@ class Credential(CommonModelNameNotUnique):
return needed
def get_absolute_url(self):
return reverse('main:credential_detail', args=(self.pk,))
return reverse('api:credential_detail', args=(self.pk,))
def clean(self):
if self.user and self.team:
@@ -1142,7 +1142,7 @@ class Team(CommonModelNameNotUnique):
organization = models.ForeignKey('Organization', blank=False, null=True, on_delete=SET_NULL, related_name='teams')
def get_absolute_url(self):
return reverse('main:team_detail', args=(self.pk,))
return reverse('api:team_detail', args=(self.pk,))
class Project(CommonModel):
'''
@@ -1377,7 +1377,7 @@ class Project(CommonModel):
return project_update
def get_absolute_url(self):
return reverse('main:project_detail', args=(self.pk,))
return reverse('api:project_detail', args=(self.pk,))
def get_project_path(self, check_if_exists=True):
local_path = os.path.basename(self.local_path)
@@ -1435,7 +1435,7 @@ class ProjectUpdate(CommonTask):
)
def get_absolute_url(self):
return reverse('main:project_update_detail', args=(self.pk,))
return reverse('api:project_update_detail', args=(self.pk,))
def _get_parent_instance(self):
return self.project
@@ -1507,7 +1507,7 @@ class Permission(CommonModelNameNotUnique):
))
def get_absolute_url(self):
return reverse('main:permission_detail', args=(self.pk,))
return reverse('api:permission_detail', args=(self.pk,))
# TODO: other job types (later)
@@ -1607,7 +1607,7 @@ class JobTemplate(CommonModel):
return job
def get_absolute_url(self):
return reverse('main:job_template_detail', args=(self.pk,))
return reverse('api:job_template_detail', args=(self.pk,))
def can_start_without_user_input(self):
'''
@@ -1719,7 +1719,7 @@ class Job(CommonTask):
)
def get_absolute_url(self):
return reverse('main:job_detail', args=(self.pk,))
return reverse('api:job_detail', args=(self.pk,))
extra_vars_dict = VarsDictProperty('extra_vars', True)
@@ -1829,7 +1829,7 @@ class JobHostSummary(models.Model):
self.processed, self.skipped)
def get_absolute_url(self):
return reverse('main:job_host_summary_detail', args=(self.pk,))
return reverse('api:job_host_summary_detail', args=(self.pk,))
def save(self, *args, **kwargs):
self.failed = bool(self.dark or self.failures)
@@ -1982,7 +1982,7 @@ class JobEvent(models.Model):
)
def get_absolute_url(self):
return reverse('main:job_event_detail', args=(self.pk,))
return reverse('api:job_event_detail', args=(self.pk,))
def __unicode__(self):
return u'%s @ %s' % (self.get_event_display(), self.created.isoformat())

View File

@@ -1,37 +0,0 @@
# 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='*')

View File

@@ -1,188 +0,0 @@
# 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.main.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

View File

@@ -1,18 +0,0 @@
# 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

File diff suppressed because it is too large Load Diff

View File

@@ -1,120 +0,0 @@
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 }}
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

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

View File

@@ -1,6 +0,0 @@
{% 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

@@ -1,4 +0,0 @@
The root of the AWX REST API.
Make a GET request to this resource to obtain information about the available
API versions.

View File

@@ -1,12 +0,0 @@
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

@@ -1,4 +0,0 @@
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

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

View File

@@ -1,31 +0,0 @@
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

@@ -1,9 +0,0 @@
# 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

@@ -1,7 +0,0 @@
# 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" %}

View File

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

View File

@@ -1,7 +0,0 @@
# 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" %}

View File

@@ -1,7 +0,0 @@
# 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" %}

View File

@@ -1,31 +0,0 @@
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

@@ -1,13 +0,0 @@
# 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 "main/_new_in_awx.md" %}

View File

@@ -1,18 +0,0 @@
# 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 "main/_new_in_awx.md" %}

View File

@@ -1,15 +0,0 @@
# 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 "main/_result_fields_common.md" %}
{% include "main/_new_in_awx.md" %}

View File

@@ -1,10 +0,0 @@
# 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

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

View File

@@ -1,15 +0,0 @@
# 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

@@ -1,33 +0,0 @@
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

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

View File

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

View File

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

View File

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

View File

@@ -1,4 +0,0 @@
# 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

@@ -1,13 +0,0 @@
# 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 "main/_new_in_awx.md" %}

View File

@@ -1,18 +0,0 @@
# 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 "main/_new_in_awx.md" %}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,7 +0,0 @@
# 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" %}

View File

@@ -1,7 +0,0 @@
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.

View File

@@ -44,7 +44,7 @@ class InventoryTest(BaseTest):
)
def test_get_inventory_list(self):
url = reverse('main:inventory_list')
url = reverse('api:inventory_list')
qs = Inventory.objects.filter(active=True).distinct()
# Check list view with invalid authentication.
@@ -66,7 +66,7 @@ class InventoryTest(BaseTest):
self.check_get_list(url, self.nobody_django_user, nobody_qs)
def test_post_inventory_list(self):
url = reverse('main:inventory_list')
url = reverse('api:inventory_list')
# Check post to list view with invalid authentication.
new_inv_0 = dict(name='inventory-c', description='baz', organization=self.organizations[0].pk)
@@ -95,8 +95,8 @@ class InventoryTest(BaseTest):
data = self.post(url, data=new_inv_denied, expect=403)
def test_get_inventory_detail(self):
url_a = reverse('main:inventory_detail', args=(self.inventory_a.pk,))
url_b = reverse('main:inventory_detail', args=(self.inventory_b.pk,))
url_a = reverse('api:inventory_detail', args=(self.inventory_a.pk,))
url_b = reverse('api:inventory_detail', args=(self.inventory_b.pk,))
# Check detail view with invalid authentication.
self.check_invalid_auth(url_a)
@@ -125,8 +125,8 @@ class InventoryTest(BaseTest):
data = self.get(url_b, expect=403)
def test_put_inventory_detail(self):
url_a = reverse('main:inventory_detail', args=(self.inventory_a.pk,))
url_b = reverse('main:inventory_detail', args=(self.inventory_b.pk,))
url_a = reverse('api:inventory_detail', args=(self.inventory_a.pk,))
url_b = reverse('api:inventory_detail', args=(self.inventory_b.pk,))
# Check put to detail view with invalid authentication.
self.check_invalid_auth(url_a, methods=('put',))
@@ -178,7 +178,7 @@ class InventoryTest(BaseTest):
# Via AC-376:
# Create an inventory. Leave the description empty.
# Edit the new inventory, change the Name, click Save.
list_url = reverse('main:inventory_list')
list_url = reverse('api:inventory_list')
new_data = dict(name='inventory-c', description='',
organization=self.organizations[0].pk)
new_id = max(Inventory.objects.values_list('pk', flat=True)) + 1
@@ -186,7 +186,7 @@ class InventoryTest(BaseTest):
data = self.post(list_url, data=new_data, expect=201)
self.assertEqual(data['id'], new_id)
self.assertEqual(data['description'], '')
url_c = reverse('main:inventory_detail', args=(new_id,))
url_c = reverse('api:inventory_detail', args=(new_id,))
data = self.get(url_c, expect=200)
self.assertEqual(data['description'], '')
data['description'] = None
@@ -194,8 +194,8 @@ class InventoryTest(BaseTest):
self.put(url_c, data, expect=200)
def test_delete_inventory_detail(self):
url_a = reverse('main:inventory_detail', args=(self.inventory_a.pk,))
url_b = reverse('main:inventory_detail', args=(self.inventory_b.pk,))
url_a = reverse('api:inventory_detail', args=(self.inventory_a.pk,))
url_b = reverse('api:inventory_detail', args=(self.inventory_b.pk,))
# Create test hosts and groups within each inventory.
self.inventory_a.hosts.create(name='host-a')
@@ -246,11 +246,11 @@ class InventoryTest(BaseTest):
def test_main_line(self):
# some basic URLs...
inventories = reverse('main:inventory_list')
inventories_1 = reverse('main:inventory_detail', args=(self.inventory_a.pk,))
inventories_2 = reverse('main:inventory_detail', args=(self.inventory_b.pk,))
hosts = reverse('main:host_list')
groups = reverse('main:group_list')
inventories = reverse('api:inventory_list')
inventories_1 = reverse('api:inventory_detail', args=(self.inventory_a.pk,))
inventories_2 = reverse('api:inventory_detail', args=(self.inventory_b.pk,))
hosts = reverse('api:host_list')
groups = reverse('api:group_list')
# a super user can add hosts (but inventory ID is required)
@@ -326,7 +326,7 @@ class InventoryTest(BaseTest):
new_group_c = dict(name='web4', inventory=inv.pk)
new_group_d = dict(name='web5', inventory=inv.pk)
new_group_e = dict(name='web6', inventory=inv.pk)
groups = reverse('main:group_list')
groups = reverse('api:group_list')
data0 = self.post(groups, data=invalid, expect=400, auth=self.get_super_credentials())
data0 = self.post(groups, data=new_group_a, expect=201, auth=self.get_super_credentials())
@@ -360,7 +360,7 @@ class InventoryTest(BaseTest):
#################################################
# HOSTS->inventories POST via subcollection
url = reverse('main:inventory_hosts_list', args=(self.inventory_a.pk,))
url = reverse('api:inventory_hosts_list', args=(self.inventory_a.pk,))
new_host_a = dict(name='web100.example.com')
new_host_b = dict(name='web101.example.com')
new_host_c = dict(name='web102.example.com')
@@ -377,7 +377,7 @@ class InventoryTest(BaseTest):
added_by_collection_c = self.post(url, data=new_host_c, expect=403, auth=self.get_nobody_credentials())
# a normal user with edit permission on the inventory can associate hosts with inventories
url5 = reverse('main:inventory_hosts_list', args=(inv.pk,))
url5 = reverse('api:inventory_hosts_list', args=(inv.pk,))
added_by_collection_d = self.post(url5, data=new_host_d, expect=201, auth=self.get_other_credentials())
got = self.get(url5, expect=200, auth=self.get_other_credentials())
self.assertEquals(got['count'], 4)
@@ -392,9 +392,9 @@ class InventoryTest(BaseTest):
##################################################
# GROUPS->inventories POST via subcollection
root_groups = reverse('main:inventory_root_groups_list', args=(self.inventory_a.pk,))
root_groups = reverse('api:inventory_root_groups_list', args=(self.inventory_a.pk,))
url = reverse('main:inventory_groups_list', args=(self.inventory_a.pk,))
url = reverse('api:inventory_groups_list', args=(self.inventory_a.pk,))
new_group_a = dict(name='web100')
new_group_b = dict(name='web101')
new_group_c = dict(name='web102')
@@ -411,7 +411,7 @@ class InventoryTest(BaseTest):
added_by_collection = self.post(url, data=new_group_c, expect=403, auth=self.get_nobody_credentials())
# a normal user with edit permissions on the inventory can associate groups with inventories
url5 = reverse('main:inventory_groups_list', args=(inv.pk,))
url5 = reverse('api:inventory_groups_list', args=(inv.pk,))
added_by_collection = self.post(url5, data=new_group_d, expect=201, auth=self.get_other_credentials())
# make sure duplicates give 400s
self.post(url5, data=new_group_d, expect=400, auth=self.get_other_credentials())
@@ -436,7 +436,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_data', args=(added_by_collection_a['id'],))
vdata_url = reverse('api:host_variable_data', args=(added_by_collection_a['id'],))
got = self.get(vdata_url, expect=200, auth=self.get_super_credentials())
self.assertEquals(got, {})
@@ -467,8 +467,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_data', args=(groups[0].pk,))
vdata2_url = reverse('main:group_variable_data', args=(groups[1].pk,))
vdata1_url = reverse('api:group_variable_data', args=(groups[0].pk,))
vdata2_url = reverse('api: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())
@@ -493,7 +493,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_data', args=(self.inventory_a.pk,))
vdata_url = reverse('api: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())
@@ -536,14 +536,14 @@ class InventoryTest(BaseTest):
groups[0].save()
# access
url1 = reverse('main:group_hosts_list', args=(groups[0].pk,))
url1 = reverse('api:group_hosts_list', args=(groups[0].pk,))
data = self.get(url1, expect=200, auth=self.get_normal_credentials())
self.assertEquals(data['count'], 2)
self.assertTrue(host1.pk in [x['id'] for x in data['results']])
self.assertTrue(host3.pk in [x['id'] for x in data['results']])
# addition
url = reverse('main:host_detail', args=(host2.pk,))
url = reverse('api:host_detail', args=(host2.pk,))
got = self.get(url, expect=200, auth=self.get_normal_credentials())
self.assertEquals(got['id'], host2.pk)
posted = self.post(url1, data=got, expect=204, auth=self.get_normal_credentials())
@@ -596,15 +596,15 @@ class InventoryTest(BaseTest):
g2.save()
# a super user can set subgroups
subgroups_url = reverse('main:group_children_list',
subgroups_url = reverse('api:group_children_list',
args=(Group.objects.get(name='web2').pk,))
child_url = reverse('main:group_detail',
child_url = reverse('api:group_detail',
args=(Group.objects.get(name='web4').pk,))
subgroups_url2 = reverse('main:group_children_list',
subgroups_url2 = reverse('api:group_children_list',
args=(Group.objects.get(name='web6').pk,))
subgroups_url3 = reverse('main:group_children_list',
subgroups_url3 = reverse('api:group_children_list',
args=(Group.objects.get(name='web100').pk,))
subgroups_url4 = reverse('main:group_children_list',
subgroups_url4 = reverse('api:group_children_list',
args=(Group.objects.get(name='web101').pk,))
got = self.get(child_url, expect=200, auth=self.get_super_credentials())
self.post(subgroups_url, data=got, expect=204, auth=self.get_super_credentials())
@@ -625,7 +625,7 @@ class InventoryTest(BaseTest):
# a group can't be it's own grandparent
subsub = posted2['related']['children']
# this is the grandparent
original_url = reverse('main:group_detail', args=(Group.objects.get(name='web6').pk,))
original_url = reverse('api:group_detail', args=(Group.objects.get(name='web6').pk,))
parent_data = self.get(original_url, expect=200, auth=self.get_super_credentials())
# now posting to kid's children collection...
self.post(subsub, data=parent_data, expect=403, auth=self.get_super_credentials())
@@ -648,7 +648,7 @@ class InventoryTest(BaseTest):
# slight detour
# can see all hosts under a group, even if it has subgroups
# this URL is NOT postable
all_hosts = reverse('main:group_all_hosts_list',
all_hosts = reverse('api:group_all_hosts_list',
args=(Group.objects.get(name='web2').pk,))
self.assertEqual(Group.objects.get(name='web2').hosts.count(), 3)
data = self.get(all_hosts, expect=200, auth=self.get_normal_credentials())
@@ -669,7 +669,7 @@ class InventoryTest(BaseTest):
self.assertTrue(removed_group.parents.count())
self.assertTrue(removed_group.active)
for parent in removed_group.parents.all():
parent_children_url = reverse('main:group_children_list', args=(parent.pk,))
parent_children_url = reverse('api:group_children_list', args=(parent.pk,))
data = {'id': removed_group.pk, 'disassociate': 1}
self.post(parent_children_url, data, expect=204, auth=self.get_super_credentials())
removed_group = Group.objects.get(pk=result['id'])
@@ -678,7 +678,7 @@ class InventoryTest(BaseTest):
# Removing a group from a hierarchy should migrate its children to the
# parent. The group itself will be deleted (marked inactive), and all
# relationships removed.
url = reverse('main:group_children_list', args=(gx2.pk,))
url = reverse('api:group_children_list', args=(gx2.pk,))
data = {
'id': gx3.pk,
'disassociate': 1,
@@ -691,7 +691,7 @@ class InventoryTest(BaseTest):
#self.assertTrue(gx4 in gx2.children.all())
# Try with invalid hostnames and invalid IPs.
hosts = reverse('main:host_list')
hosts = reverse('api:host_list')
invalid_expect = 400 # hostname validation is disabled for now.
data = dict(name='', inventory=inv.pk)
with self.current_user(self.super_django_user):
@@ -752,7 +752,7 @@ class InventoryTest(BaseTest):
h_d.groups.add(g_d)
# Old, slow 1.2 way.
url = reverse('main:inventory_script_view', args=(i_a.pk,))
url = reverse('api:inventory_script_view', args=(i_a.pk,))
with self.current_user(self.super_django_user):
response = self.get(url, expect=200)
self.assertTrue('all' in response)
@@ -772,7 +772,7 @@ class InventoryTest(BaseTest):
self.assertEqual(response, h.variables_dict)
# New 1.3 way.
url = reverse('main:inventory_script_view', args=(i_a.pk,))
url = reverse('api:inventory_script_view', args=(i_a.pk,))
url = '%s?hostvars=1' % url
with self.current_user(self.super_django_user):
response = self.get(url, expect=200)
@@ -792,7 +792,7 @@ class InventoryTest(BaseTest):
g_d = self.inventory_a.groups.create(name='D')
g_d.parents.add(g_c)
url = reverse('main:inventory_tree_view', args=(self.inventory_a.pk,))
url = reverse('api:inventory_tree_view', args=(self.inventory_a.pk,))
with self.current_user(self.super_django_user):
response = self.get(url, expect=200)

View File

@@ -436,7 +436,7 @@ class JobTemplateTest(BaseJobTestMixin, django.test.TestCase):
'job_tags', 'host_config_key',)
def test_get_job_template_list(self):
url = reverse('main:job_template_list')
url = reverse('api:job_template_list')
qs = JobTemplate.objects.distinct()
fields = self.JOB_TEMPLATE_FIELDS
@@ -474,7 +474,7 @@ class JobTemplateTest(BaseJobTestMixin, django.test.TestCase):
# FIXME: Check with other credentials.
def test_post_job_template_list(self):
url = reverse('main:job_template_list')
url = reverse('api:job_template_list')
data = dict(
name = 'new job template',
job_type = PERM_INVENTORY_DEPLOY,
@@ -489,7 +489,7 @@ class JobTemplateTest(BaseJobTestMixin, django.test.TestCase):
# sue can always add job templates.
with self.current_user(self.user_sue):
response = self.post(url, data, expect=201)
detail_url = reverse('main:job_template_detail',
detail_url = reverse('api:job_template_detail',
args=(response['id'],))
self.assertEquals(response['url'], detail_url)
@@ -530,7 +530,7 @@ class JobTemplateTest(BaseJobTestMixin, django.test.TestCase):
def test_get_job_template_detail(self):
jt = self.jt_eng_run
url = reverse('main:job_template_detail', args=(jt.pk,))
url = reverse('api:job_template_detail', args=(jt.pk,))
# Test with no auth and with invalid login.
self.check_invalid_auth(url)
@@ -551,7 +551,7 @@ class JobTemplateTest(BaseJobTestMixin, django.test.TestCase):
def test_put_job_template_detail(self):
jt = self.jt_eng_run
url = reverse('main:job_template_detail', args=(jt.pk,))
url = reverse('api:job_template_detail', args=(jt.pk,))
# Test with no auth and with invalid login.
self.check_invalid_auth(url, methods=('put',))# 'patch'))
@@ -568,7 +568,7 @@ class JobTemplateTest(BaseJobTestMixin, django.test.TestCase):
def test_get_job_template_job_list(self):
jt = self.jt_eng_run
url = reverse('main:job_template_jobs_list', args=(jt.pk,))
url = reverse('api:job_template_jobs_list', args=(jt.pk,))
# Test with no auth and with invalid login.
self.check_invalid_auth(url)
@@ -586,7 +586,7 @@ class JobTemplateTest(BaseJobTestMixin, django.test.TestCase):
def test_post_job_template_job_list(self):
jt = self.jt_eng_run
url = reverse('main:job_template_jobs_list', args=(jt.pk,))
url = reverse('api:job_template_jobs_list', args=(jt.pk,))
data = dict(
name='new job from template',
credential=self.cred_bob.pk,
@@ -604,7 +604,7 @@ class JobTemplateTest(BaseJobTestMixin, django.test.TestCase):
class JobTest(BaseJobTestMixin, django.test.TestCase):
def test_get_job_list(self):
url = reverse('main:job_list')
url = reverse('api:job_list')
# Test with no auth and with invalid login.
self.check_invalid_auth(url)
@@ -622,7 +622,7 @@ class JobTest(BaseJobTestMixin, django.test.TestCase):
# FIXME: Check with other credentials.
def test_post_job_list(self):
url = reverse('main:job_list')
url = reverse('api:job_list')
data = dict(
name='new job without template',
job_type=PERM_INVENTORY_DEPLOY,
@@ -652,7 +652,7 @@ class JobTest(BaseJobTestMixin, django.test.TestCase):
def test_get_job_detail(self):
job = self.job_ops_east_run
url = reverse('main:job_detail', args=(job.pk,))
url = reverse('api:job_detail', args=(job.pk,))
# Test with no auth and with invalid login.
self.check_invalid_auth(url)
@@ -668,7 +668,7 @@ class JobTest(BaseJobTestMixin, django.test.TestCase):
def test_put_job_detail(self):
job = self.job_ops_west_run
url = reverse('main:job_detail', args=(job.pk,))
url = reverse('api:job_detail', args=(job.pk,))
# Test with no auth and with invalid login.
self.check_invalid_auth(url, methods=('put',))# 'patch'))
@@ -697,7 +697,7 @@ class JobTest(BaseJobTestMixin, django.test.TestCase):
# FIXME: Check with other credentials and readonly fields.
def _test_mainline(self):
url = reverse('main:job_list')
url = reverse('api:job_list')
# job templates
data = self.get('/api/v1/job_templates/', expect=401)
@@ -769,7 +769,7 @@ class JobStartCancelTest(BaseJobTestMixin, django.test.LiveServerTestCase):
def test_job_start(self):
job = self.job_ops_east_run
url = reverse('main:job_start', args=(job.pk,))
url = reverse('api:job_start', args=(job.pk,))
# Test with no auth and with invalid login.
self.check_invalid_auth(url)
@@ -797,7 +797,7 @@ class JobStartCancelTest(BaseJobTestMixin, django.test.LiveServerTestCase):
# Test with a job that prompts for SSH and sudo passwords.
job = self.job_sup_run
url = reverse('main:job_start', args=(job.pk,))
url = reverse('api:job_start', args=(job.pk,))
with self.current_user(self.user_sue):
response = self.get(url)
self.assertTrue(response['can_start'])
@@ -821,7 +821,7 @@ class JobStartCancelTest(BaseJobTestMixin, django.test.LiveServerTestCase):
credential=self.cred_greg,
created_by=self.user_sue,
)
url = reverse('main:job_start', args=(job.pk,))
url = reverse('api:job_start', args=(job.pk,))
with self.current_user(self.user_sue):
response = self.get(url)
self.assertTrue(response['can_start'])
@@ -841,7 +841,7 @@ class JobStartCancelTest(BaseJobTestMixin, django.test.LiveServerTestCase):
credential=self.cred_greg,
created_by=self.user_sue,
)
url = reverse('main:job_start', args=(job.pk,))
url = reverse('api:job_start', args=(job.pk,))
with self.current_user(self.user_sue):
response = self.get(url)
self.assertTrue(response['can_start'])
@@ -858,7 +858,7 @@ class JobStartCancelTest(BaseJobTestMixin, django.test.LiveServerTestCase):
def test_job_cancel(self):
job = self.job_ops_east_run
url = reverse('main:job_cancel', args=(job.pk,))
url = reverse('api:job_cancel', args=(job.pk,))
# Test with no auth and with invalid login.
self.check_invalid_auth(url)
@@ -887,7 +887,7 @@ class JobStartCancelTest(BaseJobTestMixin, django.test.LiveServerTestCase):
job.start()
# Check that the job detail has been updated.
url = reverse('main:job_detail', args=(job.pk,))
url = reverse('api:job_detail', args=(job.pk,))
with self.current_user(self.user_sue):
response = self.get(url)
self.assertEqual(response['status'], 'successful',
@@ -895,7 +895,7 @@ class JobStartCancelTest(BaseJobTestMixin, django.test.LiveServerTestCase):
self.assertTrue(response['result_stdout'])
# Test job events for completed job.
url = reverse('main:job_job_events_list', args=(job.pk,))
url = reverse('api:job_job_events_list', args=(job.pk,))
with self.current_user(self.user_sue):
response = self.get(url)
qs = job.job_events.all()
@@ -908,13 +908,13 @@ class JobStartCancelTest(BaseJobTestMixin, django.test.LiveServerTestCase):
for job_event in job.job_events.all():
if job_event.host:
host_ids.add(job_event.host.pk)
url = reverse('main:job_event_detail', args=(job_event.pk,))
url = reverse('api:job_event_detail', args=(job_event.pk,))
with self.current_user(self.user_sue):
response = self.get(url)
# Also test job event list for each host.
for host in Host.objects.filter(pk__in=host_ids):
url = reverse('main:host_job_events_list', args=(host.pk,))
url = reverse('api:host_job_events_list', args=(host.pk,))
with self.current_user(self.user_sue):
response = self.get(url)
qs = host.job_events.all()
@@ -924,7 +924,7 @@ class JobStartCancelTest(BaseJobTestMixin, django.test.LiveServerTestCase):
# Test job event list for groups.
for group in self.inv_ops_east.groups.all():
url = reverse('main:group_job_events_list', args=(group.pk,))
url = reverse('api:group_job_events_list', args=(group.pk,))
with self.current_user(self.user_sue):
response = self.get(url)
qs = group.job_events.all()
@@ -933,7 +933,7 @@ class JobStartCancelTest(BaseJobTestMixin, django.test.LiveServerTestCase):
self.check_list_ids(response, qs)
# Test global job event list.
url = reverse('main:job_event_list')
url = reverse('api:job_event_list')
with self.current_user(self.user_sue):
response = self.get(url)
qs = JobEvent.objects.all()
@@ -942,7 +942,7 @@ class JobStartCancelTest(BaseJobTestMixin, django.test.LiveServerTestCase):
self.check_list_ids(response, qs)
# Test job host summaries for completed job.
url = reverse('main:job_job_host_summaries_list', args=(job.pk,))
url = reverse('api:job_job_host_summaries_list', args=(job.pk,))
with self.current_user(self.user_sue):
response = self.get(url)
qs = job.job_host_summaries.all()
@@ -956,14 +956,14 @@ class JobStartCancelTest(BaseJobTestMixin, django.test.LiveServerTestCase):
# Test individual job host summary records.
for job_host_summary in job.job_host_summaries.all():
url = reverse('main:job_host_summary_detail',
url = reverse('api:job_host_summary_detail',
args=(job_host_summary.pk,))
with self.current_user(self.user_sue):
response = self.get(url)
# Test job host summaries for each host.
for host in Host.objects.filter(pk__in=host_ids):
url = reverse('main:host_job_host_summaries_list', args=(host.pk,))
url = reverse('api:host_job_host_summaries_list', args=(host.pk,))
with self.current_user(self.user_sue):
response = self.get(url)
qs = host.job_host_summaries.all()
@@ -973,7 +973,7 @@ class JobStartCancelTest(BaseJobTestMixin, django.test.LiveServerTestCase):
# Test job host summaries for groups.
for group in self.inv_ops_east.groups.all():
url = reverse('main:group_job_host_summaries_list', args=(group.pk,))
url = reverse('api:group_job_host_summaries_list', args=(group.pk,))
with self.current_user(self.user_sue):
response = self.get(url)
qs = group.job_host_summaries.all()
@@ -1115,7 +1115,7 @@ class JobTemplateCallbackTest(BaseJobTestMixin, django.test.LiveServerTestCase):
job_template = jt
break
self.assertTrue(job_template)
url = reverse('main:job_template_callback', args=(job_template.pk,))
url = reverse('api:job_template_callback', args=(job_template.pk,))
data = dict(host_config_key=job_template.host_config_key)
# Test a POST to start a new job.
@@ -1254,7 +1254,7 @@ class JobTemplateCallbackTest(BaseJobTestMixin, django.test.LiveServerTestCase):
job_template = jt
break
self.assertTrue(job_template)
url = reverse('main:job_template_callback', args=(job_template.pk,))
url = reverse('api:job_template_callback', args=(job_template.pk,))
data = dict(host_config_key=job_template.host_config_key)
# Should get an error when multiple hosts match to the same IP.
@@ -1278,7 +1278,7 @@ class JobTemplateCallbackTest(BaseJobTestMixin, django.test.LiveServerTestCase):
job_template = jt
break
self.assertTrue(job_template)
url = reverse('main:job_template_callback', args=(job_template.pk,))
url = reverse('api:job_template_callback', args=(job_template.pk,))
data = dict(host_config_key=job_template.host_config_key)
# Test POST to start a new job when the template has no credential.
@@ -1300,7 +1300,7 @@ class JobTemplateCallbackTest(BaseJobTestMixin, django.test.LiveServerTestCase):
job_template = jt
break
self.assertTrue(job_template)
url = reverse('main:job_template_callback', args=(job_template.pk,))
url = reverse('api:job_template_callback', args=(job_template.pk,))
data = dict(host_config_key=job_template.host_config_key)
# Test POST to start a new job when the credential would require user

View File

@@ -14,7 +14,7 @@ from awx.main.tests.base import BaseTest
class OrganizationsTest(BaseTest):
def collection(self):
return reverse('main:organization_list')
return reverse('api:organization_list')
def setUp(self):
super(OrganizationsTest, self).setUp()
@@ -53,7 +53,7 @@ class OrganizationsTest(BaseTest):
self.organizations[1].admins.add(self.normal_django_user)
def test_get_organization_list(self):
url = reverse('main:organization_list')
url = reverse('api:organization_list')
# no credentials == 401
self.options(url, expect=401)
@@ -280,7 +280,7 @@ class OrganizationsTest(BaseTest):
def test_post_item_subobjects_users(self):
url = reverse('main:organization_users_list', args=(self.organizations[1].pk,))
url = reverse('api:organization_users_list', args=(self.organizations[1].pk,))
users = self.get(url, expect=200, auth=self.get_normal_credentials())
self.assertEqual(users['count'], 2)
self.post(url, dict(id=self.normal_django_user.pk), expect=204, auth=self.get_normal_credentials())
@@ -293,7 +293,7 @@ class OrganizationsTest(BaseTest):
# post a completely new user to verify we can add users to the subcollection directly
new_user = dict(username='NewUser9000')
which_org = self.normal_django_user.admin_of_organizations.all()[0]
url = reverse('main:organization_users_list', args=(which_org.pk,))
url = reverse('api:organization_users_list', args=(which_org.pk,))
posted = self.post(url, new_user, expect=201, auth=self.get_normal_credentials())
all_users = self.get(url, expect=200, auth=self.get_normal_credentials())
@@ -301,7 +301,7 @@ class OrganizationsTest(BaseTest):
def test_post_item_subobjects_admins(self):
url = reverse('main:organization_admins_list', args=(self.organizations[1].pk,))
url = reverse('api:organization_admins_list', args=(self.organizations[1].pk,))
admins = self.get(url, expect=200, auth=self.get_normal_credentials())
self.assertEqual(admins['count'], 2)
self.post(url, dict(id=self.other_django_user.pk), expect=204, auth=self.get_normal_credentials())
@@ -395,7 +395,7 @@ class OrganizationsTest(BaseTest):
self.delete(self.collection(), expect=405, auth=self.get_super_credentials())
def test_invalid_post_data(self):
url = reverse('main:organization_list')
url = reverse('api:organization_list')
# API should gracefully handle data of an invalid type.
self.post(url, expect=400, data=None, auth=self.get_super_credentials())
self.post(url, expect=400, data=99, auth=self.get_super_credentials())
@@ -403,7 +403,7 @@ class OrganizationsTest(BaseTest):
self.post(url, expect=400, data=3.14, auth=self.get_super_credentials())
self.post(url, expect=400, data=True, auth=self.get_super_credentials())
self.post(url, expect=400, data=[1,2,3], auth=self.get_super_credentials())
url = reverse('main:organization_users_list', args=(self.organizations[0].pk,))
url = reverse('api:organization_users_list', args=(self.organizations[0].pk,))
self.post(url, expect=400, data=None, auth=self.get_super_credentials())
self.post(url, expect=400, data=99, auth=self.get_super_credentials())
self.post(url, expect=400, data='abcd', auth=self.get_super_credentials())

View File

@@ -38,7 +38,7 @@ class ProjectsTest(BaseTest):
# tests for users, projects, and teams
def collection(self):
return reverse('main:project_list')
return reverse('api:project_list')
def setUp(self):
super(ProjectsTest, self).setUp()
@@ -148,7 +148,7 @@ class ProjectsTest(BaseTest):
def test_api_config(self):
# superuser can read all config data.
url = reverse('main:api_v1_config_view')
url = reverse('api:api_v1_config_view')
response = self.get(url, expect=200, auth=self.get_super_credentials())
self.assertTrue('project_base_dir' in response)
self.assertEqual(response['project_base_dir'], settings.PROJECTS_ROOT)
@@ -188,7 +188,7 @@ class ProjectsTest(BaseTest):
# PROJECTS - LISTING
# can get projects list
projects = reverse('main:project_list')
projects = reverse('api:project_list')
# invalid auth
self.get(projects, expect=401)
self.get(projects, expect=401, auth=self.get_invalid_credentials())
@@ -217,7 +217,7 @@ class ProjectsTest(BaseTest):
auth=self.get_super_credentials())
# can edit project using same local path.
project_detail = reverse('main:project_detail', args=(response['id'],))
project_detail = reverse('api:project_detail', args=(response['id'],))
project_data = self.get(project_detail, expect=200,
auth=self.get_super_credentials())
response = self.put(project_detail, project_data, expect=200,
@@ -235,7 +235,7 @@ class ProjectsTest(BaseTest):
# =====================================================================
# PROJECTS - ACCESS
project = reverse('main:project_detail', args=(self.projects[3].pk,))
project = reverse('api:project_detail', args=(self.projects[3].pk,))
self.get(project, expect=200, auth=self.get_super_credentials())
self.get(project, expect=200, auth=self.get_normal_credentials())
self.get(project, expect=200, auth=self.get_other_credentials())
@@ -246,18 +246,18 @@ class ProjectsTest(BaseTest):
self.get(project, expect=404, auth=self.get_normal_credentials())
# can list playbooks for projects
proj_playbooks = reverse('main:project_playbooks', args=(self.projects[2].pk,))
proj_playbooks = reverse('api: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)
# can list member organizations for projects
proj_orgs = reverse('main:project_organizations_list', args=(self.projects[0].pk,))
proj_orgs = reverse('api:project_organizations_list', args=(self.projects[0].pk,))
# only usable as superuser
got = self.get(proj_orgs, expect=200, auth=self.get_normal_credentials())
got = self.get(proj_orgs, expect=200, auth=self.get_super_credentials())
self.get(proj_orgs, expect=403, auth=self.get_other_credentials())
self.assertEquals(got['count'], 1)
self.assertEquals(got['results'][0]['url'], reverse('main:organization_detail', args=(self.organizations[0].pk,)))
self.assertEquals(got['results'][0]['url'], reverse('api:organization_detail', args=(self.organizations[0].pk,)))
# post to create new org associated with this project.
self.post(proj_orgs, data={'name': 'New Org'}, expect=201, auth=self.get_super_credentials())
@@ -267,8 +267,8 @@ class ProjectsTest(BaseTest):
# =====================================================================
# TEAMS
all_teams = reverse('main:team_list')
team1 = reverse('main:team_detail', args=(self.team1.pk,))
all_teams = reverse('api:team_list')
team1 = reverse('api:team_detail', args=(self.team1.pk,))
# can list teams
got = self.get(all_teams, expect=200, auth=self.get_super_credentials())
@@ -277,7 +277,7 @@ class ProjectsTest(BaseTest):
# can get teams
got = self.get(team1, expect=200, auth=self.get_super_credentials())
self.assertEquals(got['url'], reverse('main:team_detail', args=(self.team1.pk,)))
self.assertEquals(got['url'], reverse('api:team_detail', args=(self.team1.pk,)))
got = self.get(team1, expect=200, auth=self.get_normal_credentials())
got = self.get(team1, expect=403, auth=self.get_other_credentials())
self.team1.users.add(User.objects.get(username='other'))
@@ -303,7 +303,7 @@ class ProjectsTest(BaseTest):
url5 = posted1['url']
new_team = Team.objects.create(name='newTeam4', organization=self.organizations[1])
url = reverse('main:team_detail', args=(new_team.pk,))
url = reverse('api:team_detail', args=(new_team.pk,))
# can delete teams
self.delete(url, expect=401)
@@ -316,7 +316,7 @@ class ProjectsTest(BaseTest):
# ORGANIZATION TEAMS
# can list organization teams (filtered by user) -- this is an org admin function
org_teams = reverse('main:organization_teams_list', args=(self.organizations[1].pk,))
org_teams = reverse('api:organization_teams_list', args=(self.organizations[1].pk,))
data1 = self.get(org_teams, expect=401)
data2 = self.get(org_teams, expect=403, auth=self.get_nobody_credentials())
data3 = self.get(org_teams, expect=403, auth=self.get_other_credentials())
@@ -346,7 +346,7 @@ class ProjectsTest(BaseTest):
# TEAM PROJECTS
team = Team.objects.filter(active=True, organization__pk=self.organizations[1].pk)[0]
team_projects = reverse('main:team_projects_list', args=(team.pk,))
team_projects = reverse('api:team_projects_list', args=(team.pk,))
p1 = self.projects[0]
team.projects.add(p1)
@@ -363,7 +363,7 @@ class ProjectsTest(BaseTest):
# TEAMS USER MEMBERSHIP
team = Team.objects.filter(active=True, organization__pk=self.organizations[1].pk)[0]
team_users = reverse('main:team_users_list', args=(team.pk,))
team_users = reverse('api:team_users_list', args=(team.pk,))
for x in team.users.all():
team.users.remove(x)
team.save()
@@ -377,7 +377,7 @@ class ProjectsTest(BaseTest):
self.get(team_users, expect=200, auth=self.get_super_credentials())
# can add users to teams (but only users I can see)
all_users = self.get(reverse('main:user_list'), expect=200, auth=self.get_normal_credentials())
all_users = self.get(reverse('api:user_list'), expect=200, auth=self.get_normal_credentials())
for x in all_users['results']:
self.post(team_users, data=x, expect=403, auth=self.get_nobody_credentials())
self.post(team_users, data=x, expect=204, auth=self.get_normal_credentials())
@@ -397,7 +397,7 @@ class ProjectsTest(BaseTest):
# from a user, can see what teams they are on (related resource)
other = User.objects.get(username = 'other')
url = reverse('main:user_teams_list', args=(other.pk,))
url = reverse('api:user_teams_list', args=(other.pk,))
self.get(url, expect=401)
self.get(url, expect=401, auth=self.get_invalid_credentials())
self.get(url, expect=403, auth=self.get_nobody_credentials())
@@ -413,7 +413,7 @@ class ProjectsTest(BaseTest):
# =====================================================================
# USER PROJECTS
url = reverse('main:user_projects_list', args=(other.pk,))
url = reverse('api:user_projects_list', args=(other.pk,))
# from a user, can see what projects they can see based on team association
# though this resource doesn't do anything else
@@ -427,8 +427,8 @@ class ProjectsTest(BaseTest):
# =====================================================================
# CREDENTIALS
other_creds = reverse('main:user_credentials_list', args=(other.pk,))
team_creds = reverse('main:team_credentials_list', args=(team.pk,))
other_creds = reverse('api:user_credentials_list', args=(other.pk,))
team_creds = reverse('api:team_credentials_list', args=(team.pk,))
new_credentials = dict(
name = 'credential',
@@ -476,7 +476,7 @@ class ProjectsTest(BaseTest):
self.get(team_creds, expect=403, auth=self.get_nobody_credentials())
# Check /api/v1/credentials (GET)
url = reverse('main:credential_list')
url = reverse('api:credential_list')
with self.current_user(self.super_django_user):
self.options(url)
self.head(url)
@@ -498,8 +498,8 @@ class ProjectsTest(BaseTest):
d_cred_user = dict(id=cred_user.pk, name='x', sudo_password='blippy', user=cred_user.user.pk)
d_cred_user2 = dict(id=cred_user.pk, name='x', sudo_password='blippy', user=self.super_django_user.pk)
d_cred_team = dict(id=cred_team.pk, name='x', sudo_password='blippy', team=cred_team.team.pk)
edit_creds1 = reverse('main:credential_detail', args=(cred_user.pk,))
edit_creds2 = reverse('main:credential_detail', args=(cred_team.pk,))
edit_creds1 = reverse('api:credential_detail', args=(cred_user.pk,))
edit_creds2 = reverse('api:credential_detail', args=(cred_team.pk,))
self.put(edit_creds1, data=d_cred_user, expect=401)
self.put(edit_creds1, data=d_cred_user, expect=401, auth=self.get_invalid_credentials())
@@ -517,13 +517,13 @@ class ProjectsTest(BaseTest):
self.put(edit_creds2, data=d_cred_team, expect=403, auth=self.get_other_credentials())
cred_put_t['disassociate'] = 1
team_url = reverse('main:team_credentials_list', args=(cred_put_t['team'],))
team_url = reverse('api:team_credentials_list', args=(cred_put_t['team'],))
self.post(team_url, data=cred_put_t, expect=204, auth=self.get_normal_credentials())
# can remove credentials from a user (via disassociate) - this will delete the credential.
cred_put_u['disassociate'] = 1
url = cred_put_u['url']
user_url = reverse('main:user_credentials_list', args=(cred_put_u['user'],))
user_url = reverse('api:user_credentials_list', args=(cred_put_u['user'],))
self.post(user_url, data=cred_put_u, expect=204, auth=self.get_normal_credentials())
# can delete a credential directly -- probably won't be used too often
@@ -560,13 +560,13 @@ class ProjectsTest(BaseTest):
permission_type=PERM_INVENTORY_DEPLOY
)
url = reverse('main:user_permissions_list', args=(user.pk,))
url = reverse('api:user_permissions_list', args=(user.pk,))
posted = self.post(url, user_permission, expect=201, auth=self.get_super_credentials())
url2 = posted['url']
got = self.get(url2, expect=200, auth=self.get_other_credentials())
# cannot add permissions that apply to both team and user
url = reverse('main:user_permissions_list', args=(user.pk,))
url = reverse('api:user_permissions_list', args=(user.pk,))
user_permission['name'] = 'user permission 2'
user_permission['team'] = team.pk
self.post(url, user_permission, expect=400, auth=self.get_super_credentials())
@@ -584,26 +584,26 @@ class ProjectsTest(BaseTest):
self.post(url, user_permission, expect=400, auth=self.get_super_credentials())
# can add permissions on a team
url = reverse('main:team_permissions_list', args=(team.pk,))
url = reverse('api:team_permissions_list', args=(team.pk,))
posted = self.post(url, team_permission, expect=201, auth=self.get_super_credentials())
url2 = posted['url']
# check we can get that permission back
got = self.get(url2, expect=200, auth=self.get_other_credentials())
# cannot add permissions that apply to both team and user
url = reverse('main:team_permissions_list', args=(team.pk,))
url = reverse('api:team_permissions_list', args=(team.pk,))
team_permission['name'] += '2'
team_permission['user'] = user.pk
self.post(url, team_permission, expect=400, auth=self.get_super_credentials())
# can list permissions on a user
url = reverse('main:user_permissions_list', args=(user.pk,))
url = reverse('api:user_permissions_list', args=(user.pk,))
got = self.get(url, expect=200, auth=self.get_super_credentials())
got = self.get(url, expect=200, auth=self.get_other_credentials())
got = self.get(url, expect=403, auth=self.get_nobody_credentials())
# can list permissions on a team
url = reverse('main:team_permissions_list', args=(team.pk,))
url = reverse('api:team_permissions_list', args=(team.pk,))
got = self.get(url, expect=200, auth=self.get_super_credentials())
got = self.get(url, expect=200, auth=self.get_other_credentials())
got = self.get(url, expect=403, auth=self.get_nobody_credentials())
@@ -1335,7 +1335,7 @@ class ProjectUpdatesTest(BaseTransactionTest):
scm_username='nobody',
scm_password='ASK',
)
url = reverse('main:project_update_view', args=(project.pk,))
url = reverse('api:project_update_view', args=(project.pk,))
with self.current_user(self.super_django_user):
response = self.get(url, expect=200)
self.assertTrue(response['can_update'])
@@ -1359,7 +1359,7 @@ class ProjectUpdatesTest(BaseTransactionTest):
scm_key_data=TEST_SSH_KEY_DATA_LOCKED,
scm_key_unlock='ASK',
)
url = reverse('main:project_update_view', args=(project.pk,))
url = reverse('api:project_update_view', args=(project.pk,))
with self.current_user(self.super_django_user):
response = self.get(url, expect=200)
self.assertTrue(response['can_update'])

View File

@@ -23,7 +23,7 @@ __all__ = ['UsersTest', 'LdapTest']
class UsersTest(BaseTest):
def collection(self):
return reverse('main:user_list')
return reverse('api:user_list')
def setUp(self):
super(UsersTest, self).setUp()
@@ -34,7 +34,7 @@ class UsersTest(BaseTest):
self.organizations[0].users.add(self.normal_django_user)
def test_only_super_user_or_org_admin_can_add_users(self):
url = reverse('main:user_list')
url = reverse('api:user_list')
new_user = dict(username='blippy')
new_user2 = dict(username='blippy2')
self.post(url, expect=401, data=new_user, auth=None)
@@ -46,7 +46,7 @@ class UsersTest(BaseTest):
self.post(url, expect=400, data=new_user2, auth=self.get_normal_credentials())
def test_auth_token_login(self):
auth_token_url = reverse('main:auth_token_view')
auth_token_url = reverse('api:auth_token_view')
# Always returns a 405 for any GET request, regardless of credentials.
self.get(auth_token_url, expect=405, auth=None)
@@ -69,7 +69,7 @@ class UsersTest(BaseTest):
auth_token = response['token']
# Verify we can access our own user information with the auth token.
response = self.get(reverse('main:user_me_list'), expect=200,
response = self.get(reverse('api:user_me_list'), expect=200,
auth=auth_token)
self.assertEquals(response['results'][0]['username'], 'normal')
self.assertEquals(response['count'], 1)
@@ -77,7 +77,7 @@ class UsersTest(BaseTest):
# If we simulate a different remote address, should not be able to use
# the first auth token.
remote_addr = '127.0.0.2'
response = self.get(reverse('main:user_me_list'), expect=401,
response = self.get(reverse('api:user_me_list'), expect=401,
auth=auth_token, remote_addr=remote_addr)
self.assertEqual(response['detail'], 'Invalid token')
@@ -97,28 +97,28 @@ class UsersTest(BaseTest):
# Verify we can access our own user information with the second auth
# token from the other remote address.
response = self.get(reverse('main:user_me_list'), expect=200,
response = self.get(reverse('api:user_me_list'), expect=200,
auth=auth_token2, remote_addr=remote_addr)
self.assertEquals(response['results'][0]['username'], 'normal')
self.assertEquals(response['count'], 1)
# The second auth token also can't be used from the first address, but
# the first auth token is still valid from its address.
response = self.get(reverse('main:user_me_list'), expect=401,
response = self.get(reverse('api:user_me_list'), expect=401,
auth=auth_token2)
self.assertEqual(response['detail'], 'Invalid token')
response_header = response.response.get('WWW-Authenticate', '')
self.assertEqual(response_header.split()[0], 'Token')
response = self.get(reverse('main:user_me_list'), expect=200,
response = self.get(reverse('api:user_me_list'), expect=200,
auth=auth_token)
# A request without authentication should ask for Basic by default.
response = self.get(reverse('main:user_me_list'), expect=401)
response = self.get(reverse('api:user_me_list'), expect=401)
response_header = response.response.get('WWW-Authenticate', '')
self.assertEqual(response_header.split()[0], 'Basic')
# A request that attempts Basic auth should request Basic auth again.
response = self.get(reverse('main:user_me_list'), expect=401,
response = self.get(reverse('api:user_me_list'), expect=401,
auth=('invalid', 'password'))
response_header = response.response.get('WWW-Authenticate', '')
self.assertEqual(response_header.split()[0], 'Basic')
@@ -126,21 +126,21 @@ class UsersTest(BaseTest):
# Invalidate a key (simulate expiration), now token auth should fail
# with the first token, but still work with the second.
self.normal_django_user.auth_tokens.get(key=auth_token).invalidate()
response = self.get(reverse('main:user_me_list'), expect=401,
response = self.get(reverse('api:user_me_list'), expect=401,
auth=auth_token)
self.assertEqual(response['detail'], 'Token is expired')
response = self.get(reverse('main:user_me_list'), expect=200,
response = self.get(reverse('api:user_me_list'), expect=200,
auth=auth_token2, remote_addr=remote_addr)
# Token auth should be denied if the user is inactive.
self.normal_django_user.mark_inactive()
response = self.get(reverse('main:user_me_list'), expect=401,
response = self.get(reverse('api:user_me_list'), expect=401,
auth=auth_token2, remote_addr=remote_addr)
self.assertEqual(response['detail'], 'User inactive or deleted')
def test_ordinary_user_can_modify_some_fields_about_himself_but_not_all_and_passwords_work(self):
detail_url = reverse('main:user_detail', args=(self.other_django_user.pk,))
detail_url = reverse('api:user_detail', args=(self.other_django_user.pk,))
data = self.get(detail_url, expect=200, auth=self.get_other_credentials())
# can't change first_name, last_name, etc
@@ -182,7 +182,7 @@ class UsersTest(BaseTest):
def test_user_created_with_password_can_login(self):
# this is something an org admin can do...
url = reverse('main:user_list')
url = reverse('api:user_list')
data = dict(username='username', password='password')
data2 = dict(username='username2', password='password2')
data = self.post(url, expect=201, data=data, auth=self.get_normal_credentials())
@@ -206,16 +206,16 @@ class UsersTest(BaseTest):
self.assertTrue(orig.username != 'change')
def test_password_not_shown_in_get_operations_for_list_or_detail(self):
url = reverse('main:user_detail', args=(self.super_django_user.pk,))
url = reverse('api:user_detail', args=(self.super_django_user.pk,))
data = self.get(url, expect=200, auth=self.get_super_credentials())
self.assertTrue('password' not in data)
url = reverse('main:user_list')
url = reverse('api:user_list')
data = self.get(url, expect=200, auth=self.get_super_credentials())
self.assertTrue('password' not in data['results'][0])
def test_user_list_filtered(self):
url = reverse('main:user_list')
url = reverse('api:user_list')
data3 = self.get(url, expect=200, auth=self.get_super_credentials())
self.assertEquals(data3['count'], 4)
data2 = self.get(url, expect=200, auth=self.get_normal_credentials())
@@ -225,22 +225,22 @@ class UsersTest(BaseTest):
def test_super_user_can_delete_a_user_but_only_marked_inactive(self):
user_pk = self.normal_django_user.pk
url = reverse('main:user_detail', args=(user_pk,))
url = reverse('api:user_detail', args=(user_pk,))
data = self.delete(url, expect=204, auth=self.get_super_credentials())
data = self.get(url, expect=404, auth=self.get_super_credentials())
obj = User.objects.get(pk=user_pk)
self.assertEquals(obj.is_active, False)
def test_non_org_admin_user_cannot_delete_any_user_including_himself(self):
url1 = reverse('main:user_detail', args=(self.super_django_user.pk,))
url2 = reverse('main:user_detail', args=(self.normal_django_user.pk,))
url3 = reverse('main:user_detail', args=(self.other_django_user.pk,))
url1 = reverse('api:user_detail', args=(self.super_django_user.pk,))
url2 = reverse('api:user_detail', args=(self.normal_django_user.pk,))
url3 = reverse('api:user_detail', args=(self.other_django_user.pk,))
data = self.delete(url1, expect=403, auth=self.get_other_credentials())
data = self.delete(url2, expect=403, auth=self.get_other_credentials())
data = self.delete(url3, expect=403, auth=self.get_other_credentials())
def test_there_exists_an_obvious_url_where_a_user_may_find_his_user_record(self):
url = reverse('main:user_me_list')
url = reverse('api:user_me_list')
data = self.get(url, expect=401, auth=None)
data = self.get(url, expect=401, auth=self.get_invalid_credentials())
data = self.get(url, expect=200, auth=self.get_normal_credentials())
@@ -254,7 +254,7 @@ class UsersTest(BaseTest):
self.assertEquals(data['count'], 1)
def test_superuser_can_change_admin_only_fields_about_himself(self):
url = reverse('main:user_detail', args=(self.super_django_user.pk,))
url = reverse('api:user_detail', args=(self.super_django_user.pk,))
data = self.get(url, expect=200, auth=self.get_super_credentials())
data['username'] += '2'
data['first_name'] += ' Awesome'
@@ -267,7 +267,7 @@ class UsersTest(BaseTest):
def test_user_related_resources(self):
# organizations the user is a member of, should be 1
url = reverse('main:user_organizations_list',
url = reverse('api:user_organizations_list',
args=(self.normal_django_user.pk,))
data = self.get(url, expect=200, auth=self.get_normal_credentials())
self.assertEquals(data['count'], 1)
@@ -280,7 +280,7 @@ class UsersTest(BaseTest):
data = self.get(url, expect=403, auth=self.get_nobody_credentials())
# organizations the user is an admin of, should be 1
url = reverse('main:user_admin_of_organizations_list',
url = reverse('api:user_admin_of_organizations_list',
args=(self.normal_django_user.pk,))
data = self.get(url, expect=200, auth=self.get_normal_credentials())
self.assertEquals(data['count'], 1)
@@ -293,7 +293,7 @@ class UsersTest(BaseTest):
data = self.get(url, expect=403, auth=self.get_nobody_credentials())
# teams the user is on, should be 0
url = reverse('main:user_teams_list', args=(self.normal_django_user.pk,))
url = reverse('api:user_teams_list', args=(self.normal_django_user.pk,))
data = self.get(url, expect=200, auth=self.get_normal_credentials())
self.assertEquals(data['count'], 0)
# also accessible via superuser
@@ -305,15 +305,15 @@ class UsersTest(BaseTest):
data = self.get(url, expect=403, auth=self.get_nobody_credentials())
# verify org admin can still read other user data too
url = reverse('main:user_organizations_list',
url = reverse('api:user_organizations_list',
args=(self.other_django_user.pk,))
data = self.get(url, expect=200, auth=self.get_normal_credentials())
self.assertEquals(data['count'], 1)
url = reverse('main:user_admin_of_organizations_list',
url = reverse('api:user_admin_of_organizations_list',
args=(self.other_django_user.pk,))
data = self.get(url, expect=200, auth=self.get_normal_credentials())
self.assertEquals(data['count'], 0)
url = reverse('main:user_teams_list',
url = reverse('api:user_teams_list',
args=(self.other_django_user.pk,))
data = self.get(url, expect=200, auth=self.get_normal_credentials())
self.assertEquals(data['count'], 0)
@@ -323,7 +323,7 @@ class UsersTest(BaseTest):
# FIXME: add test that shows posting a projects w/o id to /organizations/2/projects/ can create a new one & associate
def test_user_list_ordering(self):
base_url = reverse('main:user_list')
base_url = reverse('api:user_list')
base_qs = User.objects.distinct()
# Check list view with ordering by name.
@@ -351,7 +351,7 @@ class UsersTest(BaseTest):
def test_user_list_filtering(self):
# Also serves as general-purpose testing for custom API filters.
base_url = reverse('main:user_list')
base_url = reverse('api:user_list')
base_qs = User.objects.distinct()
# Filter by username.
@@ -651,7 +651,7 @@ class UsersTest(BaseTest):
self.check_get_list(url, self.super_django_user, base_qs, expect=400)
def test_user_list_pagination(self):
base_url = reverse('main:user_list')
base_url = reverse('api:user_list')
base_qs = User.objects.distinct()
# Check list view with page size of 1.
@@ -682,7 +682,7 @@ class UsersTest(BaseTest):
# limit=0)
def test_user_list_searching(self):
base_url = reverse('main:user_list')
base_url = reverse('api:user_list')
base_qs = User.objects.distinct()
# Check search query parameter.
@@ -826,12 +826,12 @@ class LdapTest(BaseTest):
self.use_test_setting(name)
user = self.check_login()
self.setup_users()
url = reverse('main:api_v1_config_view')
url = reverse('api:api_v1_config_view')
with self.current_user(self.super_django_user):
response = self.get(url, expect=200)
user_ldap_fields = response.get('user_ldap_fields', [])
self.assertTrue(user_ldap_fields)
url = reverse('main:user_detail', args=(user.pk,))
url = reverse('api:user_detail', args=(user.pk,))
for user_field in user_ldap_fields:
with self.current_user(self.super_django_user):
data = self.get(url, expect=200)

View File

@@ -1,170 +1,7 @@
# Copyright (c) 2013 AnsibleWorks, Inc.
# All Rights Reserved.
from django.conf.urls import include, patterns, url as original_url
from django.conf import settings
from django.conf.urls import *
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.main.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.main.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.main.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.main.views',
url(r'^(?P<pk>[0-9]+)/$', 'project_update_detail'),
url(r'^(?P<pk>[0-9]+)/cancel/$', 'project_update_cancel'),
)
team_urls = patterns('awx.main.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.main.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.main.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.main.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.main.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.main.views',
url(r'^(?P<pk>[0-9]+)/$', 'inventory_update_detail'),
url(r'^(?P<pk>[0-9]+)/cancel/$', 'inventory_update_cancel'),
)
credential_urls = patterns('awx.main.views',
url(r'^$', 'credential_list'),
url(r'^(?P<pk>[0-9]+)/$', 'credential_detail'),
# See also credentials resources on users/teams.
)
permission_urls = patterns('awx.main.views',
url(r'^(?P<pk>[0-9]+)/$', 'permission_detail'),
)
job_template_urls = patterns('awx.main.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.main.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.main.views',
url(r'^(?P<pk>[0-9]+)/$', 'job_host_summary_detail'),
)
job_event_urls = patterns('awx.main.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.main.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.main.views',
url(r'^$', 'api_root_view'),
url(r'^v1/', include(v1_urls)),
)
urlpatterns = patterns()

File diff suppressed because it is too large Load Diff