Merge pull request #534 from cchurch/django18

Django 1.8 and DRF 3.3 Support
This commit is contained in:
Wayne Witzel III
2016-02-04 14:28:44 -05:00
139 changed files with 2756 additions and 1425 deletions

2
.gitignore vendored
View File

@@ -53,6 +53,7 @@ npm-debug.log
/DEBUG /DEBUG
# Testing # Testing
.cache
.coverage .coverage
.tox .tox
coverage.xml coverage.xml
@@ -60,6 +61,7 @@ htmlcov
pep8.txt pep8.txt
scratch scratch
testem.log testem.log
awx/awx_test.sqlite3-journal
# Mac OS X # Mac OS X
*.DS_Store *.DS_Store

View File

@@ -154,7 +154,7 @@ endif
.PHONY: clean rebase push requirements requirements_dev requirements_jenkins \ .PHONY: clean rebase push requirements requirements_dev requirements_jenkins \
real-requirements real-requirements_dev real-requirements_jenkins \ real-requirements real-requirements_dev real-requirements_jenkins \
develop refresh adduser syncdb migrate dbchange dbshell runserver celeryd \ develop refresh adduser migrate dbchange dbshell runserver celeryd \
receiver test test_unit test_coverage coverage_html test_jenkins dev_build \ receiver test test_unit test_coverage coverage_html test_jenkins dev_build \
release_build release_clean sdist rpmtar mock-rpm mock-srpm rpm-sign \ release_build release_clean sdist rpmtar mock-rpm mock-srpm rpm-sign \
build-ui sync-ui test-ui build-ui-for-coverage test-ui-for-coverage \ build-ui sync-ui test-ui build-ui-for-coverage test-ui-for-coverage \
@@ -280,13 +280,9 @@ refresh: clean requirements_dev version_file develop migrate
adduser: adduser:
$(PYTHON) manage.py createsuperuser $(PYTHON) manage.py createsuperuser
# Create initial database tables (excluding migrations).
syncdb:
$(PYTHON) manage.py syncdb --noinput
# Create database tables and apply any new migrations. # Create database tables and apply any new migrations.
migrate: syncdb migrate:
$(PYTHON) manage.py migrate --noinput $(PYTHON) manage.py migrate --noinput --fake-initial
# Run after making changes to the models to create a new migration. # Run after making changes to the models to create a new migration.
dbchange: dbchange:

View File

@@ -75,10 +75,26 @@ def prepare_env():
settings.DATABASES['default'][opt] = os.environ['AWX_TEST_DATABASE_%s' % opt] settings.DATABASES['default'][opt] = os.environ['AWX_TEST_DATABASE_%s' % opt]
# Disable capturing all SQL queries in memory when in DEBUG mode. # Disable capturing all SQL queries in memory when in DEBUG mode.
if settings.DEBUG and not getattr(settings, 'SQL_DEBUG', True): if settings.DEBUG and not getattr(settings, 'SQL_DEBUG', True):
from django.db.backends import BaseDatabaseWrapper from django.db.backends.base.base import BaseDatabaseWrapper
from django.db.backends.util import CursorWrapper from django.db.backends.utils import CursorWrapper
BaseDatabaseWrapper.make_debug_cursor = lambda self, cursor: CursorWrapper(cursor, self) BaseDatabaseWrapper.make_debug_cursor = lambda self, cursor: CursorWrapper(cursor, self)
# Use the default devserver addr/port defined in settings for runserver.
default_addr = getattr(settings, 'DEVSERVER_DEFAULT_ADDR', '127.0.0.1')
default_port = getattr(settings, 'DEVSERVER_DEFAULT_PORT', 8000)
from django.core.management.commands import runserver as core_runserver
original_handle = core_runserver.Command.handle
def handle(self, *args, **options):
if not options.get('addrport'):
options['addrport'] = '%s:%d' % (default_addr, int(default_port))
elif options.get('addrport').isdigit():
options['addrport'] = '%s:%d' % (default_addr, int(options['addrport']))
return original_handle(self, *args, **options)
core_runserver.Command.handle = handle
def manage(): def manage():
# Prepare the AWX environment. # Prepare the AWX environment.
prepare_env() prepare_env()

View File

@@ -8,9 +8,10 @@ import re
from django.core.exceptions import FieldError, ValidationError from django.core.exceptions import FieldError, ValidationError
from django.db import models from django.db import models
from django.db.models import Q from django.db.models import Q
from django.db.models.related import RelatedObject
from django.db.models.fields import FieldDoesNotExist from django.db.models.fields import FieldDoesNotExist
from django.db.models.fields.related import ForeignObjectRel
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.utils.encoding import force_text
# Django REST Framework # Django REST Framework
from rest_framework.exceptions import ParseError from rest_framework.exceptions import ParseError
@@ -46,7 +47,7 @@ class TypeFilterBackend(BaseFilterBackend):
def filter_queryset(self, request, queryset, view): def filter_queryset(self, request, queryset, view):
try: try:
types = None types = None
for key, value in request.QUERY_PARAMS.items(): for key, value in request.query_params.items():
if key == 'type': if key == 'type':
if ',' in value: if ',' in value:
types = value.split(',') types = value.split(',')
@@ -107,23 +108,21 @@ class FieldLookupBackend(BaseFilterBackend):
'last_updated': 'last_job_run', 'last_updated': 'last_job_run',
}.get(name, name) }.get(name, name)
new_parts.append(name)
if name == 'pk': if name == 'pk':
field = model._meta.pk field = model._meta.pk
else: else:
field = model._meta.get_field_by_name(name)[0] field = model._meta.get_field_by_name(name)[0]
if n < (len(parts) - 2): model = getattr(field, 'related_model', None) or field.model
if getattr(field, 'rel', None):
model = field.rel.to
else:
model = field.model
new_parts.append(name)
if parts: if parts:
new_parts.append(parts[-1]) new_parts.append(parts[-1])
new_lookup = '__'.join(new_parts) new_lookup = '__'.join(new_parts)
return field, new_lookup return field, new_lookup
def to_python_related(self, value): def to_python_related(self, value):
value = unicode(value) value = force_text(value)
if value.lower() in ('none', 'null'): if value.lower() in ('none', 'null'):
return None return None
else: else:
@@ -134,7 +133,7 @@ class FieldLookupBackend(BaseFilterBackend):
return to_python_boolean(value, allow_none=True) return to_python_boolean(value, allow_none=True)
elif isinstance(field, models.BooleanField): elif isinstance(field, models.BooleanField):
return to_python_boolean(value) return to_python_boolean(value)
elif isinstance(field, RelatedObject): elif isinstance(field, ForeignObjectRel):
return self.to_python_related(value) return self.to_python_related(value)
else: else:
return field.to_python(value) return field.to_python(value)
@@ -159,12 +158,12 @@ class FieldLookupBackend(BaseFilterBackend):
def filter_queryset(self, request, queryset, view): def filter_queryset(self, request, queryset, view):
try: try:
# Apply filters specified via QUERY_PARAMS. Each entry in the lists # Apply filters specified via query_params. Each entry in the lists
# below is (negate, field, value). # below is (negate, field, value).
and_filters = [] and_filters = []
or_filters = [] or_filters = []
chain_filters = [] chain_filters = []
for key, values in request.QUERY_PARAMS.lists(): for key, values in request.query_params.lists():
if key in self.RESERVED_NAMES: if key in self.RESERVED_NAMES:
continue continue
@@ -246,7 +245,7 @@ class OrderByBackend(BaseFilterBackend):
def filter_queryset(self, request, queryset, view): def filter_queryset(self, request, queryset, view):
try: try:
order_by = None order_by = None
for key, value in request.QUERY_PARAMS.items(): for key, value in request.query_params.items():
if key in ('order', 'order_by'): if key in ('order', 'order_by'):
order_by = value order_by = value
if ',' in value: if ',' in value:

View File

@@ -12,6 +12,7 @@ from django.conf import settings
from django.db import connection from django.db import connection
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.template.loader import render_to_string
from django.utils.encoding import smart_text
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
# Django REST Framework # Django REST Framework
@@ -19,7 +20,6 @@ from rest_framework.authentication import get_authorization_header
from rest_framework.exceptions import PermissionDenied 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.request import clone_request
from rest_framework import status from rest_framework import status
from rest_framework import views from rest_framework import views
@@ -155,18 +155,6 @@ class APIView(views.APIView):
context = self.get_description_context() context = self.get_description_context()
return render_to_string(template_list, context) return render_to_string(template_list, context)
def metadata(self, request):
'''
Add version number where view was added to Tower.
'''
ret = super(APIView, self).metadata(request)
added_in_version = '1.2'
for version in ('3.0.0', '2.4.0', '2.3.0', '2.2.0', '2.1.0', '2.0.0', '1.4.8', '1.4.5', '1.4', '1.3'):
if getattr(self, 'new_in_%s' % version.replace('.', ''), False):
added_in_version = version
break
ret['added_in_version'] = added_in_version
return ret
class GenericAPIView(generics.GenericAPIView, APIView): class GenericAPIView(generics.GenericAPIView, APIView):
# Base class for all model-based views. # Base class for all model-based views.
@@ -188,8 +176,12 @@ class GenericAPIView(generics.GenericAPIView, APIView):
def get_queryset(self): def get_queryset(self):
#if hasattr(self.request.user, 'get_queryset'): #if hasattr(self.request.user, 'get_queryset'):
# return self.request.user.get_queryset(self.model) # return self.request.user.get_queryset(self.model)
#else: if self.queryset is not None:
return super(GenericAPIView, self).get_queryset() return self.queryset._clone()
elif self.model is not None:
return self.model._default_manager.all()
else:
return super(GenericAPIView, self).get_queryset()
def get_description_context(self): def get_description_context(self):
# Set instance attributes needed to get serializer metadata. # Set instance attributes needed to get serializer metadata.
@@ -201,69 +193,13 @@ class GenericAPIView(generics.GenericAPIView, APIView):
if hasattr(self.model, "_meta"): if hasattr(self.model, "_meta"):
if hasattr(self.model._meta, "verbose_name"): if hasattr(self.model._meta, "verbose_name"):
d.update({ d.update({
'model_verbose_name': unicode(self.model._meta.verbose_name), 'model_verbose_name': smart_text(self.model._meta.verbose_name),
'model_verbose_name_plural': unicode(self.model._meta.verbose_name_plural), 'model_verbose_name_plural': smart_text(self.model._meta.verbose_name_plural),
}) })
d.update({'serializer_fields': self.get_serializer().metadata()}) d['serializer_fields'] = self.metadata_class().get_serializer_info(self.get_serializer())
d['settings'] = settings d['settings'] = settings
return d return d
def metadata(self, request):
'''
Add field information for GET requests (so field names/labels are
available even when we can't POST/PUT).
'''
ret = super(GenericAPIView, self).metadata(request)
actions = ret.get('actions', {})
# Remove read only fields from PUT/POST data.
for method in ('POST', 'PUT'):
fields = actions.get(method, {})
for field, meta in fields.items():
if not isinstance(meta, dict):
continue
if meta.pop('read_only', False):
fields.pop(field)
if 'GET' in self.allowed_methods:
cloned_request = clone_request(request, 'GET')
try:
# Test global permissions
self.check_permissions(cloned_request)
# Test object permissions
if hasattr(self, 'retrieve'):
try:
self.get_object()
except Http404:
# Http404 should be acceptable and the serializer
# metadata should be populated. Except this so the
# outer "else" clause of the try-except-else block
# will be executed.
pass
except (exceptions.APIException, PermissionDenied):
pass
else:
# If user has appropriate permissions for the view, include
# appropriate metadata about the fields that should be supplied.
serializer = self.get_serializer()
actions['GET'] = serializer.metadata()
if hasattr(serializer, 'get_types'):
ret['types'] = serializer.get_types()
# Remove fields labeled as write_only, remove field attributes
# that aren't relevant for retrieving data.
for field, meta in actions['GET'].items():
if not isinstance(meta, dict):
continue
meta.pop('required', None)
meta.pop('read_only', None)
meta.pop('default', None)
meta.pop('min_length', None)
meta.pop('max_length', None)
if meta.pop('write_only', False):
actions['GET'].pop(field)
if actions:
ret['actions'] = actions
if getattr(self, 'search_fields', None):
ret['search_fields'] = self.search_fields
return ret
class MongoAPIView(GenericAPIView): class MongoAPIView(GenericAPIView):
@@ -337,8 +273,8 @@ class SubListAPIView(ListAPIView):
def get_description_context(self): def get_description_context(self):
d = super(SubListAPIView, self).get_description_context() 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': smart_text(self.parent_model._meta.verbose_name),
'parent_model_verbose_name_plural': unicode(self.parent_model._meta.verbose_name_plural), 'parent_model_verbose_name_plural': smart_text(self.parent_model._meta.verbose_name_plural),
}) })
return d return d
@@ -388,10 +324,10 @@ class SubListCreateAPIView(SubListAPIView, ListCreateAPIView):
# Make a copy of the data provided (since it's readonly) in order to # Make a copy of the data provided (since it's readonly) in order to
# inject additional data. # inject additional data.
if hasattr(request.DATA, 'dict'): if hasattr(request.data, 'dict'):
data = request.DATA.dict() data = request.data.dict()
else: else:
data = request.DATA data = request.data
# add the parent key to the post data using the pk from the URL # add the parent key to the post data using the pk from the URL
parent_key = getattr(self, 'parent_key', None) parent_key = getattr(self, 'parent_key', None)
@@ -405,7 +341,7 @@ class SubListCreateAPIView(SubListAPIView, ListCreateAPIView):
status=status.HTTP_400_BAD_REQUEST) status=status.HTTP_400_BAD_REQUEST)
# Verify we have permission to add the object as given. # Verify we have permission to add the object as given.
if not request.user.can_access(self.model, 'add', serializer.init_data): if not request.user.can_access(self.model, 'add', serializer.initial_data):
raise PermissionDenied() raise PermissionDenied()
# save the object through the serializer, reload and returned the saved # save the object through the serializer, reload and returned the saved
@@ -424,8 +360,8 @@ class SubListCreateAttachDetachAPIView(SubListCreateAPIView):
created = False created = False
parent = self.get_parent_object() parent = self.get_parent_object()
relationship = getattr(parent, self.relationship) relationship = getattr(parent, self.relationship)
sub_id = request.DATA.get('id', None) sub_id = request.data.get('id', None)
data = request.DATA data = request.data
# Create the sub object if an ID is not provided. # Create the sub object if an ID is not provided.
if not sub_id: if not sub_id:
@@ -462,7 +398,7 @@ class SubListCreateAttachDetachAPIView(SubListCreateAPIView):
return Response(status=status.HTTP_204_NO_CONTENT) return Response(status=status.HTTP_204_NO_CONTENT)
def unattach(self, request, *args, **kwargs): def unattach(self, request, *args, **kwargs):
sub_id = request.DATA.get('id', None) sub_id = request.data.get('id', None)
if not sub_id: if not sub_id:
data = dict(msg='"id" is required to disassociate') data = dict(msg='"id" is required to disassociate')
return Response(data, status=status.HTTP_400_BAD_REQUEST) return Response(data, status=status.HTTP_400_BAD_REQUEST)
@@ -486,10 +422,10 @@ class SubListCreateAttachDetachAPIView(SubListCreateAPIView):
return Response(status=status.HTTP_204_NO_CONTENT) return Response(status=status.HTTP_204_NO_CONTENT)
def post(self, request, *args, **kwargs): def post(self, request, *args, **kwargs):
if not isinstance(request.DATA, dict): if not isinstance(request.data, dict):
return Response('invalid type for post data', return Response('invalid type for post data',
status=status.HTTP_400_BAD_REQUEST) status=status.HTTP_400_BAD_REQUEST)
if 'disassociate' in request.DATA: if 'disassociate' in request.data:
return self.unattach(request, *args, **kwargs) return self.unattach(request, *args, **kwargs)
else: else:
return self.attach(request, *args, **kwargs) return self.attach(request, *args, **kwargs)
@@ -499,9 +435,6 @@ class RetrieveAPIView(generics.RetrieveAPIView, GenericAPIView):
class RetrieveUpdateAPIView(RetrieveAPIView, generics.RetrieveUpdateAPIView): class RetrieveUpdateAPIView(RetrieveAPIView, generics.RetrieveUpdateAPIView):
def pre_save(self, obj):
super(RetrieveUpdateAPIView, self).pre_save(obj)
def update(self, request, *args, **kwargs): def update(self, request, *args, **kwargs):
self.update_filter(request, *args, **kwargs) self.update_filter(request, *args, **kwargs)
return super(RetrieveUpdateAPIView, self).update(request, *args, **kwargs) return super(RetrieveUpdateAPIView, self).update(request, *args, **kwargs)

139
awx/api/metadata.py Normal file
View File

