Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,33 @@
# Changelog

## v0.6.0

* Add a json parameter to `ansible.command` and `ansible.command_local` actions. When False (the default), stdout is not changed. When True, this replaces ansible's stdout with a valid JSON object. We do this by using ansible's `--tree` argument to save the output to a temporary directory, and then sending a json object where the node name is the key, and the ansible output is the value.

```
$ st2 run ansible.command_local become=true module_name=setup json=True
.
id: 594d6657c4da5f08e9ec7c51
status: succeeded
parameters:
become: true
json: true
module_name: setup
result:
failed: false
return_code: 0
stderr: ''
stdout:
127.0.0.1:
ansible_facts:
ansible_all_ipv4_addresses:
...
changed: false
succeeded: true

```


## v0.5.0

* Added ability to use yaml structures to pass arbitrarily complex values through extra_vars. key=value and @file syntax is still supported. Example usage:
Expand Down
46 changes: 46 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,52 @@ ansible all -c local -i '127.0.0.1,' -a 'echo $TERM'
ansible all --connection=local --inventory-file='127.0.0.1,' --args='echo $TERM'
```

##### structured output from `ansible.command` and `ansible.command_local`:

To get a JSON object back from `ansible.command*`, set the json parameter to True. This uses ansible's `--tree` output to generate a JSON object with one element per node: `{"node-name": node-ouput}`


```
$ st2 run ansible.command_local become=true module_name=setup json=True
.
id: 594d6657c4da5f08e9ec7c51
status: succeeded
parameters:
become: true
json: true
module_name: setup
result:
failed: false
return_code: 0
stderr: ''
stdout:
127.0.0.1:
ansible_facts:
ansible_all_ipv4_addresses:
...
changed: false
succeeded: true
```

With this structured output, you could use the setup module as the first step of a workflow, and then base additional workflow steps on variables from ansible's setup. It should work similarly for other modules.

For instance, if you needed the default ipv4 address of a node, you could publish the appropriate ansible_fact like this (in an action-chain workflow):

```yaml
chain:
-
name: ansible_setup
ref: ansible.command_local
parameters:
become: True
json: True
module_name: setup
publish:
default_ip: "{{ ansible_setup.stdout['127.0.0.1'].ansible_facts.ansible_default_ipv4.address }}"
```



#### `ansible.playbook` examples
```sh
# run some simple playbook
Expand Down
53 changes: 52 additions & 1 deletion actions/ansible.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
#!/usr/bin/env python

from __future__ import print_function

import json
import os
import shutil
import sys
from lib.ansible_base import AnsibleBaseRunner
import tempfile
from lib.ansible_base import AnsibleBaseRunner, ParamaterConflict

__all__ = [
'AnsibleRunner'
Expand Down Expand Up @@ -32,8 +38,53 @@ class AnsibleRunner(AnsibleBaseRunner):
}

def __init__(self, *args, **kwargs):
self.tree_dir = None
self.one_line = False
if '--one_line' in args:
self.one_line = True
super(AnsibleRunner, self).__init__(*args, **kwargs)

def handle_json_arg(self):
if next((True for arg in self.args if arg.startswith('--tree')), False):
msg = "--json uses --tree internally. Setting both --tree and --json is not supported."
raise ParamaterConflict(msg)
execution_id = os.environ.get('ST2_ACTION_EXECUTION_ID', 'EXECUTION_ID_NA')
self.tree_dir = tempfile.mkdtemp(prefix='{}.'.format(execution_id))

tree_arg = '--tree={}'.format(self.tree_dir)
self.args.append(tree_arg)

# This sends all ansible stdout to /dev/null - if there's anything in there that's not in
# the --tree output, then it will be lost. Hopefully ansible doesn't print anything truly
# important... If something breaks, I guess we'll just have to run it without --json
# to see what is going on.
self.stdout = open(os.devnull, 'w')

def output_json(self):
output = {}
for host in os.listdir(self.tree_dir):
# one file per host in tree dir; name of host is name of file
with open(os.path.join(self.tree_dir, host), 'r') as host_output:
try:
output[host] = json.load(host_output)
except ValueError:
# something is messed up in the json, so include it as a string.
host_output.seek(0)
output[host] = host_output.read()
if self.one_line:
print(json.dumps(output))
else:
print(json.dumps(output, indent=2))

