mirror of
https://github.com/ansible/awx.git
synced 2026-01-18 13:11:19 -03:30
Added config resource with project base dir and local paths, various other API cleanup.
This commit is contained in:
parent
abd81b7492
commit
e435951fe4
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.
|
||||
'''
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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):
|
||||
|
||||
# =====================================================================
|
||||
|
||||
@ -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)),
|
||||
|
||||
@ -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):
|
||||
|
||||
|
||||
@ -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'
|
||||
|
||||
|
||||
@ -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 {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user