@@ -0,0 +1,139 @@
# Copyright (c) 2016 Ansible, Inc.
# All Rights Reserved.
# Django
from django.core.exceptions import PermissionDenied
from django.http import Http404
# Django REST Framework
from rest_framework import exceptions
from rest_framework import metadata
from rest_framework import serializers
from rest_framework.request import clone_request
# Ansible Tower
from awx.main.models import InventorySource
class Metadata(metadata.SimpleMetadata):
def get_field_info(self, field):
field_info = super(Metadata, self).get_field_info(field)
# Indicate if a field has a default value.
# FIXME: Still isn't showing all default values?
try:
field_info['default'] = field.get_default()
except serializers.SkipField:
pass
# Indicate if a field is write-only.
if getattr(field, 'write_only', False):
field_info['write_only'] = True
# Update choices to be a list of 2-tuples instead of list of dicts with
# value/display_name.
if 'choices' in field_info:
choices = []
for choice in field_info['choices']:
if isinstance(choice, dict):
choices.append((choice.get('value'), choice.get('display_name')))
else:
choices.append(choice)
field_info['choices'] = choices
# Special handling of inventory source_region choices that vary based on
# selected inventory source.
if field.field_name == 'source_regions':
for cp in ('azure', 'ec2', 'gce', 'rax'):
get_regions = getattr(InventorySource, 'get_%s_region_choices' % cp)
field_info['%s_region_choices' % cp] = get_regions()
# Special handling of group_by choices for EC2.
if field.field_name == 'group_by':
for cp in ('ec2',):
get_group_by_choices = getattr(InventorySource, 'get_%s_group_by_choices' % cp)
field_info['%s_group_by_choices' % cp] = get_group_by_choices()
# Update type of fields returned...
if field.field_name == 'type':
field_info['type'] = 'multiple choice'
elif field.field_name == 'url':
field_info['type'] = 'string'
elif field.field_name in ('related', 'summary_fields'):
field_info['type'] = 'object'
elif field.field_name in ('created', 'modified'):
field_info['type'] = 'datetime'
return field_info
def determine_actions(self, request, view):
# Add field information for GET requests (so field names/labels are
# available even when we can't POST/PUT).
actions = {}
for method in {'GET', 'PUT', 'POST'} & set(view.allowed_methods):
view.request = clone_request(request, method)
try:
# Test global permissions
if hasattr(view, 'check_permissions'):
view.check_permissions(view.request)
# Test object permissions
if method == 'PUT' and hasattr(view, 'get_object'):
view.get_object()
except (exceptions.APIException, PermissionDenied, Http404):
continue
else:
# If user has appropriate permissions for the view, include
# appropriate metadata about the fields that should be supplied.
serializer = view.get_serializer()
actions[method] = self.get_serializer_info(serializer)
finally:
view.request = request
for field, meta in actions[method].items():
if not isinstance(meta, dict):
continue
# Add type choices if available from the serializer.
if field == 'type' and hasattr(serializer, 'get_type_choices'):
meta['choices'] = serializer.get_type_choices()
# For GET method, remove meta attributes that aren't relevant
# when reading a field and remove write-only fields.
if method == 'GET':
meta.pop('required', None)
meta.pop('read_only', None)
meta.pop('default', None)
meta.pop('min_length', None)
meta.pop('max_length', None)
if meta.pop('write_only', False):
actions['GET'].pop(field)
# For PUT/POST methods, remove read-only fields.
if method in ('PUT', 'POST'):
if meta.pop('read_only', False):
actions[method].pop(field)
return actions
def determine_metadata(self, request, view):
metadata = super(Metadata, self).determine_metadata(request, view)
# Add version number in which view was added to Tower.
added_in_version = '1.2'
for version in ('3.0.0', '2.4.0', '2.3.0', '2.2.0', '2.1.0', '2.0.0', '1.4.8', '1.4.5', '1.4', '1.3'):
if getattr(view, 'new_in_%s' % version.replace('.', ''), False):
added_in_version = version
break
metadata['added_in_version'] = added_in_version
# Add type(s) handled by this view/serializer.
serializer = view.get_serializer()
if hasattr(serializer, 'get_types'):
metadata['types'] = serializer.get_types()
# Add search fields if available from the view.
if getattr(view, 'search_fields', None):
metadata['search_fields'] = view.search_fields
return metadata

View File

@@ -2,36 +2,26 @@
# All Rights Reserved. # All Rights Reserved.
# Django REST Framework # Django REST Framework
from rest_framework import serializers, pagination from rest_framework import pagination
from rest_framework.templatetags.rest_framework import replace_query_param from rest_framework.utils.urls import remove_query_param, replace_query_param
class NextPageField(pagination.NextPageField):
'''Pagination field to output URL path.'''
def to_native(self, value): class Pagination(pagination.PageNumberPagination):
if not value.has_next():
page_size_query_param = 'page_size'
def get_next_link(self):
if not self.page.has_next():
return None return None
page = value.next_page_number() url = self.request and self.request.get_full_path() or ''
request = self.context.get('request') page_number = self.page.next_page_number()
url = request and request.get_full_path() or '' return replace_query_param(url, self.page_query_param, page_number)
return replace_query_param(url, self.page_field, page)
class PreviousPageField(pagination.NextPageField): def get_previous_link(self):
'''Pagination field to output URL path.''' if not self.page.has_previous():
def to_native(self, value):
if not value.has_previous():
return None return None
page = value.previous_page_number() url = self.request and self.request.get_full_path() or ''
request = self.context.get('request') page_number = self.page.previous_page_number()
url = request and request.get_full_path() or '' if page_number == 1:
return replace_query_param(url, self.page_field, page) return remove_query_param(url, self.page_query_param)
return replace_query_param(url, self.page_query_param, page_number)
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

@@ -61,7 +61,7 @@ class ModelAccessPermission(permissions.BasePermission):
else: else:
if obj: if obj:
return True return True
return check_user_access(request.user, view.model, 'add', request.DATA) return check_user_access(request.user, view.model, 'add', request.data)
def check_put_permissions(self, request, view, obj=None): def check_put_permissions(self, request, view, obj=None):
if not obj: if not obj:
@@ -70,10 +70,10 @@ class ModelAccessPermission(permissions.BasePermission):
return True return True
if getattr(view, 'is_variable_data', False): if getattr(view, 'is_variable_data', False):
return check_user_access(request.user, view.model, 'change', obj, return check_user_access(request.user, view.model, 'change', obj,
dict(variables=request.DATA)) dict(variables=request.data))
else: else:
return check_user_access(request.user, view.model, 'change', obj, return check_user_access(request.user, view.model, 'change', obj,
request.DATA) request.data)
def check_patch_permissions(self, request, view, obj=None): def check_patch_permissions(self, request, view, obj=None):
return self.check_put_permissions(request, view, obj) return self.check_put_permissions(request, view, obj)
@@ -127,7 +127,7 @@ class ModelAccessPermission(permissions.BasePermission):
def has_permission(self, request, view, obj=None): def has_permission(self, request, view, obj=None):
logger.debug('has_permission(user=%s method=%s data=%r, %s, %r)', logger.debug('has_permission(user=%s method=%s data=%r, %s, %r)',
request.user, request.method, request.DATA, request.user, request.method, request.data,
view.__class__.__name__, obj) view.__class__.__name__, obj)
try: try:
response = self.check_permissions(request, view, obj) response = self.check_permissions(request, view, obj)
@@ -156,7 +156,7 @@ class JobTemplateCallbackPermission(ModelAccessPermission):
# Require method to be POST, host_config_key to be specified and match # Require method to be POST, host_config_key to be specified and match
# the requested job template, and require the job template to be # the requested job template, and require the job template to be
# active in order to proceed. # active in order to proceed.
host_config_key = request.DATA.get('host_config_key', '') host_config_key = request.data.get('host_config_key', '')
if request.method.lower() != 'post': if request.method.lower() != 'post':
raise PermissionDenied() raise PermissionDenied()
elif not host_config_key: elif not host_config_key:

View File

@@ -4,6 +4,7 @@
# Django REST Framework # Django REST Framework
from rest_framework import renderers from rest_framework import renderers
class BrowsableAPIRenderer(renderers.BrowsableAPIRenderer): class BrowsableAPIRenderer(renderers.BrowsableAPIRenderer):
''' '''
Customizations to the default browsable API renderer. Customizations to the default browsable API renderer.
@@ -16,14 +17,16 @@ class BrowsableAPIRenderer(renderers.BrowsableAPIRenderer):
return renderers.JSONRenderer() return renderers.JSONRenderer()
return renderer return renderer
def get_raw_data_form(self, view, method, request): def get_raw_data_form(self, data, view, method, request):
# Set a flag on the view to indiciate to the view/serializer that we're
# creating a raw data form for the browsable API.
try: try:
setattr(view, '_raw_data_form_marker', True) setattr(view, '_raw_data_form_marker', True)
return super(BrowsableAPIRenderer, self).get_raw_data_form(view, method, request) return super(BrowsableAPIRenderer, self).get_raw_data_form(data, view, method, request)
finally: finally:
delattr(view, '_raw_data_form_marker') delattr(view, '_raw_data_form_marker')
def get_rendered_html_form(self, view, method, request): def get_rendered_html_form(self, data, view, method, request):
'''Never show auto-generated form (only raw form).''' '''Never show auto-generated form (only raw form).'''
obj = getattr(view, 'object', None) obj = getattr(view, 'object', None)
if not self.show_form_for_method(view, method, request, obj): if not self.show_form_for_method(view, method, request, obj):
@@ -31,9 +34,10 @@ class BrowsableAPIRenderer(renderers.BrowsableAPIRenderer):
if method in ('DELETE', 'OPTIONS'): if method in ('DELETE', 'OPTIONS'):
return True # Don't actually need to return a form return True # Don't actually need to return a form
def get_context(self, data, accepted_media_type, renderer_context): def get_filter_form(self, data, view, request):
context = super(BrowsableAPIRenderer, self).get_context(data, accepted_media_type, renderer_context) # Don't show filter form in browsable API.
return context return
class PlainTextRenderer(renderers.BaseRenderer): class PlainTextRenderer(renderers.BaseRenderer):
@@ -45,9 +49,12 @@ class PlainTextRenderer(renderers.BaseRenderer):
data = unicode(data) data = unicode(data)
return data.encode(self.charset) return data.encode(self.charset)
class DownloadTextRenderer(PlainTextRenderer): class DownloadTextRenderer(PlainTextRenderer):
format = "txt_download" format = "txt_download"
class AnsiTextRenderer(PlainTextRenderer): class AnsiTextRenderer(PlainTextRenderer):
media_type = 'text/plain' media_type = 'text/plain'

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
# Copyright (c) 2015 Ansible, Inc. # Copyright (c) 2015 Ansible, Inc.
# All Rights Reserved. # All Rights Reserved.
from ordereddict import OrderedDict from collections import OrderedDict
import copy import copy
import functools import functools
@@ -23,21 +23,22 @@ def paginated(method):
def func(self, request, *args, **kwargs): def func(self, request, *args, **kwargs):
# Manually spin up pagination. # Manually spin up pagination.
# How many results do we show? # How many results do we show?
limit = api_settings.PAGINATE_BY paginator_class = api_settings.DEFAULT_PAGINATION_CLASS
if request.QUERY_PARAMS.get(api_settings.PAGINATE_BY_PARAM, False): limit = paginator_class.page_size
limit = request.QUERY_PARAMS[api_settings.PAGINATE_BY_PARAM] if request.query_params.get(paginator_class.page_size_query_param, False):
if api_settings.MAX_PAGINATE_BY: limit = request.query_params[paginator_class.page_size_query_param]
limit = min(api_settings.MAX_PAGINATE_BY, limit) if paginator_class.max_page_size:
limit = min(paginator_class.max_page_size, limit)
limit = int(limit) limit = int(limit)
# Get the order parameter if it's given # Get the order parameter if it's given
if request.QUERY_PARAMS.get("ordering", False): if request.query_params.get("ordering", False):
ordering = request.QUERY_PARAMS["ordering"] ordering = request.query_params["ordering"]
else: else:
ordering = None ordering = None
# What page are we on? # What page are we on?
page = int(request.QUERY_PARAMS.get('page', 1)) page = int(request.query_params.get('page', 1))
offset = (page - 1) * limit offset = (page - 1) * limit
# Add the limit, offset, page, and order variables to the keyword arguments # Add the limit, offset, page, and order variables to the keyword arguments

View File

