Skip to content

Commit edaad61

Browse files
committed
WIP: Add json parameter to clean up stdout into valid JSON
1 parent ddde896 commit edaad61

File tree

6 files changed

+138
-2
lines changed

6 files changed

+138
-2
lines changed

actions/ansible.py

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
#!/usr/bin/env python
22

3+
import json
4+
import os
5+
import shutil
36
import sys
4-
from lib.ansible_base import AnsibleBaseRunner
7+
import tempfile
8+
from lib.ansible_base import AnsibleBaseRunner, ParamaterConflict
59

610
__all__ = [
711
'AnsibleRunner'
@@ -33,6 +37,45 @@ class AnsibleRunner(AnsibleBaseRunner):
3337

3438
def __init__(self, *args, **kwargs):
3539
super(AnsibleRunner, self).__init__(*args, **kwargs)
40+
self.tree_dir = None
41+
self.one_line = False
42+
if '--one_line' in args:
43+
self.one_line = True
44+
45+
def handle_json_arg(self):
46+
if next((True for arg in self.args if arg.startswith('--tree')), False):
47+
msg = "--json uses --tree internally. Setting both --tree and --json is not supported."
48+
raise ParamaterConflict(msg)
49+
execution_id = os.environ.get('ST2_ACTION_EXECUTION_ID', 'EXECUTION_ID_NA')
50+
self.tree_dir = tempfile.mkdtemp(prefix='{}.'.format(execution_id))
51+
52+
tree_arg = '--tree {}'.format(self.tree_dir)
53+
self.args.append(tree_arg)
54+
self.stdout = open(os.devnull, 'w')
55+
56+
def output_json(self):
57+
output = {}
58+
for host in os.listdir(self.tree_dir):
59+
# one file per host in tree dir; name of host is name of file
60+
with open(os.path.join(self.tree_dir, host), 'r') as host_output:
61+
try:
62+
output[host] = json.load(host_output)
63+
except ValueError:
64+
# something is messed up in the json, so include it as a string.
65+
host_output.seek(0)
66+
output[host] = host_output.read()
67+
if self.one_line:
68+
print(json.dumps(output))
69+
else:
70+
print(json.dumps(output, indent=2))
71+
72+
def cleanup(self):
73+
shutil.rmtree(self.tree_dir)
74+
self.stdout.close()
75+
76+
def post_execute(self):
77+
self.output_json()
78+
self.cleanup()
3679

3780

3881
if __name__ == '__main__':

actions/ansible_playbook.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
#!/usr/bin/env python
22

3+
from __future__ import print_function
4+
5+
import os
6+
import subprocess
7+
import re
38
import sys
9+
410
from lib.ansible_base import AnsibleBaseRunner
511

612
__all__ = [
@@ -38,6 +44,52 @@ class AnsiblePlaybookRunner(AnsibleBaseRunner):
3844
def __init__(self, *args, **kwargs):
3945
super(AnsiblePlaybookRunner, self).__init__(*args, **kwargs)
4046

47+
def handle_json_arg(self):
48+
os.environ['ANSIBLE_CALLBAKC_PLUGIN'] = 'json'
49+
self.stdout = subprocess.PIPE
50+
# TODO: --json is probably not be compatible with other options
51+
# like syntax-check, verbose, and possibly others
52+
53+
def popen_call(self, p):
54+
"""
55+
:param subprocess.Popen p:
56+
:return:
57+
"""
58+
if not self.json_output:
59+
return super(AnsiblePlaybookRunner, self).popen_call(p)
60+
61+
# lines that should go to stderr instead of stdout
62+
stderr_lines = re.compile(
63+
r'('
64+
65+
# these were identified in this closed PR:
66+
# https://github.com/ansible/ansible/pull/17448/files
67+
# see https://github.com/ansible/ansible/issues/17122
68+
r"^\tto retry, use: --limit @.*$"
69+
r'|'
70+
r"^skipping playbook include '.*' due to conditional test failure$"
71+
r'|'
72+
r"^statically included: .*$"
73+
74+
# other possibilities:
75+
# r'|'
76+
# r'^to see the full traceback, use -vvv$'
77+
# r'|'
78+
# r"^The full traceback was:$"
79+
# but then I would somehow have to include the traceback as well...
80+
81+
r')'
82+
)
83+
84+
while p.poll() is None:
85+
line = p.stdout.readline()
86+
if re.match(stderr_lines, line):
87+
print(line, file=sys.stderr)
88+
else:
89+
print(line)
90+
91+
return p.returncode
92+
4193

4294
if __name__ == '__main__':
4395
AnsiblePlaybookRunner(sys.argv).execute()

actions/command.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,3 +105,6 @@ parameters:
105105
version:
106106
description: "Show ansible version number and exit"
107107
type: boolean
108+
json:
109+
description: "Clean up Ansible's output to ensure it is valid jSON"
110+
type: boolean

actions/command_local.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,3 +105,6 @@ parameters:
105105
version:
106106
description: "Show ansible version number and exit"
107107
type: boolean
108+
json:
109+
description: "Clean up Ansible's output to ensure it is valid jSON"
110+
type: boolean

actions/lib/ansible_base.py

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,11 @@ def __init__(self, args):
2323
:param args: Input command line arguments
2424
:type args: ``list``
2525
"""
26+
self.json_output = False
27+
self.stdout = None
28+
2629
self.args = args[1:]
30+
self._json_output_arg()
2731
self._parse_extra_vars() # handle multiple entries in --extra_vars arg
2832
self._prepend_venv_path()
2933

@@ -96,17 +100,41 @@ def _prepend_venv_path():
96100

97101
os.environ['PATH'] = ':'.join(new_path)
98102

103+
def _json_output_arg(self):
104+
for i, arg in enumerate(self.args):
105+
if '--json' in arg and hasattr(self, 'handle_json_arg'):
106+
self.json_output = True
107+
self.handle_json_arg()
108+
# if ansible-playbook, add env arg
109+
del self.args[i] # The json arg is a ST2 specific addition & should not pass on.
110+
break
111+
elif '--one_line' in arg:
112+
self.one_line = True
113+
114+
def handle_json_arg(self):
115+
pass
116+
117+
def popen_call(self, p):
118+
return p.wait()
119+
99120
def execute(self):
100121
"""
101122
Execute the command and stream stdout and stderr output
102123
from child process as it appears without delay.
103124
Terminate with child's exit code.
104125
"""
105-
exit_code = subprocess.call(self.cmd, env=os.environ.copy())
126+
p = subprocess.Popen(self.cmd, env=os.environ.copy(), stdout=self.stdout)
127+
exit_code = self.popen_call(p)
106128
if exit_code is not 0:
107129
sys.stderr.write('Executed command "%s"\n' % ' '.join(self.cmd))
130+
131+
self.post_execute()
132+
108133
sys.exit(exit_code)
109134

135+
def post_execute(self):
136+
pass
137+
110138
@property
111139
@shell.replace_args('REPLACEMENT_RULES')
112140
def cmd(self):
@@ -139,3 +167,7 @@ def binary(self):
139167
sys.exit(1)
140168

141169
return binary_path
170+
171+
172+
class ParamaterConflict(Exception):
173+
pass

actions/playbook.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,3 +117,6 @@ parameters:
117117
version:
118118
description: "Show ansible version number and exit"
119119
type: boolean
120+
json:
121+
description: "Clean up Ansible's output to ensure it is valid jSON"
122+
type: boolean

0 commit comments

Comments
 (0)