Merge branch 'system_tracking_api'

Conflicts:
	awx/fact/tests/models/fact/fact_transform.py
	awx/fact/tests/models/fact/fact_transform_pymongo.py
	awx/main/tests/commands/run_fact_cache_receiver.py
This commit is contained in:
Chris Meyers
2015-05-11 10:17:52 -04:00
33 changed files with 1703 additions and 303 deletions

View File

@@ -19,6 +19,11 @@ from rest_framework.filters import BaseFilterBackend
# Ansible Tower
from awx.main.utils import get_type_for_model, to_python_boolean
class MongoFilterBackend(BaseFilterBackend):
def filter_queryset(self, request, queryset, view):
return queryset
class ActiveOnlyBackend(BaseFilterBackend):
'''
Filter to show only objects where is_active/active is True.
@@ -61,7 +66,7 @@ class TypeFilterBackend(BaseFilterBackend):
queryset = queryset.filter(polymorphic_ctype_id__in=types_pks)
elif model_type in types:
queryset = queryset
else:
else:
queryset = queryset.none()
return queryset
except FieldError, e:

View File

@@ -31,7 +31,8 @@ __all__ = ['APIView', 'GenericAPIView', 'ListAPIView', 'SimpleListAPIView',
'ListCreateAPIView', 'SubListAPIView', 'SubListCreateAPIView',
'SubListCreateAttachDetachAPIView', 'RetrieveAPIView',
'RetrieveUpdateAPIView', 'RetrieveDestroyAPIView',
'RetrieveUpdateDestroyAPIView', 'DestroyAPIView']
'RetrieveUpdateDestroyAPIView', 'DestroyAPIView',
'MongoAPIView', 'MongoListAPIView']
logger = logging.getLogger('awx.api.generics')
@@ -164,7 +165,6 @@ class APIView(views.APIView):
ret['added_in_version'] = added_in_version
return ret
class GenericAPIView(generics.GenericAPIView, APIView):
# Base class for all model-based views.
@@ -195,11 +195,13 @@ class GenericAPIView(generics.GenericAPIView, APIView):
if not hasattr(self, 'format_kwarg'):
self.format_kwarg = 'format'
d = super(GenericAPIView, self).get_description_context()
d.update({
'model_verbose_name': unicode(self.model._meta.verbose_name),
'model_verbose_name_plural': unicode(self.model._meta.verbose_name_plural),
'serializer_fields': self.get_serializer().metadata(),
})
if hasattr(self.model, "_meta"):
if hasattr(self.model._meta, "verbose_name"):
d.update({
'model_verbose_name': unicode(self.model._meta.verbose_name),
'model_verbose_name_plural': unicode(self.model._meta.verbose_name_plural),
})
d.update({'serializer_fields': self.get_serializer().metadata()})
return d
def metadata(self, request):
@@ -252,6 +254,27 @@ class GenericAPIView(generics.GenericAPIView, APIView):
ret['search_fields'] = self.search_fields
return ret
class MongoAPIView(GenericAPIView):
def get_parent_object(self):
parent_filter = {
self.lookup_field: self.kwargs.get(self.lookup_field, None),
}
return get_object_or_404(self.parent_model, **parent_filter)
def check_parent_access(self, parent=None):
parent = parent or self.get_parent_object()
parent_access = getattr(self, 'parent_access', 'read')
if parent_access in ('read', 'delete'):
args = (self.parent_model, parent_access, parent)
else:
args = (self.parent_model, parent_access, parent, None)
if not self.request.user.can_access(*args):
raise PermissionDenied()
class MongoListAPIView(generics.ListAPIView, MongoAPIView):
pass
class SimpleListAPIView(generics.ListAPIView, GenericAPIView):
def get_queryset(self):

View File

@@ -8,6 +8,8 @@ import logging
from dateutil import rrule
from ast import literal_eval
from rest_framework_mongoengine.serializers import MongoEngineModelSerializer
# PyYAML
import yaml
@@ -33,9 +35,11 @@ from polymorphic import PolymorphicModel
# AWX
from awx.main.constants import SCHEDULEABLE_PROVIDERS
from awx.main.models import * # noqa
from awx.main.utils import get_type_for_model, get_model_for_type
from awx.main.utils import get_type_for_model, get_model_for_type, build_url, timestamp_apiformat
from awx.main.redact import REPLACE_STR
from awx.fact.models import * # noqa
logger = logging.getLogger('awx.api.serializers')
# Fields that should be summarized regardless of object type.
@@ -774,6 +778,7 @@ class InventorySerializer(BaseSerializerWithVariables):
activity_stream = reverse('api:inventory_activity_stream_list', args=(obj.pk,)),
scan_job_templates = reverse('api:inventory_scan_job_template_list', args=(obj.pk,)),
ad_hoc_commands = reverse('api:inventory_ad_hoc_commands_list', args=(obj.pk,)),
single_fact = reverse('api:inventory_single_fact_view', args=(obj.pk,)),
))
if obj.organization and obj.organization.active:
res['organization'] = reverse('api:organization_detail', args=(obj.organization.pk,))
@@ -826,6 +831,8 @@ class HostSerializer(BaseSerializerWithVariables):
inventory_sources = reverse('api:host_inventory_sources_list', args=(obj.pk,)),
ad_hoc_commands = reverse('api:host_ad_hoc_commands_list', args=(obj.pk,)),
ad_hoc_command_events = reverse('api:host_ad_hoc_command_events_list', args=(obj.pk,)),
fact_versions = reverse('api:host_fact_versions_list', args=(obj.pk,)),
single_fact = reverse('api:host_single_fact_view', args=(obj.pk,)),
))
if obj.inventory and obj.inventory.active:
res['inventory'] = reverse('api:inventory_detail', args=(obj.inventory.pk,))
@@ -927,6 +934,7 @@ class GroupSerializer(BaseSerializerWithVariables):
activity_stream = reverse('api:group_activity_stream_list', args=(obj.pk,)),
inventory_sources = reverse('api:group_inventory_sources_list', args=(obj.pk,)),
ad_hoc_commands = reverse('api:group_ad_hoc_commands_list', args=(obj.pk,)),
single_fact = reverse('api:group_single_fact_view', args=(obj.pk,)),
))
if obj.inventory and obj.inventory.active:
res['inventory'] = reverse('api:inventory_detail', args=(obj.inventory.pk,))
@@ -1537,12 +1545,10 @@ class JobRelaunchSerializer(JobSerializer):
obj = self.context.get('obj')
if not obj.credential or obj.credential.active is False:
raise serializers.ValidationError(dict(credential=["Credential not found or deleted."]))
if obj.job_type != PERM_INVENTORY_SCAN and (obj.project is None or not obj.project.active):
raise serializers.ValidationError(dict(errors=["Job Template Project is missing or undefined"]))
if obj.inventory is None or not obj.inventory.active:
raise serializers.ValidationError(dict(errors=["Job Template Inventory is missing or undefined"]))
return attrs
class AdHocCommandSerializer(UnifiedJobSerializer):
@@ -2010,3 +2016,30 @@ class AuthTokenSerializer(serializers.Serializer):
raise serializers.ValidationError('Unable to login with provided credentials.')
else:
raise serializers.ValidationError('Must include "username" and "password"')
class FactVersionSerializer(MongoEngineModelSerializer):
related = serializers.SerializerMethodField('get_related')
class Meta:
model = FactVersion
fields = ('related', 'module', 'timestamp',)
def get_related(self, obj):
host_obj = self.context.get('host_obj')
res = {}
params = {
'datetime': timestamp_apiformat(obj.timestamp),
'module': obj.module,
}
res.update(dict(
fact_view = build_url('api:host_fact_compare_view', args=(host_obj.pk,), get=params),
))
return res
class FactSerializer(MongoEngineModelSerializer):
class Meta:
model = Fact
depth = 2
fields = ('timestamp', 'host', 'module', 'fact')

View File

@@ -75,6 +75,7 @@ inventory_urls = patterns('awx.api.views',
url(r'^(?P<pk>[0-9]+)/activity_stream/$', 'inventory_activity_stream_list'),
url(r'^(?P<pk>[0-9]+)/scan_job_templates/$', 'inventory_scan_job_template_list'),
url(r'^(?P<pk>[0-9]+)/ad_hoc_commands/$', 'inventory_ad_hoc_commands_list'),
url(r'^(?P<pk>[0-9]+)/single_fact/$', 'inventory_single_fact_view'),
)
host_urls = patterns('awx.api.views',
@@ -89,6 +90,9 @@ host_urls = patterns('awx.api.views',
url(r'^(?P<pk>[0-9]+)/inventory_sources/$', 'host_inventory_sources_list'),
url(r'^(?P<pk>[0-9]+)/ad_hoc_commands/$', 'host_ad_hoc_commands_list'),
url(r'^(?P<pk>[0-9]+)/ad_hoc_command_events/$', 'host_ad_hoc_command_events_list'),
url(r'^(?P<pk>[0-9]+)/single_fact/$', 'host_single_fact_view'),
url(r'^(?P<pk>[0-9]+)/fact_versions/$', 'host_fact_versions_list'),
url(r'^(?P<pk>[0-9]+)/fact_view/$', 'host_fact_compare_view'),
)
group_urls = patterns('awx.api.views',
@@ -104,6 +108,7 @@ group_urls = patterns('awx.api.views',
url(r'^(?P<pk>[0-9]+)/activity_stream/$', 'group_activity_stream_list'),
url(r'^(?P<pk>[0-9]+)/inventory_sources/$', 'group_inventory_sources_list'),
url(r'^(?P<pk>[0-9]+)/ad_hoc_commands/$', 'group_ad_hoc_commands_list'),
url(r'^(?P<pk>[0-9]+)/single_fact/$', 'group_single_fact_view'),
)
inventory_source_urls = patterns('awx.api.views',

View File

@@ -46,6 +46,7 @@ from awx.main.access import get_user_queryset
from awx.main.ha import is_ha_environment
from awx.api.authentication import TaskAuthentication
from awx.api.utils.decorators import paginated
from awx.api.filters import MongoFilterBackend
from awx.api.generics import get_view_name
from awx.api.generics import * # noqa
from awx.main.models import * # noqa
@@ -53,6 +54,7 @@ from awx.main.utils import * # noqa
from awx.api.permissions import * # noqa
from awx.api.renderers import * # noqa
from awx.api.serializers import * # noqa
from awx.fact.models import * # noqa
def api_exception_handler(exc):
'''
@@ -922,6 +924,27 @@ class InventoryScanJobTemplateList(SubListAPIView):
qs = self.request.user.get_queryset(self.model)
return qs.filter(job_type=PERM_INVENTORY_SCAN, inventory=parent)
class InventorySingleFactView(MongoAPIView):
model = Fact
parent_model = Inventory
new_in_220 = True
serializer_class = FactSerializer
filter_backends = (MongoFilterBackend,)
def get(self, request, *args, **kwargs):
fact_key = request.QUERY_PARAMS.get("fact_key", None)
fact_value = request.QUERY_PARAMS.get("fact_value", None)
datetime_spec = request.QUERY_PARAMS.get("timestamp", None)
module_spec = request.QUERY_PARAMS.get("module", 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)
datetime_actual = dateutil.parser.parse(datetime_spec) if datetime_spec is not None else now()
inventory_obj = self.get_parent_object()
fact_data = Fact.get_single_facts([h.name for h in inventory_obj.hosts.all()], fact_key, fact_value, datetime_actual, module_spec)
return Response(dict(results=FactSerializer(fact_data).data if fact_data is not None else []))
class HostList(ListCreateAPIView):
@@ -986,6 +1009,83 @@ class HostActivityStreamList(SubListAPIView):
qs = self.request.user.get_queryset(self.model)
return qs.filter(Q(host=parent) | Q(inventory=parent.inventory))
class HostFactVersionsList(MongoListAPIView):
serializer_class = FactVersionSerializer
parent_model = Host
new_in_220 = True
filter_backends = (MongoFilterBackend,)
def get_queryset(self):
from_spec = self.request.QUERY_PARAMS.get('from', None)
to_spec = self.request.QUERY_PARAMS.get('to', None)
module_spec = self.request.QUERY_PARAMS.get('module', None)
host = self.get_parent_object()
self.check_parent_access(host)
try:
fact_host = FactHost.objects.get(hostname=host.name)
except FactHost.DoesNotExist:
return None
kv = {
'host': fact_host.id,
}
if module_spec is not None:
kv['module'] = module_spec
if from_spec is not None:
from_actual = dateutil.parser.parse(from_spec)
kv['timestamp__gt'] = from_actual
if to_spec is not None:
to_actual = dateutil.parser.parse(to_spec)
kv['timestamp__lte'] = to_actual
return FactVersion.objects.filter(**kv).order_by("-timestamp")
def list(self, *args, **kwargs):
queryset = self.get_queryset() or []
serializer = FactVersionSerializer(queryset, many=True, context=dict(host_obj=self.get_parent_object()))
return Response(dict(results=serializer.data))
class HostSingleFactView(MongoAPIView):
model = Fact
parent_model = Host
new_in_220 = True
serializer_class = FactSerializer
filter_backends = (MongoFilterBackend,)
def get(self, request, *args, **kwargs):
fact_key = request.QUERY_PARAMS.get("fact_key", None)
fact_value = request.QUERY_PARAMS.get("fact_value", None)
datetime_spec = request.QUERY_PARAMS.get("timestamp", None)
module_spec = request.QUERY_PARAMS.get("module", 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)
datetime_actual = dateutil.parser.parse(datetime_spec) if datetime_spec is not None else now()
host_obj = self.get_parent_object()
fact_data = Fact.get_single_facts([host_obj.name], fact_key, fact_value, datetime_actual, module_spec)
return Response(dict(results=FactSerializer(fact_data).data if fact_data is not None else []))
class HostFactCompareView(MongoAPIView):
new_in_220 = True
parent_model = Host
serializer_class = FactSerializer
filter_backends = (MongoFilterBackend,)
def get(self, request, *args, **kwargs):
datetime_spec = request.QUERY_PARAMS.get('datetime', None)
module_spec = request.QUERY_PARAMS.get('module', "ansible")
datetime_actual = dateutil.parser.parse(datetime_spec) if datetime_spec is not None else now()
host_obj = self.get_parent_object()
fact_entry = Fact.get_host_version(host_obj.name, datetime_actual, module_spec)
host_data = FactSerializer(fact_entry).data if fact_entry is not None else {}
return Response(host_data)
class GroupList(ListCreateAPIView):
@@ -1125,6 +1225,28 @@ class GroupDetail(RetrieveUpdateDestroyAPIView):
obj.mark_inactive_recursive()
return Response(status=status.HTTP_204_NO_CONTENT)
class GroupSingleFactView(MongoAPIView):
model = Fact
parent_model = Group
new_in_220 = True
serializer_class = FactSerializer
filter_backends = (MongoFilterBackend,)
def get(self, request, *args, **kwargs):
fact_key = request.QUERY_PARAMS.get("fact_key", None)
fact_value = request.QUERY_PARAMS.get("fact_value", None)
datetime_spec = request.QUERY_PARAMS.get("timestamp", None)
module_spec = request.QUERY_PARAMS.get("module", 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)
datetime_actual = dateutil.parser.parse(datetime_spec) if datetime_spec is not None else now()
group_obj = self.get_parent_object()
fact_data = Fact.get_single_facts([h.name for h in group_obj.hosts.all()], fact_key, fact_value, datetime_actual, module_spec)
return Response(dict(results=FactSerializer(fact_data).data if fact_data is not None else []))
class InventoryGroupsList(SubListCreateAttachDetachAPIView):
model = Group