@@ -12,6 +12,7 @@ import socket
import sys import sys
import errno import errno
from base64 import b64encode from base64 import b64encode
from collections import OrderedDict
# Django # Django
from django.conf import settings from django.conf import settings
@@ -21,7 +22,7 @@ from django.core.exceptions import FieldError
from django.db.models import Q, Count from django.db.models import Q, Count
from django.db import IntegrityError, transaction from django.db import IntegrityError, transaction
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from django.utils.datastructures import SortedDict from django.utils.encoding import force_text
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
from django.utils.timezone import now from django.utils.timezone import now
from django.views.decorators.csrf import csrf_exempt from django.views.decorators.csrf import csrf_exempt
@@ -31,14 +32,16 @@ from django.http import HttpResponse
# Django REST Framework # Django REST Framework
from rest_framework.exceptions import PermissionDenied, ParseError from rest_framework.exceptions import PermissionDenied, ParseError
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.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 exception_handler from rest_framework.views import exception_handler
from rest_framework import status from rest_framework import status
# Django REST Framework YAML
from rest_framework_yaml.parsers import YAMLParser
from rest_framework_yaml.renderers import YAMLRenderer
# MongoEngine # MongoEngine
import mongoengine import mongoengine
@@ -71,7 +74,7 @@ from awx.fact.models import * # noqa
from awx.main.utils import emit_websocket_notification from awx.main.utils import emit_websocket_notification
from awx.main.conf import tower_settings from awx.main.conf import tower_settings
def api_exception_handler(exc): def api_exception_handler(exc, context):
''' '''
Override default API exception handler to catch IntegrityError exceptions. Override default API exception handler to catch IntegrityError exceptions.
''' '''
@@ -79,8 +82,7 @@ def api_exception_handler(exc):
exc = ParseError(exc.args[0]) exc = ParseError(exc.args[0])
if isinstance(exc, FieldError): if isinstance(exc, FieldError):
exc = ParseError(exc.args[0]) exc = ParseError(exc.args[0])
return exception_handler(exc) return exception_handler(exc, context)
class ApiRootView(APIView): class ApiRootView(APIView):
@@ -110,7 +112,7 @@ class ApiV1RootView(APIView):
def get(self, request, format=None): def get(self, request, format=None):
''' list top level resources ''' ''' list top level resources '''
data = SortedDict() data = OrderedDict()
data['authtoken'] = reverse('api:auth_token_view') data['authtoken'] = reverse('api:auth_token_view')
data['ping'] = reverse('api:api_v1_ping_view') data['ping'] = reverse('api:api_v1_ping_view')
data['config'] = reverse('api:api_v1_config_view') data['config'] = reverse('api:api_v1_config_view')
@@ -224,20 +226,20 @@ class ApiV1ConfigView(APIView):
def post(self, request): def post(self, request):
if not request.user.is_superuser: if not request.user.is_superuser:
return Response(None, status=status.HTTP_404_NOT_FOUND) return Response(None, status=status.HTTP_404_NOT_FOUND)
if not type(request.DATA) == dict: if not type(request.data) == dict:
return Response({"error": "Invalid license data"}, status=status.HTTP_400_BAD_REQUEST) return Response({"error": "Invalid license data"}, status=status.HTTP_400_BAD_REQUEST)
if "eula_accepted" not in request.DATA: if "eula_accepted" not in request.data:
return Response({"error": "Missing 'eula_accepted' property"}, status=status.HTTP_400_BAD_REQUEST) return Response({"error": "Missing 'eula_accepted' property"}, status=status.HTTP_400_BAD_REQUEST)
try: try:
eula_accepted = to_python_boolean(request.DATA["eula_accepted"]) eula_accepted = to_python_boolean(request.data["eula_accepted"])
except ValueError: except ValueError:
return Response({"error": "'eula_accepted' value is invalid"}, status=status.HTTP_400_BAD_REQUEST) return Response({"error": "'eula_accepted' value is invalid"}, status=status.HTTP_400_BAD_REQUEST)
if not eula_accepted: if not eula_accepted:
return Response({"error": "'eula_accepted' must be True"}, status=status.HTTP_400_BAD_REQUEST) return Response({"error": "'eula_accepted' must be True"}, status=status.HTTP_400_BAD_REQUEST)
request.DATA.pop("eula_accepted") request.data.pop("eula_accepted")
try: try:
data_actual = json.dumps(request.DATA) data_actual = json.dumps(request.data)
except Exception: except Exception:
# FIX: Log # FIX: Log
return Response({"error": "Invalid JSON"}, status=status.HTTP_400_BAD_REQUEST) return Response({"error": "Invalid JSON"}, status=status.HTTP_400_BAD_REQUEST)
@@ -306,7 +308,7 @@ class DashboardView(APIView):
def get(self, request, format=None): def get(self, request, format=None):
''' Show Dashboard Details ''' ''' Show Dashboard Details '''
data = SortedDict() data = OrderedDict()
data['related'] = {'jobs_graph': reverse('api:dashboard_jobs_graph_view'), data['related'] = {'jobs_graph': reverse('api:dashboard_jobs_graph_view'),
'inventory_graph': reverse('api:dashboard_inventory_graph_view')} 'inventory_graph': reverse('api:dashboard_inventory_graph_view')}
user_inventory = get_user_queryset(request.user, Inventory) user_inventory = get_user_queryset(request.user, Inventory)
@@ -411,8 +413,8 @@ class DashboardJobsGraphView(APIView):
new_in_200 = True new_in_200 = True
def get(self, request, format=None): def get(self, request, format=None):
period = request.QUERY_PARAMS.get('period', 'month') period = request.query_params.get('period', 'month')
job_type = request.QUERY_PARAMS.get('job_type', 'all') job_type = request.query_params.get('job_type', 'all')
user_unified_jobs = get_user_queryset(request.user, UnifiedJob) user_unified_jobs = get_user_queryset(request.user, UnifiedJob)
@@ -460,7 +462,7 @@ class DashboardInventoryGraphView(APIView):
new_in_200 = True new_in_200 = True
def get(self, request, format=None): def get(self, request, format=None):
period = request.QUERY_PARAMS.get('period', 'month') period = request.query_params.get('period', 'month')
end_date = now() end_date = now()
if period == 'month': if period == 'month':
@@ -476,7 +478,7 @@ class DashboardInventoryGraphView(APIView):
start_date = start_date.replace(minute=0, second=0, microsecond=0) start_date = start_date.replace(minute=0, second=0, microsecond=0)
delta = dateutil.relativedelta.relativedelta(hours=1) delta = dateutil.relativedelta.relativedelta(hours=1)
else: else:
raise ParseError(u'Unknown period "%s"' % unicode(period)) raise ParseError(u'Unknown period "%s"' % force_text(period))
host_stats = [] host_stats = []
date = start_date date = start_date
@@ -527,7 +529,7 @@ class AuthView(APIView):
new_in_240 = True new_in_240 = True
def get(self, request): def get(self, request):
data = SortedDict() data = OrderedDict()
err_backend, err_message = request.session.get('social_auth_error', (None, None)) err_backend, err_message = request.session.get('social_auth_error', (None, None))
auth_backends = load_backends(settings.AUTHENTICATION_BACKENDS).items() auth_backends = load_backends(settings.AUTHENTICATION_BACKENDS).items()
# Return auth backends in consistent order: Google, GitHub, SAML. # Return auth backends in consistent order: Google, GitHub, SAML.
@@ -567,27 +569,27 @@ class AuthTokenView(APIView):
model = AuthToken model = AuthToken
def post(self, request): def post(self, request):
serializer = self.serializer_class(data=request.DATA) serializer = self.serializer_class(data=request.data)
if serializer.is_valid(): if serializer.is_valid():
request_hash = AuthToken.get_request_hash(self.request) request_hash = AuthToken.get_request_hash(self.request)
try: try:
token = AuthToken.objects.filter(user=serializer.object['user'], token = AuthToken.objects.filter(user=serializer.validated_data['user'],
request_hash=request_hash, request_hash=request_hash,
expires__gt=now(), expires__gt=now(),
reason='')[0] reason='')[0]
token.refresh() token.refresh()
except IndexError: except IndexError:
token = AuthToken.objects.create(user=serializer.object['user'], token = AuthToken.objects.create(user=serializer.validated_data['user'],
request_hash=request_hash) request_hash=request_hash)
# Get user un-expired tokens that are not invalidated that are # Get user un-expired tokens that are not invalidated that are
# over the configured limit. # over the configured limit.
# Mark them as invalid and inform the user # Mark them as invalid and inform the user
invalid_tokens = AuthToken.get_tokens_over_limit(serializer.object['user']) invalid_tokens = AuthToken.get_tokens_over_limit(serializer.validated_data['user'])
for t in invalid_tokens: for t in invalid_tokens:
# TODO: send socket notification # TODO: send socket notification
emit_websocket_notification('/socket.io/control', emit_websocket_notification('/socket.io/control',
'limit_reached', 'limit_reached',
dict(reason=unicode(AuthToken.reason_long('limit_reached'))), dict(reason=force_text(AuthToken.reason_long('limit_reached'))),
token_key=t.key) token_key=t.key)
t.invalidate(reason='limit_reached') t.invalidate(reason='limit_reached')
@@ -769,7 +771,7 @@ class ProjectList(ListCreateAPIView):
# Not optimal, but make sure the project status and last_updated fields # Not optimal, but make sure the project status and last_updated fields
# are up to date here... # are up to date here...
projects_qs = Project.objects.filter(active=True) projects_qs = Project.objects.filter(active=True)
projects_qs = projects_qs.select_related('current_update', 'last_updated') projects_qs = projects_qs.select_related('current_job', 'last_job')
for project in projects_qs: for project in projects_qs:
project._set_status_and_last_job_run() project._set_status_and_last_job_run()
return super(ProjectList, self).get(request, *args, **kwargs) return super(ProjectList, self).get(request, *args, **kwargs)
@@ -994,15 +996,15 @@ class UserDetail(RetrieveUpdateDestroyAPIView):
def update_filter(self, request, *args, **kwargs): def update_filter(self, request, *args, **kwargs):
''' make sure non-read-only fields that can only be edited by admins, are only edited by admins ''' ''' make sure non-read-only fields that can only be edited by admins, are only edited by admins '''
obj = self.get_object() obj = self.get_object()
can_change = request.user.can_access(User, 'change', obj, request.DATA) can_change = request.user.can_access(User, 'change', obj, request.data)
can_admin = request.user.can_access(User, 'admin', obj, request.DATA) can_admin = request.user.can_access(User, 'admin', obj, request.data)
if can_change and not can_admin: if can_change and not can_admin:
admin_only_edit_fields = ('last_name', 'first_name', 'username', admin_only_edit_fields = ('last_name', 'first_name', 'username',
'is_active', 'is_superuser') 'is_active', 'is_superuser')
changed = {} changed = {}
for field in admin_only_edit_fields: for field in admin_only_edit_fields:
left = getattr(obj, field, None) left = getattr(obj, field, None)
right = request.DATA.get(field, None) right = request.data.get(field, None)
if left is not None and right is not None and left != right: if left is not None and right is not None and left != right:
changed[field] = (left, right) changed[field] = (left, right)
if changed: if changed:
@@ -1061,11 +1063,11 @@ class InventoryScriptDetail(RetrieveUpdateDestroyAPIView):
serializer_class = CustomInventoryScriptSerializer serializer_class = CustomInventoryScriptSerializer
def destroy(self, request, *args, **kwargs): def destroy(self, request, *args, **kwargs):
obj = self.get_object() instance = self.get_object()
can_delete = request.user.can_access(self.model, 'delete', obj) can_delete = request.user.can_access(self.model, 'delete', instance)
if not can_delete: if not can_delete:
raise PermissionDenied("Cannot delete inventory script") raise PermissionDenied("Cannot delete inventory script")
for inv_src in InventorySource.objects.filter(source_script=obj): for inv_src in InventorySource.objects.filter(source_script=instance):
inv_src.source_script = None inv_src.source_script = None
inv_src.save() inv_src.save()
return super(InventoryScriptDetail, self).destroy(request, *args, **kwargs) return super(InventoryScriptDetail, self).destroy(request, *args, **kwargs)
@@ -1137,10 +1139,10 @@ class InventorySingleFactView(MongoAPIView):
raise LicenseForbids('Your license does not permit use ' raise LicenseForbids('Your license does not permit use '
'of system tracking.') 'of system tracking.')
fact_key = request.QUERY_PARAMS.get("fact_key", None) fact_key = request.query_params.get("fact_key", None)
fact_value = request.QUERY_PARAMS.get("fact_value", None) fact_value = request.query_params.get("fact_value", None)
datetime_spec = request.QUERY_PARAMS.get("timestamp", None) datetime_spec = request.query_params.get("timestamp", None)
module_spec = request.QUERY_PARAMS.get("module", None) module_spec = request.query_params.get("module", None)
if fact_key is None or fact_value is None or module_spec is None: if fact_key is None or fact_value is None or module_spec is None:
return Response({"error": "Missing fields"}, status=status.HTTP_400_BAD_REQUEST) return Response({"error": "Missing fields"}, status=status.HTTP_400_BAD_REQUEST)
@@ -1231,9 +1233,9 @@ class HostFactVersionsList(MongoListAPIView):
filter_backends = (MongoFilterBackend,) filter_backends = (MongoFilterBackend,)
def get_queryset(self): def get_queryset(self):
from_spec = self.request.QUERY_PARAMS.get('from', None) from_spec = self.request.query_params.get('from', None)
to_spec = self.request.QUERY_PARAMS.get('to', None) to_spec = self.request.query_params.get('to', None)
module_spec = self.request.QUERY_PARAMS.get('module', None) module_spec = self.request.query_params.get('module', None)
if not feature_enabled("system_tracking"): if not feature_enabled("system_tracking"):
raise LicenseForbids("Your license does not permit use " raise LicenseForbids("Your license does not permit use "
@@ -1285,10 +1287,10 @@ class HostSingleFactView(MongoAPIView):
raise LicenseForbids('Your license does not permit use ' raise LicenseForbids('Your license does not permit use '
'of system tracking.') 'of system tracking.')
fact_key = request.QUERY_PARAMS.get("fact_key", None) fact_key = request.query_params.get("fact_key", None)
fact_value = request.QUERY_PARAMS.get("fact_value", None) fact_value = request.query_params.get("fact_value", None)
datetime_spec = request.QUERY_PARAMS.get("timestamp", None) datetime_spec = request.query_params.get("timestamp", None)
module_spec = request.QUERY_PARAMS.get("module", None) module_spec = request.query_params.get("module", None)
if fact_key is None or fact_value is None or module_spec is None: if fact_key is None or fact_value is None or module_spec is None:
return Response({"error": "Missing fields"}, status=status.HTTP_400_BAD_REQUEST) return Response({"error": "Missing fields"}, status=status.HTTP_400_BAD_REQUEST)
@@ -1310,8 +1312,8 @@ class HostFactCompareView(MongoAPIView):
raise LicenseForbids('Your license does not permit use ' raise LicenseForbids('Your license does not permit use '
'of system tracking.') 'of system tracking.')
datetime_spec = request.QUERY_PARAMS.get('datetime', None) datetime_spec = request.query_params.get('datetime', None)
module_spec = request.QUERY_PARAMS.get('module', "ansible") module_spec = request.query_params.get('module', "ansible")
datetime_actual = dateutil.parser.parse(datetime_spec) if datetime_spec is not None else now() datetime_actual = dateutil.parser.parse(datetime_spec) if datetime_spec is not None else now()
host_obj = self.get_parent_object() host_obj = self.get_parent_object()
@@ -1333,7 +1335,7 @@ class GroupChildrenList(SubListCreateAttachDetachAPIView):
relationship = 'children' relationship = 'children'
def unattach(self, request, *args, **kwargs): def unattach(self, request, *args, **kwargs):
sub_id = request.DATA.get('id', None) sub_id = request.data.get('id', None)
if sub_id is not None: if sub_id is not None:
return super(GroupChildrenList, self).unattach(request, *args, **kwargs) return super(GroupChildrenList, self).unattach(request, *args, **kwargs)
parent = self.get_parent_object() parent = self.get_parent_object()
@@ -1345,7 +1347,7 @@ class GroupChildrenList(SubListCreateAttachDetachAPIView):
Special case for disassociating a child group from the parent. If the Special case for disassociating a child group from the parent. If the
child group has no more parents, then automatically mark it inactive. child group has no more parents, then automatically mark it inactive.
''' '''
sub_id = request.DATA.get('id', None) sub_id = request.data.get('id', None)
if not sub_id: if not sub_id:
data = dict(msg='"id" is required to disassociate') data = dict(msg='"id" is required to disassociate')
return Response(data, status=status.HTTP_400_BAD_REQUEST) return Response(data, status=status.HTTP_400_BAD_REQUEST)
@@ -1394,12 +1396,12 @@ class GroupHostsList(SubListCreateAttachDetachAPIView):
def create(self, request, *args, **kwargs): def create(self, request, *args, **kwargs):
parent_group = Group.objects.get(id=self.kwargs['pk']) parent_group = Group.objects.get(id=self.kwargs['pk'])
existing_hosts = Host.objects.filter(inventory=parent_group.inventory, name=request.DATA['name']) existing_hosts = Host.objects.filter(inventory=parent_group.inventory, name=request.data['name'])
if existing_hosts.count() > 0 and ('variables' not in request.DATA or if existing_hosts.count() > 0 and ('variables' not in request.data or
request.DATA['variables'] == '' or request.data['variables'] == '' or
request.DATA['variables'] == '{}' or request.data['variables'] == '{}' or
request.DATA['variables'] == '---'): request.data['variables'] == '---'):
request.DATA['id'] = existing_hosts[0].id request.data['id'] = existing_hosts[0].id
return self.attach(request, *args, **kwargs) return self.attach(request, *args, **kwargs)
return super(GroupHostsList, self).create(request, *args, **kwargs) return super(GroupHostsList, self).create(request, *args, **kwargs)
@@ -1483,10 +1485,10 @@ class GroupSingleFactView(MongoAPIView):
raise LicenseForbids('Your license does not permit use ' raise LicenseForbids('Your license does not permit use '
'of system tracking.') 'of system tracking.')
fact_key = request.QUERY_PARAMS.get("fact_key", None) fact_key = request.query_params.get("fact_key", None)
fact_value = request.QUERY_PARAMS.get("fact_value", None) fact_value = request.query_params.get("fact_value", None)
datetime_spec = request.QUERY_PARAMS.get("timestamp", None) datetime_spec = request.query_params.get("timestamp", None)
module_spec = request.QUERY_PARAMS.get("module", None) module_spec = request.query_params.get("module", None)
if fact_key is None or fact_value is None or module_spec is None: if fact_key is None or fact_value is None or module_spec is None:
return Response({"error": "Missing fields"}, status=status.HTTP_400_BAD_REQUEST) return Response({"error": "Missing fields"}, status=status.HTTP_400_BAD_REQUEST)
@@ -1547,33 +1549,33 @@ class InventoryScriptView(RetrieveAPIView):
filter_backends = () filter_backends = ()
def retrieve(self, request, *args, **kwargs): def retrieve(self, request, *args, **kwargs):
self.object = self.get_object() obj = self.get_object()
hostname = request.QUERY_PARAMS.get('host', '') hostname = request.query_params.get('host', '')
hostvars = bool(request.QUERY_PARAMS.get('hostvars', '')) hostvars = bool(request.query_params.get('hostvars', ''))
show_all = bool(request.QUERY_PARAMS.get('all', '')) show_all = bool(request.query_params.get('all', ''))
if show_all: if show_all:
hosts_q = dict(active=True) hosts_q = dict(active=True)
else: else:
hosts_q = dict(active=True, enabled=True) hosts_q = dict(active=True, enabled=True)
if hostname: if hostname:
host = get_object_or_404(self.object.hosts, name=hostname, **hosts_q) host = get_object_or_404(obj.hosts, name=hostname, **hosts_q)
data = host.variables_dict data = host.variables_dict
else: else:
data = SortedDict() data = OrderedDict()
if self.object.variables_dict: if obj.variables_dict:
all_group = data.setdefault('all', SortedDict()) all_group = data.setdefault('all', OrderedDict())
all_group['vars'] = self.object.variables_dict all_group['vars'] = obj.variables_dict
# Add hosts without a group to the all group. # Add hosts without a group to the all group.
groupless_hosts_qs = self.object.hosts.filter(groups__isnull=True, **hosts_q).order_by('name') groupless_hosts_qs = obj.hosts.filter(groups__isnull=True, **hosts_q).order_by('name')
groupless_hosts = list(groupless_hosts_qs.values_list('name', flat=True)) groupless_hosts = list(groupless_hosts_qs.values_list('name', flat=True))
if groupless_hosts: if groupless_hosts:
all_group = data.setdefault('all', SortedDict()) all_group = data.setdefault('all', OrderedDict())
all_group['hosts'] = groupless_hosts all_group['hosts'] = groupless_hosts
# Build in-memory mapping of groups and their hosts. # Build in-memory mapping of groups and their hosts.
group_hosts_kw = dict(group__inventory_id=self.object.id, group__active=True, group_hosts_kw = dict(group__inventory_id=obj.id, group__active=True,
host__inventory_id=self.object.id, host__active=True) host__inventory_id=obj.id, host__active=True)
if 'enabled' in hosts_q: if 'enabled' in hosts_q:
group_hosts_kw['host__enabled'] = hosts_q['enabled'] group_hosts_kw['host__enabled'] = hosts_q['enabled']
group_hosts_qs = Group.hosts.through.objects.filter(**group_hosts_kw) group_hosts_qs = Group.hosts.through.objects.filter(**group_hosts_kw)
@@ -1586,8 +1588,8 @@ class InventoryScriptView(RetrieveAPIView):
# Build in-memory mapping of groups and their children. # Build in-memory mapping of groups and their children.
group_parents_qs = Group.parents.through.objects.filter( group_parents_qs = Group.parents.through.objects.filter(
from_group__inventory_id=self.object.id, from_group__active=True, from_group__inventory_id=obj.id, from_group__active=True,
to_group__inventory_id=self.object.id, to_group__active=True, to_group__inventory_id=obj.id, to_group__active=True,
) )
group_parents_qs = group_parents_qs.order_by('from_group__name') group_parents_qs = group_parents_qs.order_by('from_group__name')
group_parents_qs = group_parents_qs.values_list('from_group_id', 'from_group__name', 'to_group_id') group_parents_qs = group_parents_qs.values_list('from_group_id', 'from_group__name', 'to_group_id')
@@ -1597,28 +1599,27 @@ class InventoryScriptView(RetrieveAPIView):
group_children.append(from_group_name) group_children.append(from_group_name)
# Now use in-memory maps to build up group info. # Now use in-memory maps to build up group info.
for group in self.object.groups.filter(active=True): for group in obj.groups.filter(active=True):
group_info = SortedDict() group_info = OrderedDict()
group_info['hosts'] = group_hosts_map.get(group.id, []) group_info['hosts'] = group_hosts_map.get(group.id, [])
group_info['children'] = group_children_map.get(group.id, []) group_info['children'] = group_children_map.get(group.id, [])
group_info['vars'] = group.variables_dict group_info['vars'] = group.variables_dict
data[group.name] = group_info data[group.name] = group_info
if hostvars: if hostvars:
data.setdefault('_meta', SortedDict()) data.setdefault('_meta', OrderedDict())
data['_meta'].setdefault('hostvars', SortedDict()) data['_meta'].setdefault('hostvars', OrderedDict())
for host in self.object.hosts.filter(**hosts_q): for host in obj.hosts.filter(**hosts_q):
data['_meta']['hostvars'][host.name] = host.variables_dict data['_meta']['hostvars'][host.name] = host.variables_dict
# workaround for Ansible inventory bug (github #3687), localhost # workaround for Ansible inventory bug (github #3687), localhost
# must be explicitly listed in the all group for dynamic inventory # must be explicitly listed in the all group for dynamic inventory
# scripts to pick it up. # scripts to pick it up.
localhost_names = ('localhost', '127.0.0.1', '::1') localhost_names = ('localhost', '127.0.0.1', '::1')
localhosts_qs = self.object.hosts.filter(name__in=localhost_names, localhosts_qs = obj.hosts.filter(name__in=localhost_names, **hosts_q)
**hosts_q)
localhosts = list(localhosts_qs.values_list('name', flat=True)) localhosts = list(localhosts_qs.values_list('name', flat=True))
if localhosts: if localhosts:
all_group = data.setdefault('all', SortedDict()) all_group = data.setdefault('all', OrderedDict())
all_group_hosts = all_group.get('hosts', []) all_group_hosts = all_group.get('hosts', [])
all_group_hosts.extend(localhosts) all_group_hosts.extend(localhosts)
all_group['hosts'] = sorted(set(all_group_hosts)) all_group['hosts'] = sorted(set(all_group_hosts))
@@ -1657,13 +1658,6 @@ class InventoryTreeView(RetrieveAPIView):
group_children_map) group_children_map)
return Response(tree_data) return Response(tree_data)
def get_description_context(self):
d = super(InventoryTreeView, self).get_description_context()
d.update({
'serializer_fields': GroupTreeSerializer().metadata(),
})
return d
class InventoryInventorySourcesList(SubListAPIView): class InventoryInventorySourcesList(SubListAPIView):
model = InventorySource model = InventorySource
@@ -1828,23 +1822,23 @@ class JobTemplateLaunch(RetrieveAPIView, GenericAPIView):
if not request.user.can_access(self.model, 'start', obj): if not request.user.can_access(self.model, 'start', obj):
raise PermissionDenied() raise PermissionDenied()
if 'credential' not in request.DATA and 'credential_id' in request.DATA: if 'credential' not in request.data and 'credential_id' in request.data:
request.DATA['credential'] = request.DATA['credential_id'] request.data['credential'] = request.data['credential_id']
passwords = {} passwords = {}
serializer = self.serializer_class(data=request.DATA, context={'obj': obj, 'data': request.DATA, 'passwords': passwords}) serializer = self.serializer_class(instance=obj, data=request.data, context={'obj': obj, 'data': request.data, 'passwords': passwords})
if not serializer.is_valid(): if not serializer.is_valid():
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
# At this point, a credential is gauranteed to exist at serializer.object.credential # At this point, a credential is gauranteed to exist at serializer.instance.credential
if not request.user.can_access(Credential, 'read', serializer.object.credential): if not request.user.can_access(Credential, 'read', serializer.instance.credential):
raise PermissionDenied() raise PermissionDenied()
kv = { kv = {
'credential': serializer.object.credential.pk, 'credential': serializer.instance.credential.pk,
} }
if 'extra_vars' in request.DATA: if 'extra_vars' in request.data:
kv['extra_vars'] = request.DATA['extra_vars'] kv['extra_vars'] = request.data['extra_vars']
kv.update(passwords) kv.update(passwords)
new_job = obj.create_unified_job(**kv) new_job = obj.create_unified_job(**kv)
@@ -1892,7 +1886,7 @@ class JobTemplateSurveySpec(GenericAPIView):
if not request.user.can_access(self.model, 'change', obj, None): if not request.user.can_access(self.model, 'change', obj, None):
raise PermissionDenied() raise PermissionDenied()
try: try:
obj.survey_spec = json.dumps(request.DATA) obj.survey_spec = json.dumps(request.data)
except ValueError: except ValueError:
# TODO: Log # TODO: Log
return Response(dict(error="Invalid JSON when parsing survey spec"), status=status.HTTP_400_BAD_REQUEST) return Response(dict(error="Invalid JSON when parsing survey spec"), status=status.HTTP_400_BAD_REQUEST)
@@ -2040,7 +2034,7 @@ class JobTemplateCallback(GenericAPIView):
def post(self, request, *args, **kwargs): def post(self, request, *args, **kwargs):
extra_vars = None extra_vars = None
if request.content_type == "application/json": if request.content_type == "application/json":
extra_vars = request.DATA.get("extra_vars", None) extra_vars = request.data.get("extra_vars", None)
# Permission class should have already validated host_config_key. # Permission class should have already validated host_config_key.
job_template = self.get_object() job_template = self.get_object()
# Attempt to find matching hosts based on remote address. # Attempt to find matching hosts based on remote address.
@@ -2144,8 +2138,8 @@ class SystemJobTemplateLaunch(GenericAPIView):
if not request.user.can_access(self.model, 'start', obj): if not request.user.can_access(self.model, 'start', obj):
raise PermissionDenied() raise PermissionDenied()
new_job = obj.create_unified_job(**request.DATA) new_job = obj.create_unified_job(**request.data)
new_job.signal_start(**request.DATA) new_job.signal_start(**request.data)
data = dict(system_job=new_job.id) data = dict(system_job=new_job.id)
return Response(data, status=status.HTTP_202_ACCEPTED) return Response(data, status=status.HTTP_202_ACCEPTED)
@@ -2223,7 +2217,7 @@ class JobStart(GenericAPIView):
if not request.user.can_access(self.model, 'start', obj): if not request.user.can_access(self.model, 'start', obj):
raise PermissionDenied() raise PermissionDenied()
if obj.can_start: if obj.can_start:
result = obj.signal_start(**request.DATA) result = obj.signal_start(**request.data)
if not result: if not result:
data = dict(passwords_needed_to_start=obj.passwords_needed_to_start) data = dict(passwords_needed_to_start=obj.passwords_needed_to_start)
return Response(data, status=status.HTTP_400_BAD_REQUEST) return Response(data, status=status.HTTP_400_BAD_REQUEST)
@@ -2262,15 +2256,15 @@ class JobRelaunch(RetrieveAPIView, GenericAPIView):
if not request.user.can_access(self.model, 'start', obj): if not request.user.can_access(self.model, 'start', obj):
raise PermissionDenied() raise PermissionDenied()
# Note: is_valid() may modify request.DATA # Note: is_valid() may modify request.data
# It will remove any key/value pair who's key is not in the 'passwords_needed_to_start' list # It will remove any key/value pair who's key is not in the 'passwords_needed_to_start' list
serializer = self.serializer_class(data=request.DATA, context={'obj': obj, 'data': request.DATA}) serializer = self.serializer_class(data=request.data, context={'obj': obj, 'data': request.data})
if not serializer.is_valid(): if not serializer.is_valid():
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
obj.launch_type = 'relaunch' obj.launch_type = 'relaunch'
new_job = obj.copy() new_job = obj.copy()
result = new_job.signal_start(**request.DATA) result = new_job.signal_start(**request.data)
if not result: if not result:
data = dict(passwords_needed_to_start=new_job.passwords_needed_to_start) data = dict(passwords_needed_to_start=new_job.passwords_needed_to_start)
return Response(data, status=status.HTTP_400_BAD_REQUEST) return Response(data, status=status.HTTP_400_BAD_REQUEST)
@@ -2357,13 +2351,11 @@ class JobJobEventsList(BaseJobEventsList):
# Post allowed for job event callback only. # Post allowed for job event callback only.
def post(self, request, *args, **kwargs): def post(self, request, *args, **kwargs):
parent_obj = get_object_or_404(self.parent_model, pk=self.kwargs['pk']) parent_obj = get_object_or_404(self.parent_model, pk=self.kwargs['pk'])
data = request.DATA.copy() data = request.data.copy()
data['job'] = parent_obj.pk data['job'] = parent_obj.pk
serializer = self.get_serializer(data=data) serializer = self.get_serializer(data=data)
if serializer.is_valid(): if serializer.is_valid():
self.pre_save(serializer.object) self.instance = serializer.save()
self.object = serializer.save(force_insert=True)
self.post_save(self.object, created=True)
headers = {'Location': serializer.data['url']} headers = {'Location': serializer.data['url']}
return Response(serializer.data, status=status.HTTP_201_CREATED, return Response(serializer.data, status=status.HTTP_201_CREATED,
headers=headers) headers=headers)
@@ -2392,16 +2384,16 @@ class JobJobPlaysList(BaseJobEventsList):
# doing this here for the moment until/unless we need to implement more # doing this here for the moment until/unless we need to implement more
# complex filtering (since we aren't under a serializer) # complex filtering (since we aren't under a serializer)
if "id__in" in request.QUERY_PARAMS: if "id__in" in request.query_params:
qs = qs.filter(id__in=[int(filter_id) for filter_id in request.QUERY_PARAMS["id__in"].split(",")]) qs = qs.filter(id__in=[int(filter_id) for filter_id in request.query_params["id__in"].split(",")])
elif "id__gt" in request.QUERY_PARAMS: elif "id__gt" in request.query_params:
qs = qs.filter(id__gt=request.QUERY_PARAMS['id__gt']) qs = qs.filter(id__gt=request.query_params['id__gt'])
elif "id__lt" in request.QUERY_PARAMS: elif "id__lt" in request.query_params:
qs = qs.filter(id__lt=request.QUERY_PARAMS['id__lt']) qs = qs.filter(id__lt=request.query_params['id__lt'])
if "failed" in request.QUERY_PARAMS: if "failed" in request.query_params:
qs = qs.filter(failed=(request.QUERY_PARAMS['failed'].lower() == 'true')) qs = qs.filter(failed=(request.query_params['failed'].lower() == 'true'))
if "play__icontains" in request.QUERY_PARAMS: if "play__icontains" in request.query_params:
qs = qs.filter(play__icontains=request.QUERY_PARAMS['play__icontains']) qs = qs.filter(play__icontains=request.query_params['play__icontains'])
count = qs.count() count = qs.count()
@@ -2465,10 +2457,10 @@ class JobJobTasksList(BaseJobEventsList):
return ({'detail': 'job not found'}, -1, status.HTTP_404_NOT_FOUND) return ({'detail': 'job not found'}, -1, status.HTTP_404_NOT_FOUND)
job = job[0] job = job[0]
if 'event_id' not in request.QUERY_PARAMS: if 'event_id' not in request.query_params:
return ({'detail': '"event_id" not provided'}, -1, status.HTTP_400_BAD_REQUEST) return ({'detail': '"event_id" not provided'}, -1, status.HTTP_400_BAD_REQUEST)
parent_task = job.job_events.filter(pk=int(request.QUERY_PARAMS.get('event_id', -1))) parent_task = job.job_events.filter(pk=int(request.query_params.get('event_id', -1)))
if not parent_task.exists(): if not parent_task.exists():
return ({'detail': 'parent event not found'}, -1, status.HTTP_404_NOT_FOUND) return ({'detail': 'parent event not found'}, -1, status.HTTP_404_NOT_FOUND)
parent_task = parent_task[0] parent_task = parent_task[0]
@@ -2507,16 +2499,16 @@ class JobJobTasksList(BaseJobEventsList):
# doing this here for the moment until/unless we need to implement more # doing this here for the moment until/unless we need to implement more
# complex filtering (since we aren't under a serializer) # complex filtering (since we aren't under a serializer)
if "id__in" in request.QUERY_PARAMS: if "id__in" in request.query_params:
qs = qs.filter(id__in=[int(filter_id) for filter_id in request.QUERY_PARAMS["id__in"].split(",")]) qs = qs.filter(id__in=[int(filter_id) for filter_id in request.query_params["id__in"].split(",")])
elif "id__gt" in request.QUERY_PARAMS: elif "id__gt" in request.query_params:
qs = qs.filter(id__gt=request.QUERY_PARAMS['id__gt']) qs = qs.filter(id__gt=request.query_params['id__gt'])
elif "id__lt" in request.QUERY_PARAMS: elif "id__lt" in request.query_params:
qs = qs.filter(id__lt=request.QUERY_PARAMS['id__lt']) qs = qs.filter(id__lt=request.query_params['id__lt'])
if "failed" in request.QUERY_PARAMS: if "failed" in request.query_params:
qs = qs.filter(failed=(request.QUERY_PARAMS['failed'].lower() == 'true')) qs = qs.filter(failed=(request.query_params['failed'].lower() == 'true'))
if "task__icontains" in request.QUERY_PARAMS: if "task__icontains" in request.query_params:
qs = qs.filter(task__icontains=request.QUERY_PARAMS['task__icontains']) qs = qs.filter(task__icontains=request.query_params['task__icontains'])
if ordering is not None: if ordering is not None:
qs = qs.order_by(ordering) qs = qs.order_by(ordering)
@@ -2594,7 +2586,7 @@ class AdHocCommandList(ListCreateAPIView):
def create(self, request, *args, **kwargs): def create(self, request, *args, **kwargs):
# Inject inventory ID and limit if parent objects is a host/group. # Inject inventory ID and limit if parent objects is a host/group.
if hasattr(self, 'get_parent_object') and not getattr(self, 'parent_key', None): if hasattr(self, 'get_parent_object') and not getattr(self, 'parent_key', None):
data = request.DATA data = request.data
# HACK: Make request data mutable. # HACK: Make request data mutable.
if getattr(data, '_mutable', None) is False: if getattr(data, '_mutable', None) is False:
data._mutable = True data._mutable = True
@@ -2604,11 +2596,11 @@ class AdHocCommandList(ListCreateAPIView):
data['limit'] = parent_obj.name data['limit'] = parent_obj.name
# Check for passwords needed before creating ad hoc command. # Check for passwords needed before creating ad hoc command.
credential_pk = get_pk_from_dict(request.DATA, 'credential') credential_pk = get_pk_from_dict(request.data, 'credential')
if credential_pk: if credential_pk:
credential = get_object_or_400(Credential, pk=credential_pk) credential = get_object_or_400(Credential, pk=credential_pk)
needed = credential.passwords_needed needed = credential.passwords_needed
provided = dict([(field, request.DATA.get(field, '')) for field in needed]) provided = dict([(field, request.data.get(field, '')) for field in needed])
if not all(provided.values()): if not all(provided.values()):
data = dict(passwords_needed_to_start=needed) data = dict(passwords_needed_to_start=needed)
return Response(data, status=status.HTTP_400_BAD_REQUEST) return Response(data, status=status.HTTP_400_BAD_REQUEST)
@@ -2619,7 +2611,7 @@ class AdHocCommandList(ListCreateAPIView):
# Start ad hoc command running when created. # Start ad hoc command running when created.
ad_hoc_command = get_object_or_400(self.model, pk=response.data['id']) ad_hoc_command = get_object_or_400(self.model, pk=response.data['id'])
result = ad_hoc_command.signal_start(**request.DATA) result = ad_hoc_command.signal_start(**request.data)
if not result: if not result:
data = dict(passwords_needed_to_start=ad_hoc_command.passwords_needed_to_start) data = dict(passwords_needed_to_start=ad_hoc_command.passwords_needed_to_start)
return Response(data, status=status.HTTP_400_BAD_REQUEST) return Response(data, status=status.HTTP_400_BAD_REQUEST)
@@ -2702,21 +2694,21 @@ class AdHocCommandRelaunch(GenericAPIView):
data[field[:-3]] = getattr(obj, field) data[field[:-3]] = getattr(obj, field)
else: else:
data[field] = getattr(obj, field) data[field] = getattr(obj, field)
serializer = self.get_serializer(data=data) serializer = AdHocCommandSerializer(data=data, context=self.get_serializer_context())
if not serializer.is_valid(): if not serializer.is_valid():
return Response(serializer.errors, return Response(serializer.errors,
status=status.HTTP_400_BAD_REQUEST) status=status.HTTP_400_BAD_REQUEST)
# Check for passwords needed before copying ad hoc command. # Check for passwords needed before copying ad hoc command.
needed = obj.passwords_needed_to_start needed = obj.passwords_needed_to_start
provided = dict([(field, request.DATA.get(field, '')) for field in needed]) provided = dict([(field, request.data.get(field, '')) for field in needed])
if not all(provided.values()): if not all(provided.values()):
data = dict(passwords_needed_to_start=needed) data = dict(passwords_needed_to_start=needed)
return Response(data, status=status.HTTP_400_BAD_REQUEST) return Response(data, status=status.HTTP_400_BAD_REQUEST)
# Copy and start the new ad hoc command. # Copy and start the new ad hoc command.
new_ad_hoc_command = obj.copy() new_ad_hoc_command = obj.copy()
result = new_ad_hoc_command.signal_start(**request.DATA) result = new_ad_hoc_command.signal_start(**request.data)
if not result: if not result:
data = dict(passwords_needed_to_start=new_ad_hoc_command.passwords_needed_to_start) data = dict(passwords_needed_to_start=new_ad_hoc_command.passwords_needed_to_start)
return Response(data, status=status.HTTP_400_BAD_REQUEST) return Response(data, status=status.HTTP_400_BAD_REQUEST)
@@ -2773,13 +2765,11 @@ class AdHocCommandAdHocCommandEventsList(BaseAdHocCommandEventsList):
if request.user: if request.user:
raise PermissionDenied() raise PermissionDenied()
parent_obj = get_object_or_404(self.parent_model, pk=self.kwargs['pk']) parent_obj = get_object_or_404(self.parent_model, pk=self.kwargs['pk'])
data = request.DATA.copy() data = request.data.copy()
data['ad_hoc_command'] = parent_obj.pk data['ad_hoc_command'] = parent_obj
serializer = self.get_serializer(data=data) serializer = self.get_serializer(data=data)
if serializer.is_valid(): if serializer.is_valid():
self.pre_save(serializer.object) self.instance = serializer.save()
self.object = serializer.save(force_insert=True)
self.post_save(self.object, created=True)
headers = {'Location': serializer.data['url']} headers = {'Location': serializer.data['url']}
return Response(serializer.data, status=status.HTTP_201_CREATED, return Response(serializer.data, status=status.HTTP_201_CREATED,
headers=headers) headers=headers)
@@ -2870,11 +2860,11 @@ class UnifiedJobStdout(RetrieveAPIView):
return Response(response_message) return Response(response_message)
if request.accepted_renderer.format in ('html', 'api', 'json'): if request.accepted_renderer.format in ('html', 'api', 'json'):
content_format = request.QUERY_PARAMS.get('content_format', 'html') content_format = request.query_params.get('content_format', 'html')
content_encoding = request.QUERY_PARAMS.get('content_encoding', None) content_encoding = request.query_params.get('content_encoding', None)
start_line = request.QUERY_PARAMS.get('start_line', 0) start_line = request.query_params.get('start_line', 0)
end_line = request.QUERY_PARAMS.get('end_line', None) end_line = request.query_params.get('end_line', None)
dark_val = request.QUERY_PARAMS.get('dark', '') dark_val = request.query_params.get('dark', '')
dark = bool(dark_val and dark_val[0].lower() in ('1', 't', 'y')) dark = bool(dark_val and dark_val[0].lower() in ('1', 't', 'y'))
content_only = bool(request.accepted_renderer.format in ('api', 'json')) content_only = bool(request.accepted_renderer.format in ('api', 'json'))
dark_bg = (content_only and dark) or (not content_only and (dark or not dark_val)) dark_bg = (content_only and dark) or (not content_only and (dark or not dark_val))
@@ -2973,7 +2963,7 @@ class SettingsList(ListCreateAPIView):
def get_queryset(self): def get_queryset(self):
class SettingsIntermediary(object): class SettingsIntermediary(object):
def __init__(self, key, description, category, value, def __init__(self, key, description, category, value,
value_type, user): value_type, user=None):
self.key = key self.key = key
self.description = description self.description = description
self.category = category self.category = category
@@ -3004,8 +2994,7 @@ class SettingsList(ListCreateAPIView):
m_entry['description'], m_entry['description'],
m_entry['category'], m_entry['category'],
m_entry['default'], m_entry['default'],
m_entry['type'], m_entry['type']))
None))
return settings_actual return settings_actual
def delete(self, request, *args, **kwargs): def delete(self, request, *args, **kwargs):
@@ -3023,7 +3012,7 @@ class SettingsReset(APIView):
# NOTE: Extend more with user settings # NOTE: Extend more with user settings
if not request.user.can_access(TowerSettings, 'delete', None): if not request.user.can_access(TowerSettings, 'delete', None):
raise PermissionDenied() raise PermissionDenied()
settings_key = request.DATA.get('key', None) settings_key = request.data.get('key', None)
if settings_key is not None: if settings_key is not None:
TowerSettings.objects.filter(key=settings_key).delete() TowerSettings.objects.filter(key=settings_key).delete()
return Response(status=status.HTTP_204_NO_CONTENT) return Response(status=status.HTTP_204_NO_CONTENT)

