diff --git a/awx/api/serializers.py b/awx/api/serializers.py index 140eb12402..c242cdee1f 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 @@ -1477,23 +1477,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): @@ -3274,23 +3262,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): @@ -3470,6 +3446,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 158d4da1db..2dd0d5467d 100644 --- a/awx/api/views.py +++ b/awx/api/views.py @@ -4569,7 +4569,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 a6e2c67c74..4361f01853 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': @@ -588,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): diff --git a/awx/ui/client/features/output/status.service.js b/awx/ui/client/features/output/status.service.js index d387f8db41..d7b74d2279 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) { @@ -41,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 }); } @@ -61,12 +61,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; @@ -130,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'); + } } }; @@ -173,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;