mirror of
https://github.com/ansible/awx.git
synced 2024-10-31 23:51:09 +03:00
Merge pull request #2114 from jakemcdermott/job-results/host-counts
reduce the minimum number of http requests required to load job details from 7 to 3
This commit is contained in:
commit
9137b4acef
@ -1419,6 +1419,48 @@ class ProjectUpdateSerializer(UnifiedJobSerializer, ProjectOptionsSerializer):
|
||||
return res
|
||||
|
||||
|
||||
class ProjectUpdateDetailSerializer(ProjectUpdateSerializer):
|
||||
|
||||
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 = ProjectUpdate
|
||||
fields = ('*', 'host_status_counts', 'playbook_counts',)
|
||||
|
||||
def get_playbook_counts(self, obj):
|
||||
task_count = obj.project_update_events.filter(event='playbook_on_task_start').count()
|
||||
play_count = obj.project_update_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.project_update_events.only('event_data').get(event='playbook_on_stats').event_data
|
||||
except ProjectUpdateEvent.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 ProjectUpdateListSerializer(ProjectUpdateSerializer, UnifiedJobListSerializer):
|
||||
|
||||
pass
|
||||
|
@ -1418,7 +1418,7 @@ class ProjectUpdateList(ListAPIView):
|
||||
class ProjectUpdateDetail(UnifiedJobDeletionMixin, RetrieveDestroyAPIView):
|
||||
|
||||
model = ProjectUpdate
|
||||
serializer_class = ProjectUpdateSerializer
|
||||
serializer_class = ProjectUpdateDetailSerializer
|
||||
|
||||
|
||||
class ProjectUpdateEventsList(SubListAPIView):
|
||||
|
@ -11,12 +11,14 @@ from awx.api.serializers import (
|
||||
JobDetailSerializer,
|
||||
JobSerializer,
|
||||
JobOptionsSerializer,
|
||||
ProjectUpdateDetailSerializer,
|
||||
)
|
||||
|
||||
from awx.main.models import (
|
||||
Label,
|
||||
Job,
|
||||
JobEvent,
|
||||
ProjectUpdateEvent,
|
||||
)
|
||||
|
||||
|
||||
@ -142,10 +144,52 @@ class TestJobDetailSerializerGetHostStatusCountFields(object):
|
||||
|
||||
assert host_status_counts == {'ok': 1, 'changed': 1, 'dark': 2}
|
||||
|
||||
def test_host_status_counts_is_empty_dict_without_stats_event(self, job, mocker):
|
||||
def test_host_status_counts_is_empty_dict_without_stats_event(self, job):
|
||||
job.job_events = JobEvent.objects.none()
|
||||
|
||||
serializer = JobDetailSerializer()
|
||||
host_status_counts = serializer.get_host_status_counts(job)
|
||||
|
||||
assert host_status_counts == {}
|
||||
|
||||
|
||||
class TestProjectUpdateDetailSerializerGetHostStatusCountFields(object):
|
||||
|
||||
def test_hosts_are_counted_once(self, project_update, mocker):
|
||||
mock_event = ProjectUpdateEvent(**{
|
||||
'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))
|
||||
project_update.project_update_events.only = mocker.MagicMock(return_value=mock_qs)
|
||||
|
||||
serializer = ProjectUpdateDetailSerializer()
|
||||
host_status_counts = serializer.get_host_status_counts(project_update)
|
||||
|
||||
assert host_status_counts == {'ok': 1, 'changed': 1, 'dark': 2}
|
||||
|
||||
def test_host_status_counts_is_empty_dict_without_stats_event(self, project_update):
|
||||
project_update.project_update_events = ProjectUpdateEvent.objects.none()
|
||||
|
||||
serializer = ProjectUpdateDetailSerializer()
|
||||
host_status_counts = serializer.get_host_status_counts(project_update)
|
||||
|
||||
assert host_status_counts == {}
|
||||
|
@ -17,7 +17,7 @@ function JobEventsApiService ($http, $q) {
|
||||
this.state = { current: 0, count: 0 };
|
||||
};
|
||||
|
||||
this.fetch = () => this.getFirst().then(() => this);
|
||||
this.fetch = () => this.getLast().then(() => this);
|
||||
|
||||
this.getFirst = () => {
|
||||
const page = 1;
|
||||
|
@ -516,7 +516,7 @@ function getExtraVarsDetails () {
|
||||
}
|
||||
|
||||
function getLabelDetails () {
|
||||
const jobLabels = _.get(resource.model.get('related.labels'), 'results', []);
|
||||
const jobLabels = _.get(resource.model.get('summary_fields.labels'), 'results', []);
|
||||
|
||||
if (jobLabels.length < 1) {
|
||||
return null;
|
||||
|
@ -26,19 +26,20 @@ const PAGE_LIMIT = 5;
|
||||
const PAGE_SIZE = 50;
|
||||
const ORDER_BY = 'counter';
|
||||
const WS_PREFIX = 'ws';
|
||||
const API_ROOT = '/api/v2/';
|
||||
|
||||
function resolveResource (
|
||||
$state,
|
||||
$stateParams,
|
||||
Job,
|
||||
ProjectUpdate,
|
||||
AdHocCommand,
|
||||
SystemJob,
|
||||
WorkflowJob,
|
||||
InventoryUpdate,
|
||||
$stateParams,
|
||||
qs,
|
||||
Wait,
|
||||
eventsApi,
|
||||
Events,
|
||||
) {
|
||||
const { id, type, handleErrors } = $stateParams;
|
||||
const { job_event_search } = $stateParams; // eslint-disable-line camelcase
|
||||
@ -46,24 +47,28 @@ function resolveResource (
|
||||
const { name, key } = getWebSocketResource(type);
|
||||
|
||||
let Resource;
|
||||
let related = 'events';
|
||||
let related;
|
||||
|
||||
switch (type) {
|
||||
case 'project':
|
||||
Resource = ProjectUpdate;
|
||||
related = `project_updates/${id}/events/`;
|
||||
break;
|
||||
case 'playbook':
|
||||
Resource = Job;
|
||||
related = 'job_events';
|
||||
related = `jobs/${id}/job_events/`;
|
||||
break;
|
||||
case 'command':
|
||||
Resource = AdHocCommand;
|
||||
related = `ad_hoc_commands/${id}/events/`;
|
||||
break;
|
||||
case 'system':
|
||||
Resource = SystemJob;
|
||||
related = `system_jobs/${id}/events/`;
|
||||
break;
|
||||
case 'inventory':
|
||||
Resource = InventoryUpdate;
|
||||
related = `inventory_updates/${id}/events/`;
|
||||
break;
|
||||
// case 'workflow':
|
||||
// todo: integrate workflow chart components into this view
|
||||
@ -89,30 +94,15 @@ function resolveResource (
|
||||
Object.assign(config.params, query);
|
||||
}
|
||||
|
||||
let model;
|
||||
Events.init(`${API_ROOT}${related}`, config.params);
|
||||
|
||||
Wait('start');
|
||||
const resourcePromise = new Resource(['get', 'options'], [id, id])
|
||||
.then(job => {
|
||||
const endpoint = `${job.get('url')}${related}/`;
|
||||
eventsApi.init(endpoint, config.params);
|
||||
|
||||
const promises = [job.getStats(), eventsApi.fetch()];
|
||||
|
||||
if (job.has('related.labels')) {
|
||||
promises.push(job.extend('get', 'labels'));
|
||||
}
|
||||
|
||||
model = job;
|
||||
return Promise.all(promises);
|
||||
})
|
||||
.then(([stats, events]) => ({
|
||||
const promise = Promise.all([new Resource(['get', 'options'], [id, id]), Events.fetch()])
|
||||
.then(([model, events]) => ({
|
||||
id,
|
||||
type,
|
||||
stats,
|
||||
model,
|
||||
events,
|
||||
related,
|
||||
ws: {
|
||||
events: `${WS_PREFIX}-${key}-${id}`,
|
||||
status: `${WS_PREFIX}-${name}`,
|
||||
@ -125,13 +115,14 @@ function resolveResource (
|
||||
}));
|
||||
|
||||
if (!handleErrors) {
|
||||
return resourcePromise
|
||||
return promise
|
||||
.finally(() => Wait('stop'));
|
||||
}
|
||||
|
||||
return resourcePromise
|
||||
return promise
|
||||
.catch(({ data, status }) => {
|
||||
qs.error(data, status);
|
||||
|
||||
return $state.go($state.current, $state.params, { reload: true });
|
||||
})
|
||||
.finally(() => Wait('stop'));
|
||||
@ -218,13 +209,13 @@ function JobsRun ($stateRegistry, $filter, strings) {
|
||||
],
|
||||
resource: [
|
||||
'$state',
|
||||
'$stateParams',
|
||||
'JobModel',
|
||||
'ProjectUpdateModel',
|
||||
'AdHocCommandModel',
|
||||
'SystemJobModel',
|
||||
'WorkflowJobModel',
|
||||
'InventoryUpdateModel',
|
||||
'$stateParams',
|
||||
'QuerySet',
|
||||
'Wait',
|
||||
'JobEventsApiService',
|
||||
|
@ -26,13 +26,13 @@ function JobStatsController (strings, { subscribe }) {
|
||||
strings.get('tooltips.COLLAPSE_OUTPUT') :
|
||||
strings.get('tooltips.EXPAND_OUTPUT');
|
||||
|
||||
unsubscribe = subscribe(({ running, elapsed, counts, stats, hosts }) => {
|
||||
unsubscribe = subscribe(({ running, elapsed, counts, hosts }) => {
|
||||
vm.plays = counts.plays;
|
||||
vm.tasks = counts.tasks;
|
||||
vm.hosts = counts.hosts;
|
||||
vm.elapsed = elapsed;
|
||||
vm.running = running;
|
||||
vm.setHostStatusCounts(stats, hosts);
|
||||
vm.setHostStatusCounts(hosts);
|
||||
});
|
||||
};
|
||||
|
||||
@ -40,7 +40,9 @@ function JobStatsController (strings, { subscribe }) {
|
||||
unsubscribe();
|
||||
};
|
||||
|
||||
vm.setHostStatusCounts = (stats, counts) => {
|
||||
vm.setHostStatusCounts = counts => {
|
||||
let statsAreAvailable;
|
||||
|
||||
Object.keys(counts).forEach(key => {
|
||||
const count = counts[key];
|
||||
const statusBarElement = $(`.HostStatusBar-${key}`);
|
||||
@ -48,9 +50,11 @@ function JobStatsController (strings, { subscribe }) {
|
||||
statusBarElement.css('flex', `${count} 0 auto`);
|
||||
|
||||
vm.tooltips[key] = createStatsBarTooltip(key, count);
|
||||
|
||||
if (count) statsAreAvailable = true;
|
||||
});
|
||||
|
||||
vm.statsAreAvailable = stats;
|
||||
vm.statsAreAvailable = statsAreAvailable;
|
||||
};
|
||||
|
||||
vm.toggleExpanded = () => {
|
||||
|
@ -1,3 +1,4 @@
|
||||
/* eslint camelcase: 0 */
|
||||
const JOB_START = 'playbook_on_start';
|
||||
const JOB_END = 'playbook_on_stats';
|
||||
const PLAY_START = 'playbook_on_play_start';
|
||||
@ -12,7 +13,7 @@ function JobStatusService (moment, message) {
|
||||
this.dispatch = () => message.dispatch('status', this.state);
|
||||
this.subscribe = listener => message.subscribe('status', listener);
|
||||
|
||||
this.init = ({ model, stats }) => {
|
||||
this.init = ({ model }) => {
|
||||
this.created = model.get('created');
|
||||
this.job = model.get('id');
|
||||
this.jobType = model.get('type');
|
||||
@ -24,7 +25,6 @@ function JobStatusService (moment, message) {
|
||||
|
||||
this.state = {
|
||||
running: false,
|
||||
stats: false,
|
||||
counts: {
|
||||
plays: 0,
|
||||
tasks: 0,
|
||||
@ -41,13 +41,37 @@ function JobStatusService (moment, message) {
|
||||
},
|
||||
};
|
||||
|
||||
this.setStatsEvent(stats);
|
||||
this.updateStats();
|
||||
this.updateRunningState();
|
||||
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'));
|
||||
}
|
||||
} else {
|
||||
const hostStatusCounts = this.createHostStatusCounts(this.state.status);
|
||||
|
||||
this.setHostStatusCounts(hostStatusCounts);
|
||||
this.setPlaybookCounts({ task_count: 1, play_count: 1 });
|
||||
}
|
||||
|
||||
this.updateRunningState();
|
||||
this.dispatch();
|
||||
};
|
||||
|
||||
this.createHostStatusCounts = status => {
|
||||
if (_.includes(COMPLETE, status)) {
|
||||
return { ok: 1 };
|
||||
}
|
||||
|
||||
if (_.includes(INCOMPLETE, status)) {
|
||||
return { failures: 1 };
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
this.pushStatusEvent = data => {
|
||||
const isJobStatusEvent = (this.job === data.unified_job_id);
|
||||
const isProjectStatusEvent = (this.project && (this.project === data.project_id));
|
||||
@ -112,7 +136,6 @@ function JobStatusService (moment, message) {
|
||||
this.updateHostCounts();
|
||||
|
||||
if (this.statsEvent) {
|
||||
this.state.stats = true;
|
||||
this.setFinished(this.statsEvent.created);
|
||||
this.setJobStatus(this.statsEvent.failed ? 'failed' : 'successful');
|
||||
}
|
||||
@ -199,9 +222,25 @@ function JobStatusService (moment, message) {
|
||||
};
|
||||
|
||||
this.setHostStatusCounts = counts => {
|
||||
counts = counts || {};
|
||||
|
||||
HOST_STATUS_KEYS.forEach(key => {
|
||||
counts[key] = counts[key] || 0;
|
||||
});
|
||||
|
||||
if (!this.state.counts.hosts) {
|
||||
this.state.counts.hosts = Object.keys(counts)
|
||||
.reduce((sum, key) => sum + counts[key], 0);
|
||||
}
|
||||
|
||||
this.state.hosts = counts;
|
||||
};
|
||||
|
||||
this.setPlaybookCounts = ({ play_count, task_count }) => {
|
||||
this.state.counts.plays = play_count;
|
||||
this.state.counts.tasks = task_count;
|
||||
};
|
||||
|
||||
this.resetCounts = () => {
|
||||
this.state.counts.plays = 0;
|
||||
this.state.counts.tasks = 0;
|
||||
|
@ -19,17 +19,12 @@ function postRelaunch (params) {
|
||||
return $http(req);
|
||||
}
|
||||
|
||||
function getStats () {
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
|
||||
function AdHocCommandModel (method, resource, config) {
|
||||
BaseModel.call(this, 'ad_hoc_commands');
|
||||
|
||||
this.Constructor = AdHocCommandModel;
|
||||
this.postRelaunch = postRelaunch.bind(this);
|
||||
this.getRelaunch = getRelaunch.bind(this);
|
||||
this.getStats = getStats.bind(this);
|
||||
|
||||
return this.create(method, resource, config);
|
||||
}
|
||||
|
@ -1,14 +1,8 @@
|
||||
let BaseModel;
|
||||
|
||||
function getStats () {
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
|
||||
function InventoryUpdateModel (method, resource, config) {
|
||||
BaseModel.call(this, 'inventory_updates');
|
||||
|
||||
this.getStats = getStats.bind(this);
|
||||
|
||||
this.Constructor = InventoryUpdateModel;
|
||||
|
||||
return this.create(method, resource, config);
|
||||
|
@ -23,31 +23,6 @@ function postRelaunch (params) {
|
||||
return $http(req);
|
||||
}
|
||||
|
||||
function getStats () {
|
||||
if (!this.has('GET', 'id')) {
|
||||
return Promise.reject(new Error('No property, id, exists'));
|
||||
}
|
||||
|
||||
if (!this.has('GET', 'related.job_events')) {
|
||||
return Promise.reject(new Error('No related property, job_events, exists'));
|
||||
}
|
||||
|
||||
const req = {
|
||||
method: 'GET',
|
||||
url: `${this.path}${this.get('id')}/job_events/`,
|
||||
params: { event: 'playbook_on_stats' },
|
||||
};
|
||||
|
||||
return $http(req)
|
||||
.then(({ data }) => {
|
||||
if (data.results.length > 0) {
|
||||
return data.results[0];
|
||||
}
|
||||
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
function getCredentials (id) {
|
||||
const req = {
|
||||
method: 'GET',
|
||||
@ -64,7 +39,6 @@ function JobModel (method, resource, config) {
|
||||
|
||||
this.postRelaunch = postRelaunch.bind(this);
|
||||
this.getRelaunch = getRelaunch.bind(this);
|
||||
this.getStats = getStats.bind(this);
|
||||
this.getCredentials = getCredentials.bind(this);
|
||||
|
||||
return this.create(method, resource, config);
|
||||
|
@ -1,50 +1,20 @@
|
||||
let $http;
|
||||
let BaseModel;
|
||||
|
||||
function getStats () {
|
||||
if (!this.has('GET', 'id')) {
|
||||
return Promise.reject(new Error('No property, id, exists'));
|
||||
}
|
||||
|
||||
if (!this.has('GET', 'related.events')) {
|
||||
return Promise.reject(new Error('No related property, events, exists'));
|
||||
}
|
||||
|
||||
const req = {
|
||||
method: 'GET',
|
||||
url: `${this.path}${this.get('id')}/events/`,
|
||||
params: { event: 'playbook_on_stats' },
|
||||
};
|
||||
|
||||
return $http(req)
|
||||
.then(({ data }) => {
|
||||
if (data.results.length > 0) {
|
||||
return data.results[0];
|
||||
}
|
||||
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
function ProjectUpdateModel (method, resource, config) {
|
||||
BaseModel.call(this, 'project_updates');
|
||||
|
||||
this.getStats = getStats.bind(this);
|
||||
|
||||
this.Constructor = ProjectUpdateModel;
|
||||
|
||||
return this.create(method, resource, config);
|
||||
}
|
||||
|
||||
function ProjectUpdateModelLoader (_$http_, _BaseModel_) {
|
||||
$http = _$http_;
|
||||
function ProjectUpdateModelLoader (_BaseModel_) {
|
||||
BaseModel = _BaseModel_;
|
||||
|
||||
return ProjectUpdateModel;
|
||||
}
|
||||
|
||||
ProjectUpdateModelLoader.$inject = [
|
||||
'$http',
|
||||
'BaseModel'
|
||||
];
|
||||
|
||||
|
@ -1,14 +1,8 @@
|
||||
let BaseModel;
|
||||
|
||||
function getStats () {
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
|
||||
function SystemJobModel (method, resource, config) {
|
||||
BaseModel.call(this, 'system_jobs');
|
||||
|
||||
this.getStats = getStats.bind(this);
|
||||
|
||||
this.Constructor = SystemJobModel;
|
||||
|
||||
return this.create(method, resource, config);
|
||||
|
Loading…
Reference in New Issue
Block a user