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