From b85c98afd2927c7d67f1d105ebd86fb27c977ed1 Mon Sep 17 00:00:00 2001 From: Chris Church Date: Mon, 12 Dec 2016 16:34:36 -0500 Subject: [PATCH 01/17] Split job event data between callback queue and stdout. Send most of event data directly over queue and capture only stdout/counter/start_line/end_line in celery task; recombine into single event in callback receiver. --- awx/lib/tower_display_callback/display.py | 2 +- awx/lib/tower_display_callback/events.py | 66 ++++++++++++++++++- awx/lib/tower_display_callback/module.py | 4 +- .../commands/run_callback_receiver.py | 23 +++++-- 4 files changed, 85 insertions(+), 10 deletions(-) diff --git a/awx/lib/tower_display_callback/display.py b/awx/lib/tower_display_callback/display.py index 128c9349c7..ad5e8ba37a 100644 --- a/awx/lib/tower_display_callback/display.py +++ b/awx/lib/tower_display_callback/display.py @@ -26,7 +26,7 @@ import uuid from ansible.utils.display import Display # Tower Display Callback -from tower_display_callback.events import event_context +from .events import event_context __all__ = [] diff --git a/awx/lib/tower_display_callback/events.py b/awx/lib/tower_display_callback/events.py index 86fab2895b..0909ed460d 100644 --- a/awx/lib/tower_display_callback/events.py +++ b/awx/lib/tower_display_callback/events.py @@ -22,14 +22,75 @@ import base64 import contextlib import datetime import json +import logging import multiprocessing import os import threading import uuid +# Kombu +from kombu import Connection, Exchange, Producer + __all__ = ['event_context'] +class CallbackQueueEventDispatcher(object): + + def __init__(self): + self.callback_connection = os.getenv('CALLBACK_CONNECTION', None) + self.connection_queue = os.getenv('CALLBACK_QUEUE', '') + self.connection = None + self.exchange = None + self._init_logging() + + def _init_logging(self): + try: + self.job_callback_debug = int(os.getenv('JOB_CALLBACK_DEBUG', '0')) + except ValueError: + self.job_callback_debug = 0 + self.logger = logging.getLogger('awx.plugins.callback.job_event_callback') + if self.job_callback_debug >= 2: + self.logger.setLevel(logging.DEBUG) + elif self.job_callback_debug >= 1: + self.logger.setLevel(logging.INFO) + else: + self.logger.setLevel(logging.WARNING) + handler = logging.StreamHandler() + formatter = logging.Formatter('%(levelname)-8s %(process)-8d %(message)s') + handler.setFormatter(formatter) + self.logger.addHandler(handler) + self.logger.propagate = False + + def dispatch(self, obj): + if not self.callback_connection or not self.connection_queue: + return + active_pid = os.getpid() + for retry_count in xrange(4): + try: + if not hasattr(self, 'connection_pid'): + self.connection_pid = active_pid + if self.connection_pid != active_pid: + self.connection = None + if self.connection is None: + self.connection = Connection(self.callback_connection) + self.exchange = Exchange(self.connection_queue, type='direct') + + producer = Producer(self.connection) + producer.publish(obj, + serializer='json', + compression='bzip2', + exchange=self.exchange, + declare=[self.exchange], + routing_key=self.connection_queue) + return + except Exception, e: + self.logger.info('Publish Job Event Exception: %r, retry=%d', e, + retry_count, exc_info=True) + retry_count += 1 + if retry_count >= 3: + break + + class EventContext(object): ''' Store global and local (per thread/process) data associated with callback @@ -38,6 +99,7 @@ class EventContext(object): def __init__(self): self.display_lock = multiprocessing.RLock() + self.dispatcher = CallbackQueueEventDispatcher() def add_local(self, **kwargs): if not hasattr(self, '_local'): @@ -136,7 +198,9 @@ class EventContext(object): fileobj.flush() def dump_begin(self, fileobj): - self.dump(fileobj, self.get_begin_dict()) + begin_dict = self.get_begin_dict() + self.dispatcher.dispatch(begin_dict) + self.dump(fileobj, {'uuid': begin_dict['uuid']}) def dump_end(self, fileobj): self.dump(fileobj, self.get_end_dict(), flush=True) diff --git a/awx/lib/tower_display_callback/module.py b/awx/lib/tower_display_callback/module.py index e61ef17624..59faa7ac79 100644 --- a/awx/lib/tower_display_callback/module.py +++ b/awx/lib/tower_display_callback/module.py @@ -29,8 +29,8 @@ from ansible.plugins.callback import CallbackBase from ansible.plugins.callback.default import CallbackModule as DefaultCallbackModule # Tower Display Callback -from tower_display_callback.events import event_context -from tower_display_callback.minimal import CallbackModule as MinimalCallbackModule +from .events import event_context +from .minimal import CallbackModule as MinimalCallbackModule class BaseCallbackModule(CallbackBase): diff --git a/awx/main/management/commands/run_callback_receiver.py b/awx/main/management/commands/run_callback_receiver.py index c0105b2587..7b959f6781 100644 --- a/awx/main/management/commands/run_callback_receiver.py +++ b/awx/main/management/commands/run_callback_receiver.py @@ -21,6 +21,7 @@ logger = logging.getLogger('awx.main.commands.run_callback_receiver') class CallbackBrokerWorker(ConsumerMixin): def __init__(self, connection): self.connection = connection + self.partial_events = {} def get_consumers(self, Consumer, channel): return [Consumer(queues=[Queue(settings.CALLBACK_QUEUE, @@ -31,18 +32,28 @@ class CallbackBrokerWorker(ConsumerMixin): def process_task(self, body, message): try: - if 'event' not in body: - raise Exception('Payload does not have an event') if 'job_id' not in body and 'ad_hoc_command_id' not in body: raise Exception('Payload does not have a job_id or ad_hoc_command_id') if settings.DEBUG: logger.info('Body: {}'.format(body)) logger.info('Message: {}'.format(message)) try: - if 'job_id' in body: - JobEvent.create_from_data(**body) - elif 'ad_hoc_command_id' in body: - AdHocCommandEvent.create_from_data(**body) + # If event came directly from callback without counter/stdout, + # save it until the rest of the event arrives. + if 'counter' not in body: + if 'uuid' in body: + self.partial_events[body['uuid']] = body + # If event has counter, try to combine it with any event data + # already received for the same uuid, then create the actual + # job event record. + else: + if 'uuid' in body: + partial_event = self.partial_events.pop(body['uuid'], {}) + body.update(partial_event) + if 'job_id' in body: + JobEvent.create_from_data(**body) + elif 'ad_hoc_command_id' in body: + AdHocCommandEvent.create_from_data(**body) except DatabaseError as e: logger.error('Database Error Saving Job Event: {}'.format(e)) except Exception as exc: From 074fe8a3d10515b5b70b76d91fda0dc882b8a7df Mon Sep 17 00:00:00 2001 From: jaredevantabor Date: Mon, 12 Dec 2016 16:33:37 -0800 Subject: [PATCH 02/17] adding some fields to the job results left hand side panel --- .../src/job-results/job-results.controller.js | 3 +- .../src/job-results/job-results.partial.html | 104 +++++------------- 2 files changed, 32 insertions(+), 75 deletions(-) diff --git a/awx/ui/client/src/job-results/job-results.controller.js b/awx/ui/client/src/job-results/job-results.controller.js index b2f83ede02..ce92fb95f7 100644 --- a/awx/ui/client/src/job-results/job-results.controller.js +++ b/awx/ui/client/src/job-results/job-results.controller.js @@ -31,6 +31,7 @@ export default ['jobData', 'jobDataOptions', 'jobLabels', 'jobFinished', 'count' $scope.machine_credential_link = getTowerLink('credential'); $scope.cloud_credential_link = getTowerLink('cloud_credential'); $scope.network_credential_link = getTowerLink('network_credential'); + $scope.schedule_link = getTowerLink('schedule'); }; // uses options to set scope variables to their readable string @@ -64,7 +65,7 @@ export default ['jobData', 'jobDataOptions', 'jobLabels', 'jobFinished', 'count' // turn related api browser routes into tower routes getTowerLinks(); - + // the links below can't be set in getTowerLinks because the // links on the UI don't directly match the corresponding URL // on the API browser diff --git a/awx/ui/client/src/job-results/job-results.partial.html b/awx/ui/client/src/job-results/job-results.partial.html index b12c7af487..85e4bfedf0 100644 --- a/awx/ui/client/src/job-results/job-results.partial.html +++ b/awx/ui/client/src/job-results/job-results.partial.html @@ -62,6 +62,20 @@
+ +
+ +
+ + {{ status_label }} +
+
+
@@ -135,6 +149,22 @@
+ + +
@@ -348,80 +378,6 @@
- - - - - - - - - - - -
- -
- -
- - {{ status_label }} -
-
- + +
+ +
+
+
+ +
@@ -401,23 +414,6 @@ - -
From 49259e3588a47f226df864a4fcd4291304be4b20 Mon Sep 17 00:00:00 2001 From: jaredevantabor Date: Tue, 13 Dec 2016 16:26:29 -0800 Subject: [PATCH 05/17] adding explanation to left hand panel --- .../src/job-results/job-results.controller.js | 4 +++- .../src/job-results/job-results.partial.html | 16 +++++++++++++--- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/awx/ui/client/src/job-results/job-results.controller.js b/awx/ui/client/src/job-results/job-results.controller.js index ce92fb95f7..adfe541bc4 100644 --- a/awx/ui/client/src/job-results/job-results.controller.js +++ b/awx/ui/client/src/job-results/job-results.controller.js @@ -85,7 +85,9 @@ export default ['jobData', 'jobDataOptions', 'jobLabels', 'jobFinished', 'count' jobData.summary_fields.source_workflow_job.id){ $scope.workflow_result_link = `/#/workflows/${jobData.summary_fields.source_workflow_job.id}`; } - + if(jobData.result_traceback) { + $scope.job.result_traceback = jobData.result_traceback.trim().split('\n').join('
'); + } // use options labels to manipulate display of details getTowerLabels(); diff --git a/awx/ui/client/src/job-results/job-results.partial.html b/awx/ui/client/src/job-results/job-results.partial.html index 340d4b1ca5..4b2d8be6ea 100644 --- a/awx/ui/client/src/job-results/job-results.partial.html +++ b/awx/ui/client/src/job-results/job-results.partial.html @@ -85,14 +85,24 @@
+ +
+ +
+ {{job.job_explanation}} +
+
+
-
@@ -410,7 +420,7 @@
- --> + From 24ee84b0946f0fe21db887c8429b6ff894eb4abf Mon Sep 17 00:00:00 2001 From: Michael Abashian Date: Tue, 13 Dec 2016 17:54:17 -0500 Subject: [PATCH 06/17] Fixed various bugs after auditing the workflow details page --- .../workflow-chart/workflow-chart.block.less | 8 +++ .../workflow-chart.directive.js | 67 +++++++++++++------ .../templates/workflows/workflow.service.js | 10 ++- .../workflow-results.partial.html | 43 +++++------- 4 files changed, 76 insertions(+), 52 deletions(-) diff --git a/awx/ui/client/src/templates/workflows/workflow-chart/workflow-chart.block.less b/awx/ui/client/src/templates/workflows/workflow-chart/workflow-chart.block.less index 7263edbf02..4399e6504a 100644 --- a/awx/ui/client/src/templates/workflows/workflow-chart/workflow-chart.block.less +++ b/awx/ui/client/src/templates/workflows/workflow-chart/workflow-chart.block.less @@ -105,3 +105,11 @@ .WorkflowChart-activeNode { fill: @default-link; } +.WorkflowChart-elapsedHolder { + background-color: @b7grey; + color: @default-bg; + height: 13px; + width: 39px; + padding: 1px 3px; + border-radius: 4px; +} diff --git a/awx/ui/client/src/templates/workflows/workflow-chart/workflow-chart.directive.js b/awx/ui/client/src/templates/workflows/workflow-chart/workflow-chart.directive.js index cb6eeafa28..e91cc7c2d8 100644 --- a/awx/ui/client/src/templates/workflows/workflow-chart/workflow-chart.directive.js +++ b/awx/ui/client/src/templates/workflows/workflow-chart/workflow-chart.directive.js @@ -4,8 +4,8 @@ * All Rights Reserved *************************************************/ -export default [ '$state', - function($state) { +export default [ '$state', 'moment', + function($state, moment) { return { scope: { @@ -188,6 +188,7 @@ export default [ '$state', .attr("dy", ".35em") .attr("class", "WorkflowChart-startText") .text(function () { return "START"; }) + .attr("display", function() { return scope.mode === 'details' ? 'none' : null;}) .call(add_node); } else { @@ -223,10 +224,10 @@ export default [ '$state', .style("display", function(d) { return d.isActiveEdit ? null : "none"; }); thisNode.append("text") - .attr("x", function(d){ return (scope.mode === 'details' && d.job && d.job.jobStatus) ? 20 : rectW / 2; }) - .attr("y", function(d){ return (scope.mode === 'details' && d.job && d.job.jobStatus) ? 10 : rectH / 2; }) + .attr("x", function(d){ return (scope.mode === 'details' && d.job && d.job.status) ? 20 : rectW / 2; }) + .attr("y", function(d){ return (scope.mode === 'details' && d.job && d.job.status) ? 10 : rectH / 2; }) .attr("dy", ".35em") - .attr("text-anchor", function(d){ return (scope.mode === 'details' && d.job && d.job.jobStatus) ? "inherit" : "middle"; }) + .attr("text-anchor", function(d){ return (scope.mode === 'details' && d.job && d.job.status) ? "inherit" : "middle"; }) .attr("class", "WorkflowChart-defaultText WorkflowChart-nameText") .text(function (d) { return (d.unifiedJobTemplate && d.unifiedJobTemplate.name) ? d.unifiedJobTemplate.name : ""; @@ -291,7 +292,7 @@ export default [ '$state', .attr("y", rectH - 10) .attr("dy", ".35em") .attr("class", "WorkflowChart-detailsLink") - .style("display", function(d){ return d.job && d.job.jobStatus && d.job.unified_job_id ? null : "none"; }) + .style("display", function(d){ return d.job && d.job.status && d.job.id ? null : "none"; }) .text(function () { return "DETAILS"; }) @@ -386,7 +387,7 @@ export default [ '$state', let statusClass = "WorkflowChart-nodeStatus "; if(d.job){ - switch(d.job.jobStatus) { + switch(d.job.status) { case "pending": statusClass = "workflowChart-nodeStatus--running"; break; @@ -402,15 +403,37 @@ export default [ '$state', case "failed": statusClass = "workflowChart-nodeStatus--failed"; break; + case "error": + statusClass = "workflowChart-nodeStatus--failed"; + break; } } return statusClass; }) - .style("display", function(d) { return d.job && d.job.jobStatus ? null : "none"; }) + .style("display", function(d) { return d.job && d.job.status ? null : "none"; }) .attr("cy", 10) .attr("cx", 10) .attr("r", 6); + + thisNode.append("foreignObject") + .attr("x", 5) + .attr("y", 43) + .style("font-size","0.7em") + .attr("class", "WorkflowChart-elapsed") + .html(function (d) { + if(d.job && d.job.elapsed) { + let elapsedMs = d.job.elapsed * 1000; + let elapsedMoment = moment.duration(elapsedMs); + let paddedElapsedMoment = Math.floor(elapsedMoment.asHours()) < 10 ? "0" + Math.floor(elapsedMoment.asHours()) : Math.floor(elapsedMoment.asHours()); + let elapsedString = paddedElapsedMoment + moment.utc(elapsedMs).format(":mm:ss"); + return "
" + elapsedString + "
"; + } + else { + return ""; + } + }) + .style("display", function(d) { return (d.job && d.job.elapsed) ? null : "none"; }); } }); @@ -588,7 +611,7 @@ export default [ '$state', let statusClass = "WorkflowChart-nodeStatus "; if(d.job){ - switch(d.job.jobStatus) { + switch(d.job.status) { case "pending": statusClass += "workflowChart-nodeStatus--running"; break; @@ -604,17 +627,20 @@ export default [ '$state', case "failed": statusClass += "workflowChart-nodeStatus--failed"; break; + case "error": + statusClass = "workflowChart-nodeStatus--failed"; + break; } } return statusClass; }) - .style("display", function(d) { return d.job && d.job.jobStatus ? null : "none"; }) + .style("display", function(d) { return d.job && d.job.status ? null : "none"; }) .transition() .duration(0) .attr("r", 6) .each(function(d) { - if(d.job && d.job.jobStatus && (d.job.jobStatus === "pending" || d.job.jobStatus === "waiting" || d.job.jobStatus === "running")) { + if(d.job && d.job.status && (d.job.status === "pending" || d.job.status === "waiting" || d.job.status === "running")) { // Pulse the circle var circle = d3.select(this); (function repeat() { @@ -631,15 +657,15 @@ export default [ '$state', }); t.selectAll(".WorkflowChart-nameText") - .attr("x", function(d){ return (scope.mode === 'details' && d.job && d.job.jobStatus) ? 20 : rectW / 2; }) - .attr("y", function(d){ return (scope.mode === 'details' && d.job && d.job.jobStatus) ? 10 : rectH / 2; }) - .attr("text-anchor", function(d){ return (scope.mode === 'details' && d.job && d.job.jobStatus) ? "inherit" : "middle"; }) + .attr("x", function(d){ return (scope.mode === 'details' && d.job && d.job.status) ? 20 : rectW / 2; }) + .attr("y", function(d){ return (scope.mode === 'details' && d.job && d.job.status) ? 10 : rectH / 2; }) + .attr("text-anchor", function(d){ return (scope.mode === 'details' && d.job && d.job.status) ? "inherit" : "middle"; }) .text(function (d) { return (d.unifiedJobTemplate && d.unifiedJobTemplate.name) ? wrap(d.unifiedJobTemplate.name) : ""; }); t.selectAll(".WorkflowChart-detailsLink") - .style("display", function(d){ return d.job && d.job.jobStatus && d.job.unified_job_id ? null : "none"; }); + .style("display", function(d){ return d.job && d.job.status && d.job.id ? null : "none"; }); t.selectAll(".WorkflowChart-incompleteText") .style("display", function(d){ return d.unifiedJobTemplate || d.placeholder ? "none" : null; }); @@ -650,6 +676,9 @@ export default [ '$state', t.selectAll(".WorkflowChart-activeNode") .style("display", function(d) { return d.isActiveEdit ? null : "none"; }); + t.selectAll(".WorkflowChart-elapsed") + .style("display", function(d) { return (d.job && d.job.elapsed) ? null : "none"; }); + } function add_node() { @@ -702,15 +731,15 @@ export default [ '$state', d3.select(this).style("text-decoration", null); }); this.on("click", function(d) { - if(d.job.unified_job_id && d.unifiedJobTemplate) { + if(d.job.id && d.unifiedJobTemplate) { if(d.unifiedJobTemplate.unified_job_type === 'job') { - $state.go('jobDetail', {id: d.job.unified_job_id}); + $state.go('jobDetail', {id: d.job.id}); } else if(d.unifiedJobTemplate.unified_job_type === 'inventory_update') { - $state.go('inventorySyncStdout', {id: d.job.unified_job_id}); + $state.go('inventorySyncStdout', {id: d.job.id}); } else if(d.unifiedJobTemplate.unified_job_type === 'project_update') { - $state.go('scmUpdateStdout', {id: d.job.unified_job_id}); + $state.go('scmUpdateStdout', {id: d.job.id}); } } }); diff --git a/awx/ui/client/src/templates/workflows/workflow.service.js b/awx/ui/client/src/templates/workflows/workflow.service.js index 4e551c7582..f355c306f2 100644 --- a/awx/ui/client/src/templates/workflows/workflow.service.js +++ b/awx/ui/client/src/templates/workflows/workflow.service.js @@ -224,10 +224,8 @@ export default [function(){ } if(params.nodesObj[params.nodeId].summary_fields.job) { - treeNode.job = { - jobStatus: params.nodesObj[params.nodeId].summary_fields.job.status, - unified_job_id: params.nodesObj[params.nodeId].summary_fields.job.id - }; + treeNode.job = _.clone(params.nodesObj[params.nodeId].summary_fields.job); + //treeNode.job.unified_job_id = params.nodesObj[params.nodeId].summary_fields.job.id; } if(params.nodesObj[params.nodeId].summary_fields.unified_job_template) { @@ -282,8 +280,8 @@ export default [function(){ if(matchingNode) { matchingNode.job = { - jobStatus: params.status, - unified_job_id: params.unified_job_id + status: params.status, + id: params.unified_job_id }; } diff --git a/awx/ui/client/src/workflow-results/workflow-results.partial.html b/awx/ui/client/src/workflow-results/workflow-results.partial.html index ac04b114e8..b35ca65768 100644 --- a/awx/ui/client/src/workflow-results/workflow-results.partial.html +++ b/awx/ui/client/src/workflow-results/workflow-results.partial.html @@ -62,6 +62,21 @@
+ +
+ + +
+
@@ -85,32 +100,6 @@
- -
- - -
- - -
- -
- Workflow Job -
-
-
@@ -265,7 +254,7 @@
- + From 0ff5c06b2b2ae4e4bc7393d952df0dd18bcbcbfc Mon Sep 17 00:00:00 2001 From: Michael Abashian Date: Wed, 14 Dec 2016 09:41:32 -0500 Subject: [PATCH 07/17] Removed moment injection from the workflow chart directive - not needed --- .../workflows/workflow-chart/workflow-chart.directive.js | 4 ++-- awx/ui/client/src/templates/workflows/workflow.service.js | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/awx/ui/client/src/templates/workflows/workflow-chart/workflow-chart.directive.js b/awx/ui/client/src/templates/workflows/workflow-chart/workflow-chart.directive.js index e91cc7c2d8..e4557284b6 100644 --- a/awx/ui/client/src/templates/workflows/workflow-chart/workflow-chart.directive.js +++ b/awx/ui/client/src/templates/workflows/workflow-chart/workflow-chart.directive.js @@ -4,8 +4,8 @@ * All Rights Reserved *************************************************/ -export default [ '$state', 'moment', - function($state, moment) { +export default [ '$state', + function($state) { return { scope: { diff --git a/awx/ui/client/src/templates/workflows/workflow.service.js b/awx/ui/client/src/templates/workflows/workflow.service.js index f355c306f2..64b939ddcb 100644 --- a/awx/ui/client/src/templates/workflows/workflow.service.js +++ b/awx/ui/client/src/templates/workflows/workflow.service.js @@ -225,7 +225,6 @@ export default [function(){ if(params.nodesObj[params.nodeId].summary_fields.job) { treeNode.job = _.clone(params.nodesObj[params.nodeId].summary_fields.job); - //treeNode.job.unified_job_id = params.nodesObj[params.nodeId].summary_fields.job.id; } if(params.nodesObj[params.nodeId].summary_fields.unified_job_template) { From 94aafda7dfd3a1dbea16111841bdbbcedff444af Mon Sep 17 00:00:00 2001 From: Ken Hoes Date: Wed, 14 Dec 2016 10:15:07 -0500 Subject: [PATCH 08/17] Added logging, timeouts, changed dropdown labels, updated system section --- .../configuration-auth.controller.js | 6 +- .../auth-form/configuration-auth.partial.html | 7 +- .../configuration/configuration.block.less | 19 +- .../configuration/configuration.controller.js | 36 +++- .../jobs-form/configuration-jobs.form.js | 14 +- awx/ui/client/src/configuration/main.js | 13 +- .../configuration-system.controller.js | 165 ++++++++++++++++-- .../configuration-system.partial.html | 31 +++- .../sub-forms/system-activity-stream.form.js | 38 ++++ .../sub-forms/system-logging.form.js | 63 +++++++ .../system-form/sub-forms/system-misc.form.js | 42 +++++ 11 files changed, 396 insertions(+), 38 deletions(-) create mode 100644 awx/ui/client/src/configuration/system-form/sub-forms/system-activity-stream.form.js create mode 100644 awx/ui/client/src/configuration/system-form/sub-forms/system-logging.form.js create mode 100644 awx/ui/client/src/configuration/system-form/sub-forms/system-misc.form.js diff --git a/awx/ui/client/src/configuration/auth-form/configuration-auth.controller.js b/awx/ui/client/src/configuration/auth-form/configuration-auth.controller.js index 33d1a53c37..5c3d31a0bc 100644 --- a/awx/ui/client/src/configuration/auth-form/configuration-auth.controller.js +++ b/awx/ui/client/src/configuration/auth-form/configuration-auth.controller.js @@ -99,9 +99,9 @@ export default [ var dropdownOptions = [ {label: i18n._('Azure AD'), value: 'azure'}, - {label: i18n._('Github'), value: 'github'}, - {label: i18n._('Github Org'), value: 'github_org'}, - {label: i18n._('Github Team'), value: 'github_team'}, + {label: i18n._('GitHub'), value: 'github'}, + {label: i18n._('GitHub Org'), value: 'github_org'}, + {label: i18n._('GithHub Team'), value: 'github_team'}, {label: i18n._('Google OAuth2'), value: 'google_oauth'}, {label: i18n._('LDAP'), value: 'ldap'}, {label: i18n._('RADIUS'), value: 'radius'}, diff --git a/awx/ui/client/src/configuration/auth-form/configuration-auth.partial.html b/awx/ui/client/src/configuration/auth-form/configuration-auth.partial.html index 5efeeed532..71192e17c6 100644 --- a/awx/ui/client/src/configuration/auth-form/configuration-auth.partial.html +++ b/awx/ui/client/src/configuration/auth-form/configuration-auth.partial.html @@ -1,7 +1,7 @@
- -
+
+
Sub Category
+
+
diff --git a/awx/ui/client/src/configuration/configuration.block.less b/awx/ui/client/src/configuration/configuration.block.less index de4dae2e2c..0b2a1ea0ee 100644 --- a/awx/ui/client/src/configuration/configuration.block.less +++ b/awx/ui/client/src/configuration/configuration.block.less @@ -21,11 +21,26 @@ margin-left: 0; } -.Form-nav--dropdown { - width: 175px; +.Form-nav--dropdownContainer { + width: 285px; margin-top: -52px; margin-bottom: 22px; margin-left: auto; + display: flex; + justify-content: space-between; +} + +.Form-nav--dropdown { + width: 60%; +} + +.Form-nav--dropdownLabel { + text-transform: uppercase; + color: @default-interface-txt; + font-size: 14px; + font-weight: bold; + padding-right: 5px; + padding-top: 5px; } .Form-tabRow { diff --git a/awx/ui/client/src/configuration/configuration.controller.js b/awx/ui/client/src/configuration/configuration.controller.js index e5d3d6049d..153434ef84 100644 --- a/awx/ui/client/src/configuration/configuration.controller.js +++ b/awx/ui/client/src/configuration/configuration.controller.js @@ -17,8 +17,10 @@ export default [ 'configurationLdapForm', 'configurationRadiusForm', 'configurationSamlForm', + 'systemActivityStreamForm', + 'systemLoggingForm', + 'systemMiscForm', 'ConfigurationJobsForm', - 'ConfigurationSystemForm', 'ConfigurationUiForm', function( $scope, $rootScope, $state, $stateParams, $timeout, $q, Alert, ClearScope, @@ -33,8 +35,10 @@ export default [ configurationLdapForm, configurationRadiusForm, configurationSamlForm, + systemActivityStreamForm, + systemLoggingForm, + systemMiscForm, ConfigurationJobsForm, - ConfigurationSystemForm, ConfigurationUiForm ) { var vm = this; @@ -48,8 +52,10 @@ export default [ 'ldap': configurationLdapForm, 'radius': configurationRadiusForm, 'saml': configurationSamlForm, + 'activity_stream': systemActivityStreamForm, + 'logging': systemLoggingForm, + 'misc': systemMiscForm, 'jobs': ConfigurationJobsForm, - 'system': ConfigurationSystemForm, 'ui': ConfigurationUiForm }; @@ -84,19 +90,24 @@ export default [ lastForm: '', currentForm: '', currentAuth: '', + currentSystem: '', setCurrent: function(form) { this.lastForm = this.currentForm; this.currentForm = form; }, - setCurrentAuth: function(form) { - this.currentAuth = form; - this.setCurrent(this.currentAuth); - }, getCurrent: function() { return this.currentForm; }, currentFormName: function() { return 'configuration_' + this.currentForm + '_template_form'; + }, + setCurrentAuth: function(form) { + this.currentAuth = form; + this.setCurrent(this.currentAuth); + }, + setCurrentSystem: function(form) { + this.currentSystem = form; + this.setCurrent(this.currentSystem); } }; @@ -182,6 +193,7 @@ export default [ } function active(setForm) { + // Authentication and System's sub-module dropdowns handled first: if (setForm === 'auth') { // Default to 'azure' on first load if (formTracker.currentAuth === '') { @@ -190,7 +202,15 @@ export default [ // If returning to auth tab reset current form to previously viewed formTracker.setCurrentAuth(formTracker.currentAuth); } - } else { + } else if (setForm === 'system') { + if (formTracker.currentSystem === '') { + formTracker.setCurrentSystem('misc'); + } else { + // If returning to system tab reset current form to previously viewed + formTracker.setCurrentSystem(formTracker.currentSystem); + } + } + else { formTracker.setCurrent(setForm); } vm.activeTab = setForm; diff --git a/awx/ui/client/src/configuration/jobs-form/configuration-jobs.form.js b/awx/ui/client/src/configuration/jobs-form/configuration-jobs.form.js index 05b9d664a7..caf0392c24 100644 --- a/awx/ui/client/src/configuration/jobs-form/configuration-jobs.form.js +++ b/awx/ui/client/src/configuration/jobs-form/configuration-jobs.form.js @@ -46,7 +46,19 @@ }, AWX_PROOT_ENABLED: { type: 'toggleSwitch', - } + }, + DEFAULT_JOB_TIMEOUT: { + type: 'text', + reset: 'DEFAULT_JOB_TIMEOUT', + }, + DEFAULT_INVENTORY_UPDATE_TIMEOUT: { + type: 'text', + reset: 'DEFAULT_INVENTORY_UPDATE_TIMEOUT', + }, + DEFAULT_PROJECT_UPDATE_TIMEOUT: { + type: 'text', + reset: 'DEFAULT_PROJECT_UPDATE_TIMEOUT', + }, }, buttons: { diff --git a/awx/ui/client/src/configuration/main.js b/awx/ui/client/src/configuration/main.js index 74dce80c8f..f37bab3d84 100644 --- a/awx/ui/client/src/configuration/main.js +++ b/awx/ui/client/src/configuration/main.js @@ -20,8 +20,12 @@ import configurationLdapForm from './auth-form/sub-forms/auth-ldap.form.js'; import configurationRadiusForm from './auth-form/sub-forms/auth-radius.form.js'; import configurationSamlForm from './auth-form/sub-forms/auth-saml.form'; +//system sub-forms +import systemActivityStreamForm from './system-form/sub-forms/system-activity-stream.form.js'; +import systemLoggingForm from './system-form/sub-forms/system-logging.form.js'; +import systemMiscForm from './system-form/sub-forms/system-misc.form.js'; + import configurationJobsForm from './jobs-form/configuration-jobs.form'; -import configurationSystemForm from './system-form/configuration-system.form'; import configurationUiForm from './ui-form/configuration-ui.form'; export default @@ -36,10 +40,15 @@ angular.module('configuration', []) .factory('configurationLdapForm', configurationLdapForm) .factory('configurationRadiusForm', configurationRadiusForm) .factory('configurationSamlForm', configurationSamlForm) + //system forms + .factory('systemActivityStreamForm', systemActivityStreamForm) + .factory('systemLoggingForm', systemLoggingForm) + .factory('systemMiscForm', systemMiscForm) + //other forms .factory('ConfigurationJobsForm', configurationJobsForm) - .factory('ConfigurationSystemForm', configurationSystemForm) .factory('ConfigurationUiForm', configurationUiForm) + //helpers and services .factory('ConfigurationUtils', ConfigurationUtils) .service('ConfigurationService', configurationService) diff --git a/awx/ui/client/src/configuration/system-form/configuration-system.controller.js b/awx/ui/client/src/configuration/system-form/configuration-system.controller.js index c22131c2bc..2774c8ade5 100644 --- a/awx/ui/client/src/configuration/system-form/configuration-system.controller.js +++ b/awx/ui/client/src/configuration/system-form/configuration-system.controller.js @@ -5,22 +5,120 @@ *************************************************/ export default [ - '$rootScope', '$scope', '$state', 'AngularCodeMirror', 'Authorization', 'ConfigurationSystemForm', 'ConfigurationService', - 'ConfigurationUtils', 'GenerateForm', + '$rootScope', '$scope', '$state', '$stateParams', + 'AngularCodeMirror', + 'systemActivityStreamForm', + 'systemLoggingForm', + 'systemMiscForm', + 'ConfigurationService', + 'ConfigurationUtils', + 'CreateSelect2', + 'GenerateForm', + 'i18n', function( - $rootScope, $scope, $state, AngularCodeMirror, Authorization, ConfigurationSystemForm, ConfigurationService, ConfigurationUtils, GenerateForm + $rootScope, $scope, $state, $stateParams, + AngularCodeMirror, + systemActivityStreamForm, + systemLoggingForm, + systemMiscForm, + ConfigurationService, + ConfigurationUtils, + CreateSelect2, + GenerateForm, + i18n ) { var systemVm = this; - var generator = GenerateForm; - var form = ConfigurationSystemForm; - var keys = _.keys(form.fields); - _.each(keys, function(key) { - addFieldInfo(form, key); + var generator = GenerateForm; + var formTracker = $scope.$parent.vm.formTracker; + var dropdownValue = 'misc'; + var activeSystemForm = 'misc'; + + if ($stateParams.currentTab === 'system') { + formTracker.setCurrentSystem(activeSystemForm); + } + + var activeForm = function() { + if(!$scope.$parent[formTracker.currentFormName()].$dirty) { + systemVm.activeSystemForm = systemVm.dropdownValue; + formTracker.setCurrentSystem(systemVm.activeSystemForm); + } else { + var msg = i18n._('You have unsaved changes. Would you like to proceed without saving?'); + var title = i18n._('Warning: Unsaved Changes'); + var buttons = [{ + label: i18n._('Discard changes'), + "class": "btn Form-cancelButton", + "id": "formmodal-cancel-button", + onClick: function() { + $scope.$parent.vm.populateFromApi(); + $scope.$parent[formTracker.currentFormName()].$setPristine(); + systemVm.activeSystemForm = systemVm.dropdownValue; + formTracker.setCurrentSystem(systemVm.activeSystemForm); + $('#FormModal-dialog').dialog('close'); + } + }, { + label: i18n._('Save changes'), + onClick: function() { + $scope.$parent.vm.formSave() + .then(function() { + $scope.$parent[formTracker.currentFormName()].$setPristine(); + $scope.$parent.vm.populateFromApi(); + systemVm.activeSystemForm = systemVm.dropdownValue; + formTracker.setCurrentSystem(systemVm.activeSystemForm); + $('#FormModal-dialog').dialog('close'); + }); + }, + "class": "btn btn-primary", + "id": "formmodal-save-button" + }]; + $scope.$parent.vm.triggerModal(msg, title, buttons); + } + formTracker.setCurrentSystem(systemVm.activeSystemForm); + }; + + var dropdownOptions = [ + {label: i18n._('Misc. System'), value: 'misc'}, + {label: i18n._('Activity Stream'), value: 'activity_stream'}, + {label: i18n._('Logging'), value: 'logging'}, + ]; + + CreateSelect2({ + element: '#system-configure-dropdown-nav', + multiple: false, }); - // Disable the save button for system auditors - form.buttons.save.disabled = $rootScope.user_is_system_auditor; + var systemForms = [{ + formDef: systemLoggingForm, + id: 'system-logging-form' + }, { + formDef: systemActivityStreamForm, + id: 'system-activity-stream-form' + }, { + formDef: systemMiscForm, + id: 'system-misc-form' + }]; + + var forms = _.pluck(systemForms, 'formDef'); + _.each(forms, function(form) { + var keys = _.keys(form.fields); + _.each(keys, function(key) { + if($scope.$parent.configDataResolve[key].type === 'choice') { + // Create options for dropdowns + var optionsGroup = key + '_options'; + $scope.$parent[optionsGroup] = []; + _.each($scope.$parent.configDataResolve[key].choices, function(choice){ + $scope.$parent[optionsGroup].push({ + name: choice[0], + label: choice[1], + value: choice[0] + }); + }); + } + addFieldInfo(form, key); + }); + // Disable the save button for system auditors + form.buttons.save.disabled = $rootScope.user_is_system_auditor; + }); function addFieldInfo(form, key) { _.extend(form.fields[key], { @@ -29,21 +127,56 @@ export default [ name: key, toggleSource: key, dataPlacement: 'top', + placeholder: ConfigurationUtils.formatPlaceholder($scope.$parent.configDataResolve[key].placeholder, key) || null, dataTitle: $scope.$parent.configDataResolve[key].label, required: $scope.$parent.configDataResolve[key].required, ngDisabled: $rootScope.user_is_system_auditor }); } - generator.inject(form, { - id: 'configure-system-form', - mode: 'edit', - scope: $scope.$parent, - related: true + $scope.$parent.parseType = 'json'; + + _.each(systemForms, function(form) { + generator.inject(form.formDef, { + id: form.id, + mode: 'edit', + scope: $scope.$parent, + related: true + }); + }); + + var dropdownRendered = false; + + $scope.$on('populated', function() { + + var opts = []; + if($scope.$parent.LOG_AGGREGATOR_TYPE !== null) { + _.each(ConfigurationUtils.listToArray($scope.$parent.LOG_AGGREGATOR_TYPE), function(type) { + opts.push({ + id: type, + text: type + }); + }); + } + + if(!dropdownRendered) { + dropdownRendered = true; + CreateSelect2({ + element: '#configuration_logging_template_LOG_AGGREGATOR_TYPE', + multiple: true, + placeholder: i18n._('Select types'), + opts: opts + }); + } + }); angular.extend(systemVm, { - + activeForm: activeForm, + activeSystemForm: activeSystemForm, + dropdownOptions: dropdownOptions, + dropdownValue: dropdownValue, + systemForms: systemForms }); } ]; diff --git a/awx/ui/client/src/configuration/system-form/configuration-system.partial.html b/awx/ui/client/src/configuration/system-form/configuration-system.partial.html index 7ff91b4441..0b039e761b 100644 --- a/awx/ui/client/src/configuration/system-form/configuration-system.partial.html +++ b/awx/ui/client/src/configuration/system-form/configuration-system.partial.html @@ -1,9 +1,34 @@
- +
+
Sub Category
+
+ +
+
-
+ +
+
+ +
+
+
+
+ +
+
+
+
+ +
+
diff --git a/awx/ui/client/src/configuration/system-form/sub-forms/system-activity-stream.form.js b/awx/ui/client/src/configuration/system-form/sub-forms/system-activity-stream.form.js new file mode 100644 index 0000000000..09cf80eccd --- /dev/null +++ b/awx/ui/client/src/configuration/system-form/sub-forms/system-activity-stream.form.js @@ -0,0 +1,38 @@ +/************************************************* + * Copyright (c) 2016 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + + export default ['i18n', function(i18n) { + return { + name: 'configuration_activity_stream_template', + showActions: true, + showHeader: false, + + fields: { + ACTIVITY_STREAM_ENABLED: { + type: 'toggleSwitch', + }, + ACTIVITY_STREAM_ENABLED_FOR_INVENTORY_SYNC: { + type: 'toggleSwitch' + } + }, + + buttons: { + reset: { + ngClick: 'vm.resetAllConfirm()', + label: i18n._('Reset All'), + class: 'Form-button--left Form-cancelButton' + }, + cancel: { + ngClick: 'vm.formCancel()', + }, + save: { + ngClick: 'vm.formSave()', + ngDisabled: true + } + } + }; + } +]; diff --git a/awx/ui/client/src/configuration/system-form/sub-forms/system-logging.form.js b/awx/ui/client/src/configuration/system-form/sub-forms/system-logging.form.js new file mode 100644 index 0000000000..2329fde488 --- /dev/null +++ b/awx/ui/client/src/configuration/system-form/sub-forms/system-logging.form.js @@ -0,0 +1,63 @@ +/************************************************* + * Copyright (c) 2016 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + + export default ['i18n', function(i18n) { + return { + name: 'configuration_logging_template', + showActions: true, + showHeader: false, + + fields: { + LOG_AGGREGATOR_HOST: { + type: 'text', + reset: 'LOG_AGGREGATOR_HOST' + }, + LOG_AGGREGATOR_PORT: { + type: 'text', + reset: 'LOG_AGGREGATOR_PORT' + }, + LOG_AGGREGATOR_TYPE: { + type: 'select', + reset: 'LOG_AGGREGATOR_TYPE', + ngOptions: 'type.label for type in LOG_AGGREGATOR_TYPE_options track by type.value', + }, + LOG_AGGREGATOR_USERNAME: { + type: 'text', + reset: 'LOG_AGGREGATOR_USERNAME' + }, + LOG_AGGREGATOR_PASSWORD: { + type: 'text', + reset: 'LOG_AGGREGATOR_PASSWORD' + }, + LOG_AGGREGATOR_LOGGERS: { + type: 'textarea', + reset: 'LOG_AGGREGATOR_PASSWORD' + }, + LOG_AGGREGATOR_INDIVIDUAL_FACTS: { + type: 'toggleSwitch', + }, + LOG_AGGREGATOR_ENABLED: { + type: 'toggleSwitch', + } + }, + + buttons: { + reset: { + ngClick: 'vm.resetAllConfirm()', + label: i18n._('Reset All'), + class: 'Form-button--left Form-cancelButton' + }, + cancel: { + ngClick: 'vm.formCancel()', + }, + save: { + ngClick: 'vm.formSave()', + ngDisabled: true + } + } + }; + } +]; diff --git a/awx/ui/client/src/configuration/system-form/sub-forms/system-misc.form.js b/awx/ui/client/src/configuration/system-form/sub-forms/system-misc.form.js new file mode 100644 index 0000000000..690418f323 --- /dev/null +++ b/awx/ui/client/src/configuration/system-form/sub-forms/system-misc.form.js @@ -0,0 +1,42 @@ +/************************************************* + * Copyright (c) 2016 Ansible, Inc. + * + * All Rights Reserved + *************************************************/ + +export default ['i18n', function(i18n) { + return { + showHeader: false, + name: 'configuration_misc_template', + showActions: true, + + fields: { + TOWER_URL_BASE: { + type: 'text', + reset: 'TOWER_URL_BASE', + }, + TOWER_ADMIN_ALERTS: { + type: 'toggleSwitch', + }, + ORG_ADMINS_CAN_SEE_ALL_USERS: { + type: 'toggleSwitch', + } + }, + + buttons: { + reset: { + ngClick: 'vm.resetAllConfirm()', + label: i18n._('Reset All'), + class: 'Form-button--left Form-cancelButton' + }, + cancel: { + ngClick: 'vm.formCancel()', + }, + save: { + ngClick: 'vm.formSave()', + ngDisabled: true + } + } + }; +} +]; From c4c5e674274d0e2237d2f70131abcf85ac22be54 Mon Sep 17 00:00:00 2001 From: Michael Abashian Date: Wed, 14 Dec 2016 10:16:33 -0500 Subject: [PATCH 09/17] Properly redirect after creating a new credential --- awx/ui/client/src/helpers/Credentials.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/awx/ui/client/src/helpers/Credentials.js b/awx/ui/client/src/helpers/Credentials.js index a58c37bce8..ee7d187c39 100644 --- a/awx/ui/client/src/helpers/Credentials.js +++ b/awx/ui/client/src/helpers/Credentials.js @@ -289,14 +289,12 @@ angular.module('CredentialsHelper', ['Utilities']) Wait('stop'); var base = $location.path().replace(/^\//, '').split('/')[0]; - if (base === 'credentials') { - ReturnToCaller(); + if (base === 'credentials') { + $state.go('credentials.edit', {credential_id: data.id}, {reload: true}); } else { ReturnToCaller(1); } - $state.go('credentials.edit', {credential_id: data.id}, {reload: true}); - }) .error(function (data, status) { Wait('stop'); From b947367606217c7de81f46b0af6645c931d37d14 Mon Sep 17 00:00:00 2001 From: Chris Church Date: Wed, 14 Dec 2016 11:54:21 -0500 Subject: [PATCH 10/17] Add default for ldap group type. --- awx/sso/conf.py | 1 + 1 file changed, 1 insertion(+) diff --git a/awx/sso/conf.py b/awx/sso/conf.py index c3ab7f7e56..237209a017 100644 --- a/awx/sso/conf.py +++ b/awx/sso/conf.py @@ -331,6 +331,7 @@ register( category=_('LDAP'), category_slug='ldap', feature_required='ldap', + default='MemberDNGroupType', ) register( From 13798d352c33d71161bdcd38320e1420911271b4 Mon Sep 17 00:00:00 2001 From: Wayne Witzel III Date: Wed, 14 Dec 2016 12:49:50 -0500 Subject: [PATCH 11/17] use DjangoJSONEncoder --- awx/main/consumers.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/awx/main/consumers.py b/awx/main/consumers.py index a8c56a264d..2cb1f450f2 100644 --- a/awx/main/consumers.py +++ b/awx/main/consumers.py @@ -6,6 +6,7 @@ from channels import Group from channels.sessions import channel_session from django.contrib.auth.models import User +from django.core.serializers.json import DjangoJSONEncoder from awx.main.models.organization import AuthToken @@ -86,4 +87,4 @@ def ws_receive(message): def emit_channel_notification(group, payload): - Group(group).send({"text": json.dumps(payload)}) + Group(group).send({"text": json.dumps(payload, cls=DjangoJSONEncoder)}) From b9aab38185212a3e215db2743a3defa1ad75a17b Mon Sep 17 00:00:00 2001 From: Chris Church Date: Wed, 14 Dec 2016 13:20:08 -0500 Subject: [PATCH 12/17] Handle TypeError when lookup is not valid for a given field. --- awx/api/filters.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awx/api/filters.py b/awx/api/filters.py index 5146ff0cd2..6885d73326 100644 --- a/awx/api/filters.py +++ b/awx/api/filters.py @@ -242,7 +242,7 @@ class FieldLookupBackend(BaseFilterBackend): queryset = queryset.filter(q) queryset = queryset.filter(*args).distinct() return queryset - except (FieldError, FieldDoesNotExist, ValueError) as e: + except (FieldError, FieldDoesNotExist, ValueError, TypeError) as e: raise ParseError(e.args[0]) except ValidationError as e: raise ParseError(e.messages) From 46e74a24f05b2cd97a5309ed9316fa9683cea96a Mon Sep 17 00:00:00 2001 From: jaredevantabor Date: Wed, 14 Dec 2016 10:27:22 -0800 Subject: [PATCH 13/17] adding status line to beginning of left side panel --- .../src/job-results/job-results.partial.html | 51 +++++-------------- .../workflow-results.partial.html | 20 +++++++- 2 files changed, 31 insertions(+), 40 deletions(-) diff --git a/awx/ui/client/src/job-results/job-results.partial.html b/awx/ui/client/src/job-results/job-results.partial.html index 4b2d8be6ea..c7fd2a1e0d 100644 --- a/awx/ui/client/src/job-results/job-results.partial.html +++ b/awx/ui/client/src/job-results/job-results.partial.html @@ -62,6 +62,19 @@
+ +
+ +
+ + {{ status_label }} +
+
+
@@ -387,44 +400,6 @@
- - - - - - -
diff --git a/awx/ui/client/src/workflow-results/workflow-results.partial.html b/awx/ui/client/src/workflow-results/workflow-results.partial.html index ac04b114e8..86f0ad13a4 100644 --- a/awx/ui/client/src/workflow-results/workflow-results.partial.html +++ b/awx/ui/client/src/workflow-results/workflow-results.partial.html @@ -62,6 +62,19 @@
+ +
+ +
+ + {{ status_label }} +
+
+
@@ -180,7 +193,7 @@
- +
@@ -195,7 +208,10 @@
+ fa icon-job-{{ workflow.status }}" + aw-tool-tip="Job {{status_label}}" + aw-tip-placement="top" + data-original-title> {{ workflow.name }}
From 0a6d2f179e2a33555cb1be69777b90ab5d76bec8 Mon Sep 17 00:00:00 2001 From: Chris Church Date: Wed, 14 Dec 2016 13:50:25 -0500 Subject: [PATCH 14/17] Extend stdout background to width of text. --- awx/static/api/api.css | 3 +++ 1 file changed, 3 insertions(+) diff --git a/awx/static/api/api.css b/awx/static/api/api.css index 61d51fae12..3b18c4273d 100644 --- a/awx/static/api/api.css +++ b/awx/static/api/api.css @@ -151,6 +151,9 @@ body .prettyprint .lit { body .prettyprint .str { color: #D9534F; } +body div.ansi_back { + display: inline-block; +} body .well.tab-content { padding: 20px; From a61e729ebbc27a85e492010f3571cb659c8f8762 Mon Sep 17 00:00:00 2001 From: Matthew Jones Date: Wed, 14 Dec 2016 15:05:28 -0500 Subject: [PATCH 15/17] Purge event res dict if it is over a certain size Also purge/update some old settings values --- awx/lib/tower_display_callback/events.py | 4 +++- awx/main/tasks.py | 1 + awx/settings/defaults.py | 17 +++++------------ 3 files changed, 9 insertions(+), 13 deletions(-) diff --git a/awx/lib/tower_display_callback/events.py b/awx/lib/tower_display_callback/events.py index 0909ed460d..c17cf2c7f1 100644 --- a/awx/lib/tower_display_callback/events.py +++ b/awx/lib/tower_display_callback/events.py @@ -173,7 +173,9 @@ class EventContext(object): if event_data.get(key, False): event = key break - + max_res = int(os.getenv("MAX_EVENT_RES", 700000)) + if event not in ('playbook_on_stats',) and "res" in event_data and len(str(event_data['res'])) > max_res: + event_data['res'] = {} event_dict = dict(event=event, event_data=event_data) for key in event_data.keys(): if key in ('job_id', 'ad_hoc_command_id', 'uuid', 'parent_uuid', 'created', 'artifact_data'): diff --git a/awx/main/tasks.py b/awx/main/tasks.py index e0dcae0fca..addbe4c8f2 100644 --- a/awx/main/tasks.py +++ b/awx/main/tasks.py @@ -809,6 +809,7 @@ class RunJob(BaseTask): env['REST_API_URL'] = settings.INTERNAL_API_URL env['REST_API_TOKEN'] = job.task_auth_token or '' env['TOWER_HOST'] = settings.TOWER_URL_BASE + env['MAX_EVENT_RES'] = settings.MAX_EVENT_RES_DATA env['CALLBACK_QUEUE'] = settings.CALLBACK_QUEUE env['CALLBACK_CONNECTION'] = settings.BROKER_URL if getattr(settings, 'JOB_CALLBACK_DEBUG', False): diff --git a/awx/settings/defaults.py b/awx/settings/defaults.py index fbb9c8fb73..9c758dfb13 100644 --- a/awx/settings/defaults.py +++ b/awx/settings/defaults.py @@ -150,7 +150,11 @@ ALLOWED_HOSTS = [] REMOTE_HOST_HEADERS = ['REMOTE_ADDR', 'REMOTE_HOST'] # Note: This setting may be overridden by database settings. -STDOUT_MAX_BYTES_DISPLAY = 1048576 +STDOUT_MAX_BYTES_DISPLAY = 10485760 + +# The maximum size of the ansible callback event's res data structure +# beyond this limit and the value will be removed +MAX_EVENT_RES_DATA = 700000 # Note: This setting may be overridden by database settings. EVENT_STDOUT_MAX_BYTES_DISPLAY = 1024 @@ -522,17 +526,6 @@ ANSIBLE_FORCE_COLOR = True # the celery task. AWX_TASK_ENV = {} -# Maximum number of job events processed by the callback receiver worker process -# before it recycles -JOB_EVENT_RECYCLE_THRESHOLD = 3000 - -# Number of workers used to proecess job events in parallel -JOB_EVENT_WORKERS = 4 - -# Maximum number of job events that can be waiting on a single worker queue before -# it can be skipped as too busy -JOB_EVENT_MAX_QUEUE_SIZE = 100 - # Flag to enable/disable updating hosts M2M when saving job events. CAPTURE_JOB_EVENT_HOSTS = False From 2988ac19047eccc620f6807f81ab09fbe861823c Mon Sep 17 00:00:00 2001 From: Chris Church Date: Wed, 14 Dec 2016 15:12:27 -0500 Subject: [PATCH 16/17] Only use known stats keys for determining hostnames to use for job host summaries. --- awx/main/models/jobs.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/awx/main/models/jobs.py b/awx/main/models/jobs.py index b7acf21775..19b62c7694 100644 --- a/awx/main/models/jobs.py +++ b/awx/main/models/jobs.py @@ -1073,8 +1073,8 @@ class JobEvent(CreatedModifiedModel): from awx.main.models.inventory import Host hostnames = set() try: - for v in self.event_data.values(): - hostnames.update(v.keys()) + for stat in ('changed', 'dark', 'failures', 'ok', 'processed', 'skipped'): + hostnames.update(self.event_data.get(stat, {}).keys()) except AttributeError: # In case event_data or v isn't a dict. pass with ignore_inventory_computed_fields(): From 1cd2a762bec82a1da79def356684506dbc43d3f3 Mon Sep 17 00:00:00 2001 From: Matthew Jones Date: Wed, 14 Dec 2016 15:20:04 -0500 Subject: [PATCH 17/17] Reset max bytes display --- awx/settings/defaults.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awx/settings/defaults.py b/awx/settings/defaults.py index 9c758dfb13..876ba56c85 100644 --- a/awx/settings/defaults.py +++ b/awx/settings/defaults.py @@ -150,7 +150,7 @@ ALLOWED_HOSTS = [] REMOTE_HOST_HEADERS = ['REMOTE_ADDR', 'REMOTE_HOST'] # Note: This setting may be overridden by database settings. -STDOUT_MAX_BYTES_DISPLAY = 10485760 +STDOUT_MAX_BYTES_DISPLAY = 1048576 # The maximum size of the ansible callback event's res data structure # beyond this limit and the value will be removed