Initial implementation of fact api endpoints

This commit is contained in:
Matthew Jones 2015-05-05 14:47:58 -04:00
parent 2e040e9de3
commit e784595119
7 changed files with 185 additions and 16 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,9 @@ import logging
from dateutil import rrule
from ast import literal_eval
import mongoengine
from rest_framework_mongoengine.serializers import MongoEngineModelSerializer
# PyYAML
import yaml
@ -36,6 +39,8 @@ from awx.main.models import * # noqa
from awx.main.utils import get_type_for_model, get_model_for_type
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.
@ -1537,12 +1542,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 +2013,17 @@ 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):
class Meta:
model = FactVersion
fields = ('module', 'timestamp',)
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(FactSerializer(fact_data).data if fact_data is not None else {})
class HostList(ListCreateAPIView):
@ -986,6 +1009,79 @@ 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 from_spec is not None and 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")
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(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 +1221,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(FactSerializer(fact_data).data if fact_data is not None else {})
class InventoryGroupsList(SubListCreateAttachDetachAPIView):
model = Group

View File

@ -17,4 +17,4 @@ try:
connect(settings.MONGO_DB)
register_key_transform(get_db())
except ConnectionError:
logger.warn('Failed to establish connect to MongDB "%s"' % (settings.MONGO_DB))
logger.warn('Failed to establish connect to MongoDB "%s"' % (settings.MONGO_DB))

View File

@ -78,7 +78,7 @@ class Fact(Document):
}
try:
facts = Fact.objects.filter(**kv)
facts = Fact.objects.filter(**kv).order_by("-timestamp")
if not facts:
return None
return facts[0]
@ -97,7 +97,7 @@ class Fact(Document):
'module': module,
}
return FactVersion.objects.filter(**kv).values_list('timestamp')
return FactVersion.objects.filter(**kv).order_by("-timestamp").values_list('timestamp')
@staticmethod
def get_single_facts(hostnames, fact_key, fact_value, timestamp, module):
@ -126,6 +126,9 @@ class Fact(Document):
}
fields = {
'fact.%s.$' % fact_key : 1,
'host': 1,
'timestamp': 1,
'module': 1,
}
facts = Fact._get_collection().find(kv, fields)
#fact_objs = [Fact(**f) for f in facts]
@ -136,11 +139,10 @@ class Fact(Document):
fact_objs.append(Fact(**f))
return fact_objs
class FactVersion(Document):
timestamp = DateTimeField(required=True)
host = ReferenceField(FactHost, required=True)
module = StringField(max_length=50, required=True)
module = StringField(max_length=50, required=True)
fact = ReferenceField(Fact, required=True)
# TODO: Consider using hashed index on module. django-mongo may not support this but
# executing raw js will
@ -150,4 +152,3 @@ class FactVersion(Document):
'module'
]
}