1
0
mirror of https://github.com/ansible/awx.git synced 2024-11-02 18:21:12 +03:00

Initial backend implementation for AC-25, activity stream/audit love

This commit is contained in:
Matthew Jones 2013-11-08 04:39:53 -05:00
parent 49a8de4efe
commit 25704af172
7 changed files with 201 additions and 0 deletions

View File

@ -141,6 +141,11 @@ job_event_urls = patterns('awx.api.views',
url(r'^(?P<pk>[0-9]+)/hosts/$', 'job_event_hosts_list'),
)
# activity_stream_urls = patterns('awx.api.views',
# url(r'^$', 'activity_stream_list'),
# url(r'^(?P<pk>[0-9]+)/$', 'activity_stream_detail'),
# )
v1_urls = patterns('awx.api.views',
url(r'^$', 'api_v1_root_view'),
url(r'^config/$', 'api_v1_config_view'),
@ -162,6 +167,7 @@ v1_urls = patterns('awx.api.views',
url(r'^jobs/', include(job_urls)),
url(r'^job_host_summaries/', include(job_host_summary_urls)),
url(r'^job_events/', include(job_event_urls)),
# url(r'^activity_stream/', include(activity_stream_urls)),
)
urlpatterns = patterns('awx.api.views',

24
awx/main/middleware.py Normal file
View File

@ -0,0 +1,24 @@
from django.conf import settings
from django.db.models.signals import pre_save
from django.utils.functional import curry
from awx.main.models import ActivityStream
class ActvitiyStreamMiddleware(object):
def process_request(self, request):
if hasattr(request, 'user') and hasattr(request.user, 'is_authenticated') and request.user.is_authenticated():
user = request.user
else:
user = None
set_actor = curry(self.set_actor, user)
pre_save.connect(set_actor, sender=ActivityStream, dispatch_uid=(self.__class__, request), weak=False)
def process_response(self, request, response):
pre_save.disconnect(dispatch_uid=(self.__class__, request))
return response
def set_actor(self, user, sender, instance, **kwargs):
if sender == ActivityStream and isinstance(user, settings.AUTH_USER_MODEL) and instance.user is None:
instance.user = user

View File

@ -38,6 +38,7 @@ from djcelery.models import TaskMeta
from awx.lib.compat import slugify
from awx.main.fields import AutoOneToOneField
from awx.main.utils import encrypt_field, decrypt_field
from awx.main.registrar import activity_stream_registrar
__all__ = ['PrimordialModel', 'Organization', 'Team', 'Project',
'ProjectUpdate', 'Credential', 'Inventory', 'Host', 'Group',
@ -2242,6 +2243,31 @@ class AuthToken(models.Model):
def __unicode__(self):
return self.key
class ActivityStream(models.Model):
'''
Model used to describe activity stream (audit) events
'''
OPERATION_CHOICES = [
('create', _('Entity Created')),
('update', _("Entity Updated")),
('delete', _("Entity Deleted")),
('associate', _("Entity Associated with another Entity")),
('disaassociate', _("Entity was Disassociated with another Entity"))
]
user = models.ForeignKey('auth.User', null=True, on_delete=SET_NULL)
operation = models.CharField(max_length=9, choices=OPERATION_CHOICES)
timestamp = models.DateTimeField(auto_now_add=True)
changes = models.TextField(blank=True)
object1_id = models.PositiveIntegerField(db_index=True)
object1_type = models.TextField()
object2_id = models.PositiveIntegerField(db_index=True)
object2_type = models.TextField()
object_relationship_type = models.TextField()
# TODO: reporting (MPD)
# Add mark_inactive method to User model.
@ -2276,3 +2302,20 @@ _PythonSerializer.handle_m2m_field = _new_handle_m2m_field
# Import signal handlers only after models have been defined.
import awx.main.signals
activity_stream_registrar.connect(Organization)
activity_stream_registrar.connect(Inventory)
activity_stream_registrar.connect(Host)
activity_stream_registrar.connect(Group)
activity_stream_registrar.connect(InventorySource)
activity_stream_registrar.connect(InventoryUpdate)
activity_stream_registrar.connect(Credential)
activity_stream_registrar.connect(Team)
activity_stream_registrar.connect(Project)
activity_stream_registrar.connect(ProjectUpdate)
activity_stream_registrar.connect(Permission)
activity_stream_registrar.connect(JobTemplate)
activity_stream_registrar.connect(Job)
activity_stream_registrar.connect(JobHostSummary)
activity_stream_registrar.connect(JobEvent)
activity_stream_registrar.connect(Profile)

34
awx/main/registrar.py Normal file
View File

@ -0,0 +1,34 @@
from django.db.models.signals import pre_save, post_save, post_delete, m2m_changed
from signals import activity_stream_create, activity_stream_update, activity_stream_delete
class ActivityStreamRegistrar(object):
def __init__(self):
self.models = []
def connect(self, model):
#(receiver, sender=model, dispatch_uid=self._dispatch_uid(signal, model))
if model not in self.models:
self.models.append(model)
post_save.connect(activity_stream_create, sender=model, dispatch_uid=self.__class__ + str(model) + "_create")
pre_save.connect(activity_stream_update, sender=model, dispatch_uid=self.__class__ + str(model) + "_update")
post_delete.connect(activity_stream_delete, sender=model, dispatch_uid=self.__class__ + str(model) + "_delete")
for m2mfield in model._meta.many_to_many:
m2m_attr = get_attr(model, m2mfield.name)
m2m_changed.connect(activity_stream_associate, sender=m2m_attr.through,
dispatch_uid=self.__class__ + str(m2m_attr.through) + "_associate")
def disconnect(self, model):
if model in self.models:
post_save.disconnect(dispatch_uid=self.__class__ + str(model) + "_create")
pre_save.disconnect(dispatch_uid=self.__class__ + str(model) + "_update")
post_delete.disconnect(dispatch_uid=self.__class__ + str(model) + "_delete")
self.models.pop(model)
for m2mfield in model._meta.many_to_many:
m2m_attr = get_attr(model, m2mfield.name)
m2m_changed.disconnect(dispatch_uid=self.__class__ + str(m2m_attr.through) + "_associate")
activity_stream_registrar = ActivityStreamRegistrar()

View File

@ -11,6 +11,7 @@ from django.dispatch import receiver
# AWX
from awx.main.models import *
from awx.main.utils import model_instance_diff
__all__ = []
@ -168,3 +169,59 @@ def update_host_last_job_after_job_deleted(sender, **kwargs):
hosts_pks = getattr(instance, '_saved_hosts_pks', [])
for host in Host.objects.filter(pk__in=hosts_pks):
_update_host_last_jhs(host)
# Set via ActivityStreamRegistrar to record activity stream events
def activity_stream_create(sender, instance, created, **kwargs):
if created:
activity_entry = ActivityStream(
operation='create',
object1_id=instance.id,
object1_type=instance.__class__)
activity_entry.save()
def activity_stream_update(sender, instance, **kwargs):
try:
old = sender.objects.get(id=instance.id)
except sender.DoesNotExist:
pass
new = instance
changes = model_instance_diff(old, new)
activity_entry = ActivityStream(
operation='update',
object1_id=instance.id,
object1_type=instance.__class__,
changes=json.dumps(changes))
activity_entry.save()
def activity_stream_delete(sender, instance, **kwargs):
activity_entry = ActivityStream(
operation='delete',
object1_id=instance.id,
object1_type=instance.__class__)
activity_entry.save()
def activity_stream_associate(sender, instance, **kwargs):
if 'pre_add' in kwargs['action'] or 'pre_remove' in kwargs['action']:
if kwargs['action'] == 'pre_add':
action = 'associate'
elif kwargs['action'] == 'pre_remove':
action = 'disassociate'
else:
return
obj1 = instance
obj1_id = obj1.id
obj_rel = str(sender)
for entity_acted in kwargs['pk_set']:
obj2 = entity_acted
obj2_id = entity_acted.id
activity_entry = ActivityStream(
operation=action,
object1_id=obj1_id,
object1_type=obj1.__class__,
object2_id=obj2_id,
object2_type=obj2.__class__,
object_relationship_type=obj_rel)
activity_entry.save()

View File

@ -10,6 +10,9 @@ import subprocess
import sys
import urlparse
# Django
from django.db.models import Model
# Django REST Framework
from rest_framework.exceptions import ParseError, PermissionDenied
@ -219,3 +222,36 @@ def update_scm_url(scm_type, url, username=True, password=True):
new_url = urlparse.urlunsplit([parts.scheme, netloc, parts.path,
parts.query, parts.fragment])
return new_url
def model_instance_diff(old, new):
"""
Calculate the differences between two model instances. One of the instances may be None (i.e., a newly
created model or deleted model). This will cause all fields with a value to have changed (from None).
"""
if not(old is None or isinstance(old, Model)):
raise TypeError('The supplied old instance is not a valid model instance.')
if not(new is None or isinstance(new, Model)):
raise TypeError('The supplied new instance is not a valid model instance.')
diff = {}
if old is not None and new is not None:
fields = set(old._meta.fields + new._meta.fields)
elif old is not None:
fields = set(old._meta.fields)
elif new is not None:
fields = set(new._meta.fields)
else:
fields = set()
for field in fields:
old_value = str(getattr(old, field.name, None))
new_value = str(getattr(new, field.name, None))
if old_value != new_value:
diff[field.name] = (old_value, new_value)
if len(diff) == 0:
diff = None
return diff

View File

@ -105,6 +105,7 @@ TEMPLATE_CONTEXT_PROCESSORS += (
MIDDLEWARE_CLASSES += (
'django.middleware.transaction.TransactionMiddleware',
# Middleware loaded after this point will be subject to transactions.
'awx.main.middleware.ActivityStreamMiddleware'
)
TEMPLATE_DIRS = (