Added config resource with project base dir and local paths, various other API cleanup.

This commit is contained in:
Chris Church 2013-06-14 23:15:31 -04:00
parent abd81b7492
commit e435951fe4
8 changed files with 163 additions and 83 deletions

View File

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

View File

@ -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.
'''

View File

@ -9,9 +9,8 @@ from django.contrib.auth.models import User
from django.core.urlresolvers import reverse
from django.core.exceptions import ObjectDoesNotExist
# Django REST framework
from rest_framework import serializers, pagination
from rest_framework.templatetags.rest_framework import replace_query_param
# Django REST Framework
from rest_framework import serializers
# AnsibleWorks
from ansibleworks.main.models import *
@ -19,37 +18,6 @@ from ansibleworks.main.models import *
BASE_FIELDS = ('id', 'url', 'related', 'summary_fields', 'created',
'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
SUMMARIZABLE_FKS = (
'organization', 'host', 'group', 'inventory', 'project', 'team', 'job', 'job_template',
@ -142,11 +110,10 @@ class OrganizationSerializer(BaseSerializer):
class ProjectSerializer(BaseSerializer):
playbooks = serializers.Field(source='playbooks')
local_path_choices = serializers.SerializerMethodField('get_local_path_choices')
class Meta:
model = Project
fields = BASE_FIELDS + ('local_path', 'local_path_choices')
fields = BASE_FIELDS + ('local_path',)
# 'default_playbook', 'scm_type')
def get_related(self, obj):
@ -157,9 +124,6 @@ class ProjectSerializer(BaseSerializer):
))
return res
def get_local_path_choices(self, obj):
return Project.get_local_path_choices()
class ProjectPlaybooksSerializer(ProjectSerializer):
class Meta:

View File

@ -4,10 +4,12 @@
import datetime
import json
from django.conf import settings
from django.contrib.auth.models import User as DjangoUser
import django.test
from django.test.client import Client
from django.core.urlresolvers import reverse
from ansibleworks.main.models import *
from ansibleworks.main.tests.base import BaseTest
@ -139,6 +141,30 @@ class ProjectsTest(BaseTest):
write_test_file(project, 'tasks/blah.yml', TEST_PLAYBOOK)
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):
# =====================================================================

View File

@ -114,6 +114,7 @@ job_events_urls = patterns('ansibleworks.main.views',
v1_urls = patterns('ansibleworks.main.views',
url(r'^$', 'api_v1_root_view'),
url(r'^config/$', 'api_v1_config_view'),
url(r'^authtoken/$', 'auth_token_view'),
url(r'^me/$', 'users_me_list'),
url(r'^organizations/', include(organizations_urls)),

View File

@ -1,32 +1,33 @@
# Copyright (c) 2013 AnsibleWorks, Inc.
# All Rights Reserved.
from django.http import HttpResponse
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
# Python
import datetime
import re
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.base_views import *
from ansibleworks.main.models import *
from ansibleworks.main.rbac import *
from ansibleworks.main.serializers import *
def handle_error(request, status=404):
context = {}
@ -93,17 +94,63 @@ class ApiV1RootView(APIView):
jobs = reverse('main:job_list'),
authtoken = reverse('main:auth_token_view'),
me = reverse('main:users_me_list'),
config = reverse('main:api_v1_config_view'),
)
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):
'''
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
# FIXME: Show a better form for HTML view
# FIXME: How to make this view discoverable?
class OrganizationsList(BaseList):

View File

@ -23,22 +23,6 @@ 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 = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
@ -148,6 +132,22 @@ INSTALLED_APPS = (
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.
SERVER_EMAIL = 'root@localhost'

View File

@ -74,7 +74,8 @@ html body .prettyprint {
background: #f5f5f5;
}
html body .str,
html body .atv {
html body .atv,
html body code {
color: #074979;
}
html body .str a {