From fedd1cf22f603c38e66e8d7f9265027ee3e22a05 Mon Sep 17 00:00:00 2001 From: AlanCoding Date: Tue, 14 May 2019 11:10:31 -0400 Subject: [PATCH] Replace JobOrigin with ActivityStream.action_node --- awx/api/serializers.py | 2 +- .../0080_v360_replace_job_origin.py | 31 ++++++++++++ awx/main/models/__init__.py | 2 +- awx/main/models/activity_stream.py | 19 +++++++- awx/main/models/ha.py | 48 +------------------ .../functional/models/test_activity_stream.py | 19 ++++++++ 6 files changed, 71 insertions(+), 50 deletions(-) create mode 100644 awx/main/migrations/0080_v360_replace_job_origin.py diff --git a/awx/api/serializers.py b/awx/api/serializers.py index 07699a4268..ad001e44cc 100644 --- a/awx/api/serializers.py +++ b/awx/api/serializers.py @@ -4984,7 +4984,7 @@ class ActivityStreamSerializer(BaseSerializer): class Meta: model = ActivityStream fields = ('*', '-name', '-description', '-created', '-modified', 'timestamp', 'operation', - 'changes', 'object1', 'object2', 'object_association', 'object_type') + 'changes', 'object1', 'object2', 'object_association', 'action_node', 'object_type') def get_fields(self): ret = super(ActivityStreamSerializer, self).get_fields() diff --git a/awx/main/migrations/0080_v360_replace_job_origin.py b/awx/main/migrations/0080_v360_replace_job_origin.py new file mode 100644 index 0000000000..05e76316f6 --- /dev/null +++ b/awx/main/migrations/0080_v360_replace_job_origin.py @@ -0,0 +1,31 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.20 on 2019-05-14 14:51 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('main', '0079_v360_rm_implicit_oauth2_apps'), + ] + + operations = [ + migrations.RemoveField( + model_name='joborigin', + name='instance', + ), + migrations.RemoveField( + model_name='joborigin', + name='unified_job', + ), + migrations.AddField( + model_name='activitystream', + name='action_node', + field=models.CharField(blank=True, default='', editable=False, help_text='The cluster node the activity took place on.', max_length=512), + ), + migrations.DeleteModel( + name='JobOrigin', + ), + ] diff --git a/awx/main/models/__init__.py b/awx/main/models/__init__.py index 337e44bf42..c105dd4efc 100644 --- a/awx/main/models/__init__.py +++ b/awx/main/models/__init__.py @@ -35,7 +35,7 @@ from awx.main.models.ad_hoc_commands import AdHocCommand # noqa from awx.main.models.schedules import Schedule # noqa from awx.main.models.activity_stream import ActivityStream # noqa from awx.main.models.ha import ( # noqa - Instance, InstanceGroup, JobOrigin, TowerScheduleState, + Instance, InstanceGroup, TowerScheduleState, ) from awx.main.models.rbac import ( # noqa Role, batch_role_ancestor_rebuilding, get_roles_on_resource, diff --git a/awx/main/models/activity_stream.py b/awx/main/models/activity_stream.py index 60d79c3072..bcc2ab20ef 100644 --- a/awx/main/models/activity_stream.py +++ b/awx/main/models/activity_stream.py @@ -7,6 +7,7 @@ from awx.main.fields import JSONField # Django from django.db import models +from django.conf import settings from django.utils.encoding import smart_str from django.utils.translation import ugettext_lazy as _ @@ -35,6 +36,13 @@ class ActivityStream(models.Model): timestamp = models.DateTimeField(auto_now_add=True) changes = models.TextField(blank=True) deleted_actor = JSONField(null=True) + action_node = models.CharField( + blank=True, + default='', + editable=False, + max_length=512, + help_text=_("The cluster node the activity took place on."), + ) object_relationship_type = models.TextField(blank=True) object1 = models.TextField() @@ -78,7 +86,13 @@ class ActivityStream(models.Model): def __str__(self): operation = self.operation if 'operation' in self.__dict__ else '_delayed_' - timestamp = self.timestamp.isoformat() if 'timestamp' in self.__dict__ else '_delayed_' + if 'timestamp' in self.__dict__: + if self.timestamp: + timestamp = self.timestamp.isoformat() + else: + timestamp = self.timestamp + else: + timestamp = '_delayed_' return u'%s-%s-pk=%s' % (operation, timestamp, self.pk) def get_absolute_url(self, request=None): @@ -97,4 +111,7 @@ class ActivityStream(models.Model): if 'update_fields' in kwargs and 'deleted_actor' not in kwargs['update_fields']: kwargs['update_fields'].append('deleted_actor') + hostname_char_limit = self._meta.get_field('action_node').max_length + self.action_node = settings.CLUSTER_HOST_ID[:hostname_char_limit] + super(ActivityStream, self).save(*args, **kwargs) diff --git a/awx/main/models/ha.py b/awx/main/models/ha.py index c2fa368f11..15c2241c4f 100644 --- a/awx/main/models/ha.py +++ b/awx/main/models/ha.py @@ -19,14 +19,11 @@ from awx.api.versioning import reverse from awx.main.managers import InstanceManager, InstanceGroupManager from awx.main.fields import JSONField from awx.main.models.base import BaseModel, HasEditsMixin -from awx.main.models.inventory import InventoryUpdate -from awx.main.models.jobs import Job -from awx.main.models.projects import ProjectUpdate from awx.main.models.unified_jobs import UnifiedJob from awx.main.utils import get_cpu_capacity, get_mem_capacity, get_system_task_capacity from awx.main.models.mixins import RelatedJobsMixin -__all__ = ('Instance', 'InstanceGroup', 'JobOrigin', 'TowerScheduleState', 'TowerAnalyticsState') +__all__ = ('Instance', 'InstanceGroup', 'TowerScheduleState', 'TowerAnalyticsState') class HasPolicyEditsMixin(HasEditsMixin): @@ -266,24 +263,6 @@ class TowerAnalyticsState(SingletonModel): last_run = models.DateTimeField(auto_now_add=True) -class JobOrigin(models.Model): - """A model representing the relationship between a unified job and - the instance that was responsible for starting that job. - - It may be possible that a job has no origin (the common reason for this - being that the job was started on Tower < 2.1 before origins were a thing). - This is fine, and code should be able to handle it. A job with no origin - is always assumed to *not* have the current instance as its origin. - """ - unified_job = models.OneToOneField(UnifiedJob, related_name='job_origin', on_delete=models.CASCADE) - instance = models.ForeignKey(Instance, on_delete=models.CASCADE) - created = models.DateTimeField(auto_now_add=True) - modified = models.DateTimeField(auto_now=True) - - class Meta: - app_label = 'main' - - def schedule_policy_task(): from awx.main.tasks import apply_cluster_membership_policies connection.on_commit(lambda: apply_cluster_membership_policies.apply_async()) @@ -311,31 +290,6 @@ def on_instance_deleted(sender, instance, using, **kwargs): schedule_policy_task() -# Unfortunately, the signal can't just be connected against UnifiedJob; it -# turns out that creating a model's subclass doesn't fire the signal for the -# superclass model. -@receiver(post_save, sender=InventoryUpdate) -@receiver(post_save, sender=Job) -@receiver(post_save, sender=ProjectUpdate) -def on_job_create(sender, instance, created=False, raw=False, **kwargs): - """When a new job is created, save a record of its origin (the machine - that started the job). - """ - # Sanity check: We only want to create a JobOrigin record in cases where - # we are making a new record, and in normal situations. - # - # In other situations, we simply do nothing. - if raw or not created: - return - - # Create the JobOrigin record, which attaches to the current instance - # (which started the job). - job_origin, new = JobOrigin.objects.get_or_create( - instance=Instance.objects.me(), - unified_job=instance, - ) - - class UnifiedJobTemplateInstanceGroupMembership(models.Model): unifiedjobtemplate = models.ForeignKey( diff --git a/awx/main/tests/functional/models/test_activity_stream.py b/awx/main/tests/functional/models/test_activity_stream.py index d218d18a18..4ea6a83ab0 100644 --- a/awx/main/tests/functional/models/test_activity_stream.py +++ b/awx/main/tests/functional/models/test_activity_stream.py @@ -277,3 +277,22 @@ def test_saved_passwords_hidden_activity(workflow_job_template, job_template_wit changes = json.loads(entry.changes) assert 'survey_passwords' not in changes assert json.loads(changes['extra_data'])['bbbb'] == '$encrypted$' + + +@pytest.mark.django_db +def test_cluster_node_recorded(inventory, project): + jt = JobTemplate.objects.create(name='testjt', inventory=inventory, project=project) + with mock.patch('awx.main.models.activity_stream.settings.CLUSTER_HOST_ID', 'foo_host'): + job = jt.create_unified_job() + entry = ActivityStream.objects.filter(job=job).first() + assert entry.action_node == 'foo_host' + + +@pytest.mark.django_db +def test_cluster_node_long_node_name(inventory, project): + jt = JobTemplate.objects.create(name='testjt', inventory=inventory, project=project) + with mock.patch('awx.main.models.activity_stream.settings.CLUSTER_HOST_ID', 'f' * 700): + job = jt.create_unified_job() + # node name is very long, we just want to make sure it does not error + entry = ActivityStream.objects.filter(job=job).first() + assert entry.action_node.startswith('ffffff')