View File

@@ -684,7 +684,7 @@ class ProjectAccess(BaseAccess):
def get_queryset(self): def get_queryset(self):
qs = Project.objects.filter(active=True).distinct() qs = Project.objects.filter(active=True).distinct()
qs = qs.select_related('modified_by', 'credential', 'current_update', 'last_update') qs = qs.select_related('modified_by', 'credential', 'current_job', 'last_job')
if self.user.is_superuser: if self.user.is_superuser:
return qs return qs
team_ids = set(Team.objects.filter(users__in=[self.user]).values_list('id', flat=True)) team_ids = set(Team.objects.filter(users__in=[self.user]).values_list('id', flat=True))
@@ -1280,7 +1280,7 @@ class AdHocCommandEventAccess(BaseAccess):
def get_queryset(self): def get_queryset(self):
qs = self.model.objects.distinct() qs = self.model.objects.distinct()
qs = qs.select_related('created_by', 'modified_by', 'ad_hoc_command', 'host') qs = qs.select_related('ad_hoc_command', 'host')
if self.user.is_superuser: if self.user.is_superuser:
return qs return qs
@@ -1308,8 +1308,7 @@ class JobHostSummaryAccess(BaseAccess):
def get_queryset(self): def get_queryset(self):
qs = self.model.objects.distinct() qs = self.model.objects.distinct()
qs = qs.select_related('created_by', 'modified_by', 'job', 'job__job_template', qs = qs.select_related('job', 'job__job_template', 'host')
'host')
if self.user.is_superuser: if self.user.is_superuser:
return qs return qs
job_qs = self.user.get_queryset(Job) job_qs = self.user.get_queryset(Job)
@@ -1334,8 +1333,7 @@ class JobEventAccess(BaseAccess):
def get_queryset(self): def get_queryset(self):
qs = self.model.objects.distinct() qs = self.model.objects.distinct()
qs = qs.select_related('created_by', 'modified_by', 'job', 'job__job_template', qs = qs.select_related('job', 'job__job_template', 'host', 'parent')
'host', 'parent')
qs = qs.prefetch_related('hosts', 'children') qs = qs.prefetch_related('hosts', 'children')
# Filter certain "internal" events generated by async polling. # Filter certain "internal" events generated by async polling.
@@ -1495,7 +1493,6 @@ class ActivityStreamAccess(BaseAccess):
def get_queryset(self): def get_queryset(self):
qs = self.model.objects.distinct() qs = self.model.objects.distinct()
#qs = qs.select_related('created_by')
qs = qs.select_related('actor') qs = qs.select_related('actor')
qs = qs.prefetch_related('organization', 'user', 'inventory', 'host', 'group', 'inventory_source', qs = qs.prefetch_related('organization', 'user', 'inventory', 'host', 'group', 'inventory_source',
'inventory_update', 'credential', 'team', 'project', 'project_update', 'inventory_update', 'credential', 'team', 'project', 'project_update',

View File

@@ -37,16 +37,14 @@ class TowerConfiguration(object):
if key not in settings_manifest: if key not in settings_manifest:
raise AttributeError("Tower Setting with key '{0}' does not exist".format(key)) raise AttributeError("Tower Setting with key '{0}' does not exist".format(key))
settings_entry = settings_manifest[key] settings_entry = settings_manifest[key]
settings_actual = TowerSettings.objects.filter(key=key) try:
if not settings_actual.exists(): settings_actual = TowerSettings.objects.get(key=key)
except TowerSettings.DoesNotExist:
settings_actual = TowerSettings(key=key, settings_actual = TowerSettings(key=key,
description=settings_entry['description'], description=settings_entry['description'],
category=settings_entry['category'], category=settings_entry['category'],
value=value,
value_type=settings_entry['type']) value_type=settings_entry['type'])
else: settings_actual.value_converted = value
settings_actual = settings_actual[0]
settings_actual.value = value
settings_actual.save() settings_actual.save()
tower_settings = TowerConfiguration() tower_settings = TowerConfiguration()

View File

@@ -5,9 +5,6 @@
from django.db import models from django.db import models
from django.db.models.fields.related import SingleRelatedObjectDescriptor from django.db.models.fields.related import SingleRelatedObjectDescriptor
# South
from south.modelsinspector import add_introspection_rules
__all__ = ['AutoOneToOneField'] __all__ = ['AutoOneToOneField']
# Based on AutoOneToOneField from django-annoying: # Based on AutoOneToOneField from django-annoying:
@@ -20,8 +17,8 @@ class AutoSingleRelatedObjectDescriptor(SingleRelatedObjectDescriptor):
try: try:
return super(AutoSingleRelatedObjectDescriptor, return super(AutoSingleRelatedObjectDescriptor,
self).__get__(instance, instance_type) self).__get__(instance, instance_type)
except self.related.model.DoesNotExist: except self.related.related_model.DoesNotExist:
obj = self.related.model(**{self.related.field.name: instance}) obj = self.related.related_model(**{self.related.field.name: instance})
if self.related.field.rel.parent_link: if self.related.field.rel.parent_link:
raise NotImplementedError('not supported with polymorphic!') raise NotImplementedError('not supported with polymorphic!')
for f in instance._meta.local_fields: for f in instance._meta.local_fields:
@@ -35,6 +32,3 @@ class AutoOneToOneField(models.OneToOneField):
def contribute_to_related_class(self, cls, related): def contribute_to_related_class(self, cls, related):
setattr(cls, related.get_accessor_name(), setattr(cls, related.get_accessor_name(),
AutoSingleRelatedObjectDescriptor(related)) AutoSingleRelatedObjectDescriptor(related))
add_introspection_rules([([AutoOneToOneField], [], {})],
[r'^awx\.main\.fields\.AutoOneToOneField'])

View File

@@ -1110,13 +1110,14 @@ class Command(NoArgsCommand):
for db_group in self.inventory.groups.filter(name__in=group_names): for db_group in self.inventory.groups.filter(name__in=group_names):
mem_group = self.all_group.all_groups[db_group.name] mem_group = self.all_group.all_groups[db_group.name]
group_group_count += len(mem_group.children) group_group_count += len(mem_group.children)
child_names = set([g.name for g in mem_group.children]) all_child_names = sorted([g.name for g in mem_group.children])
db_children_qs = self.inventory.groups.filter(name__in=child_names) for offset2 in xrange(0, len(all_child_names), self._batch_size):
# FIXME: May fail unit tests when len(child_names) > 1000. child_names = all_child_names[offset2:(offset2 + self._batch_size)]
for db_child in db_children_qs.filter(children__id=db_group.id): db_children_qs = self.inventory.groups.filter(name__in=child_names)
self.logger.info('Group "%s" already child of group "%s"', db_child.name, db_group.name) for db_child in db_children_qs.filter(children__id=db_group.id):
for db_child in db_children_qs.exclude(children__id=db_group.id): self.logger.info('Group "%s" already child of group "%s"', db_child.name, db_group.name)
self._batch_add_m2m(db_group.children, db_child) for db_child in db_children_qs.exclude(children__id=db_group.id):
self._batch_add_m2m(db_group.children, db_child)
self.logger.info('Group "%s" added as child of "%s"', db_child.name, db_group.name) self.logger.info('Group "%s" added as child of "%s"', db_child.name, db_group.name)
self._batch_add_m2m(db_group.children, flush=True) self._batch_add_m2m(db_group.children, flush=True)
if settings.SQL_DEBUG: if settings.SQL_DEBUG:

View File

@@ -16,7 +16,7 @@ from django.conf import settings
from django.core.management.base import NoArgsCommand from django.core.management.base import NoArgsCommand
from django.db import transaction, DatabaseError from django.db import transaction, DatabaseError
from django.utils.dateparse import parse_datetime from django.utils.dateparse import parse_datetime
from django.utils.tzinfo import FixedOffset from django.utils.timezone import FixedOffset
from django.db import connection from django.db import connection
# AWX # AWX

View File

@@ -13,7 +13,7 @@ class HostManager(models.Manager):
def active_count(self): def active_count(self):
"""Return count of active, unique hosts for licensing.""" """Return count of active, unique hosts for licensing."""
try: try:
return self.filter(active=True, inventory__active=True).distinct('name').count() return self.filter(active=True, inventory__active=True).order_by('name').distinct('name').count()
except NotImplementedError: # For unit tests only, SQLite doesn't support distinct('name') except NotImplementedError: # For unit tests only, SQLite doesn't support distinct('name')
return len(set(self.filter(active=True, inventory__active=True).values_list('name', flat=True))) return len(set(self.filter(active=True, inventory__active=True).values_list('name', flat=True)))

View File

@@ -0,0 +1,991 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2016 Ansible, Inc.
# All Rights Reserved.
from __future__ import unicode_literals
from django.db import migrations, models
import django.utils.timezone
import jsonfield.fields
import django.db.models.deletion
from django.conf import settings
import taggit.managers
import awx.main.fields
class Migration(migrations.Migration):
dependencies = [
('taggit', '0002_auto_20150616_2121'),
('contenttypes', '0002_remove_content_type_name'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='ActivityStream',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('operation', models.CharField(max_length=13, choices=[(b'create', 'Entity Created'), (b'update', 'Entity Updated'), (b'delete', 'Entity Deleted'), (b'associate', 'Entity Associated with another Entity'), (b'disassociate', 'Entity was Disassociated with another Entity')])),
('timestamp', models.DateTimeField(auto_now_add=True)),
('changes', models.TextField(blank=True)),
('object_relationship_type', models.TextField(blank=True)),
('object1', models.TextField()),
('object2', models.TextField()),
('actor', models.ForeignKey(related_name='activity_stream', on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, null=True)),
],
),
migrations.CreateModel(
name='AdHocCommandEvent',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('created', models.DateTimeField(default=None, editable=False)),
('modified', models.DateTimeField(default=None, editable=False)),
('host_name', models.CharField(default=b'', max_length=1024, editable=False)),
('event', models.CharField(max_length=100, choices=[(b'runner_on_failed', 'Host Failed'), (b'runner_on_ok', 'Host OK'), (b'runner_on_unreachable', 'Host Unreachable')])),
('event_data', jsonfield.fields.JSONField(default={}, blank=True)),
('failed', models.BooleanField(default=False, editable=False)),
('changed', models.BooleanField(default=False, editable=False)),
('counter', models.PositiveIntegerField(default=0)),
],
options={
'ordering': ('-pk',),
},
),
migrations.CreateModel(
name='AuthToken',
fields=[
('key', models.CharField(max_length=40, serialize=False, primary_key=True)),
('created', models.DateTimeField(auto_now_add=True)),
('modified', models.DateTimeField(auto_now=True)),
('expires', models.DateTimeField(default=django.utils.timezone.now)),
('request_hash', models.CharField(default=b'', max_length=40, blank=True)),
('reason', models.CharField(default=b'', help_text='Reason the auth token was invalidated.', max_length=1024, blank=True)),
('user', models.ForeignKey(related_name='auth_tokens', to=settings.AUTH_USER_MODEL)),
],
),
migrations.CreateModel(
name='Credential',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('created', models.DateTimeField(default=None, editable=False)),
('modified', models.DateTimeField(default=None, editable=False)),
('description', models.TextField(default=b'', blank=True)),
('active', models.BooleanField(default=True, editable=False)),
('name', models.CharField(max_length=512)),
('kind', models.CharField(default=b'ssh', max_length=32, choices=[(b'ssh', 'Machine'), (b'scm', 'Source Control'), (b'aws', 'Amazon Web Services'), (b'rax', 'Rackspace'), (b'vmware', 'VMware vCenter'), (b'gce', 'Google Compute Engine'), (b'azure', 'Microsoft Azure'), (b'openstack', 'OpenStack')])),
('cloud', models.BooleanField(default=False, editable=False)),
('host', models.CharField(default=b'', help_text='The hostname or IP address to use.', max_length=1024, verbose_name='Host', blank=True)),
('username', models.CharField(default=b'', help_text='Username for this credential.', max_length=1024, verbose_name='Username', blank=True)),
('password', models.CharField(default=b'', help_text='Password for this credential (or "ASK" to prompt the user for machine credentials).', max_length=1024, verbose_name='Password', blank=True)),
('security_token', models.CharField(default=b'', help_text='Security Token for this credential', max_length=1024, verbose_name='Security Token', blank=True)),
('project', models.CharField(default=b'', help_text='The identifier for the project.', max_length=100, verbose_name='Project', blank=True)),
('ssh_key_data', models.TextField(default=b'', help_text='RSA or DSA private key to be used instead of password.', verbose_name='SSH private key', blank=True)),
('ssh_key_unlock', models.CharField(default=b'', help_text='Passphrase to unlock SSH private key if encrypted (or "ASK" to prompt the user for machine credentials).', max_length=1024, verbose_name='SSH key unlock', blank=True)),
('become_method', models.CharField(default=b'', help_text='Privilege escalation method.', max_length=32, blank=True, choices=[(b'', 'None'), (b'sudo', 'Sudo'), (b'su', 'Su'), (b'pbrun', 'Pbrun'), (b'pfexec', 'Pfexec')])),
('become_username', models.CharField(default=b'', help_text='Privilege escalation username.', max_length=1024, blank=True)),
('become_password', models.CharField(default=b'', help_text='Password for privilege escalation method.', max_length=1024, blank=True)),
('vault_password', models.CharField(default=b'', help_text='Vault password (or "ASK" to prompt the user).', max_length=1024, blank=True)),
('created_by', models.ForeignKey(related_name="{u'class': 'credential', u'app_label': 'main'}(class)s_created+", on_delete=django.db.models.deletion.SET_NULL, default=None, editable=False, to=settings.AUTH_USER_MODEL, null=True)),
('modified_by', models.ForeignKey(related_name="{u'class': 'credential', u'app_label': 'main'}(class)s_modified+", on_delete=django.db.models.deletion.SET_NULL, default=None, editable=False, to=settings.AUTH_USER_MODEL, null=True)),
('tags', taggit.managers.TaggableManager(to='taggit.Tag', through='taggit.TaggedItem', blank=True, help_text='A comma-separated list of tags.', verbose_name='Tags')),
],
options={
'ordering': ('kind', 'name'),
},
),
migrations.CreateModel(
name='CustomInventoryScript',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('created', models.DateTimeField(default=None, editable=False)),
('modified', models.DateTimeField(default=None, editable=False)),
('description', models.TextField(default=b'', blank=True)),
('active', models.BooleanField(default=True, editable=False)),
('name', models.CharField(max_length=512)),
('script', models.TextField(default=b'', help_text='Inventory script contents', blank=True)),
('created_by', models.ForeignKey(related_name="{u'class': 'custominventoryscript', u'app_label': 'main'}(class)s_created+", on_delete=django.db.models.deletion.SET_NULL, default=None, editable=False, to=settings.AUTH_USER_MODEL, null=True)),
('modified_by', models.ForeignKey(related_name="{u'class': 'custominventoryscript', u'app_label': 'main'}(class)s_modified+", on_delete=django.db.models.deletion.SET_NULL, default=None, editable=False, to=settings.AUTH_USER_MODEL, null=True)),
],
options={
'ordering': ('name',),
},
),
migrations.CreateModel(
name='Group',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('created', models.DateTimeField(default=None, editable=False)),
('modified', models.DateTimeField(default=None, editable=False)),
('description', models.TextField(default=b'', blank=True)),
('active', models.BooleanField(default=True, editable=False)),
('name', models.CharField(max_length=512)),
('variables', models.TextField(default=b'', help_text='Group variables in JSON or YAML format.', blank=True)),
('total_hosts', models.PositiveIntegerField(default=0, help_text='Total number of hosts directly or indirectly in this group.', editable=False)),
('has_active_failures', models.BooleanField(default=False, help_text='Flag indicating whether this group has any hosts with active failures.', editable=False)),
('hosts_with_active_failures', models.PositiveIntegerField(default=0, help_text='Number of hosts in this group with active failures.', editable=False)),
('total_groups', models.PositiveIntegerField(default=0, help_text='Total number of child groups contained within this group.', editable=False)),
('groups_with_active_failures', models.PositiveIntegerField(default=0, help_text='Number of child groups within this group that have active failures.', editable=False)),
('has_inventory_sources', models.BooleanField(default=False, help_text='Flag indicating whether this group was created/updated from any external inventory sources.', editable=False)),
('created_by', models.ForeignKey(related_name="{u'class': 'group', u'app_label': 'main'}(class)s_created+", on_delete=django.db.models.deletion.SET_NULL, default=None, editable=False, to=settings.AUTH_USER_MODEL, null=True)),
],
options={
'ordering': ('name',),
},
),
migrations.CreateModel(
name='Host',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('created', models.DateTimeField(default=None, editable=False)),
('modified', models.DateTimeField(default=None, editable=False)),
('description', models.TextField(default=b'', blank=True)),
('active', models.BooleanField(default=True, editable=False)),
('name', models.CharField(max_length=512)),
('enabled', models.BooleanField(default=True, help_text='Is this host online and available for running jobs?')),
('instance_id', models.CharField(default=b'', max_length=100, blank=True)),
('variables', models.TextField(default=b'', help_text='Host variables in JSON or YAML format.', blank=True)),
('has_active_failures', models.BooleanField(default=False, help_text='Flag indicating whether the last job failed for this host.', editable=False)),
('has_inventory_sources', models.BooleanField(default=False, help_text='Flag indicating whether this host was created/updated from any external inventory sources.', editable=False)),
('created_by', models.ForeignKey(related_name="{u'class': 'host', u'app_label': 'main'}(class)s_created+", on_delete=django.db.models.deletion.SET_NULL, default=None, editable=False, to=settings.AUTH_USER_MODEL, null=True)),
],
options={
'ordering': ('inventory', 'name'),
},
),
migrations.CreateModel(
name='Instance',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('uuid', models.CharField(unique=True, max_length=40)),
('hostname', models.CharField(unique=True, max_length=250)),
('primary', models.BooleanField(default=False)),
('created', models.DateTimeField(auto_now_add=True)),
('modified', models.DateTimeField(auto_now=True)),
],
),
migrations.CreateModel(
name='Inventory',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('created', models.DateTimeField(default=None, editable=False)),
('modified', models.DateTimeField(default=None, editable=False)),
('description', models.TextField(default=b'', blank=True)),
('active', models.BooleanField(default=True, editable=False)),
('name', models.CharField(unique=True, max_length=512)),
('variables', models.TextField(default=b'', help_text='Inventory variables in JSON or YAML format.', blank=True)),
('has_active_failures', models.BooleanField(default=False, help_text='Flag indicating whether any hosts in this inventory have failed.', editable=False)),
('total_hosts', models.PositiveIntegerField(default=0, help_text='Total number of hosts in this inventory.', editable=False)),
('hosts_with_active_failures', models.PositiveIntegerField(default=0, help_text='Number of hosts in this inventory with active failures.', editable=False)),
('total_groups', models.PositiveIntegerField(default=0, help_text='Total number of groups in this inventory.', editable=False)),
('groups_with_active_failures', models.PositiveIntegerField(default=0, help_text='Number of groups in this inventory with active failures.', editable=False)),
('has_inventory_sources', models.BooleanField(default=False, help_text='Flag indicating whether this inventory has any external inventory sources.', editable=False)),
('total_inventory_sources', models.PositiveIntegerField(default=0, help_text='Total number of external inventory sources configured within this inventory.', editable=False)),
('inventory_sources_with_failures', models.PositiveIntegerField(default=0, help_text='Number of external inventory sources in this inventory with failures.', editable=False)),
('created_by', models.ForeignKey(related_name="{u'class': 'inventory', u'app_label': 'main'}(class)s_created+", on_delete=django.db.models.deletion.SET_NULL, default=None, editable=False, to=settings.AUTH_USER_MODEL, null=True)),
('modified_by', models.ForeignKey(related_name="{u'class': 'inventory', u'app_label': 'main'}(class)s_modified+", on_delete=django.db.models.deletion.SET_NULL, default=None, editable=False, to=settings.AUTH_USER_MODEL, null=True)),
],
options={
'ordering': ('name',),
'verbose_name_plural': 'inventories',
},
),
migrations.CreateModel(
name='JobEvent',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('created', models.DateTimeField(default=None, editable=False)),
('modified', models.DateTimeField(default=None, editable=False)),
('event', models.CharField(max_length=100, choices=[(b'runner_on_failed', 'Host Failed'), (b'runner_on_ok', 'Host OK'), (b'runner_on_error', 'Host Failure'), (b'runner_on_skipped', 'Host Skipped'), (b'runner_on_unreachable', 'Host Unreachable'), (b'runner_on_no_hosts', 'No Hosts Remaining'), (b'runner_on_async_poll', 'Host Polling'), (b'runner_on_async_ok', 'Host Async OK'), (b'runner_on_async_failed', 'Host Async Failure'), (b'runner_on_file_diff', 'File Difference'), (b'playbook_on_start', 'Playbook Started'), (b'playbook_on_notify', 'Running Handlers'), (b'playbook_on_no_hosts_matched', 'No Hosts Matched'), (b'playbook_on_no_hosts_remaining', 'No Hosts Remaining'), (b'playbook_on_task_start', 'Task Started'), (b'playbook_on_vars_prompt', 'Variables Prompted'), (b'playbook_on_setup', 'Gathering Facts'), (b'playbook_on_import_for_host', 'internal: on Import for Host'), (b'playbook_on_not_import_for_host', 'internal: on Not Import for Host'), (b'playbook_on_play_start', 'Play Started'), (b'playbook_on_stats', 'Playbook Complete')])),
('event_data', jsonfield.fields.JSONField(default={}, blank=True)),
('failed', models.BooleanField(default=False, editable=False)),
('changed', models.BooleanField(default=False, editable=False)),
('host_name', models.CharField(default=b'', max_length=1024, editable=False)),
('play', models.CharField(default=b'', max_length=1024, editable=False)),
('role', models.CharField(default=b'', max_length=1024, editable=False)),
('task', models.CharField(default=b'', max_length=1024, editable=False)),
('counter', models.PositiveIntegerField(default=0)),
('host', models.ForeignKey(related_name='job_events_as_primary_host', on_delete=django.db.models.deletion.SET_NULL, default=None, editable=False, to='main.Host', null=True)),
('hosts', models.ManyToManyField(related_name='job_events', editable=False, to='main.Host')),
('parent', models.ForeignKey(related_name='children', on_delete=django.db.models.deletion.SET_NULL, default=None, editable=False, to='main.JobEvent', null=True)),
],
options={
'ordering': ('pk',),
},
),
migrations.CreateModel(
name='JobHostSummary',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('created', models.DateTimeField(default=None, editable=False)),
('modified', models.DateTimeField(default=None, editable=False)),
('host_name', models.CharField(default=b'', max_length=1024, editable=False)),
('changed', models.PositiveIntegerField(default=0, editable=False)),
('dark', models.PositiveIntegerField(default=0, editable=False)),
('failures', models.PositiveIntegerField(default=0, editable=False)),
('ok', models.PositiveIntegerField(default=0, editable=False)),
('processed', models.PositiveIntegerField(default=0, editable=False)),
('skipped', models.PositiveIntegerField(default=0, editable=False)),
('failed', models.BooleanField(default=False, editable=False)),
('host', models.ForeignKey(related_name='job_host_summaries', on_delete=django.db.models.deletion.SET_NULL, default=None, editable=False, to='main.Host', null=True)),
],
options={
'ordering': ('-pk',),
'verbose_name_plural': 'job host summaries',
},
),
migrations.CreateModel(
name='JobOrigin',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('created', models.DateTimeField(auto_now_add=True)),
('modified', models.DateTimeField(auto_now=True)),
('instance', models.ForeignKey(to='main.Instance')),
],
),
migrations.CreateModel(
name='Organization',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('created', models.DateTimeField(default=None, editable=False)),
('modified', models.DateTimeField(default=None, editable=False)),
('description', models.TextField(default=b'', blank=True)),
('active', models.BooleanField(default=True, editable=False)),
('name', models.CharField(unique=True, max_length=512)),
('admins', models.ManyToManyField(related_name='admin_of_organizations', to=settings.AUTH_USER_MODEL, blank=True)),
('created_by', models.ForeignKey(related_name="{u'class': 'organization', u'app_label': 'main'}(class)s_created+", on_delete=django.db.models.deletion.SET_NULL, default=None, editable=False, to=settings.AUTH_USER_MODEL, null=True)),
('modified_by', models.ForeignKey(related_name="{u'class': 'organization', u'app_label': 'main'}(class)s_modified+", on_delete=django.db.models.deletion.SET_NULL, default=None, editable=False, to=settings.AUTH_USER_MODEL, null=True)),
('tags', taggit.managers.TaggableManager(to='taggit.Tag', through='taggit.TaggedItem', blank=True, help_text='A comma-separated list of tags.', verbose_name='Tags')),
('users', models.ManyToManyField(related_name='organizations', to=settings.AUTH_USER_MODEL, blank=True)),
],
options={
'ordering': ('name',),
},
),
migrations.CreateModel(
name='Permission',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('created', models.DateTimeField(default=None, editable=False)),
('modified', models.DateTimeField(default=None, editable=False)),
('description', models.TextField(default=b'', blank=True)),
('active', models.BooleanField(default=True, editable=False)),
('name', models.CharField(max_length=512)),
('permission_type', models.CharField(max_length=64, choices=[(b'read', 'Read Inventory'), (b'write', 'Edit Inventory'), (b'admin', 'Administrate Inventory'), (b'run', 'Deploy To Inventory'), (b'check', 'Deploy To Inventory (Dry Run)'), (b'scan', 'Scan an Inventory'), (b'create', 'Create a Job Template')])),
('run_ad_hoc_commands', models.BooleanField(default=False, help_text='Execute Commands on the Inventory')),
('created_by', models.ForeignKey(related_name="{u'class': 'permission', u'app_label': 'main'}(class)s_created+", on_delete=django.db.models.deletion.SET_NULL, default=None, editable=False, to=settings.AUTH_USER_MODEL, null=True)),
('inventory', models.ForeignKey(related_name='permissions', on_delete=django.db.models.deletion.SET_NULL, to='main.Inventory', null=True)),
('modified_by', models.ForeignKey(related_name="{u'class': 'permission', u'app_label': 'main'}(class)s_modified+", on_delete=django.db.models.deletion.SET_NULL, default=None, editable=False, to=settings.AUTH_USER_MODEL, null=True)),
('tags', taggit.managers.TaggableManager(to='taggit.Tag', through='taggit.TaggedItem', blank=True, help_text='A comma-separated list of tags.', verbose_name='Tags')),
],
),
migrations.CreateModel(
name='Profile',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('created', models.DateTimeField(default=None, editable=False)),
('modified', models.DateTimeField(default=None, editable=False)),
('ldap_dn', models.CharField(default=b'', max_length=1024)),
('user', awx.main.fields.AutoOneToOneField(related_name='profile', editable=False, to=settings.AUTH_USER_MODEL)),
],
),
migrations.CreateModel(
name='Schedule',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('created', models.DateTimeField(default=None, editable=False)),
('modified', models.DateTimeField(default=None, editable=False)),
('description', models.TextField(default=b'', blank=True)),
('active', models.BooleanField(default=True, editable=False)),
('name', models.CharField(unique=True, max_length=512)),
('enabled', models.BooleanField(default=True)),
('dtstart', models.DateTimeField(default=None, null=True, editable=False)),
('dtend', models.DateTimeField(default=None, null=True, editable=False)),
('rrule', models.CharField(max_length=255)),
('next_run', models.DateTimeField(default=None, null=True, editable=False)),
('extra_data', jsonfield.fields.JSONField(default={}, blank=True)),
('created_by', models.ForeignKey(related_name="{u'class': 'schedule', u'app_label': 'main'}(class)s_created+", on_delete=django.db.models.deletion.SET_NULL, default=None, editable=False, to=settings.AUTH_USER_MODEL, null=True)),
('modified_by', models.ForeignKey(related_name="{u'class': 'schedule', u'app_label': 'main'}(class)s_modified+", on_delete=django.db.models.deletion.SET_NULL, default=None, editable=False, to=settings.AUTH_USER_MODEL, null=True)),
('tags', taggit.managers.TaggableManager(to='taggit.Tag', through='taggit.TaggedItem', blank=True, help_text='A comma-separated list of tags.', verbose_name='Tags')),
],
options={
'ordering': ['-next_run'],
},
),
migrations.CreateModel(
name='Team',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('created', models.DateTimeField(default=None, editable=False)),
('modified', models.DateTimeField(default=None, editable=False)),
('description', models.TextField(default=b'', blank=True)),
('active', models.BooleanField(default=True, editable=False)),
('name', models.CharField(max_length=512)),
('created_by', models.ForeignKey(related_name="{u'class': 'team', u'app_label': 'main'}(class)s_created+", on_delete=django.db.models.deletion.SET_NULL, default=None, editable=False, to=settings.AUTH_USER_MODEL, null=True)),
('modified_by', models.ForeignKey(related_name="{u'class': 'team', u'app_label': 'main'}(class)s_modified+", on_delete=django.db.models.deletion.SET_NULL, default=None, editable=False, to=settings.AUTH_USER_MODEL, null=True)),
('organization', models.ForeignKey(related_name='teams', on_delete=django.db.models.deletion.SET_NULL, to='main.Organization', null=True)),
('tags', taggit.managers.TaggableManager(to='taggit.Tag', through='taggit.TaggedItem', blank=True, help_text='A comma-separated list of tags.', verbose_name='Tags')),
('users', models.ManyToManyField(related_name='teams', to=settings.AUTH_USER_MODEL, blank=True)),
],
options={
'ordering': ('organization__name', 'name'),
},
),
migrations.CreateModel(
name='UnifiedJob',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('created', models.DateTimeField(default=None, editable=False)),
('modified', models.DateTimeField(default=None, editable=False)),
('description', models.TextField(default=b'', blank=True)),
('active', models.BooleanField(default=True, editable=False)),
('name', models.CharField(max_length=512)),
('old_pk', models.PositiveIntegerField(default=None, null=True, editable=False)),
('launch_type', models.CharField(default=b'manual', max_length=20, editable=False, choices=[(b'manual', 'Manual'), (b'relaunch', 'Relaunch'), (b'callback', 'Callback'), (b'scheduled', 'Scheduled'), (b'dependency', 'Dependency')])),
('cancel_flag', models.BooleanField(default=False, editable=False)),
('status', models.CharField(default=b'new', max_length=20, editable=False, choices=[(b'new', 'New'), (b'pending', 'Pending'), (b'waiting', 'Waiting'), (b'running', 'Running'), (b'successful', 'Successful'), (b'failed', 'Failed'), (b'error', 'Error'), (b'canceled', 'Canceled')])),
('failed', models.BooleanField(default=False, editable=False)),
('started', models.DateTimeField(default=None, null=True, editable=False)),
('finished', models.DateTimeField(default=None, null=True, editable=False)),
('elapsed', models.DecimalField(editable=False, max_digits=12, decimal_places=3)),
('job_args', models.TextField(default=b'', editable=False, blank=True)),
('job_cwd', models.CharField(default=b'', max_length=1024, editable=False, blank=True)),
('job_env', jsonfield.fields.JSONField(default={}, editable=False, blank=True)),
('job_explanation', models.TextField(default=b'', editable=False, blank=True)),
('start_args', models.TextField(default=b'', editable=False, blank=True)),
('result_stdout_text', models.TextField(default=b'', editable=False, blank=True)),
('result_stdout_file', models.TextField(default=b'', editable=False, blank=True)),
('result_traceback', models.TextField(default=b'', editable=False, blank=True)),
('celery_task_id', models.CharField(default=b'', max_length=100, editable=False, blank=True)),
],
),
migrations.CreateModel(
name='UnifiedJobTemplate',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('created', models.DateTimeField(default=None, editable=False)),
('modified', models.DateTimeField(default=None, editable=False)),
('description', models.TextField(default=b'', blank=True)),
('active', models.BooleanField(default=True, editable=False)),
('name', models.CharField(max_length=512)),
('old_pk', models.PositiveIntegerField(default=None, null=True, editable=False)),
('last_job_failed', models.BooleanField(default=False, editable=False)),
('last_job_run', models.DateTimeField(default=None, null=True, editable=False)),
('has_schedules', models.BooleanField(default=False, editable=False)),
('next_job_run', models.DateTimeField(default=None, null=True, editable=False)),
('status', models.CharField(default=b'ok', max_length=32, editable=False, choices=[(b'new', 'New'), (b'pending', 'Pending'), (b'waiting', 'Waiting'), (b'running', 'Running'), (b'successful', 'Successful'), (b'failed', 'Failed'), (b'error', 'Error'), (b'canceled', 'Canceled'), (b'never updated', b'Never Updated'), (b'ok', b'OK'), (b'missing', b'Missing'), (b'none', 'No External Source'), (b'updating', 'Updating')])),
],
),
migrations.CreateModel(
name='AdHocCommand',
fields=[
('unifiedjob_ptr', models.OneToOneField(parent_link=True, auto_created=True, primary_key=True, serialize=False, to='main.UnifiedJob')),
('job_type', models.CharField(default=b'run', max_length=64, choices=[(b'run', 'Run'), (b'check', 'Check'), (b'scan', 'Scan')])),
('limit', models.CharField(default=b'', max_length=1024, blank=True)),
('module_name', models.CharField(default=b'', max_length=1024, blank=True)),
('module_args', models.TextField(default=b'', blank=True)),
('forks', models.PositiveIntegerField(default=0, blank=True)),
('verbosity', models.PositiveIntegerField(default=0, blank=True, choices=[(0, b'0 (Normal)'), (1, b'1 (Verbose)'), (2, b'2 (More Verbose)'), (3, b'3 (Debug)'), (4, b'4 (Connection Debug)'), (5, b'5 (WinRM Debug)')])),
('become_enabled', models.BooleanField(default=False)),
],
bases=('main.unifiedjob',),
),
migrations.CreateModel(
name='InventorySource',
fields=[
('unifiedjobtemplate_ptr', models.OneToOneField(parent_link=True, auto_created=True, primary_key=True, serialize=False, to='main.UnifiedJobTemplate')),
('source', models.CharField(default=b'', max_length=32, blank=True, choices=[(b'', 'Manual'), (b'file', 'Local File, Directory or Script'), (b'rax', 'Rackspace Cloud Servers'), (b'ec2', 'Amazon EC2'), (b'gce', 'Google Compute Engine'), (b'azure', 'Microsoft Azure'), (b'vmware', 'VMware vCenter'), (b'openstack', 'OpenStack'), (b'custom', 'Custom Script')])),
('source_path', models.CharField(default=b'', max_length=1024, editable=False, blank=True)),
('source_vars', models.TextField(default=b'', help_text='Inventory source variables in YAML or JSON format.', blank=True)),
('source_regions', models.CharField(default=b'', max_length=1024, blank=True)),
('instance_filters', models.CharField(default=b'', help_text='Comma-separated list of filter expressions (EC2 only). Hosts are imported when ANY of the filters match.', max_length=1024, blank=True)),
('group_by', models.CharField(default=b'', help_text='Limit groups automatically created from inventory source (EC2 only).', max_length=1024, blank=True)),
('overwrite', models.BooleanField(default=False, help_text='Overwrite local groups and hosts from remote inventory source.')),
('overwrite_vars', models.BooleanField(default=False, help_text='Overwrite local variables from remote inventory source.')),
('update_on_launch', models.BooleanField(default=False)),
('update_cache_timeout', models.PositiveIntegerField(default=0)),
],
bases=('main.unifiedjobtemplate', models.Model),
),
migrations.CreateModel(
name='InventoryUpdate',
fields=[
('unifiedjob_ptr', models.OneToOneField(parent_link=True, auto_created=True, primary_key=True, serialize=False, to='main.UnifiedJob')),
('source', models.CharField(default=b'', max_length=32, blank=True, choices=[(b'', 'Manual'), (b'file', 'Local File, Directory or Script'), (b'rax', 'Rackspace Cloud Servers'), (b'ec2', 'Amazon EC2'), (b'gce', 'Google Compute Engine'), (b'azure', 'Microsoft Azure'), (b'vmware', 'VMware vCenter'), (b'openstack', 'OpenStack'), (b'custom', 'Custom Script')])),
('source_path', models.CharField(default=b'', max_length=1024, editable=False, blank=True)),
('source_vars', models.TextField(default=b'', help_text='Inventory source variables in YAML or JSON format.', blank=True)),
('source_regions', models.CharField(default=b'', max_length=1024, blank=True)),
('instance_filters', models.CharField(default=b'', help_text='Comma-separated list of filter expressions (EC2 only). Hosts are imported when ANY of the filters match.', max_length=1024, blank=True)),
('group_by', models.CharField(default=b'', help_text='Limit groups automatically created from inventory source (EC2 only).', max_length=1024, blank=True)),
('overwrite', models.BooleanField(default=False, help_text='Overwrite local groups and hosts from remote inventory source.')),
('overwrite_vars', models.BooleanField(default=False, help_text='Overwrite local variables from remote inventory source.')),
('license_error', models.BooleanField(default=False, editable=False)),
],
bases=('main.unifiedjob', models.Model),
),
migrations.CreateModel(
name='Job',
fields=[
('unifiedjob_ptr', models.OneToOneField(parent_link=True, auto_created=True, primary_key=True, serialize=False, to='main.UnifiedJob')),
('job_type', models.CharField(default=b'run', max_length=64, choices=[(b'run', 'Run'), (b'check', 'Check'), (b'scan', 'Scan')])),
('playbook', models.CharField(default=b'', max_length=1024, blank=True)),
('forks', models.PositiveIntegerField(default=0, blank=True)),
('limit', models.CharField(default=b'', max_length=1024, blank=True)),
('verbosity', models.PositiveIntegerField(default=0, blank=True, choices=[(0, b'0 (Normal)'), (1, b'1 (Verbose)'), (2, b'2 (More Verbose)'), (3, b'3 (Debug)'), (4, b'4 (Connection Debug)'), (5, b'5 (WinRM Debug)')])),
('extra_vars', models.TextField(default=b'', blank=True)),
('job_tags', models.CharField(default=b'', max_length=1024, blank=True)),
('force_handlers', models.BooleanField(default=False)),
('skip_tags', models.CharField(default=b'', max_length=1024, blank=True)),
('start_at_task', models.CharField(default=b'', max_length=1024, blank=True)),
('become_enabled', models.BooleanField(default=False)),
],
options={
'ordering': ('id',),
},
bases=('main.unifiedjob', models.Model),
),
migrations.CreateModel(
name='JobTemplate',
fields=[
('unifiedjobtemplate_ptr', models.OneToOneField(parent_link=True, auto_created=True, primary_key=True, serialize=False, to='main.UnifiedJobTemplate')),
('job_type', models.CharField(default=b'run', max_length=64, choices=[(b'run', 'Run'), (b'check', 'Check'), (b'scan', 'Scan')])),
('playbook', models.CharField(default=b'', max_length=1024, blank=True)),
('forks', models.PositiveIntegerField(default=0, blank=True)),
('limit', models.CharField(default=b'', max_length=1024, blank=True)),
('verbosity', models.PositiveIntegerField(default=0, blank=True, choices=[(0, b'0 (Normal)'), (1, b'1 (Verbose)'), (2, b'2 (More Verbose)'), (3, b'3 (Debug)'), (4, b'4 (Connection Debug)'), (5, b'5 (WinRM Debug)')])),
('extra_vars', models.TextField(default=b'', blank=True)),
('job_tags', models.CharField(default=b'', max_length=1024, blank=True)),
('force_handlers', models.BooleanField(default=False)),
('skip_tags', models.CharField(default=b'', max_length=1024, blank=True)),
('start_at_task', models.CharField(default=b'', max_length=1024, blank=True)),
('become_enabled', models.BooleanField(default=False)),
('host_config_key', models.CharField(default=b'', max_length=1024, blank=True)),
('ask_variables_on_launch', models.BooleanField(default=False)),
('survey_enabled', models.BooleanField(default=False)),
('survey_spec', jsonfield.fields.JSONField(default={}, blank=True)),
],
options={
'ordering': ('name',),
},
bases=('main.unifiedjobtemplate', models.Model),
),
migrations.CreateModel(
name='Project',
fields=[
('unifiedjobtemplate_ptr', models.OneToOneField(parent_link=True, auto_created=True, primary_key=True, serialize=False, to='main.UnifiedJobTemplate')),
('local_path', models.CharField(help_text='Local path (relative to PROJECTS_ROOT) containing playbooks and related files for this project.', max_length=1024, blank=True)),
('scm_type', models.CharField(default=b'', max_length=8, verbose_name='SCM Type', blank=True, choices=[(b'', 'Manual'), (b'git', 'Git'), (b'hg', 'Mercurial'), (b'svn', 'Subversion')])),
('scm_url', models.CharField(default=b'', max_length=1024, verbose_name='SCM URL', blank=True)),
('scm_branch', models.CharField(default=b'', help_text='Specific branch, tag or commit to checkout.', max_length=256, verbose_name='SCM Branch', blank=True)),
('scm_clean', models.BooleanField(default=False)),
('scm_delete_on_update', models.BooleanField(default=False)),
('scm_delete_on_next_update', models.BooleanField(default=False, editable=False)),
('scm_update_on_launch', models.BooleanField(default=False)),
('scm_update_cache_timeout', models.PositiveIntegerField(default=0, blank=True)),
],
options={
'ordering': ('id',),
},
bases=('main.unifiedjobtemplate', models.Model),
),
migrations.CreateModel(
name='ProjectUpdate',
fields=[
('unifiedjob_ptr', models.OneToOneField(parent_link=True, auto_created=True, primary_key=True, serialize=False, to='main.UnifiedJob')),
('local_path', models.CharField(help_text='Local path (relative to PROJECTS_ROOT) containing playbooks and related files for this project.', max_length=1024, blank=True)),
('scm_type', models.CharField(default=b'', max_length=8, verbose_name='SCM Type', blank=True, choices=[(b'', 'Manual'), (b'git', 'Git'), (b'hg', 'Mercurial'), (b'svn', 'Subversion')])),
('scm_url', models.CharField(default=b'', max_length=1024, verbose_name='SCM URL', blank=True)),
('scm_branch', models.CharField(default=b'', help_text='Specific branch, tag or commit to checkout.', max_length=256, verbose_name='SCM Branch', blank=True)),
('scm_clean', models.BooleanField(default=False)),
('scm_delete_on_update', models.BooleanField(default=False)),
],
bases=('main.unifiedjob', models.Model),
),
migrations.CreateModel(
name='SystemJob',
fields=[
('unifiedjob_ptr', models.OneToOneField(parent_link=True, auto_created=True, primary_key=True, serialize=False, to='main.UnifiedJob')),
('job_type', models.CharField(default=b'', max_length=32, blank=True, choices=[(b'cleanup_jobs', 'Remove jobs older than a certain number of days'), (b'cleanup_activitystream', 'Remove activity stream entries older than a certain number of days'), (b'cleanup_deleted', 'Purge previously deleted items from the database'), (b'cleanup_facts', 'Purge and/or reduce the granularity of system tracking data')])),
('extra_vars', models.TextField(default=b'', blank=True)),
],
options={
'ordering': ('id',),
},
bases=('main.unifiedjob', models.Model),
),
migrations.CreateModel(
name='SystemJobTemplate',
fields=[
('unifiedjobtemplate_ptr', models.OneToOneField(parent_link=True, auto_created=True, primary_key=True, serialize=False, to='main.UnifiedJobTemplate')),
('job_type', models.CharField(default=b'', max_length=32, blank=True, choices=[(b'cleanup_jobs', 'Remove jobs older than a certain number of days'), (b'cleanup_activitystream', 'Remove activity stream entries older than a certain number of days'), (b'cleanup_deleted', 'Purge previously deleted items from the database'), (b'cleanup_facts', 'Purge and/or reduce the granularity of system tracking data')])),
],
bases=('main.unifiedjobtemplate', models.Model),
),
migrations.AddField(
model_name='unifiedjobtemplate',
name='created_by',
field=models.ForeignKey(related_name="{u'class': 'unifiedjobtemplate', u'app_label': 'main'}(class)s_created+", on_delete=django.db.models.deletion.SET_NULL, default=None, editable=False, to=settings.AUTH_USER_MODEL, null=True),
),
migrations.AddField(
model_name='unifiedjobtemplate',
name='current_job',
field=models.ForeignKey(related_name='unifiedjobtemplate_as_current_job+', on_delete=django.db.models.deletion.SET_NULL, default=None, editable=False, to='main.UnifiedJob', null=True),
),
migrations.AddField(
model_name='unifiedjobtemplate',
name='last_job',
field=models.ForeignKey(related_name='unifiedjobtemplate_as_last_job+', on_delete=django.db.models.deletion.SET_NULL, default=None, editable=False, to='main.UnifiedJob', null=True),
),
migrations.AddField(
model_name='unifiedjobtemplate',
name='modified_by',
field=models.ForeignKey(related_name="{u'class': 'unifiedjobtemplate', u'app_label': 'main'}(class)s_modified+", on_delete=django.db.models.deletion.SET_NULL, default=None, editable=False, to=settings.AUTH_USER_MODEL, null=True),
),
migrations.AddField(
model_name='unifiedjobtemplate',
name='next_schedule',
field=models.ForeignKey(related_name='unifiedjobtemplate_as_next_schedule+', on_delete=django.db.models.deletion.SET_NULL, default=None, editable=False, to='main.Schedule', null=True),
),
migrations.AddField(
model_name='unifiedjobtemplate',
name='polymorphic_ctype',
field=models.ForeignKey(related_name='polymorphic_main.unifiedjobtemplate_set+', editable=False, to='contenttypes.ContentType', null=True),
),
migrations.AddField(
model_name='unifiedjobtemplate',
name='tags',
field=taggit.managers.TaggableManager(to='taggit.Tag', through='taggit.TaggedItem', blank=True, help_text='A comma-separated list of tags.', verbose_name='Tags'),
),
migrations.AddField(
model_name='unifiedjob',
name='created_by',
field=models.ForeignKey(related_name="{u'class': 'unifiedjob', u'app_label': 'main'}(class)s_created+", on_delete=django.db.models.deletion.SET_NULL, default=None, editable=False, to=settings.AUTH_USER_MODEL, null=True),
),
migrations.AddField(
model_name='unifiedjob',
name='dependent_jobs',
field=models.ManyToManyField(related_name='_unifiedjob_dependent_jobs_+', editable=False, to='main.UnifiedJob'),
),
migrations.AddField(
model_name='unifiedjob',
name='modified_by',
field=models.ForeignKey(related_name="{u'class': 'unifiedjob', u'app_label': 'main'}(class)s_modified+", on_delete=django.db.models.deletion.SET_NULL, default=None, editable=False, to=settings.AUTH_USER_MODEL, null=True),
),
migrations.AddField(
model_name='unifiedjob',
name='polymorphic_ctype',
field=models.ForeignKey(related_name='polymorphic_main.unifiedjob_set+', editable=False, to='contenttypes.ContentType', null=True),
),
migrations.AddField(
model_name='unifiedjob',
name='schedule',
field=models.ForeignKey(on_delete=django.db.models.deletion.SET_NULL, default=None, editable=False, to='main.Schedule', null=True),
),
migrations.AddField(
model_name='unifiedjob',
name='tags',
field=taggit.managers.TaggableManager(to='taggit.Tag', through='taggit.TaggedItem', blank=True, help_text='A comma-separated list of tags.', verbose_name='Tags'),
),
migrations.AddField(
model_name='unifiedjob',
name='unified_job_template',
field=models.ForeignKey(related_name='unifiedjob_unified_jobs', on_delete=django.db.models.deletion.SET_NULL, default=None, editable=False, to='main.UnifiedJobTemplate', null=True),
),
migrations.AddField(
model_name='schedule',
name='unified_job_template',
field=models.ForeignKey(related_name='schedules', to='main.UnifiedJobTemplate'),
),
migrations.AddField(
model_name='permission',
name='team',
field=models.ForeignKey(related_name='permissions', on_delete=django.db.models.deletion.SET_NULL, blank=True, to='main.Team', null=True),
),
migrations.AddField(
model_name='permission',
name='user',
field=models.ForeignKey(related_name='permissions', on_delete=django.db.models.deletion.SET_NULL, blank=True, to=settings.AUTH_USER_MODEL, null=True),
),
migrations.AddField(
model_name='joborigin',
name='unified_job',
field=models.OneToOneField(related_name='job_origin', to='main.UnifiedJob'),
),
migrations.AddField(
model_name='inventory',
name='organization',
field=models.ForeignKey(related_name='inventories', to='main.Organization', help_text='Organization containing this inventory.'),
),
migrations.AddField(
model_name='inventory',
name='tags',
field=taggit.managers.TaggableManager(to='taggit.Tag', through='taggit.TaggedItem', blank=True, help_text='A comma-separated list of tags.', verbose_name='Tags'),
),
migrations.AddField(
model_name='host',
name='inventory',
field=models.ForeignKey(related_name='hosts', to='main.Inventory'),
),
migrations.AddField(
model_name='host',
name='last_job_host_summary',
field=models.ForeignKey(related_name='hosts_as_last_job_summary+', on_delete=django.db.models.deletion.SET_NULL, default=None, blank=True, editable=False, to='main.JobHostSummary', null=True),
),
migrations.AddField(
model_name='host',
name='modified_by',
field=models.ForeignKey(related_name="{u'class': 'host', u'app_label': 'main'}(class)s_modified+", on_delete=django.db.models.deletion.SET_NULL, default=None, editable=False, to=settings.AUTH_USER_MODEL, null=True),
),
migrations.AddField(
model_name='host',
name='tags',
field=taggit.managers.TaggableManager(to='taggit.Tag', through='taggit.TaggedItem', blank=True, help_text='A comma-separated list of tags.', verbose_name='Tags'),
),
migrations.AddField(
model_name='group',
name='hosts',
field=models.ManyToManyField(help_text='Hosts associated directly with this group.', related_name='groups', to='main.Host', blank=True),
),
migrations.AddField(
model_name='group',
name='inventory',
field=models.ForeignKey(related_name='groups', to='main.Inventory'),
),
migrations.AddField(
model_name='group',
name='modified_by',
field=models.ForeignKey(related_name="{u'class': 'group', u'app_label': 'main'}(class)s_modified+", on_delete=django.db.models.deletion.SET_NULL, default=None, editable=False, to=settings.AUTH_USER_MODEL, null=True),
),
migrations.AddField(
model_name='group',
name='parents',
field=models.ManyToManyField(related_name='children', to='main.Group', blank=True),
),
migrations.AddField(
model_name='group',
name='tags',
field=taggit.managers.TaggableManager(to='taggit.Tag', through='taggit.TaggedItem', blank=True, help_text='A comma-separated list of tags.', verbose_name='Tags'),
),
migrations.AddField(
model_name='custominventoryscript',
name='organization',
field=models.ForeignKey(related_name='custom_inventory_scripts', on_delete=django.db.models.deletion.SET_NULL, to='main.Organization', help_text='Organization owning this inventory script', null=True),
),
migrations.AddField(
model_name='custominventoryscript',
name='tags',
field=taggit.managers.TaggableManager(to='taggit.Tag', through='taggit.TaggedItem', blank=True, help_text='A comma-separated list of tags.', verbose_name='Tags'),
),
migrations.AddField(
model_name='credential',
name='team',
field=models.ForeignKey(related_name='credentials', default=None, blank=True, to='main.Team', null=True),
),
migrations.AddField(
model_name='credential',
name='user',
field=models.ForeignKey(related_name='credentials', default=None, blank=True, to=settings.AUTH_USER_MODEL, null=True),
),
migrations.AddField(
model_name='adhoccommandevent',
name='host',
field=models.ForeignKey(related_name='ad_hoc_command_events', on_delete=django.db.models.deletion.SET_NULL, default=None, editable=False, to='main.Host', null=True),
),
migrations.AddField(
model_name='activitystream',
name='credential',
field=models.ManyToManyField(to='main.Credential', blank=True),
),
migrations.AddField(
model_name='activitystream',
name='custom_inventory_script',
field=models.ManyToManyField(to='main.CustomInventoryScript', blank=True),
),
migrations.AddField(
model_name='activitystream',
name='group',
field=models.ManyToManyField(to='main.Group', blank=True),
),
migrations.AddField(
model_name='activitystream',
name='host',
field=models.ManyToManyField(to='main.Host', blank=True),
),
migrations.AddField(
model_name='activitystream',
name='inventory',
field=models.ManyToManyField(to='main.Inventory', blank=True),
),
migrations.AddField(
model_name='activitystream',
name='organization',
field=models.ManyToManyField(to='main.Organization', blank=True),
),
migrations.AddField(
model_name='activitystream',
name='permission',
field=models.ManyToManyField(to='main.Permission', blank=True),
),
migrations.AddField(
model_name='activitystream',
name='schedule',
field=models.ManyToManyField(to='main.Schedule', blank=True),
),
migrations.AddField(
model_name='activitystream',
name='team',
field=models.ManyToManyField(to='main.Team', blank=True),
),
migrations.AddField(
model_name='activitystream',
name='unified_job',
field=models.ManyToManyField(related_name='_activitystream_unified_job_+', to='main.UnifiedJob', blank=True),
),
migrations.AddField(
model_name='activitystream',
name='unified_job_template',
field=models.ManyToManyField(related_name='_activitystream_unified_job_template_+', to='main.UnifiedJobTemplate', blank=True),
),
migrations.AddField(
model_name='activitystream',
name='user',
field=models.ManyToManyField(to=settings.AUTH_USER_MODEL, blank=True),
),
migrations.AlterUniqueTogether(
name='unifiedjobtemplate',
unique_together=set([('polymorphic_ctype', 'name')]),
),
migrations.AddField(
model_name='team',
name='projects',
field=models.ManyToManyField(related_name='teams', to='main.Project', blank=True),
),
migrations.AddField(
model_name='systemjob',
name='system_job_template',
field=models.ForeignKey(related_name='jobs', on_delete=django.db.models.deletion.SET_NULL, default=None, blank=True, to='main.SystemJobTemplate', null=True),
),
migrations.AddField(
model_name='projectupdate',
name='credential',
field=models.ForeignKey(related_name='projectupdates', on_delete=django.db.models.deletion.SET_NULL, default=None, blank=True, to='main.Credential', null=True),
),
migrations.AddField(
model_name='projectupdate',
name='project',
field=models.ForeignKey(related_name='project_updates', editable=False, to='main.Project'),
),
migrations.AddField(
model_name='project',
name='credential',
field=models.ForeignKey(related_name='projects', on_delete=django.db.models.deletion.SET_NULL, default=None, blank=True, to='main.Credential', null=True),
),
migrations.AddField(
model_name='permission',
name='project',
field=models.ForeignKey(related_name='permissions', on_delete=django.db.models.deletion.SET_NULL, blank=True, to='main.Project', null=True),
),
migrations.AddField(
model_name='organization',
name='projects',
field=models.ManyToManyField(related_name='organizations', to='main.Project', blank=True),
),
migrations.AddField(
model_name='jobtemplate',
name='cloud_credential',
field=models.ForeignKey(related_name='jobtemplates_as_cloud_credential+', on_delete=django.db.models.deletion.SET_NULL, default=None, blank=True, to='main.Credential', null=True),
),
migrations.AddField(
model_name='jobtemplate',
name='credential',
field=models.ForeignKey(related_name='jobtemplates', on_delete=django.db.models.deletion.SET_NULL, default=None, blank=True, to='main.Credential', null=True),
),
migrations.AddField(
model_name='jobtemplate',
name='inventory',
field=models.ForeignKey(related_name='jobtemplates', on_delete=django.db.models.deletion.SET_NULL, to='main.Inventory', null=True),
),
migrations.AddField(
model_name='jobtemplate',
name='project',
field=models.ForeignKey(related_name='jobtemplates', on_delete=django.db.models.deletion.SET_NULL, default=None, blank=True, to='main.Project', null=True),
),
migrations.AddField(
model_name='jobhostsummary',
name='job',
field=models.ForeignKey(related_name='job_host_summaries', editable=False, to='main.Job'),
),
migrations.AddField(
model_name='jobevent',
name='job',
field=models.ForeignKey(related_name='job_events', editable=False, to='main.Job'),
),
migrations.AddField(
model_name='job',
name='cloud_credential',
field=models.ForeignKey(related_name='jobs_as_cloud_credential+', on_delete=django.db.models.deletion.SET_NULL, default=None, blank=True, to='main.Credential', null=True),
),
migrations.AddField(
model_name='job',
name='credential',
field=models.ForeignKey(related_name='jobs', on_delete=django.db.models.deletion.SET_NULL, default=None, blank=True, to='main.Credential', null=True),
),
migrations.AddField(
model_name='job',
name='hosts',
field=models.ManyToManyField(related_name='jobs', editable=False, through='main.JobHostSummary', to='main.Host'),
),
migrations.AddField(
model_name='job',
name='inventory',
field=models.ForeignKey(related_name='jobs', on_delete=django.db.models.deletion.SET_NULL, to='main.Inventory', null=True),
),
migrations.AddField(
model_name='job',
name='job_template',
field=models.ForeignKey(related_name='jobs', on_delete=django.db.models.deletion.SET_NULL, default=None, blank=True, to='main.JobTemplate', null=True),
),
migrations.AddField(
model_name='job',
name='project',
field=models.ForeignKey(related_name='jobs', on_delete=django.db.models.deletion.SET_NULL, default=None, blank=True, to='main.Project', null=True),
),
migrations.AddField(
model_name='inventoryupdate',
name='credential',
field=models.ForeignKey(related_name='inventoryupdates', on_delete=django.db.models.deletion.SET_NULL, default=None, blank=True, to='main.Credential', null=True),
),
migrations.AddField(
model_name='inventoryupdate',
name='inventory_source',
field=models.ForeignKey(related_name='inventory_updates', editable=False, to='main.InventorySource'),
),
migrations.AddField(
model_name='inventoryupdate',
name='source_script',
field=models.ForeignKey(on_delete=django.db.models.deletion.SET_NULL, default=None, blank=True, to='main.CustomInventoryScript', null=True),
),
migrations.AddField(
model_name='inventorysource',
name='credential',
field=models.ForeignKey(related_name='inventorysources', on_delete=django.db.models.deletion.SET_NULL, default=None, blank=True, to='main.Credential', null=True),
),
migrations.AddField(
model_name='inventorysource',
name='group',
field=awx.main.fields.AutoOneToOneField(related_name='inventory_source', null=True, default=None, editable=False, to='main.Group'),
),
migrations.AddField(
model_name='inventorysource',
name='inventory',
field=models.ForeignKey(related_name='inventory_sources', default=None, editable=False, to='main.Inventory', null=True),
),
migrations.AddField(
model_name='inventorysource',
name='source_script',
field=models.ForeignKey(on_delete=django.db.models.deletion.SET_NULL, default=None, blank=True, to='main.CustomInventoryScript', null=True),
),
migrations.AlterUniqueTogether(
name='inventory',
unique_together=set([('name', 'organization')]),
),
migrations.AddField(
model_name='host',
name='inventory_sources',
field=models.ManyToManyField(help_text='Inventory source(s) that created or modified this host.', related_name='hosts', editable=False, to='main.InventorySource'),
),
migrations.AddField(
model_name='host',
name='last_job',
field=models.ForeignKey(related_name='hosts_as_last_job+', on_delete=django.db.models.deletion.SET_NULL, default=None, editable=False, to='main.Job', null=True),
),
migrations.AddField(
model_name='group',
name='inventory_sources',
field=models.ManyToManyField(help_text='Inventory source(s) that created or modified this group.', related_name='groups', editable=False, to='main.InventorySource'),
),
migrations.AlterUniqueTogether(
name='custominventoryscript',
unique_together=set([('name', 'organization')]),
),
migrations.AlterUniqueTogether(
name='credential',
unique_together=set([('user', 'team', 'kind', 'name')]),
),
migrations.AddField(
model_name='adhoccommandevent',
name='ad_hoc_command',
field=models.ForeignKey(related_name='ad_hoc_command_events', editable=False, to='main.AdHocCommand'),
),
migrations.AddField(
model_name='adhoccommand',
name='credential',
field=models.ForeignKey(related_name='ad_hoc_commands', on_delete=django.db.models.deletion.SET_NULL, default=None, to='main.Credential', null=True),
),
migrations.AddField(
model_name='adhoccommand',
name='hosts',
field=models.ManyToManyField(related_name='ad_hoc_commands', editable=False, through='main.AdHocCommandEvent', to='main.Host'),
),
migrations.AddField(
model_name='adhoccommand',
name='inventory',
field=models.ForeignKey(related_name='ad_hoc_commands', on_delete=django.db.models.deletion.SET_NULL, to='main.Inventory', null=True),
),
migrations.AddField(
model_name='activitystream',
name='ad_hoc_command',
field=models.ManyToManyField(to='main.AdHocCommand', blank=True),
),
migrations.AddField(
model_name='activitystream',
name='inventory_source',
field=models.ManyToManyField(to='main.InventorySource', blank=True),
),
migrations.AddField(
model_name='activitystream',
name='inventory_update',
field=models.ManyToManyField(to='main.InventoryUpdate', blank=True),
),
migrations.AddField(
model_name='activitystream',
name='job',
field=models.ManyToManyField(to='main.Job', blank=True),
),
migrations.AddField(
model_name='activitystream',
name='job_template',
field=models.ManyToManyField(to='main.JobTemplate', blank=True),
),
migrations.AddField(
model_name='activitystream',
name='project',
field=models.ManyToManyField(to='main.Project', blank=True),
),
migrations.AddField(
model_name='activitystream',
name='project_update',
field=models.ManyToManyField(to='main.ProjectUpdate', blank=True),
),
migrations.AlterUniqueTogether(
name='team',
unique_together=set([('organization', 'name')]),
),
migrations.AlterUniqueTogether(
name='jobhostsummary',
unique_together=set([('job', 'host_name')]),
),
migrations.AlterUniqueTogether(
name='host',
unique_together=set([('name', 'inventory')]),
),
migrations.AlterUniqueTogether(
name='group',
unique_together=set([('name', 'inventory')]),
),
migrations.AlterUniqueTogether(
name='adhoccommandevent',
unique_together=set([('ad_hoc_command', 'host_name')]),
),
]

