mirror of
https://github.com/ansible/awx.git
synced 2026-04-18 16:30:21 -02:30
add postgres Fact model, update views, tests
* awx.main.models Fact added * view host fact and timeline updated to use new Postgres Fact model instead of Mongo * Removed license set start Mongo logic * added View tests * added Model tests * Removed mongo fact unit tests * point at modified jsonbfield that supports sqlite storage driver * postgresify fact cache receiver * test OPTIONS endpoint * Note: single fact view not implemented yet.
This commit is contained in:
@@ -32,7 +32,8 @@ __all__ = ['APIView', 'GenericAPIView', 'ListAPIView', 'SimpleListAPIView',
|
||||
'SubListCreateAttachDetachAPIView', 'RetrieveAPIView',
|
||||
'RetrieveUpdateAPIView', 'RetrieveDestroyAPIView',
|
||||
'RetrieveUpdateDestroyAPIView', 'DestroyAPIView',
|
||||
'MongoAPIView', 'MongoListAPIView']
|
||||
'SubDetailAPIView',
|
||||
'ParentMixin',]
|
||||
|
||||
logger = logging.getLogger('awx.api.generics')
|
||||
|
||||
@@ -200,28 +201,6 @@ class GenericAPIView(generics.GenericAPIView, APIView):
|
||||
d['settings'] = settings
|
||||
return d
|
||||
|
||||
|
||||
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):
|
||||
@@ -258,7 +237,25 @@ class ListCreateAPIView(ListAPIView, generics.ListCreateAPIView):
|
||||
# Base class for a list view that allows creating new objects.
|
||||
pass
|
||||
|
||||
class SubListAPIView(ListAPIView):
|
||||
class ParentMixin(object):
|
||||
|
||||
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 SubListAPIView(ListAPIView, ParentMixin):
|
||||
# Base class for a read-only sublist view.
|
||||
|
||||
# Subclasses should define at least:
|
||||
@@ -278,22 +275,6 @@ class SubListAPIView(ListAPIView):
|
||||
})
|
||||
return d
|
||||
|
||||
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()
|
||||
|
||||
def get_queryset(self):
|
||||
parent = self.get_parent_object()
|
||||
self.check_parent_access(parent)
|
||||
@@ -430,6 +411,9 @@ class SubListCreateAttachDetachAPIView(SubListCreateAPIView):
|
||||
else:
|
||||
return self.attach(request, *args, **kwargs)
|
||||
|
||||
class SubDetailAPIView(generics.RetrieveAPIView, GenericAPIView, ParentMixin):
|
||||
pass
|
||||
|
||||
class RetrieveAPIView(generics.RetrieveAPIView, GenericAPIView):
|
||||
pass
|
||||
|
||||
|
||||
@@ -10,8 +10,6 @@ from collections import OrderedDict
|
||||
from dateutil import rrule
|
||||
from ast import literal_eval
|
||||
|
||||
from rest_framework_mongoengine.serializers import DocumentSerializer
|
||||
|
||||
# PyYAML
|
||||
import yaml
|
||||
|
||||
@@ -46,8 +44,6 @@ from awx.main.conf import tower_settings
|
||||
from awx.api.license import feature_enabled
|
||||
from awx.api.fields import BooleanNullField, CharNullField, ChoiceNullField, EncryptedPasswordField, VerbatimField
|
||||
|
||||
from awx.fact.models import * # noqa
|
||||
|
||||
logger = logging.getLogger('awx.api.serializers')
|
||||
|
||||
# Fields that should be summarized regardless of object type.
|
||||
@@ -482,18 +478,19 @@ class BaseSerializer(serializers.ModelSerializer):
|
||||
return ret
|
||||
|
||||
|
||||
class BaseFactSerializer(DocumentSerializer):
|
||||
class BaseFactSerializer(BaseSerializer):
|
||||
|
||||
__metaclass__ = BaseSerializerMetaclass
|
||||
|
||||
def get_fields(self):
|
||||
ret = super(BaseFactSerializer, self).get_fields()
|
||||
if 'module' in ret and feature_enabled('system_tracking'):
|
||||
choices = [(o, o.title()) for o in FactVersion.objects.all().only('module').distinct('module')]
|
||||
ret['module'] = serializers.ChoiceField(source='module', choices=choices, read_only=True, required=False)
|
||||
# TODO: the values_list may pull in a LOT of entries before the distinct is called
|
||||
modules = Fact.objects.all().values_list('module', flat=True).distinct()
|
||||
choices = [(o, o.title()) for o in modules]
|
||||
ret['module'] = serializers.ChoiceField(choices=choices, read_only=True, required=False)
|
||||
return ret
|
||||
|
||||
|
||||
class UnifiedJobTemplateSerializer(BaseSerializer):
|
||||
|
||||
class Meta:
|
||||
@@ -2290,28 +2287,31 @@ class AuthTokenSerializer(serializers.Serializer):
|
||||
|
||||
|
||||
class FactVersionSerializer(BaseFactSerializer):
|
||||
related = serializers.SerializerMethodField('get_related')
|
||||
|
||||
class Meta:
|
||||
model = FactVersion
|
||||
fields = ('related', 'module', 'timestamp',)
|
||||
model = Fact
|
||||
fields = ('related', 'module', 'timestamp')
|
||||
read_only_fields = ('*',)
|
||||
|
||||
def get_related(self, obj):
|
||||
host_obj = self.context.get('host_obj')
|
||||
res = {}
|
||||
res = super(FactVersionSerializer, self).get_related(obj)
|
||||
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),
|
||||
))
|
||||
res['fact_view'] = build_url('api:host_fact_compare_view', args=(obj.host.pk,), get=params)
|
||||
return res
|
||||
|
||||
|
||||
class FactSerializer(BaseFactSerializer):
|
||||
|
||||
class Meta:
|
||||
model = Fact
|
||||
depth = 2
|
||||
fields = ('timestamp', 'host', 'module', 'fact')
|
||||
# TODO: Consider adding in host to the fields list ?
|
||||
fields = ('related', 'timestamp', 'module', 'facts', 'id', 'summary_fields', 'host')
|
||||
read_only_fields = ('*',)
|
||||
|
||||
def get_related(self, obj):
|
||||
res = super(FactSerializer, self).get_related(obj)
|
||||
res['host'] = obj.host.get_absolute_url()
|
||||
return res
|
||||
|
||||
|
||||
@@ -91,8 +91,8 @@ host_urls = patterns('awx.api.views',
|
||||
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'),
|
||||
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',
|
||||
|
||||
164
awx/api/views.py
164
awx/api/views.py
@@ -42,9 +42,6 @@ from rest_framework import status
|
||||
from rest_framework_yaml.parsers import YAMLParser
|
||||
from rest_framework_yaml.renderers import YAMLRenderer
|
||||
|
||||
# MongoEngine
|
||||
import mongoengine
|
||||
|
||||
# QSStats
|
||||
import qsstats
|
||||
|
||||
@@ -61,7 +58,6 @@ from awx.main.access import get_user_queryset
|
||||
from awx.main.ha import is_ha_environment
|
||||
from awx.api.authentication import TaskAuthentication, TokenGetAuthentication
|
||||
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.api.license import feature_enabled, feature_exists, LicenseForbids
|
||||
@@ -70,7 +66,6 @@ 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
|
||||
from awx.main.utils import emit_websocket_notification
|
||||
from awx.main.conf import tower_settings
|
||||
|
||||
@@ -250,32 +245,11 @@ class ApiV1ConfigView(APIView):
|
||||
# FIX: Log
|
||||
return Response({"error": "Invalid License"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# Sanity check: If this license includes system tracking, make
|
||||
# sure that we have a valid MongoDB to point to, and complain if
|
||||
# we do not.
|
||||
if ('features' in license_data and 'system_tracking' in license_data['features'] and
|
||||
license_data['features']['system_tracking'] and settings.MONGO_HOST == NotImplemented):
|
||||
return Response({
|
||||
'error': 'This license supports system tracking, which '
|
||||
'requires MongoDB to be installed. Since you are '
|
||||
'running in an HA environment, you will need to '
|
||||
'provide a MongoDB instance. Please re-run the '
|
||||
'installer prior to installing this license.'
|
||||
}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# If the license is valid, write it to disk.
|
||||
if license_data['valid_key']:
|
||||
tower_settings.LICENSE = data_actual
|
||||
|
||||
# Spawn a task to ensure that MongoDB is started (or stopped)
|
||||
# as appropriate, based on whether the license uses it.
|
||||
if license_data['features']['system_tracking']:
|
||||
mongodb_control.delay('start')
|
||||
else:
|
||||
mongodb_control.delay('stop')
|
||||
|
||||
# Done; return the response.
|
||||
return Response(license_data)
|
||||
|
||||
return Response({"error": "Invalid license"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
def delete(self, request):
|
||||
@@ -1125,33 +1099,6 @@ 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):
|
||||
# Sanity check: Does the license allow system tracking?
|
||||
if not feature_enabled('system_tracking'):
|
||||
raise LicenseForbids('Your license does not permit use '
|
||||
'of system tracking.')
|
||||
|
||||
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):
|
||||
|
||||
model = Host
|
||||
@@ -1225,88 +1172,43 @@ class HostActivityStreamList(SubListAPIView):
|
||||
qs = self.request.user.get_queryset(self.model)
|
||||
return qs.filter(Q(host=parent) | Q(inventory=parent.inventory))
|
||||
|
||||
class HostFactVersionsList(MongoListAPIView):
|
||||
class HostFactVersionsList(ListAPIView, ParentMixin):
|
||||
|
||||
model = Fact
|
||||
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)
|
||||
|
||||
if not feature_enabled("system_tracking"):
|
||||
raise LicenseForbids("Your license does not permit use "
|
||||
"of system tracking.")
|
||||
|
||||
host = self.get_parent_object()
|
||||
self.check_parent_access(host)
|
||||
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)
|
||||
|
||||
try:
|
||||
fact_host = FactHost.objects.get(hostname=host.name, inventory_id=host.inventory.pk)
|
||||
except FactHost.DoesNotExist:
|
||||
return None
|
||||
except mongoengine.ConnectionError:
|
||||
return Response(dict(error="System Tracking Database is disabled"), status=status.HTTP_400_BAD_REQUEST)
|
||||
if from_spec:
|
||||
from_spec = dateutil.parser.parse(from_spec)
|
||||
if to_spec:
|
||||
to_spec = dateutil.parser.parse(to_spec)
|
||||
|
||||
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")
|
||||
host_obj = self.get_parent_object()
|
||||
|
||||
return Fact.get_timeline(host_obj.id, module=module_spec, ts_from=from_spec, ts_to=to_spec)
|
||||
|
||||
def list(self, *args, **kwargs):
|
||||
queryset = self.get_queryset() or []
|
||||
try:
|
||||
serializer = FactVersionSerializer(queryset, many=True, context=dict(host_obj=self.get_parent_object()))
|
||||
except mongoengine.ConnectionError:
|
||||
return Response(dict(error="System Tracking Database is disabled"), status=status.HTTP_400_BAD_REQUEST)
|
||||
return Response(dict(results=serializer.data))
|
||||
return Response(dict(results=self.serializer_class(queryset, many=True).data))
|
||||
|
||||
class HostSingleFactView(MongoAPIView):
|
||||
class HostFactCompareView(SubDetailAPIView):
|
||||
|
||||
model = Fact
|
||||
parent_model = Host
|
||||
new_in_220 = True
|
||||
serializer_class = FactSerializer
|
||||
filter_backends = (MongoFilterBackend,)
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
# Sanity check: Does the license allow system tracking?
|
||||
if not feature_enabled('system_tracking'):
|
||||
raise LicenseForbids('Your license does not permit use '
|
||||
'of system tracking.')
|
||||
|
||||
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):
|
||||
def retrieve(self, request, *args, **kwargs):
|
||||
# Sanity check: Does the license allow system tracking?
|
||||
if not feature_enabled('system_tracking'):
|
||||
raise LicenseForbids('Your license does not permit use '
|
||||
@@ -1317,10 +1219,11 @@ class HostFactCompareView(MongoAPIView):
|
||||
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, host_obj.inventory.pk, datetime_actual, module_spec)
|
||||
host_data = FactSerializer(fact_entry).data if fact_entry is not None else {}
|
||||
|
||||
return Response(host_data)
|
||||
fact_entry = Fact.get_host_fact(host_obj.id, module_spec, datetime_actual)
|
||||
if not fact_entry:
|
||||
return Response({'detail': 'Fact not found'}, status=status.HTTP_404_NOT_FOUND)
|
||||
return Response(self.serializer_class(instance=fact_entry).data)
|
||||
|
||||
class GroupList(ListCreateAPIView):
|
||||
|
||||
@@ -1470,33 +1373,6 @@ 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):
|
||||
# Sanity check: Does the license allow system tracking?
|
||||
if not feature_enabled('system_tracking'):
|
||||
raise LicenseForbids('Your license does not permit use '
|
||||
'of system tracking.')
|
||||
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user