mirror of
https://github.com/ansible/awx.git
synced 2026-05-20 15:27:47 -02:30
Merge pull request #5955 from chrismeyersfsu/feature-jsonsearch
adds fact recent model
This commit is contained in:
@@ -1157,6 +1157,7 @@ class HostSerializer(BaseSerializerWithVariables):
|
|||||||
ad_hoc_commands = self.reverse('api:host_ad_hoc_commands_list', kwargs={'pk': obj.pk}),
|
ad_hoc_commands = self.reverse('api:host_ad_hoc_commands_list', kwargs={'pk': obj.pk}),
|
||||||
ad_hoc_command_events = self.reverse('api:host_ad_hoc_command_events_list', kwargs={'pk': obj.pk}),
|
ad_hoc_command_events = self.reverse('api:host_ad_hoc_command_events_list', kwargs={'pk': obj.pk}),
|
||||||
fact_versions = self.reverse('api:host_fact_versions_list', kwargs={'pk': obj.pk}),
|
fact_versions = self.reverse('api:host_fact_versions_list', kwargs={'pk': obj.pk}),
|
||||||
|
facts_latest = self.reverse('api:host_facts_latest_list', kwargs={'pk': obj.pk}),
|
||||||
))
|
))
|
||||||
if obj.inventory:
|
if obj.inventory:
|
||||||
res['inventory'] = self.reverse('api:inventory_detail', kwargs={'pk': obj.inventory.pk})
|
res['inventory'] = self.reverse('api:inventory_detail', kwargs={'pk': obj.inventory.pk})
|
||||||
|
|||||||
@@ -114,6 +114,7 @@ host_urls = patterns('awx.api.views',
|
|||||||
#url(r'^(?P<pk>[0-9]+)/single_fact/$', 'host_single_fact_view'),
|
#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_versions/$', 'host_fact_versions_list'),
|
||||||
url(r'^(?P<pk>[0-9]+)/fact_view/$', 'host_fact_compare_view'),
|
url(r'^(?P<pk>[0-9]+)/fact_view/$', 'host_fact_compare_view'),
|
||||||
|
url(r'^(?P<pk>[0-9]+)/facts_latest/$', 'host_facts_latest_list'),
|
||||||
)
|
)
|
||||||
|
|
||||||
group_urls = patterns('awx.api.views',
|
group_urls = patterns('awx.api.views',
|
||||||
|
|||||||
@@ -1835,6 +1835,15 @@ class HostFactCompareView(SystemTrackingEnforcementMixin, SubDetailAPIView):
|
|||||||
return Response(self.serializer_class(instance=fact_entry).data)
|
return Response(self.serializer_class(instance=fact_entry).data)
|
||||||
|
|
||||||
|
|
||||||
|
class HostFactsLatestList(SubListAPIView):
|
||||||
|
|
||||||
|
model = FactLatest
|
||||||
|
parent_model = Host
|
||||||
|
relationship = 'facts_latest'
|
||||||
|
serializer_class = FactSerializer
|
||||||
|
new_in_320 = True
|
||||||
|
|
||||||
|
|
||||||
class GroupList(ListCreateAPIView):
|
class GroupList(ListCreateAPIView):
|
||||||
|
|
||||||
model = Group
|
model = Group
|
||||||
|
|||||||
@@ -2264,7 +2264,12 @@ class RoleAccess(BaseAccess):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
class FactLatestAccess(BaseAccess):
|
||||||
|
|
||||||
|
model = FactLatest
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
return FactLatest.objects.distinct()
|
||||||
|
|
||||||
|
|
||||||
register_access(User, UserAccess)
|
register_access(User, UserAccess)
|
||||||
@@ -2299,3 +2304,4 @@ register_access(WorkflowJobTemplateNode, WorkflowJobTemplateNodeAccess)
|
|||||||
register_access(WorkflowJobNode, WorkflowJobNodeAccess)
|
register_access(WorkflowJobNode, WorkflowJobNodeAccess)
|
||||||
register_access(WorkflowJobTemplate, WorkflowJobTemplateAccess)
|
register_access(WorkflowJobTemplate, WorkflowJobTemplateAccess)
|
||||||
register_access(WorkflowJob, WorkflowJobAccess)
|
register_access(WorkflowJob, WorkflowJobAccess)
|
||||||
|
register_access(FactLatest, FactLatestAccess)
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
import json
|
import json
|
||||||
import re
|
import re
|
||||||
import sys
|
import sys
|
||||||
|
import six
|
||||||
from pyparsing import infixNotation, opAssoc, Optional, Literal, CharsNotIn
|
from pyparsing import infixNotation, opAssoc, Optional, Literal, CharsNotIn
|
||||||
|
|
||||||
# Django
|
# Django
|
||||||
@@ -26,6 +27,7 @@ from django.db.models import Q
|
|||||||
|
|
||||||
# Django-JSONField
|
# Django-JSONField
|
||||||
from jsonfield import JSONField as upstream_JSONField
|
from jsonfield import JSONField as upstream_JSONField
|
||||||
|
from jsonbfield.fields import JSONField as upstream_JSONBField
|
||||||
|
|
||||||
# AWX
|
# AWX
|
||||||
from awx.main.models.rbac import batch_role_ancestor_rebuilding, Role
|
from awx.main.models.rbac import batch_role_ancestor_rebuilding, Role
|
||||||
@@ -45,6 +47,23 @@ class JSONField(upstream_JSONField):
|
|||||||
return {}
|
return {}
|
||||||
return super(JSONField, self).from_db_value(value, expression, connection, context)
|
return super(JSONField, self).from_db_value(value, expression, connection, context)
|
||||||
|
|
||||||
|
|
||||||
|
class JSONBField(upstream_JSONBField):
|
||||||
|
def get_db_prep_value(self, value, connection, prepared=False):
|
||||||
|
if connection.vendor == 'sqlite':
|
||||||
|
# sqlite (which we use for tests) does not support jsonb;
|
||||||
|
return json.dumps(value)
|
||||||
|
return super(JSONBField, self).get_db_prep_value(
|
||||||
|
value, connection, prepared
|
||||||
|
)
|
||||||
|
|
||||||
|
def from_db_value(self, value, expression, connection, context):
|
||||||
|
# Work around a bug in django-jsonfield
|
||||||
|
# https://bitbucket.org/schinckel/django-jsonfield/issues/57/cannot-use-in-the-same-project-as-djangos
|
||||||
|
if isinstance(value, six.string_types):
|
||||||
|
return json.loads(value)
|
||||||
|
return value
|
||||||
|
|
||||||
# Based on AutoOneToOneField from django-annoying:
|
# Based on AutoOneToOneField from django-annoying:
|
||||||
# https://bitbucket.org/offline/django-annoying/src/a0de8b294db3/annoying/fields.py
|
# https://bitbucket.org/offline/django-annoying/src/a0de8b294db3/annoying/fields.py
|
||||||
|
|
||||||
@@ -381,8 +400,10 @@ class DynamicFilterField(models.TextField):
|
|||||||
last_v = new_v
|
last_v = new_v
|
||||||
last_kv = new_kv
|
last_kv = new_kv
|
||||||
contains_count += 1
|
contains_count += 1
|
||||||
|
|
||||||
if contains_count > 1:
|
if contains_count == 1 and isinstance(assembled_v, basestring):
|
||||||
|
assembled_v = '"' + assembled_v + '"'
|
||||||
|
elif contains_count > 1:
|
||||||
if type(last_v) is list:
|
if type(last_v) is list:
|
||||||
last_v.append(v)
|
last_v.append(v)
|
||||||
if type(last_v) is dict:
|
if type(last_v) is dict:
|
||||||
|
|||||||
44
awx/main/migrations/0037_v320_fact_recent.py
Normal file
44
awx/main/migrations/0037_v320_fact_recent.py
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Python
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
# Django
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
from psycopg2.extensions import AsIs
|
||||||
|
|
||||||
|
# AWX
|
||||||
|
import awx.main.fields
|
||||||
|
from awx.main.models import FactLatest
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('main', '0036_v311_insights'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='FactLatest',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||||
|
('timestamp', models.DateTimeField(default=None, help_text='Date and time of the corresponding fact scan gathering time.', editable=False)),
|
||||||
|
('module', models.CharField(max_length=128)),
|
||||||
|
('facts', awx.main.fields.JSONBField(default={}, help_text='Arbitrary JSON structure of module facts captured at timestamp for a single host.', blank=True)),
|
||||||
|
('host', models.ForeignKey(related_name='facts_latest', to='main.Host', help_text='Host for the facts that the fact scan captured.')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='fact',
|
||||||
|
name='facts',
|
||||||
|
field=awx.main.fields.JSONBField(default={}, help_text='Arbitrary JSON structure of module facts captured at timestamp for a single host.', blank=True),
|
||||||
|
),
|
||||||
|
migrations.AlterIndexTogether(
|
||||||
|
name='factlatest',
|
||||||
|
index_together=set([('timestamp', 'module', 'host')]),
|
||||||
|
),
|
||||||
|
migrations.RunSQL([("CREATE INDEX fact_latest_facts_default_gin ON %s USING gin"
|
||||||
|
"(facts jsonb_path_ops);", [AsIs(FactLatest._meta.db_table)])],
|
||||||
|
[('DROP INDEX fact_latest_facts_default_gin;', None)]),
|
||||||
|
]
|
||||||
@@ -4,9 +4,40 @@
|
|||||||
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 jsonbfield.fields import JSONField
|
from awx.main.fields import JSONBField
|
||||||
|
|
||||||
__all__ = ('Fact', )
|
__all__ = ('Fact', 'FactLatest')
|
||||||
|
|
||||||
|
|
||||||
|
class FactLatest(models.Model):
|
||||||
|
host = models.ForeignKey(
|
||||||
|
'Host',
|
||||||
|
related_name='facts_latest',
|
||||||
|
db_index=True,
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
help_text=_('Host for the facts that the fact scan captured.'),
|
||||||
|
)
|
||||||
|
timestamp = models.DateTimeField(
|
||||||
|
default=None,
|
||||||
|
editable=False,
|
||||||
|
help_text=_('Date and time of the corresponding fact scan gathering time.')
|
||||||
|
)
|
||||||
|
module = models.CharField(max_length=128)
|
||||||
|
facts = JSONBField(blank=True, default={}, help_text=_('Arbitrary JSON structure of module facts captured at timestamp for a single host.'))
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
app_label = 'main'
|
||||||
|
index_together = [
|
||||||
|
["timestamp", "module", "host"],
|
||||||
|
]
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def add_fact(host_id, module, timestamp, facts):
|
||||||
|
qs = FactLatest.objects.filter(host_id=host_id, module=module)
|
||||||
|
qs.delete()
|
||||||
|
|
||||||
|
fact_obj = FactLatest.objects.create(host_id=host_id, module=module, timestamp=timestamp, facts=facts)
|
||||||
|
return fact_obj
|
||||||
|
|
||||||
|
|
||||||
class Fact(models.Model):
|
class Fact(models.Model):
|
||||||
@@ -26,7 +57,7 @@ class Fact(models.Model):
|
|||||||
help_text=_('Date and time of the corresponding fact scan gathering time.')
|
help_text=_('Date and time of the corresponding fact scan gathering time.')
|
||||||
)
|
)
|
||||||
module = models.CharField(max_length=128)
|
module = models.CharField(max_length=128)
|
||||||
facts = JSONField(blank=True, default={}, help_text=_('Arbitrary JSON structure of module facts captured at timestamp for a single host.'))
|
facts = JSONBField(blank=True, default={}, help_text=_('Arbitrary JSON structure of module facts captured at timestamp for a single host.'))
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
app_label = 'main'
|
app_label = 'main'
|
||||||
@@ -60,6 +91,9 @@ class Fact(models.Model):
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def add_fact(host_id, module, timestamp, facts):
|
def add_fact(host_id, module, timestamp, facts):
|
||||||
|
FactLatest.add_fact(host_id=host_id, module=module, timestamp=timestamp, facts=facts)
|
||||||
|
|
||||||
fact_obj = Fact.objects.create(host_id=host_id, module=module, timestamp=timestamp, facts=facts)
|
fact_obj = Fact.objects.create(host_id=host_id, module=module, timestamp=timestamp, facts=facts)
|
||||||
fact_obj.save()
|
fact_obj.save()
|
||||||
return fact_obj
|
return fact_obj
|
||||||
|
|
||||||
|
|||||||
@@ -59,7 +59,8 @@ class TestDynamicFilterFieldFilterStringToQ():
|
|||||||
('a__b__c[]=3.14', Q(**{ "a__b__c__contains": 3.14})),
|
('a__b__c[]=3.14', Q(**{ "a__b__c__contains": 3.14})),
|
||||||
('a__b__c[]=true', Q(**{ "a__b__c__contains": True})),
|
('a__b__c[]=true', Q(**{ "a__b__c__contains": True})),
|
||||||
('a__b__c[]=false', Q(**{ "a__b__c__contains": False})),
|
('a__b__c[]=false', Q(**{ "a__b__c__contains": False})),
|
||||||
('a__b__c[]="true"', Q(**{ "a__b__c__contains": "true"})),
|
('a__b__c[]="true"', Q(**{ "a__b__c__contains": "\"true\""})),
|
||||||
|
('a__b__c[]="hello world"', Q(**{ "a__b__c__contains": "\"hello world\""})),
|
||||||
('a__b__c[]__d[]="foobar"', Q(**{ "a__b__c__contains": [{"d": ["foobar"]}]})),
|
('a__b__c[]__d[]="foobar"', Q(**{ "a__b__c__contains": [{"d": ["foobar"]}]})),
|
||||||
('a__b__c[]__d="foobar"', Q(**{ "a__b__c__contains": [{"d": "foobar"}]})),
|
('a__b__c[]__d="foobar"', Q(**{ "a__b__c__contains": [{"d": "foobar"}]})),
|
||||||
('a__b__c[]__d__e="foobar"', Q(**{ "a__b__c__contains": [{"d": {"e": "foobar"}}]})),
|
('a__b__c[]__d__e="foobar"', Q(**{ "a__b__c__contains": [{"d": {"e": "foobar"}}]})),
|
||||||
|
|||||||
Reference in New Issue
Block a user