View File

@@ -0,0 +1,34 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2016 Ansible, Inc.
# All Rights Reserved.
from __future__ import unicode_literals
from django.db import migrations, models
from django.conf import settings
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('main', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='TowerSettings',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('created', models.DateTimeField(default=None, editable=False)),
('modified', models.DateTimeField(default=None, editable=False)),
('key', models.CharField(unique=True, max_length=255)),
('description', models.TextField()),
('category', models.CharField(max_length=128)),
('value', models.TextField()),
('value_type', models.CharField(max_length=12, choices=[(b'string', 'String'), (b'int', 'Integer'), (b'float', 'Decimal'), (b'json', 'JSON'), (b'bool', 'Boolean'), (b'password', 'Password'), (b'list', 'List')])),
('user', models.ForeignKey(related_name='settings', default=None, editable=False, to=settings.AUTH_USER_MODEL, null=True)),
],
),
]

View File

@@ -1,2 +1,2 @@
# Copyright (c) 2015 Ansible, Inc. # Copyright (c) 2016 Ansible, Inc.
# All Rights Reserved. # All Rights Reserved.

View File

@@ -30,9 +30,6 @@ __all__ = ['AdHocCommand', 'AdHocCommandEvent']
class AdHocCommand(UnifiedJob): class AdHocCommand(UnifiedJob):
MODULE_NAME_CHOICES = [(x,x) for x in tower_settings.AD_HOC_COMMANDS]
MODULE_NAME_DEFAULT = 'command' if 'command' in tower_settings.AD_HOC_COMMANDS else None
class Meta(object): class Meta(object):
app_label = 'main' app_label = 'main'
@@ -61,9 +58,8 @@ class AdHocCommand(UnifiedJob):
) )
module_name = models.CharField( module_name = models.CharField(
max_length=1024, max_length=1024,
default=MODULE_NAME_DEFAULT, default='',
choices=MODULE_NAME_CHOICES, blank=True,
blank=bool(MODULE_NAME_DEFAULT),
) )
module_args = models.TextField( module_args = models.TextField(
blank=True, blank=True,
@@ -88,6 +84,12 @@ class AdHocCommand(UnifiedJob):
through='AdHocCommandEvent', through='AdHocCommandEvent',
) )
def clean_inventory(self):
inv = self.inventory
if not inv or not inv.active:
raise ValidationError('Inventory is no longer available.')
return inv
def clean_credential(self): def clean_credential(self):
cred = self.credential cred = self.credential
if cred and cred.kind != 'ssh': if cred and cred.kind != 'ssh':

