add host_status, play, and task counts to job details

This commit is contained in:
Jake McDermott
2018-06-05 22:07:44 -04:00
parent 246e63bdb6
commit f8f59c8c8c
3 changed files with 92 additions and 2 deletions

View File

@@ -9,7 +9,7 @@ import operator
import re import re
import six import six
import urllib import urllib
from collections import OrderedDict from collections import defaultdict, OrderedDict
from datetime import timedelta from datetime import timedelta
# OAuth2 # OAuth2
@@ -3130,6 +3130,48 @@ class JobSerializer(UnifiedJobSerializer, JobOptionsSerializer):
return summary_fields return summary_fields
class JobDetailSerializer(JobSerializer):
host_status_counts = serializers.SerializerMethodField(
help_text=_('A count of hosts uniquely assigned to each status.'),
)
playbook_counts = serializers.SerializerMethodField(
help_text=_('A count of all plays and tasks for the job run.'),
)
class Meta:
model = Job
fields = ('*', 'host_status_counts', 'playbook_counts',)
def get_playbook_counts(self, obj):
task_count = obj.job_events.filter(event='playbook_on_task_start').count()
play_count = obj.job_events.filter(event='playbook_on_play_start').count()
data = {'play_count': play_count, 'task_count': task_count}
return data
def get_host_status_counts(self, obj):
try:
event_data = obj.job_events.only('event_data').get(event='playbook_on_stats').event_data
except JobEvent.DoesNotExist:
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 host_status_counts
class JobCancelSerializer(BaseSerializer): class JobCancelSerializer(BaseSerializer):
can_cancel = serializers.BooleanField(read_only=True) can_cancel = serializers.BooleanField(read_only=True)

View File

@@ -4080,7 +4080,7 @@ class JobDetail(UnifiedJobDeletionMixin, RetrieveUpdateDestroyAPIView):
model = Job model = Job
metadata_class = JobTypeMetadata metadata_class = JobTypeMetadata
serializer_class = JobSerializer serializer_class = JobDetailSerializer
def update(self, request, *args, **kwargs): def update(self, request, *args, **kwargs):
obj = self.get_object() obj = self.get_object()

View File

@@ -1,4 +1,5 @@
# Python # Python
from collections import namedtuple
import pytest import pytest
import mock import mock
import json import json
@@ -7,6 +8,7 @@ from six.moves import xrange
# AWX # AWX
from awx.api.serializers import ( from awx.api.serializers import (
JobDetailSerializer,
JobSerializer, JobSerializer,
JobOptionsSerializer, JobOptionsSerializer,
) )
@@ -14,6 +16,7 @@ from awx.api.serializers import (
from awx.main.models import ( from awx.main.models import (
Label, Label,
Job, Job,
JobEvent,
) )
@@ -53,6 +56,7 @@ def jobs(mocker):
@mock.patch('awx.api.serializers.UnifiedJobTemplateSerializer.get_related', lambda x,y: {}) @mock.patch('awx.api.serializers.UnifiedJobTemplateSerializer.get_related', lambda x,y: {})
@mock.patch('awx.api.serializers.JobOptionsSerializer.get_related', lambda x,y: {}) @mock.patch('awx.api.serializers.JobOptionsSerializer.get_related', lambda x,y: {})
class TestJobSerializerGetRelated(): class TestJobSerializerGetRelated():
@pytest.mark.parametrize("related_resource_name", [ @pytest.mark.parametrize("related_resource_name", [
'job_events', 'job_events',
'relaunch', 'relaunch',
@@ -76,6 +80,7 @@ class TestJobSerializerGetRelated():
@mock.patch('awx.api.serializers.BaseSerializer.to_representation', lambda self,obj: { @mock.patch('awx.api.serializers.BaseSerializer.to_representation', lambda self,obj: {
'extra_vars': obj.extra_vars}) 'extra_vars': obj.extra_vars})
class TestJobSerializerSubstitution(): class TestJobSerializerSubstitution():
def test_survey_password_hide(self, mocker): def test_survey_password_hide(self, mocker):
job = mocker.MagicMock(**{ job = mocker.MagicMock(**{
'display_extra_vars.return_value': '{\"secret_key\": \"$encrypted$\"}', 'display_extra_vars.return_value': '{\"secret_key\": \"$encrypted$\"}',
@@ -90,6 +95,7 @@ class TestJobSerializerSubstitution():
@mock.patch('awx.api.serializers.BaseSerializer.get_summary_fields', lambda x,y: {}) @mock.patch('awx.api.serializers.BaseSerializer.get_summary_fields', lambda x,y: {})
class TestJobOptionsSerializerGetSummaryFields(): class TestJobOptionsSerializerGetSummaryFields():
def test__summary_field_labels_10_max(self, mocker, job_template, labels): def test__summary_field_labels_10_max(self, mocker, job_template, labels):
job_template.labels.all = mocker.MagicMock(**{'return_value': labels}) job_template.labels.all = mocker.MagicMock(**{'return_value': labels})
@@ -101,3 +107,45 @@ class TestJobOptionsSerializerGetSummaryFields():
def test_labels_exists(self, test_get_summary_fields, job_template): def test_labels_exists(self, test_get_summary_fields, job_template):
test_get_summary_fields(JobOptionsSerializer, job_template, 'labels') test_get_summary_fields(JobOptionsSerializer, job_template, 'labels')
class TestJobDetailSerializerGetHostStatusCountFields(object):
def test_hosts_are_counted_once(self, job, mocker):
mock_event = JobEvent(**{
'event': 'playbook_on_stats',
'event_data': {
'skipped': {
'localhost': 2,
'fiz': 1,
},
'ok': {
'localhost': 1,
'foo': 2,
},
'changed': {
'localhost': 1,
'bar': 3,
},
'dark': {
'localhost': 2,
'fiz': 2,
}
}
})
mock_qs = namedtuple('mock_qs', ['get'])(mocker.MagicMock(return_value=mock_event))
job.job_events.only = mocker.MagicMock(return_value=mock_qs)
serializer = JobDetailSerializer()
host_status_counts = serializer.get_host_status_counts(job)
assert host_status_counts == {'ok': 1, 'changed': 1, 'dark': 2}
def test_host_status_counts_is_empty_dict_without_stats_event(self, job, mocker):
job.job_events = JobEvent.objects.none()
serializer = JobDetailSerializer()
host_status_counts = serializer.get_host_status_counts(job)
assert host_status_counts == {}