From 87fccb9f4509148f5bc4f2dc723d5c2a5aba87b2 Mon Sep 17 00:00:00 2001 From: Jake McDermott Date: Fri, 29 Jun 2018 12:41:32 -0400 Subject: [PATCH 1/4] fix status list for client-generated status counts --- awx/ui/client/features/output/status.service.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/awx/ui/client/features/output/status.service.js b/awx/ui/client/features/output/status.service.js index d387f8db41..4ad8432d1a 100644 --- a/awx/ui/client/features/output/status.service.js +++ b/awx/ui/client/features/output/status.service.js @@ -7,6 +7,7 @@ const TASK_START = 'playbook_on_task_start'; const HOST_STATUS_KEYS = ['dark', 'failures', 'changed', 'ok', 'skipped']; const COMPLETE = ['successful', 'failed']; const INCOMPLETE = ['canceled', 'error']; +const UNSUCCESSFUL = ['failed'].concat(INCOMPLETE); const FINISHED = COMPLETE.concat(INCOMPLETE); function JobStatusService (moment, message) { @@ -61,12 +62,12 @@ function JobStatusService (moment, message) { }; this.createHostStatusCounts = status => { - if (_.includes(COMPLETE, status)) { - return { ok: 1 }; + if (UNSUCCESSFUL.includes(status)) { + return { failures: 1 }; } - if (_.includes(INCOMPLETE, status)) { - return { failures: 1 }; + if (COMPLETE.includes(status)) { + return { ok: 1 }; } return null; From 380bf77b639f0f27c8f98c26f9dc117af6aa8c02 Mon Sep 17 00:00:00 2001 From: Jake McDermott Date: Mon, 9 Jul 2018 09:31:35 -0400 Subject: [PATCH 2/4] move host_status_counts logic into event model --- awx/api/serializers.py | 38 +++++++------------------------------- awx/main/models/events.py | 19 +++++++++++++++++++ 2 files changed, 26 insertions(+), 31 deletions(-) diff --git a/awx/api/serializers.py b/awx/api/serializers.py index e5da287c1e..c76c75839f 100644 --- a/awx/api/serializers.py +++ b/awx/api/serializers.py @@ -9,7 +9,7 @@ import operator import re import six import urllib -from collections import defaultdict, OrderedDict +from collections import OrderedDict from datetime import timedelta # OAuth2 @@ -1474,23 +1474,11 @@ class ProjectUpdateDetailSerializer(ProjectUpdateSerializer): def get_host_status_counts(self, obj): try: - event_data = obj.project_update_events.only('event_data').get(event='playbook_on_stats').event_data + counts = obj.project_update_events.only('event_data').get(event='playbook_on_stats').get_host_status_counts() except ProjectUpdateEvent.DoesNotExist: - event_data = {} + counts = {} - host_status = {} - host_status_keys = ['skipped', 'ok', 'changed', 'failures', 'dark'] - - for key in host_status_keys: - for host in event_data.get(key, {}): - host_status[host] = key - - host_status_counts = defaultdict(lambda: 0) - - for value in host_status.values(): - host_status_counts[value] += 1 - - return host_status_counts + return counts class ProjectUpdateListSerializer(ProjectUpdateSerializer, UnifiedJobListSerializer): @@ -3271,23 +3259,11 @@ class JobDetailSerializer(JobSerializer): def get_host_status_counts(self, obj): try: - event_data = obj.job_events.only('event_data').get(event='playbook_on_stats').event_data + counts = obj.job_events.only('event_data').get(event='playbook_on_stats').get_host_status_counts() except JobEvent.DoesNotExist: - event_data = {} + counts = {} - host_status = {} - host_status_keys = ['skipped', 'ok', 'changed', 'failures', 'dark'] - - for key in host_status_keys: - for host in event_data.get(key, {}): - host_status[host] = key - - host_status_counts = defaultdict(lambda: 0) - - for value in host_status.values(): - host_status_counts[value] += 1 - - return host_status_counts + return counts class JobCancelSerializer(BaseSerializer): diff --git a/awx/main/models/events.py b/awx/main/models/events.py index a6e2c67c74..8b8637d8f2 100644 --- a/awx/main/models/events.py +++ b/awx/main/models/events.py @@ -1,5 +1,6 @@ import datetime import logging +from collections import defaultdict from django.conf import settings from django.db import models, DatabaseError @@ -39,6 +40,21 @@ def sanitize_event_keys(kwargs, valid_keys): kwargs[key] = Truncator(kwargs[key]).chars(1024) +def create_host_status_counts(event_data): + host_status = {} + host_status_keys = ['skipped', 'ok', 'changed', 'failures', 'dark'] + + for key in host_status_keys: + for host in event_data.get(key, {}): + host_status[host] = key + + host_status_counts = defaultdict(lambda: 0) + + for value in host_status.values(): + host_status_counts[value] += 1 + + return dict(host_status_counts) + class BasePlaybookEvent(CreatedModifiedModel): ''' @@ -194,6 +210,9 @@ class BasePlaybookEvent(CreatedModifiedModel): def event_level(self): return self.LEVEL_FOR_EVENT.get(self.event, 0) + def get_host_status_counts(self): + return create_host_status_counts(getattr(self, 'event_data', {})) + def get_event_display2(self): msg = self.get_event_display() if self.event == 'playbook_on_play_start': From 1b1df6341592d00fa0a415618d4831fccf88a428 Mon Sep 17 00:00:00 2001 From: Jake McDermott Date: Mon, 9 Jul 2018 09:39:09 -0400 Subject: [PATCH 3/4] add host_status_counts to adhoc command event --- awx/api/serializers.py | 19 +++++++++++++++++++ awx/api/views.py | 2 +- awx/main/models/events.py | 3 +++ 3 files changed, 23 insertions(+), 1 deletion(-) diff --git a/awx/api/serializers.py b/awx/api/serializers.py index c76c75839f..6772aa89e0 100644 --- a/awx/api/serializers.py +++ b/awx/api/serializers.py @@ -3443,6 +3443,25 @@ class AdHocCommandSerializer(UnifiedJobSerializer): return vars_validate_or_raise(value) +class AdHocCommandDetailSerializer(AdHocCommandSerializer): + + host_status_counts = serializers.SerializerMethodField( + help_text=_('A count of hosts uniquely assigned to each status.'), + ) + + class Meta: + model = AdHocCommand + fields = ('*', 'host_status_counts',) + + def get_host_status_counts(self, obj): + try: + counts = obj.ad_hoc_command_events.only('event_data').get(event='playbook_on_stats').get_host_status_counts() + except AdHocCommandEvent.DoesNotExist: + counts = {} + + return counts + + class AdHocCommandCancelSerializer(AdHocCommandSerializer): can_cancel = serializers.BooleanField(read_only=True) diff --git a/awx/api/views.py b/awx/api/views.py index bc6a785002..379f8c6926 100644 --- a/awx/api/views.py +++ b/awx/api/views.py @@ -4553,7 +4553,7 @@ class HostAdHocCommandsList(AdHocCommandList, SubListCreateAPIView): class AdHocCommandDetail(UnifiedJobDeletionMixin, RetrieveDestroyAPIView): model = AdHocCommand - serializer_class = AdHocCommandSerializer + serializer_class = AdHocCommandDetailSerializer class AdHocCommandCancel(RetrieveAPIView): diff --git a/awx/main/models/events.py b/awx/main/models/events.py index 8b8637d8f2..4361f01853 100644 --- a/awx/main/models/events.py +++ b/awx/main/models/events.py @@ -607,6 +607,9 @@ class BaseCommandEvent(CreatedModifiedModel): ''' return self.event + def get_host_status_counts(self): + return create_host_status_counts(getattr(self, 'event_data', {})) + class AdHocCommandEvent(BaseCommandEvent): From bf55c7e7e283eafc4e53198fdd9c9b914192909c Mon Sep 17 00:00:00 2001 From: Jake McDermott Date: Mon, 9 Jul 2018 09:48:55 -0400 Subject: [PATCH 4/4] use host_status_counts field for adhoc command jobs ui --- .../client/features/output/status.service.js | 36 ++++++++++++------- 1 file changed, 23 insertions(+), 13 deletions(-) diff --git a/awx/ui/client/features/output/status.service.js b/awx/ui/client/features/output/status.service.js index 4ad8432d1a..d7b74d2279 100644 --- a/awx/ui/client/features/output/status.service.js +++ b/awx/ui/client/features/output/status.service.js @@ -42,18 +42,17 @@ function JobStatusService (moment, message) { }, }; - if (model.get('type') === 'job' || model.get('type') === 'project_update') { - if (model.has('playbook_counts')) { - this.setPlaybookCounts(model.get('playbook_counts')); - } - - if (model.has('host_status_counts')) { - this.setHostStatusCounts(model.get('host_status_counts')); - } + if (model.has('host_status_counts')) { + this.setHostStatusCounts(model.get('host_status_counts')); } else { const hostStatusCounts = this.createHostStatusCounts(this.state.status); this.setHostStatusCounts(hostStatusCounts); + } + + if (model.has('playbook_counts')) { + this.setPlaybookCounts(model.get('playbook_counts')); + } else { this.setPlaybookCounts({ task_count: 1, play_count: 1 }); } @@ -131,14 +130,25 @@ function JobStatusService (moment, message) { }; this.isExpectingStatsEvent = () => (this.jobType === 'job') || - (this.jobType === 'project_update'); + (this.jobType === 'project_update') || + (this.jobType === 'ad_hoc_command'); this.updateStats = () => { this.updateHostCounts(); if (this.statsEvent) { this.setFinished(this.statsEvent.created); - this.setJobStatus(this.statsEvent.failed ? 'failed' : 'successful'); + + const failures = _.get(this.statsEvent, ['event_data', 'failures'], {}); + const dark = _.get(this.statsEvent, ['event_data', 'dark'], {}); + + if (this.statsEvent.failed || + Object.keys(failures).length > 0 || + Object.keys(dark).length > 0) { + this.setJobStatus('failed'); + } else { + this.setJobStatus('successful'); + } } }; @@ -174,9 +184,9 @@ function JobStatusService (moment, message) { this.setJobStatus = status => { const isExpectingStats = this.isExpectingStatsEvent(); - const isIncomplete = _.includes(INCOMPLETE, status); - const isFinished = _.includes(FINISHED, status); - const isAlreadyFinished = _.includes(FINISHED, this.state.status); + const isIncomplete = INCOMPLETE.includes(status); + const isFinished = FINISHED.includes(status); + const isAlreadyFinished = FINISHED.includes(this.state.status); if (isAlreadyFinished) { return;