View File

@@ -9,7 +9,6 @@ import shlex
import yaml import yaml
# Django # Django
from django.conf import settings
from django.db import models from django.db import models
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
@@ -59,14 +58,14 @@ PERMISSION_TYPE_CHOICES = [
CLOUD_INVENTORY_SOURCES = ['ec2', 'rax', 'vmware', 'gce', 'azure', 'openstack', 'custom'] CLOUD_INVENTORY_SOURCES = ['ec2', 'rax', 'vmware', 'gce', 'azure', 'openstack', 'custom']
VERBOSITY_CHOICES = getattr(settings, 'VERBOSITY_CHOICES', [ VERBOSITY_CHOICES = [
(0, '0 (Normal)'), (0, '0 (Normal)'),
(1, '1 (Verbose)'), (1, '1 (Verbose)'),
(2, '2 (More Verbose)'), (2, '2 (More Verbose)'),
(3, '3 (Debug)'), (3, '3 (Debug)'),
(4, '4 (Connection Debug)'), (4, '4 (Connection Debug)'),
(5, '5 (WinRM Debug)'), (5, '5 (WinRM Debug)'),
]) ]
class VarsDictProperty(object): class VarsDictProperty(object):
@@ -157,16 +156,6 @@ class BaseModel(models.Model):
self.save(update_fields=update_fields) self.save(update_fields=update_fields)
return update_fields return update_fields
def save(self, *args, **kwargs):
# For compatibility with Django 1.4.x, attempt to handle any calls to
# save that pass update_fields.
try:
super(BaseModel, self).save(*args, **kwargs)
except TypeError:
if 'update_fields' not in kwargs:
raise
kwargs.pop('update_fields')
super(BaseModel, self).save(*args, **kwargs)
class CreatedModifiedModel(BaseModel): class CreatedModifiedModel(BaseModel):
''' '''

View File

@@ -6,10 +6,13 @@ import json
# Django # Django
from django.db import models from django.db import models
from django.utils.encoding import smart_text
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
# Tower # Tower
from awx.main.models.base import CreatedModifiedModel from awx.main.models.base import CreatedModifiedModel
class TowerSettings(CreatedModifiedModel): class TowerSettings(CreatedModifiedModel):
class Meta: class Meta:
@@ -53,10 +56,21 @@ class TowerSettings(CreatedModifiedModel):
elif self.value_type == 'list': elif self.value_type == 'list':
converted_type = [x.strip() for x in self.value.split(',')] converted_type = [x.strip() for x in self.value.split(',')]
elif self.value_type == 'bool': elif self.value_type == 'bool':
converted_type = self.value in [True, "true", "True", 1, "1", "yes"] converted_type = smart_text(self.value).lower() in ('true', 'yes', '1')
elif self.value_type == 'string': elif self.value_type == 'string':
converted_type = self.value converted_type = self.value
else: else:
t = __builtins__[self.value_type] t = __builtins__[self.value_type]
converted_type = t(self.value) converted_type = t(self.value)
return converted_type return converted_type
@value_converted.setter
def value_converted(self, value):
if self.value_type == 'json':
self.value = json.dumps(value)
elif self.value_type == 'list':
self.value = ','.join(value)
elif self.value_type == 'bool':
self.value = smart_text(bool(value))
else:
self.value = smart_text(value)

View File

@@ -539,7 +539,7 @@ class Group(CommonModelNameNotUnique):
def mark_actual(): def mark_actual():
all_group_hosts = Group.hosts.through.objects.select_related("host", "group").filter(group__inventory=self.inventory) all_group_hosts = Group.hosts.through.objects.select_related("host", "group").filter(group__inventory=self.inventory)
group_hosts = {'groups': {}, 'hosts': {}} group_hosts = {'groups': {}, 'hosts': {}}
all_group_parents = Group.parents.through.objects.select_related("parent", "group").filter(from_group__inventory=self.inventory) all_group_parents = Group.parents.through.objects.select_related("from_group", "to_group").filter(from_group__inventory=self.inventory)
group_children = {} group_children = {}
group_parents = {} group_parents = {}
marked_hosts = [] marked_hosts = []

View File

@@ -317,3 +317,10 @@ def user_mark_inactive(user, save=True):
user.save() user.save()
User.add_to_class('mark_inactive', user_mark_inactive) User.add_to_class('mark_inactive', user_mark_inactive)
# Add get_absolute_url method to User model if not present.
if not hasattr(User, 'get_absolute_url'):
def user_get_absolute_url(user):
return reverse('api:user_detail', args=(user.pk,))
User.add_to_class('get_absolute_url', user_get_absolute_url)

View File

@@ -11,7 +11,7 @@ import urlparse
from django.conf import settings from django.conf import settings
from django.db import models from django.db import models
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.utils.encoding import smart_str from django.utils.encoding import smart_str, smart_text
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.utils.timezone import now, make_aware, get_default_timezone from django.utils.timezone import now, make_aware, get_default_timezone
@@ -181,7 +181,7 @@ class ProjectOptions(models.Model):
# Filter files in a tasks subdirectory. # Filter files in a tasks subdirectory.
if 'tasks' in playbook.split(os.sep): if 'tasks' in playbook.split(os.sep):
continue continue
results.append(playbook) results.append(smart_text(playbook))
return sorted(results, key=lambda x: smart_str(x).lower()) return sorted(results, key=lambda x: smart_str(x).lower())

View File

@@ -47,7 +47,7 @@ class ScheduleManager(ScheduleFilterMethods, models.Manager):
use_for_related_objects = True use_for_related_objects = True
def get_query_set(self): def get_queryset(self):
return ScheduleQuerySet(self.model, using=self._db) return ScheduleQuerySet(self.model, using=self._db)

View File

@@ -8,13 +8,13 @@ import logging
import re import re
import os import os
import os.path import os.path
from collections import OrderedDict
from StringIO import StringIO from StringIO import StringIO
# Django # Django
from django.conf import settings from django.conf import settings
from django.db import models from django.db import models
from django.core.exceptions import NON_FIELD_ERRORS from django.core.exceptions import NON_FIELD_ERRORS
from django.utils.datastructures import SortedDict
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.utils.timezone import now from django.utils.timezone import now
@@ -77,7 +77,7 @@ class UnifiedJobTemplate(PolymorphicModel, CommonModelNameNotUnique):
('updating', _('Updating')), # Same as running. ('updating', _('Updating')), # Same as running.
] ]
ALL_STATUS_CHOICES = SortedDict(PROJECT_STATUS_CHOICES + INVENTORY_SOURCE_STATUS_CHOICES + JOB_TEMPLATE_STATUS_CHOICES + DEPRECATED_STATUS_CHOICES).items() ALL_STATUS_CHOICES = OrderedDict(PROJECT_STATUS_CHOICES + INVENTORY_SOURCE_STATUS_CHOICES + JOB_TEMPLATE_STATUS_CHOICES + DEPRECATED_STATUS_CHOICES).items()
class Meta: class Meta:
app_label = 'main' app_label = 'main'

Some files were not shown because too many files have changed in this diff Show More