mirror of
https://github.com/ansible/awx.git
synced 2026-05-12 11:57:37 -02:30
Added config resource with project base dir and local paths, various other API cleanup.
This commit is contained in:
37
ansibleworks/main/pagination.py
Normal file
37
ansibleworks/main/pagination.py
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
# Copyright (c) 2013 AnsibleWorks, Inc.
|
||||||
|
# All Rights Reserved.
|
||||||
|
|
||||||
|
# Django REST Framework
|
||||||
|
from rest_framework import serializers, pagination
|
||||||
|
from rest_framework.templatetags.rest_framework import replace_query_param
|
||||||
|
|
||||||
|
class NextPageField(pagination.NextPageField):
|
||||||
|
'''Pagination field to output URL path.'''
|
||||||
|
|
||||||
|
def to_native(self, value):
|
||||||
|
if not value.has_next():
|
||||||
|
return None
|
||||||
|
page = value.next_page_number()
|
||||||
|
request = self.context.get('request')
|
||||||
|
url = request and request.get_full_path() or ''
|
||||||
|
return replace_query_param(url, self.page_field, page)
|
||||||
|
|
||||||
|
class PreviousPageField(pagination.NextPageField):
|
||||||
|
'''Pagination field to output URL path.'''
|
||||||
|
|
||||||
|
def to_native(self, value):
|
||||||
|
if not value.has_previous():
|
||||||
|
return None
|
||||||
|
page = value.previous_page_number()
|
||||||
|
request = self.context.get('request')
|
||||||
|
url = request and request.get_full_path() or ''
|
||||||
|
return replace_query_param(url, self.page_field, page)
|
||||||
|
|
||||||
|
class PaginationSerializer(pagination.BasePaginationSerializer):
|
||||||
|
'''
|
||||||
|
Custom pagination serializer to output only URL path (without host/port).
|
||||||
|
'''
|
||||||
|
|
||||||
|
count = serializers.Field(source='paginator.count')
|
||||||
|
next = NextPageField(source='*')
|
||||||
|
previous = PreviousPageField(source='*')
|
||||||
@@ -1,6 +1,10 @@
|
|||||||
import rest_framework.renderers
|
# Copyright (c) 2013 AnsibleWorks, Inc.
|
||||||
|
# All Rights Reserved.
|
||||||
|
|
||||||
class BrowsableAPIRenderer(rest_framework.renderers.BrowsableAPIRenderer):
|
# Django REST Framework
|
||||||
|
from rest_framework import renderers
|
||||||
|
|
||||||
|
class BrowsableAPIRenderer(renderers.BrowsableAPIRenderer):
|
||||||
'''
|
'''
|
||||||
Customizations to the default browsable API renderer.
|
Customizations to the default browsable API renderer.
|
||||||
'''
|
'''
|
||||||
|
|||||||
@@ -9,9 +9,8 @@ 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
|
||||||
|
|
||||||
# Django REST framework
|
# Django REST Framework
|
||||||
from rest_framework import serializers, pagination
|
from rest_framework import serializers
|
||||||
from rest_framework.templatetags.rest_framework import replace_query_param
|
|
||||||
|
|
||||||
# AnsibleWorks
|
# AnsibleWorks
|
||||||
from ansibleworks.main.models import *
|
from ansibleworks.main.models import *
|
||||||
@@ -19,37 +18,6 @@ from ansibleworks.main.models import *
|
|||||||
BASE_FIELDS = ('id', 'url', 'related', 'summary_fields', 'created',
|
BASE_FIELDS = ('id', 'url', 'related', 'summary_fields', 'created',
|
||||||
'creation_date', 'name', 'description')
|
'creation_date', 'name', 'description')
|
||||||
|
|
||||||
class NextPageField(pagination.NextPageField):
|
|
||||||
''' makes the pagination relative URL not full URL '''
|
|
||||||
|
|
||||||
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):
|
|
||||||
''' makes the pagination relative URL not full URL '''
|
|
||||||
|
|
||||||
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='*')
|
|
||||||
|
|
||||||
# objects that if found we should add summary info for them
|
# objects that if found we should add summary info for them
|
||||||
SUMMARIZABLE_FKS = (
|
SUMMARIZABLE_FKS = (
|
||||||
'organization', 'host', 'group', 'inventory', 'project', 'team', 'job', 'job_template',
|
'organization', 'host', 'group', 'inventory', 'project', 'team', 'job', 'job_template',
|
||||||
@@ -142,11 +110,10 @@ class OrganizationSerializer(BaseSerializer):
|
|||||||
class ProjectSerializer(BaseSerializer):
|
class ProjectSerializer(BaseSerializer):
|
||||||
|
|
||||||
playbooks = serializers.Field(source='playbooks')
|
playbooks = serializers.Field(source='playbooks')
|
||||||
local_path_choices = serializers.SerializerMethodField('get_local_path_choices')
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Project
|
model = Project
|
||||||
fields = BASE_FIELDS + ('local_path', 'local_path_choices')
|
fields = BASE_FIELDS + ('local_path',)
|
||||||
# 'default_playbook', 'scm_type')
|
# 'default_playbook', 'scm_type')
|
||||||
|
|
||||||
def get_related(self, obj):
|
def get_related(self, obj):
|
||||||
@@ -157,9 +124,6 @@ class ProjectSerializer(BaseSerializer):
|
|||||||
))
|
))
|
||||||
return res
|
return res
|
||||||
|
|
||||||
def get_local_path_choices(self, obj):
|
|
||||||
return Project.get_local_path_choices()
|
|
||||||
|
|
||||||
class ProjectPlaybooksSerializer(ProjectSerializer):
|
class ProjectPlaybooksSerializer(ProjectSerializer):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|||||||
@@ -4,10 +4,12 @@
|
|||||||
import datetime
|
import datetime
|
||||||
import json
|
import json
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
from django.contrib.auth.models import User as DjangoUser
|
from django.contrib.auth.models import User as DjangoUser
|
||||||
import django.test
|
import django.test
|
||||||
from django.test.client import Client
|
from django.test.client import Client
|
||||||
from django.core.urlresolvers import reverse
|
from django.core.urlresolvers import reverse
|
||||||
|
|
||||||
from ansibleworks.main.models import *
|
from ansibleworks.main.models import *
|
||||||
from ansibleworks.main.tests.base import BaseTest
|
from ansibleworks.main.tests.base import BaseTest
|
||||||
|
|
||||||
@@ -139,6 +141,30 @@ class ProjectsTest(BaseTest):
|
|||||||
write_test_file(project, 'tasks/blah.yml', TEST_PLAYBOOK)
|
write_test_file(project, 'tasks/blah.yml', TEST_PLAYBOOK)
|
||||||
self.assertEqual(len(project.playbooks), 1)
|
self.assertEqual(len(project.playbooks), 1)
|
||||||
|
|
||||||
|
def test_api_config(self):
|
||||||
|
# superuser can read all config data.
|
||||||
|
url = reverse('main: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)
|
||||||
|
self.assertTrue('project_local_paths' in response)
|
||||||
|
self.assertEqual(set(response['project_local_paths']),
|
||||||
|
set(Project.get_local_path_choices()))
|
||||||
|
|
||||||
|
# org admin can read config and will get project fields.
|
||||||
|
response = self.get(url, expect=200, auth=self.get_normal_credentials())
|
||||||
|
self.assertTrue('project_base_dir' in response)
|
||||||
|
self.assertTrue('project_local_paths' in response)
|
||||||
|
|
||||||
|
# regular user can read configuration, but won't have project fields.
|
||||||
|
response = self.get(url, expect=200, auth=self.get_nobody_credentials())
|
||||||
|
self.assertFalse('project_base_dir' in response)
|
||||||
|
self.assertFalse('project_local_paths' in response)
|
||||||
|
|
||||||
|
# anonymous/invalid user can't access config.
|
||||||
|
self.get(url, expect=401)
|
||||||
|
self.get(url, expect=401, auth=self.get_invalid_credentials())
|
||||||
|
|
||||||
def test_mainline(self):
|
def test_mainline(self):
|
||||||
|
|
||||||
# =====================================================================
|
# =====================================================================
|
||||||
|
|||||||
@@ -114,6 +114,7 @@ job_events_urls = patterns('ansibleworks.main.views',
|
|||||||
|
|
||||||
v1_urls = patterns('ansibleworks.main.views',
|
v1_urls = patterns('ansibleworks.main.views',
|
||||||
url(r'^$', 'api_v1_root_view'),
|
url(r'^$', 'api_v1_root_view'),
|
||||||
|
url(r'^config/$', 'api_v1_config_view'),
|
||||||
url(r'^authtoken/$', 'auth_token_view'),
|
url(r'^authtoken/$', 'auth_token_view'),
|
||||||
url(r'^me/$', 'users_me_list'),
|
url(r'^me/$', 'users_me_list'),
|
||||||
url(r'^organizations/', include(organizations_urls)),
|
url(r'^organizations/', include(organizations_urls)),
|
||||||
|
|||||||
@@ -1,32 +1,33 @@
|
|||||||
# Copyright (c) 2013 AnsibleWorks, Inc.
|
# Copyright (c) 2013 AnsibleWorks, Inc.
|
||||||
# All Rights Reserved.
|
# All Rights Reserved.
|
||||||
|
|
||||||
from django.http import HttpResponse
|
# Python
|
||||||
from django.shortcuts import render_to_response
|
|
||||||
from django.template import RequestContext
|
|
||||||
from django.views.decorators.csrf import csrf_exempt
|
|
||||||
from django.shortcuts import get_object_or_404
|
|
||||||
from ansibleworks.main.models import *
|
|
||||||
from django.contrib.auth.models import User
|
|
||||||
from ansibleworks.main.serializers import *
|
|
||||||
from ansibleworks.main.rbac import *
|
|
||||||
from django.core.urlresolvers import reverse
|
|
||||||
from rest_framework.exceptions import PermissionDenied
|
|
||||||
from rest_framework import mixins
|
|
||||||
from rest_framework import generics
|
|
||||||
from rest_framework import permissions
|
|
||||||
from rest_framework.response import Response
|
|
||||||
from rest_framework import status
|
|
||||||
from rest_framework.settings import api_settings
|
|
||||||
from rest_framework.authtoken.views import ObtainAuthToken
|
|
||||||
from rest_framework.views import APIView
|
|
||||||
import exceptions
|
|
||||||
import datetime
|
import datetime
|
||||||
import re
|
import re
|
||||||
import sys
|
import sys
|
||||||
import json as python_json
|
|
||||||
from base_views import *
|
# Django
|
||||||
|
from django.conf import settings
|
||||||
|
from django.contrib.auth.models import User
|
||||||
|
from django.core.urlresolvers import reverse
|
||||||
|
from django.shortcuts import get_object_or_404, render_to_response
|
||||||
|
from django.template import RequestContext
|
||||||
|
|
||||||
|
# Django REST Framework
|
||||||
|
from rest_framework.authtoken.views import ObtainAuthToken
|
||||||
|
from rest_framework.exceptions import PermissionDenied
|
||||||
|
from rest_framework import generics
|
||||||
|
from rest_framework.permissions import IsAuthenticated
|
||||||
|
from rest_framework.response import Response
|
||||||
|
from rest_framework.settings import api_settings
|
||||||
|
from rest_framework.views import APIView
|
||||||
|
|
||||||
|
# AnsibleWorks
|
||||||
from ansibleworks.main.access import *
|
from ansibleworks.main.access import *
|
||||||
|
from ansibleworks.main.base_views import *
|
||||||
|
from ansibleworks.main.models import *
|
||||||
|
from ansibleworks.main.rbac import *
|
||||||
|
from ansibleworks.main.serializers import *
|
||||||
|
|
||||||
def handle_error(request, status=404):
|
def handle_error(request, status=404):
|
||||||
context = {}
|
context = {}
|
||||||
@@ -93,17 +94,63 @@ class ApiV1RootView(APIView):
|
|||||||
jobs = reverse('main:job_list'),
|
jobs = reverse('main:job_list'),
|
||||||
authtoken = reverse('main:auth_token_view'),
|
authtoken = reverse('main:auth_token_view'),
|
||||||
me = reverse('main:users_me_list'),
|
me = reverse('main:users_me_list'),
|
||||||
|
config = reverse('main:api_v1_config_view'),
|
||||||
)
|
)
|
||||||
return Response(data)
|
return Response(data)
|
||||||
|
|
||||||
|
class ApiV1ConfigView(APIView):
|
||||||
|
'''
|
||||||
|
Various sitewide configuration settings (some may only be visible to
|
||||||
|
superusers or organization admins):
|
||||||
|
|
||||||
|
* `project_base_dir`: Path on the server where projects and playbooks are \
|
||||||
|
stored.
|
||||||
|
* `project_local_paths`: List of directories beneath `project_base_dir` to
|
||||||
|
use when creating/editing a project.
|
||||||
|
* `time_zone`: The configured time zone for the server.
|
||||||
|
'''
|
||||||
|
|
||||||
|
permission_classes = (IsAuthenticated,)
|
||||||
|
view_name = 'Configuration'
|
||||||
|
|
||||||
|
def get(self, request, format=None):
|
||||||
|
'''Return various sitewide configuration settings.'''
|
||||||
|
|
||||||
|
data = dict(
|
||||||
|
time_zone = settings.TIME_ZONE,
|
||||||
|
)
|
||||||
|
if request.user.is_superuser or request.user.admin_of_organizations.filter(active=True).count():
|
||||||
|
data.update(dict(
|
||||||
|
project_base_dir = settings.PROJECTS_ROOT,
|
||||||
|
project_local_paths = Project.get_local_path_choices(),
|
||||||
|
))
|
||||||
|
return Response(data)
|
||||||
|
|
||||||
class AuthTokenView(ObtainAuthToken):
|
class AuthTokenView(ObtainAuthToken):
|
||||||
'''
|
'''
|
||||||
POST username and password to obtain an auth token for subsequent requests.
|
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
|
||||||
'''
|
'''
|
||||||
|
|
||||||
renderer_classes = api_settings.DEFAULT_RENDERER_CLASSES
|
renderer_classes = api_settings.DEFAULT_RENDERER_CLASSES
|
||||||
# FIXME: Show a better form for HTML view
|
|
||||||
# FIXME: How to make this view discoverable?
|
|
||||||
|
|
||||||
class OrganizationsList(BaseList):
|
class OrganizationsList(BaseList):
|
||||||
|
|
||||||
|
|||||||
@@ -23,22 +23,6 @@ ADMINS = (
|
|||||||
|
|
||||||
MANAGERS = ADMINS
|
MANAGERS = ADMINS
|
||||||
|
|
||||||
REST_FRAMEWORK = {
|
|
||||||
'FILTER_BACKEND': 'ansibleworks.main.custom_filters.CustomFilterBackend',
|
|
||||||
'DEFAULT_PAGINATION_SERIALIZER_CLASS': 'ansibleworks.main.serializers.PaginationSerializer',
|
|
||||||
'PAGINATE_BY': 25,
|
|
||||||
'PAGINATE_BY_PARAM': 'page_size',
|
|
||||||
'DEFAULT_AUTHENTICATION_CLASSES': (
|
|
||||||
'rest_framework.authentication.BasicAuthentication',
|
|
||||||
'rest_framework.authentication.TokenAuthentication',
|
|
||||||
'rest_framework.authentication.SessionAuthentication',
|
|
||||||
),
|
|
||||||
'DEFAULT_RENDERER_CLASSES': (
|
|
||||||
'rest_framework.renderers.JSONRenderer',
|
|
||||||
'ansibleworks.main.renderers.BrowsableAPIRenderer',
|
|
||||||
),
|
|
||||||
}
|
|
||||||
|
|
||||||
DATABASES = {
|
DATABASES = {
|
||||||
'default': {
|
'default': {
|
||||||
'ENGINE': 'django.db.backends.sqlite3',
|
'ENGINE': 'django.db.backends.sqlite3',
|
||||||
@@ -148,6 +132,22 @@ INSTALLED_APPS = (
|
|||||||
|
|
||||||
INTERNAL_IPS = ('127.0.0.1',)
|
INTERNAL_IPS = ('127.0.0.1',)
|
||||||
|
|
||||||
|
REST_FRAMEWORK = {
|
||||||
|
'FILTER_BACKEND': 'ansibleworks.main.custom_filters.CustomFilterBackend',
|
||||||
|
'DEFAULT_PAGINATION_SERIALIZER_CLASS': 'ansibleworks.main.pagination.PaginationSerializer',
|
||||||
|
'PAGINATE_BY': 25,
|
||||||
|
'PAGINATE_BY_PARAM': 'page_size',
|
||||||
|
'DEFAULT_AUTHENTICATION_CLASSES': (
|
||||||
|
'rest_framework.authentication.BasicAuthentication',
|
||||||
|
'rest_framework.authentication.TokenAuthentication',
|
||||||
|
'rest_framework.authentication.SessionAuthentication',
|
||||||
|
),
|
||||||
|
'DEFAULT_RENDERER_CLASSES': (
|
||||||
|
'rest_framework.renderers.JSONRenderer',
|
||||||
|
'ansibleworks.main.renderers.BrowsableAPIRenderer',
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
# Email address that error messages come from.
|
# Email address that error messages come from.
|
||||||
SERVER_EMAIL = 'root@localhost'
|
SERVER_EMAIL = 'root@localhost'
|
||||||
|
|
||||||
|
|||||||
@@ -74,7 +74,8 @@ html body .prettyprint {
|
|||||||
background: #f5f5f5;
|
background: #f5f5f5;
|
||||||
}
|
}
|
||||||
html body .str,
|
html body .str,
|
||||||
html body .atv {
|
html body .atv,
|
||||||
|
html body code {
|
||||||
color: #074979;
|
color: #074979;
|
||||||
}
|
}
|
||||||
html body .str a {
|
html body .str a {
|
||||||
|
|||||||
Reference in New Issue
Block a user