def cleanup(self):
shutil.rmtree(self.tree_dir)
self.stdout.close()

def post_execute(self):
if self.json_output:
self.output_json()
self.cleanup()


if __name__ == '__main__':
AnsibleRunner(sys.argv).execute()
3 changes: 3 additions & 0 deletions actions/command.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -105,3 +105,6 @@ parameters:
version:
description: "Show ansible version number and exit"
type: boolean
json:
description: "Clean up Ansible's output to ensure it is valid jSON"
type: boolean
3 changes: 3 additions & 0 deletions actions/command_local.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -105,3 +105,6 @@ parameters:
version:
description: "Show ansible version number and exit"
type: boolean
json:
description: "Clean up Ansible's output to ensure it is valid jSON"
type: boolean
32 changes: 30 additions & 2 deletions actions/lib/ansible_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,11 @@ def __init__(self, args):
:param args: Input command line arguments
:type args: ``list``
"""
self.json_output = False
self.stdout = None

self.args = args[1:]
self._json_output_arg()
self._parse_extra_vars() # handle multiple entries in --extra_vars arg
self._prepend_venv_path()

Expand Down Expand Up @@ -64,7 +68,7 @@ def _parse_extra_vars(self):

# Add --extra-vars for each json object
elif t == 'json':
self.args.append("--extra-vars='{0}'".format(json.dumps(v)))
self.args.append("--extra-vars={0}".format(json.dumps(v)))

# Combine contiguous kwarg vars into a single space-separated --extra-vars kwarg
elif t == 'kwarg' and last != t:
Expand Down Expand Up @@ -96,17 +100,37 @@ def _prepend_venv_path():

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

def _json_output_arg(self):
for i, arg in enumerate(self.args):
if '--json' in arg:
self.json_output = True
self.handle_json_arg()
# if ansible-playbook, add env arg
del self.args[i] # The json arg is a ST2 specific addition & should not pass on.
break
elif '--one_line' in arg:
self.one_line = True

def handle_json_arg(self):
pass

def execute(self):
"""
Execute the command and stream stdout and stderr output
from child process as it appears without delay.
Terminate with child's exit code.
"""
exit_code = subprocess.call(self.cmd, env=os.environ.copy())
exit_code = subprocess.call(self.cmd, env=os.environ.copy(), stdout=self.stdout)
if exit_code is not 0:
sys.stderr.write('Executed command "%s"\n' % ' '.join(self.cmd))

self.post_execute()

sys.exit(exit_code)

def post_execute(self):
pass

@property
@shell.replace_args('REPLACEMENT_RULES')
def cmd(self):
Expand Down Expand Up @@ -139,3 +163,7 @@ def binary(self):
sys.exit(1)

return binary_path


class ParamaterConflict(Exception):
pass
2 changes: 1 addition & 1 deletion pack.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,6 @@ keywords:
- ansible
- cfg management
- configuration management
version : 0.5.1
version : 0.6.0
author : StackStorm, Inc.
email : info@stackstorm.com
4 changes: 2 additions & 2 deletions tests/test_actions_lib_ansiblebaserunner.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ def extra_vars_json_yaml_fixture(self, test_name):
test_yaml = self.load_yaml('extra_vars_json.yaml')
test = next(t for t in test_yaml if t['name'] == test_name)
case = test['test']
expected = ['--extra-vars=\'{}\''.format(json.dumps(e)) for e in case]
expected = ['--extra-vars={}'.format(json.dumps(e)) for e in case]
self.check_arg_parse(arg, case, expected)

def test_parse_extra_vars_json_yaml_dict(self):
Expand Down Expand Up @@ -115,7 +115,7 @@ def extra_vars_complex_yaml_fixture(self, test_name):
# this does not preserve the order exactly, but it shows that elements are correctly parsed
expected = ['--extra-vars={}'.format(e)
for e in test['expected'] if isinstance(e, six.string_types)]
expected.extend(['--extra-vars=\'{}\''.format(json.dumps(e))
expected.extend(['--extra-vars={}'.format(json.dumps(e))
for e in test['expected'] if isinstance(e, dict)])
self.check_arg_parse(arg, case, expected)

Expand Down