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:
parent
2c4d583f3e
commit
cdccfee93a
@ -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
24
awx/main/middleware.py
Normal 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
|
@ -27,3 +27,20 @@ User.add_to_class('can_access', check_user_access)
|
||||
|
||||
# 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)
|
||||
|
@ -333,3 +333,28 @@ class CommonTask(PrimordialModel):
|
||||
self.cancel_flag = True
|
||||
self.save(update_fields=['cancel_flag'])
|
||||
return self.cancel_flag
|
||||
|
||||
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()
|
||||
|
34
awx/main/registrar.py
Normal file
34
awx/main/registrar.py
Normal 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()
|
@ -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()
|
||||
|
@ -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
|
||||
|
@ -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 = (
|
||||
|
Loading…
Reference in New Issue
Block a user