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

View File

@@ -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):
# ===================================================================== # =====================================================================

View File

@@ -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)),

View File

@@ -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):

View File

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

View File

@@ -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 {