├── .circleci └── config.yml ├── .github ├── apt-packages.txt └── workflows │ ├── build_and_test.yaml │ └── release.yaml ├── .gitignore ├── CHANGES.md ├── LICENSE ├── README.md ├── actions ├── ansible.py ├── ansible_galaxy.py ├── ansible_playbook.py ├── ansible_vault.py ├── command.yaml ├── command_local.yaml ├── galaxy.install.yaml ├── galaxy.list.yaml ├── galaxy.remove.yaml ├── lib │ ├── __init__.py │ ├── ansible_base.py │ └── shell.py ├── playbook.yaml ├── vault.decrypt.yaml └── vault.encrypt.yaml ├── icon.png ├── pack.yaml ├── requirements.txt └── tests ├── fixtures ├── extra_vars.yaml ├── extra_vars_complex.yaml └── extra_vars_json.yaml └── test_actions_lib_ansiblebaserunner.py /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | jobs: 4 | circleci_is_disabled_job: 5 | docker: 6 | - image: cimg/base:stable 7 | steps: 8 | - run: 9 | shell: /bin/bash 10 | command: echo CircleCI disabled on StackStorm-Exchange 11 | 12 | workflows: 13 | version: 2 14 | circleci_is_disabled: 15 | jobs: 16 | - circleci_is_disabled_job 17 | -------------------------------------------------------------------------------- /.github/apt-packages.txt: -------------------------------------------------------------------------------- 1 | krb5-multidev 2 | -------------------------------------------------------------------------------- /.github/workflows/build_and_test.yaml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | pull_request: 5 | schedule: 6 | # NOTE: We run this weekly at 1 am UTC on every Saturday 7 | - cron: '0 1 * * 6' 8 | 9 | jobs: 10 | # This is mirrored in the release workflow. 11 | build_and_test: 12 | name: 'Build and Test' 13 | uses: StackStorm-Exchange/ci/.github/workflows/pack-build_and_test.yaml@master 14 | with: 15 | enable-common-libs: true 16 | #apt-cache-version: v0 17 | #py-cache-version: v0 18 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | branches: 6 | # the default branch 7 | - master 8 | 9 | permissions: 10 | contents: write 11 | 12 | jobs: 13 | # This mirrors build_and_test workflow 14 | build_and_test: 15 | name: 'Build and Test' 16 | uses: StackStorm-Exchange/ci/.github/workflows/pack-build_and_test.yaml@master 17 | with: 18 | enable-common-libs: true 19 | #apt-cache-version: v0 20 | #py-cache-version: v0 21 | 22 | tag_release: 23 | needs: build_and_test 24 | name: Tag Release 25 | uses: StackStorm-Exchange/ci/.github/workflows/pack-tag_release.yaml@master 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | !/actions/lib/ 20 | lib64/ 21 | parts/ 22 | sdist/ 23 | var/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *,cover 47 | .hypothesis/ 48 | 49 | # Translations 50 | *.mo 51 | *.pot 52 | 53 | # Django stuff: 54 | *.log 55 | local_settings.py 56 | 57 | # Flask stuff: 58 | instance/ 59 | .webassets-cache 60 | 61 | # Scrapy stuff: 62 | .scrapy 63 | 64 | # Sphinx documentation 65 | docs/_build/ 66 | 67 | # PyBuilder 68 | target/ 69 | 70 | # IPython Notebook 71 | .ipynb_checkpoints 72 | 73 | # pyenv 74 | .python-version 75 | 76 | # celery beat schedule file 77 | celerybeat-schedule 78 | 79 | # dotenv 80 | .env 81 | 82 | # virtualenv 83 | venv/ 84 | ENV/ 85 | 86 | # Spyder project settings 87 | .spyderproject 88 | 89 | # Rope project settings 90 | .ropeproject 91 | 92 | # Jetbrains 93 | *.iml 94 | .idea/ 95 | -------------------------------------------------------------------------------- /CHANGES.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 1.1.0 4 | 5 | * Support st2 v3.9 release 6 | * Replace self.assertItemsEqual with self.assertCountEqual 7 | 8 | ## 1.0.0 9 | 10 | * Drop Python 2.7 support 11 | 12 | ## v0.5.9 13 | * Fix pack compatibility under python 3 when unsupported implicit relative import was used (#41) 14 | 15 | ## v0.5.8 16 | * Minor linting fix 17 | 18 | ## v0.5.7 19 | * add `netaddr` to requirements. This package is required for common ansible filters such as ipaddr() and ipmath(). See https://docs.ansible.com/ansible/latest/user_guide/playbooks_filters_ipaddr.html for more details 20 | 21 | ## v0.5.6 22 | * Fix Ansible pack shebang to utilize st2 python virtualenv (#33) 23 | 24 | ## v0.5.5 25 | 26 | * Fix Jinja rendering issue for Ansible vault actions (#28) 27 | 28 | ## v0.5.4 29 | * Set default `CWD` working dir to current pack/workflow path, 30 | allowing using relative path to playbooks shipped with custom pack (#9) 31 | 32 | ## v0.5.3 33 | 34 | * Fixed a bug where JSON data was being passed incorrectly to `--extra-vars`. #19 35 | Contributed by Nick Maludy (Encore Technologies) 36 | 37 | ## v0.5.2 38 | 39 | * Added pywinrm to requirements so connection to windows hosts is possible. 40 | Contributed by Nick Maludy (Encore Technologies) 41 | 42 | ## v0.5.0 43 | 44 | * Added ability to use yaml structures to pass arbitrarily complex values through extra_vars. key=value and @file syntax is still supported. Example usage: 45 | ```yaml 46 | sample_task: 47 | action: ansible.playbook 48 | input: 49 | playbook: playbook.yml 50 | extra_vars: 51 | - key1=value1 52 | - 53 | key2: value2 54 | key3: [ value3a, value3b ] 55 | key4: 56 | - value4a 57 | - { value4bkey: value4bvalue } 58 | key5: 59 | key6: value6 60 | key7: value7 61 | - @path/to/file.yml 62 | ... 63 | ``` 64 | 65 | ## v0.4 66 | 67 | * Breaking Change: Added ability to pass in multiple extra_vars. The extra_vars parameter is now a list. Example usage: 68 | ```yaml 69 | sample_task: 70 | action: ansible.playbook 71 | input: 72 | playbook: playbook.yml 73 | extra_vars: 74 | - "@path/to/file.yml" 75 | - "@path/to/file.json" 76 | - key1=value1 77 | - key2={{ _.value2 }} 78 | ... 79 | ``` 80 | 81 | ## v0.3 82 | 83 | * Removed immutable flag for `sudo:` parameters for all actions. Default is `true`, which means that ansible commands are run with sudo (as root). Good thing is you can change it to `false` when required. 84 | 85 | ## v0.2 86 | 87 | * Breaking Change: Replaced all dashes in parameter names with underscores (adhere to the spec of Jinja/Python variables) 88 | 89 | ## v0.1.1 90 | 91 | * Prepend sandboxed path with ansible binaries to PATH env variable, allowing ansible binary discovery to follow PATH order 92 | 93 | ## v0.1.0 94 | 95 | * Initial release with actions included: 96 | * `ansible.playbook` 97 | * `ansible.command` 98 | * `ansible.command_local` 99 | * `ansible.galaxy.install` 100 | * `ansible.galaxy.list` 101 | * `ansible.galaxy.remove` 102 | * `ansible.vault.encrypt` 103 | * `ansible.vault.decrypt` 104 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://circleci.com/gh/StackStorm-Exchange/stackstorm-ansible.svg?style=shield)](https://circleci.com/gh/StackStorm-Exchange/stackstorm-ansible) 2 | 3 | # Ansible Integration Pack 4 | This pack provides [Ansible](http://www.ansible.com/) integration to perform remote operations on both local and remote machines. 5 | After [pack installation](http://docs.stackstorm.com/packs.html#getting-a-pack) all ansible executable files are available in pack virtualenv and ready to use. 6 | 7 | ## Requirements 8 | This pack installs Ansible from `pip` and therefore may require some OS-level packages to be in place. 9 | Ubuntu: 10 | ``` 11 | sudo apt-get install gcc libkrb5-dev 12 | ``` 13 | RHEL/CentOS: 14 | ``` 15 | sudo yum install gcc krb5-devel 16 | ``` 17 | 18 | ## Actions 19 | * `command` - Run single [Ad-Hoc command](http://docs.ansible.com/intro_adhoc.html). It has all the regular parameters of `ansible` executable. 20 | * `command_local` - Perform single ansible Ad-Hoc command (module) locally. 21 | * `playbook` - Action to run [Ansible Playbook](http://docs.ansible.com/playbooks.html) (`ansible-playbook` executable). 22 | * `vault.encrypt` - Encrypt ansible data files (playbooks, vars, roles, etc) with password (`ansible-vault` executable). 23 | * `vault.decrypt` - Decrypt ansible data files (playbooks, vars, roles, etc) with password (`ansible-vault` executable). 24 | * `galaxy.install` - Install role from [Ansible Galaxy](http://docs.ansible.com/galaxy.html) - hub of [community developed roles](https://galaxy.ansible.com/) (`ansible-galaxy`). 25 | * `galaxy.list` - List installed from Ansible Galaxy roles (`ansible-galaxy` executable). 26 | * `galaxy.remove` - Remove the role installed from Ansible Galaxy (`ansible-galaxy` executable). 27 | 28 | ## Examples 29 | See [StackStorm with Ansible on Vagrant demo](https://github.com/StackStorm/st2-ansible-vagrant) for more examples 30 | 31 | #### `ansible.command` examples 32 | ```sh 33 | # run ansible command with optional verbose parameter 34 | st2 run ansible.command hosts=all args='hostname -i' verbose=vv 35 | ``` 36 | 37 | Action `ansible.command_local` is helper for the `ansible.command` with predefined parameters to run the command locally. So this is the same: 38 | ```sh 39 | st2 run ansible.command_local args='echo $TERM' 40 | st2 run ansible.command connection=local inventory_file='127.0.0.1,' hosts=all args='echo $TERM' 41 | ``` 42 | which is equivalent of ansible commands: 43 | ```sh 44 | ansible all -c local -i '127.0.0.1,' -a 'echo $TERM' 45 | ansible all --connection=local --inventory-file='127.0.0.1,' --args='echo $TERM' 46 | ``` 47 | 48 | #### `ansible.playbook` examples 49 | ```sh 50 | # run some simple playbook 51 | st2 run ansible.playbook playbook=/etc/ansible/playbooks/nginx.yml 52 | 53 | # run playbook on last machine listed in inventory file 54 | st2 run ansible.playbook playbook=/etc/ansible/playbooks/nginx.yml limit='all[-1]' 55 | ``` 56 | 57 | #### `ansible.vault` examples 58 | ```sh 59 | # encrypt /tmp/nginx.yml playbook with password containing in vault.txt 60 | st2 run ansible.vault.encrypt vault_password_file=vault.txt files=/tmp/nginx.yml 61 | 62 | # decrypt /etc/ansible/nginx.yml and /etc/ansible/db.yml files 63 | st2 run ansible.vault.decrypt cwd=/etc/ansible vault_password_file=vault.txt files='nginx.yml db.yml' 64 | 65 | # decrypt all files in /etc/ansible/playbooks directory 66 | st2 run ansible.vault.decrypt cwd=/etc/ansible vault_password_file=vault.txt files='playbooks/*' 67 | ``` 68 | 69 | #### `ansible.galaxy` examples 70 | ```sh 71 | # download many roles 72 | st2 run ansible.galaxy.install roles='bennojoy.mysql kosssi.composer' 73 | 74 | # list rolex 75 | st2 run ansible.galaxy.list roles_path=/etc/ansible/roles 76 | ``` 77 | 78 | ## Tips & Tricks 79 | #### Using Ansible `extra_vars` in StackStorm Workflow 80 | This is an example from a workflow that passes several different 81 | variables to the playbook as extra-vars: 82 | 83 | ```yaml 84 | sample_task: 85 | action: ansible.playbook 86 | input: 87 | playbook: /path/to/playbook.yml 88 | extra_vars: 89 | # 90 | # as key=value pairs 91 | - key1=value1 92 | - key2=value2 93 | # 94 | # variables from a yaml (or json) file 95 | - '@/path/to/file.yml' 96 | # 97 | # an arbitrarily complex dict of variables (passed as JSON to ansible) 98 | - 99 | key3: "{{ value3 }}" 100 | key4: [ value4a, value4b ] 101 | key5: 102 | - value5a 103 | - { value5bkey: value5bvalue } 104 | key6: 105 | key7: value7 106 | key8: value8 107 | ``` 108 | 109 | #### Structured output 110 | ```sh 111 | # get structured JSON output from a playbook 112 | st2 run ansible.playbook playbook=/etc/ansible/playbooks/nginx.yml env='{"ANSIBLE_STDOUT_CALLBACK":"json"}' 113 | ``` 114 | Using the JSON stdout_callback leads to JSON output which enables access to details of the result of the playbook in actions following the playbook execution, e.g. posting the results to Slack in an action-alias. 115 | ```yaml 116 | format: | 117 | *Execution Overview* 118 | {% for host, result in execution.result.stdout.stats.iteritems() %} 119 | {{ host }}: ```{{ result }}``` 120 | {% endfor %} 121 | ``` 122 | There is, however, a bug that breaks the JSON when the playbook execution fails (example output below). See [this issue](https://github.com/ansible/ansible/issues/17122) for more information. Manual handling of this case is necessary until the bug is fixed. 123 | ``` 124 | to retry, use: --limit @/etc/ansible/playbooks/top.retry 125 | { 126 | "plays": [ 127 | { 128 | "play": { 129 | "id": "b5fe7b50-9d7d-4927-ac17-6886218bcabc", 130 | "name": "some-host.com" 131 | }, 132 | ... 133 | } 134 | ``` 135 | 136 | #### Relative path to playbooks within StackStorm workflows 137 | Current working directory (`CWD`) defaults to pack dir you're invoking Ansible pack actions from. 138 | That means if you're calling `ansible.playbook` from the `custom.workflow`, then you can use relative path to playbooks you'd ship with the `custom` pack (infra-as-code, yeah). 139 | ``` 140 | version: '2.0' 141 | custom.workflow: 142 | description: A sample workflow that demonstrates how to use relative paths to playbooks shipped with pack. 143 | type: direct 144 | tasks: 145 | a: 146 | action: ansible.playbook 147 | input: 148 | # 'ansible_play.yml' is part of the 'custom' pack 149 | playbook: "ansible_play.yml" 150 | inventory_file: "localhost," 151 | ``` 152 | This eliminates the need to specify absolute path to Ansible playbook file, located somewhere in `/opt/stackstorm/packs/...`. 153 | 154 | #### Windows Hosts 155 | Connecting to windows is possibe as of version `v0.5.2` of this pack. 156 | This is accomplished using ansible's [builtin windows support](http://docs.ansible.com/ansible/latest/intro_windows.html). 157 | 158 | Prior to executing a playbook on a Windows host, the host must be configured to 159 | accept WinRM connections. To accomplish this, execute the ansible [setup PowerShell script](https://github.com/ansible/ansible/blob/devel/examples/scripts/ConfigureRemotingForAnsible.ps1) 160 | on every Windows host you connect to. We recommend performing this on your 161 | Windows VM templates. 162 | 163 | The following `extra_vars` must be passed in when executing a playbook on a Windows host: 164 | 165 | * `ansible_user` : User to connect as (prefer user@domain.tld over domain\user) 166 | * `ansible_password` : Password to use when connecting 167 | * `ansible_connection` : Connection method to use (`winrm` for windows) 168 | * `ansible_port` : Port to use for the connection (`5986` for WinRM) 169 | * `ansible_winrm_transport` : WinRM transport to use for the connection (suggested: `ntlm` or `credssp`, for more information consult the [pywinrm documentation](https://github.com/diyan/pywinrm/). 170 | * `ansible_winrm_server_cert_validation` : Should the SSL cert be validated. (suggested: `ignore`) 171 | 172 | 173 | Connecting via NTLM using a `user@domain.tld` style login: 174 | 175 | ``` sh 176 | st2 run ansible.playbook playbook=/etc/ansible/playbooks/windows_playbook.yaml inventory_file="winvm01.domain.tld," extra_vars='["ansible_user=user@domain.tld","ansible_password=xxx","ansible_port=5986","ansible_connection=winrm","ansible_winrm_server_cert_validation=ignore","ansible_winrm_transport=ntlm"]' 177 | ``` 178 | 179 | Connecting via CredSSP using a `DOMAIN\user` style login (note the extra `\`): 180 | 181 | ``` sh 182 | st2 run ansible.playbook playbook=/etc/ansible/playbooks/windows_playbook.yaml inventory_file="winvm01.domain.tld," extra_vars='["ansible_user=DOMAIN\\\\user","ansible_password=xxx","ansible_port=5986","ansible_connection=winrm","ansible_winrm_server_cert_validation=ignore","ansible_winrm_transport=credssp"]' 183 | ``` -------------------------------------------------------------------------------- /actions/ansible.py: -------------------------------------------------------------------------------- 1 | #!/opt/stackstorm/st2/bin/python 2 | 3 | import sys 4 | from lib.ansible_base import AnsibleBaseRunner 5 | 6 | __all__ = [ 7 | 'AnsibleRunner' 8 | ] 9 | 10 | 11 | class AnsibleRunner(AnsibleBaseRunner): 12 | """ 13 | Runs ansible ad-hoc command (single module). 14 | See: http://docs.ansible.com/intro_adhoc.html 15 | Modules: http://docs.ansible.com/list_of_all_modules.html 16 | """ 17 | BINARY_NAME = 'ansible' 18 | REPLACEMENT_RULES = { 19 | '--verbose=vvvv': '-vvvv', 20 | '--verbose=vvv': '-vvv', 21 | '--verbose=vv': '-vv', 22 | '--verbose=v': '-v', 23 | '--become_method': '--become-method', 24 | '--become_user': '--become-user', 25 | '--inventory_file': '--inventory-file', 26 | '--list_hosts': '--list-hosts', 27 | '--module_path': '--module-path', 28 | '--module_name': '--module-name', 29 | '--one_line': '--one-line', 30 | '--private_key': '--private-key', 31 | '--vault_password_file': '--vault-password-file', 32 | } 33 | 34 | def __init__(self, *args, **kwargs): 35 | super(AnsibleRunner, self).__init__(*args, **kwargs) 36 | 37 | 38 | if __name__ == '__main__': 39 | AnsibleRunner(sys.argv).execute() 40 | -------------------------------------------------------------------------------- /actions/ansible_galaxy.py: -------------------------------------------------------------------------------- 1 | #!/opt/stackstorm/st2/bin/python 2 | 3 | import sys 4 | from lib.ansible_base import AnsibleBaseRunner 5 | 6 | __all__ = [ 7 | 'AnsibleGalaxyRunner' 8 | ] 9 | 10 | 11 | class AnsibleGalaxyRunner(AnsibleBaseRunner): 12 | """ 13 | Runs Ansible galaxy commands: install/remove/list. 14 | See: http://docs.ansible.com/galaxy.html 15 | """ 16 | BINARY_NAME = 'ansible-galaxy' 17 | REPLACEMENT_RULES = { 18 | '--roles_path': '--roles-path', 19 | '--ignore_errors': '--ignore-errors', 20 | '--no_deps': '--no-deps', 21 | '--role_file': '--role-file', 22 | } 23 | 24 | def __init__(self, *args, **kwargs): 25 | super(AnsibleGalaxyRunner, self).__init__(*args, **kwargs) 26 | 27 | 28 | if __name__ == '__main__': 29 | AnsibleGalaxyRunner(sys.argv).execute() 30 | -------------------------------------------------------------------------------- /actions/ansible_playbook.py: -------------------------------------------------------------------------------- 1 | #!/opt/stackstorm/st2/bin/python 2 | 3 | import sys 4 | from lib.ansible_base import AnsibleBaseRunner 5 | 6 | __all__ = [ 7 | 'AnsiblePlaybookRunner' 8 | ] 9 | 10 | 11 | class AnsiblePlaybookRunner(AnsibleBaseRunner): 12 | """ 13 | Runs Ansible playbook. 14 | See: http://docs.ansible.com/playbooks.html 15 | """ 16 | BINARY_NAME = 'ansible-playbook' 17 | REPLACEMENT_RULES = { 18 | '--verbose=vvvv': '-vvvv', 19 | '--verbose=vvv': '-vvv', 20 | '--verbose=vv': '-vv', 21 | '--verbose=v': '-v', 22 | '--become_method': '--become-method', 23 | '--become_user': '--become-user', 24 | '--flush_cache': '--flush-cache', 25 | '--force_handlers': '--force-handlers', 26 | '--inventory_file': '--inventory-file', 27 | '--list_hosts': '--list-hosts', 28 | '--list_tags': '--list-tags', 29 | '--list_tasks': '--list-tasks', 30 | '--module_path': '--module-path', 31 | '--private_key': '--private-key', 32 | '--skip_tags': '--skip-tags', 33 | '--start_at_task': '--start-at-task', 34 | '--syntax_check': '--syntax-check', 35 | '--vault_password_file': '--vault-password-file', 36 | } 37 | 38 | def __init__(self, *args, **kwargs): 39 | super(AnsiblePlaybookRunner, self).__init__(*args, **kwargs) 40 | 41 | 42 | if __name__ == '__main__': 43 | AnsiblePlaybookRunner(sys.argv).execute() 44 | -------------------------------------------------------------------------------- /actions/ansible_vault.py: -------------------------------------------------------------------------------- 1 | #!/opt/stackstorm/st2/bin/python 2 | 3 | import sys 4 | from lib.ansible_base import AnsibleBaseRunner 5 | 6 | __all__ = [ 7 | 'AnsibleVaultRunner' 8 | ] 9 | 10 | 11 | class AnsibleVaultRunner(AnsibleBaseRunner): 12 | """ 13 | Runs Ansible vault commands: encrypt/decrypt. 14 | See: https://docs.ansible.com/playbooks_vault.html 15 | """ 16 | BINARY_NAME = 'ansible-vault' 17 | REPLACEMENT_RULES = { 18 | '--vault_password_file': '--vault-password-file' 19 | } 20 | 21 | def __init__(self, *args, **kwargs): 22 | super(AnsibleVaultRunner, self).__init__(*args, **kwargs) 23 | 24 | 25 | if __name__ == '__main__': 26 | AnsibleVaultRunner(sys.argv).execute() 27 | -------------------------------------------------------------------------------- /actions/command.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: "command" 3 | description: "Run ad-hoc ansible command (module)" 4 | runner_type: "local-shell-script" 5 | entry_point: "ansible.py" 6 | enabled: true 7 | parameters: 8 | kwarg_op: 9 | description: "Lock operator type to '--'" 10 | type: string 11 | default: "--" 12 | immutable: true 13 | sudo: 14 | description: "Lock sudo, the behavior is controlled by ansible 'become_' options" 15 | type: boolean 16 | default: true 17 | cwd: 18 | description: "Working directory where the command will be executed in (default: current pack/workflow dir)" 19 | type: string 20 | default: "/opt/stackstorm/packs/{% if action_context.parent is defined %}{{ action_context.parent.pack }}{% else %}{{ action_context.pack }}{% endif %}" 21 | timeout: 22 | description: "Action timeout in seconds. Action will get killed if it doesn't finish in timeout seconds" 23 | type: integer 24 | default: 600 25 | hosts: 26 | description: "Hosts" 27 | type: string 28 | position: 0 29 | args: 30 | description: "Module arguments [-a]" 31 | type: string 32 | background: 33 | description: "Fork in Background asynchronously for X seconds [-B]" 34 | type: integer 35 | become: 36 | description: "Run operations with become (nopasswd implied) [-b]" 37 | type: boolean 38 | become_method: 39 | description: "Privilege escalation method to use. Valid choices: sudo, su, pbrun, pfexec, runas (default=sudo)" 40 | type: string 41 | enum: 42 | - sudo 43 | - su 44 | - pbrun 45 | - pfexec 46 | - runas 47 | become_user: 48 | description: "Run operations as this user. Works only with 'become'" 49 | type: string 50 | check: 51 | description: "Don't make any changes; instead, try to predict some of the changes that may occur [-C]" 52 | type: boolean 53 | connection: 54 | description: "Connection type to use (default=smart) [-c]" 55 | type: string 56 | extra_vars: 57 | description: 'List of additional variables to pass to ansible. Each variable is represented as "key=value" or "@path/to/file.yaml|json" or use a yaml dict ("key: [1, 2, 3]") to send JSON. [-e]' 58 | type: array 59 | forks: 60 | description: "Specify number of parallel processes to use (default=5) [-f]" 61 | type: integer 62 | help: 63 | description: "Show help message and exit [-h]" 64 | type: boolean 65 | inventory_file: 66 | description: "Inventory host file (default=/etc/ansible/hosts) [-i]" 67 | type: string 68 | limit: 69 | description: "Further limit selected hosts to an additional pattern [-l]" 70 | type: string 71 | list_hosts: 72 | description: "Outputs a list of matching hosts; does not execute anything else" 73 | type: boolean 74 | module_name: 75 | description: "Module name to execute (default=command) [-m]" 76 | type: string 77 | module_path: 78 | description: "Specify path(s) to module library (default=None) [-M]" 79 | type: string 80 | one_line: 81 | description: "Condense output [-o]" 82 | type: boolean 83 | poll: 84 | description: "Set the poll interval if using -B (default=15) [-P]" 85 | type: integer 86 | private_key: 87 | description: "Use this file to authenticate the connection" 88 | type: string 89 | tree: 90 | description: "Log output to this directory [-t]" 91 | type: string 92 | user: 93 | description: "Connect to remote hosts as this user (default=root) [-u]" 94 | type: string 95 | vault_password_file: 96 | description: "Vault password file" 97 | type: string 98 | verbose: 99 | description: "Verbose mode (-vvvv to enable connection debugging)" 100 | type: string 101 | enum: 102 | - v 103 | - vv 104 | - vvv 105 | - vvvv 106 | version: 107 | description: "Show ansible version number and exit" 108 | type: boolean 109 | -------------------------------------------------------------------------------- /actions/command_local.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: "command_local" 3 | description: "Run ad-hoc ansible command (module) on local machine" 4 | runner_type: "local-shell-script" 5 | entry_point: "ansible.py" 6 | enabled: true 7 | parameters: 8 | kwarg_op: 9 | description: "Lock operator type to '--'" 10 | type: string 11 | default: "--" 12 | immutable: true 13 | sudo: 14 | description: "Lock sudo, the behavior is controlled by ansible 'become_' options" 15 | type: boolean 16 | default: true 17 | cwd: 18 | description: "Working directory where the command will be executed in (default: current pack/workflow dir)" 19 | type: string 20 | default: "/opt/stackstorm/packs/{% if action_context.parent is defined %}{{ action_context.parent.pack }}{% else %}{{ action_context.pack }}{% endif %}" 21 | timeout: 22 | description: "Action timeout in seconds. Action will get killed if it doesn't finish in timeout seconds" 23 | type: integer 24 | default: 600 25 | hosts: 26 | description: "Hosts. Locked to run the command locally" 27 | type: string 28 | position: 0 29 | default: all 30 | immutable: true 31 | args: 32 | description: "Module arguments [-a]" 33 | type: string 34 | background: 35 | description: "Fork in Background asynchronously for X seconds [-B]" 36 | type: integer 37 | become: 38 | description: "Run operations with become (nopasswd implied) [-b]" 39 | type: boolean 40 | become_method: 41 | description: "Privilege escalation method to use. Valid choices: sudo, su, pbrun, pfexec, runas (default=sudo)" 42 | type: string 43 | enum: 44 | - sudo 45 | - su 46 | - pbrun 47 | - pfexec 48 | - runas 49 | become_user: 50 | description: "Run operations as this user. Works only with 'become'" 51 | type: string 52 | check: 53 | description: "Don't make any changes; instead, try to predict some of the changes that may occur [-C]" 54 | type: boolean 55 | connection: 56 | description: "Connection type to use (default=smart) [-c]. Locked to run the command locally" 57 | type: string 58 | default: local 59 | immutable: true 60 | extra_vars: 61 | description: 'List of additional variables to pass to ansible. Each variable is represented as "key=value" or "@path/to/file.yaml|json" or use a yaml dict ("key: [1, 2, 3]") to send JSON. [-e]' 62 | type: array 63 | forks: 64 | description: "Specify number of parallel processes to use (default=5) [-f]" 65 | type: integer 66 | help: 67 | description: "Show help message and exit [-h]" 68 | type: boolean 69 | inventory_file: 70 | description: "Inventory host file (default=/etc/ansible/hosts) [-i]. Locked to run the command locally" 71 | type: string 72 | default: "127.0.0.1," 73 | immutable: true 74 | limit: 75 | description: "Further limit selected hosts to an additional pattern [-l]" 76 | type: string 77 | list_hosts: 78 | description: "Outputs a list of matching hosts; does not execute anything else" 79 | type: boolean 80 | module_name: 81 | description: "Module name to execute (default=command) [-m]" 82 | type: string 83 | module_path: 84 | description: "Specify path(s) to module library (default=None) [-M]" 85 | type: string 86 | one_line: 87 | description: "Condense output [-o]" 88 | type: boolean 89 | poll: 90 | description: "Set the poll interval if using -B (default=15) [-P]" 91 | type: integer 92 | tree: 93 | description: "Log output to this directory [-t]" 94 | type: string 95 | vault_password_file: 96 | description: "Vault password file" 97 | type: string 98 | verbose: 99 | description: "Verbose mode (-vvvv to enable connection debugging)" 100 | type: string 101 | enum: 102 | - v 103 | - vv 104 | - vvv 105 | - vvvv 106 | version: 107 | description: "Show ansible version number and exit" 108 | type: boolean 109 | -------------------------------------------------------------------------------- /actions/galaxy.install.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: "galaxy.install" 3 | description: "Download & Install role from ansible galaxy" 4 | runner_type: "local-shell-script" 5 | entry_point: "ansible_galaxy.py" 6 | enabled: true 7 | parameters: 8 | kwarg_op: 9 | description: "Lock operator type to '--'" 10 | type: string 11 | default: "--" 12 | immutable: true 13 | sudo: 14 | description: "Lock sudo, the behavior is controlled by ansible 'become_' options" 15 | type: boolean 16 | default: true 17 | cwd: 18 | description: "Working directory where the command will be executed in (default: current pack/workflow dir)" 19 | type: string 20 | default: "/opt/stackstorm/packs/{% if action_context.parent is defined %}{{ action_context.parent.pack }}{% else %}{{ action_context.pack }}{% endif %}" 21 | timeout: 22 | description: "Action timeout in seconds. Action will get killed if it doesn't finish in timeout seconds" 23 | type: integer 24 | default: 300 25 | action: 26 | description: "Action to use" 27 | type: string 28 | position: 0 29 | default: install 30 | immutable: true 31 | roles: 32 | description: "Role(s) to install (separated by space)" 33 | type: string 34 | position: 1 35 | default: "" 36 | ignore_errors: 37 | description: "Ignore errors and continue with the next specified role [-i]" 38 | type: boolean 39 | no_deps: 40 | description: "Don't download roles listed as dependencies [-n]" 41 | type: boolean 42 | role_file: 43 | description: "A file with list of roles to be installed. Note that role file can contain links to .git or .tar file [-r]" 44 | type: string 45 | roles_path: 46 | description: "The path to the directory containing your roles. The default is the roles_path configured in your ansible.cfg file (/etc/ansible/roles if not configured) [-p]" 47 | type: string 48 | server: 49 | description: "The API server destination [-s]" 50 | type: string 51 | force: 52 | description: "Force overwriting an existing role [-f]" 53 | type: boolean 54 | -------------------------------------------------------------------------------- /actions/galaxy.list.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: "galaxy.list" 3 | description: "Display a list of installed roles from ansible galaxy" 4 | runner_type: "local-shell-script" 5 | entry_point: "ansible_galaxy.py" 6 | enabled: true 7 | parameters: 8 | kwarg_op: 9 | description: "Lock operator type to '--'" 10 | type: string 11 | default: "--" 12 | immutable: true 13 | sudo: 14 | description: "Lock sudo, the behavior is controlled by ansible 'become_' options" 15 | type: boolean 16 | default: true 17 | cwd: 18 | description: "Working directory where the command will be executed in (default: current pack/workflow dir)" 19 | type: string 20 | default: "/opt/stackstorm/packs/{% if action_context.parent is defined %}{{ action_context.parent.pack }}{% else %}{{ action_context.pack }}{% endif %}" 21 | timeout: 22 | description: "Action timeout in seconds. Action will get killed if it doesn't finish in timeout seconds. Lock as uneeded" 23 | type: integer 24 | default: 60 25 | immutable: true 26 | action: 27 | description: "Action to use" 28 | type: string 29 | position: 0 30 | default: list 31 | immutable: true 32 | roles_path: 33 | description: "The path to the directory containing your roles. The default is the roles_path configured in your ansible.cfg file (/etc/ansible/roles if not configured) [-p]" 34 | type: string 35 | -------------------------------------------------------------------------------- /actions/galaxy.remove.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: "galaxy.remove" 3 | description: "Remove an installed from ansible galaxy role" 4 | runner_type: "local-shell-script" 5 | entry_point: "ansible_galaxy.py" 6 | enabled: true 7 | parameters: 8 | kwarg_op: 9 | description: "Lock operator type to '--'" 10 | type: string 11 | default: "--" 12 | immutable: true 13 | sudo: 14 | description: "Lock sudo, the behavior is controlled by ansible 'become_' options" 15 | type: boolean 16 | default: true 17 | cwd: 18 | description: "Working directory where the command will be executed in (default: current pack/workflow dir)" 19 | type: string 20 | default: "/opt/stackstorm/packs/{% if action_context.parent is defined %}{{ action_context.parent.pack }}{% else %}{{ action_context.pack }}{% endif %}" 21 | timeout: 22 | description: "Action timeout in seconds. Action will get killed if it doesn't finish in timeout seconds. Lock as uneeded" 23 | type: integer 24 | default: 120 25 | immutable: true 26 | action: 27 | description: "Action to use" 28 | type: string 29 | position: 0 30 | default: remove 31 | immutable: true 32 | roles: 33 | description: "Role(s) to remove (separated by space)" 34 | type: string 35 | position: 1 36 | default: "" 37 | roles_path: 38 | description: "The path to the directory containing your roles. The default is the roles_path configured in your ansible.cfg file (/etc/ansible/roles if not configured) [-p]" 39 | type: string 40 | -------------------------------------------------------------------------------- /actions/lib/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StackStorm-Exchange/stackstorm-ansible/9d505b4ba2ce1af70f42698e77508eea559a67af/actions/lib/__init__.py -------------------------------------------------------------------------------- /actions/lib/ansible_base.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import subprocess 4 | import ast 5 | import json 6 | import six 7 | 8 | from . import shell 9 | 10 | __all__ = [ 11 | 'AnsibleBaseRunner' 12 | ] 13 | 14 | 15 | class AnsibleBaseRunner(object): 16 | """ 17 | Base class for all Ansible Runners 18 | """ 19 | BINARY_NAME = None 20 | REPLACEMENT_RULES = None 21 | 22 | def __init__(self, args): 23 | """ 24 | :param args: Input command line arguments 25 | :type args: ``list`` 26 | """ 27 | self.args = args[1:] 28 | self._parse_extra_vars() # handle multiple entries in --extra_vars arg 29 | self._prepend_venv_path() 30 | 31 | def _parse_extra_vars(self): 32 | """ 33 | This method turns the string list ("--extra_vars=[...]") passed in from the args 34 | into an actual list and adds new --extra-vars kwargs for file and k\v entries. 35 | 36 | Example (line breaks added for readability): 37 | Input from args: 38 | "--extra_vars=[u'"@path/to/vars_file.yml"', 39 | u'"key1=value1", u'"key2=value2"', 40 | {u'"key3"': u'"value3"'}]" 41 | Passed to Ansible after transformation: 42 | ... --extra-vars=@path/to/vars_file.yml 43 | --extra-vars=key1=value1 key2=value2 44 | --extra-vars='{"key3": "value3"}'... 45 | """ 46 | for i, arg in enumerate(self.args): 47 | if '--extra_vars' in arg: 48 | var_list_str = arg.split("--extra_vars=")[1] 49 | var_list = [] 50 | for n in ast.literal_eval(var_list_str): 51 | if isinstance(n, six.string_types): 52 | if n.strip().startswith("@"): 53 | var_list.append(('file', n.strip())) 54 | else: 55 | var_list.append(('kwarg', n.strip())) 56 | elif isinstance(n, dict): 57 | var_list.append(('json', n)) 58 | 59 | last = '' 60 | kv_param = '' 61 | for t, v in var_list: 62 | # Add --extra-vars for each file 63 | if t == 'file': 64 | self.args.append("--extra-vars={0}".format(v)) 65 | 66 | # Add --extra-vars for each json object 67 | elif t == 'json': 68 | self.args.append("--extra-vars={0}".format(json.dumps(v))) 69 | 70 | # Combine contiguous kwarg vars into a single space-separated --extra-vars kwarg 71 | elif t == 'kwarg' and last != t: 72 | kv_param = "--extra-vars={0}".format(v) 73 | elif t == 'kwarg': # last == t 74 | kv_param += " {0}".format(v) 75 | 76 | if last == 'kwarg' and t != last: 77 | self.args.append(kv_param) 78 | kv_param = "" 79 | 80 | last = t 81 | 82 | if kv_param: 83 | self.args.append(kv_param) 84 | 85 | del self.args[i] # Delete the original arg since we split it into separate ones 86 | break 87 | 88 | @staticmethod 89 | def _prepend_venv_path(): 90 | """ 91 | Modify PATH env variable by prepending virtualenv path with ansible binaries. 92 | This way venv ansible has precedence over globally installed ansible. 93 | """ 94 | venv_path = '/opt/stackstorm/virtualenvs/ansible/bin' 95 | old_path = os.environ.get('PATH', '').split(':') 96 | new_path = [venv_path] + old_path 97 | 98 | os.environ['PATH'] = ':'.join(new_path) 99 | 100 | def execute(self): 101 | """ 102 | Execute the command and stream stdout and stderr output 103 | from child process as it appears without delay. 104 | Terminate with child's exit code. 105 | """ 106 | exit_code = subprocess.call(self.cmd, env=os.environ.copy()) 107 | if exit_code != 0: 108 | sys.stderr.write('Executed command "%s"\n' % ' '.join(self.cmd)) 109 | sys.exit(exit_code) 110 | 111 | @property 112 | @shell.replace_args('REPLACEMENT_RULES') 113 | def cmd(self): 114 | """ 115 | Get full command line as list. 116 | 117 | :return: Command line. 118 | :rtype: ``list`` 119 | """ 120 | return [self.binary] + self.args 121 | 122 | @property 123 | def binary(self): 124 | """ 125 | Get full path to executable binary. 126 | 127 | :return: Full path to executable binary. 128 | :rtype: ``str`` 129 | """ 130 | if not self.BINARY_NAME: 131 | sys.stderr.write('Ansible binary file name was not specified') 132 | sys.exit(1) 133 | 134 | for path in os.environ.get('PATH', '').split(':'): 135 | binary_path = os.path.join(path, self.BINARY_NAME) 136 | if os.path.isfile(binary_path): 137 | break 138 | else: 139 | sys.stderr.write('Ansible binary doesnt exist. Is it installed?') 140 | sys.exit(1) 141 | 142 | return binary_path 143 | -------------------------------------------------------------------------------- /actions/lib/shell.py: -------------------------------------------------------------------------------- 1 | import functools 2 | 3 | __all__ = [ 4 | 'replace_args', 5 | ] 6 | 7 | 8 | def replace_args(attribute=None): 9 | """ 10 | Decorator to Apply replacements in a list of command line arguments. 11 | 12 | :param attribute: Class attribute name which stores replacement rules. 13 | :type attribute: ``str`` 14 | :return: 15 | :rtype: ``callable`` 16 | """ 17 | def _replace_args(f): 18 | @functools.wraps(f) 19 | def _wrapper(self, *args): 20 | def _replace(arg): 21 | for rule in rules: 22 | if arg.startswith(rule): 23 | return arg.replace(rule, rules[rule]) 24 | return arg 25 | rules = getattr(self, attribute) 26 | if not rules: 27 | return f(self, *args) 28 | return map(_replace, f(self, *args)) 29 | return _wrapper 30 | return _replace_args 31 | -------------------------------------------------------------------------------- /actions/playbook.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: "playbook" 3 | description: "Run ansible playbook" 4 | runner_type: "local-shell-script" 5 | entry_point: "ansible_playbook.py" 6 | enabled: true 7 | parameters: 8 | kwarg_op: 9 | description: "Lock operator type to '--'" 10 | type: string 11 | default: "--" 12 | immutable: true 13 | sudo: 14 | description: "Lock sudo, the behavior is controlled by ansible 'become_' options" 15 | type: boolean 16 | default: true 17 | cwd: 18 | description: "Working directory where the command will be executed in (default: current pack/workflow dir)" 19 | type: string 20 | default: "/opt/stackstorm/packs/{% if action_context.parent is defined %}{{ action_context.parent.pack }}{% else %}{{ action_context.pack }}{% endif %}" 21 | timeout: 22 | description: "Action timeout in seconds. Action will get killed if it doesn't finish in timeout seconds" 23 | type: integer 24 | default: 900 25 | playbook: 26 | description: "Playbook file" 27 | type: string 28 | position: 0 29 | background: 30 | description: "Fork in Background asynchronously for X seconds [-B]" 31 | type: integer 32 | become: 33 | description: "Run operations with become (nopasswd implied) [-b]" 34 | type: boolean 35 | become_method: 36 | description: "Privilege escalation method to use. Valid choices: sudo, su, pbrun, pfexec, runas (default=sudo)" 37 | type: string 38 | enum: 39 | - sudo 40 | - su 41 | - pbrun 42 | - pfexec 43 | - runas 44 | become_user: 45 | description: "Run operations as this user. Works only with 'become'" 46 | type: string 47 | check: 48 | description: "Don't make any changes; instead, try to predict some of the changes that may occur [-C]" 49 | type: boolean 50 | connection: 51 | description: "Connection type to use (default=smart) [-c]" 52 | type: string 53 | diff: 54 | description: "when changing (small) files and templates, show the differences in those files; works great with --check [-D]" 55 | type: boolean 56 | extra_vars: 57 | description: 'List of additional variables to pass to ansible. Each variable is represented as "key=value" or "@path/to/file.yaml|json" or use a yaml dict ("key: [1, 2, 3]") to send JSON. [-e]' 58 | type: array 59 | flush_cache: 60 | description: "Clear the fact cache" 61 | type: boolean 62 | force_handlers: 63 | description: "Run handlers even if a task fails" 64 | type: boolean 65 | forks: 66 | description: "Specify number of parallel processes to use (default=5) [-f]" 67 | type: integer 68 | help: 69 | description: "Show help message and exit [-h]" 70 | type: boolean 71 | inventory_file: 72 | description: "Inventory host file (default=/etc/ansible/hosts) [-i]" 73 | type: string 74 | limit: 75 | description: "Further limit selected hosts to an additional pattern [-l]" 76 | type: string 77 | list_hosts: 78 | description: "Outputs a list of matching hosts; does not execute anything else" 79 | type: boolean 80 | list_tags: 81 | description: "List all available tags" 82 | type: boolean 83 | list_tasks: 84 | description: "List all tasks that would be executed" 85 | type: boolean 86 | module_path: 87 | description: "Specify path(s) to module library (default=None) [-M]" 88 | type: string 89 | private_key: 90 | description: "Use this file to authenticate the connection" 91 | type: string 92 | skip_tags: 93 | description: "Only run plays and tasks whose tags do not match these values" 94 | type: string 95 | start_at_task: 96 | description: "Start the playbook at the task matching this name" 97 | type: string 98 | syntax_check: 99 | description: "Perform a syntax check on the playbook, but do not execute it" 100 | type: boolean 101 | tags: 102 | description: "Only run plays and tasks tagged with these values [-t]" 103 | type: string 104 | user: 105 | description: "Connect to remote hosts as this user (default=root) [-u]" 106 | type: string 107 | vault_password_file: 108 | description: "Vault password file" 109 | type: string 110 | verbose: 111 | description: "Verbose mode (-vvvv to enable connection debugging)" 112 | type: string 113 | enum: 114 | - v 115 | - vv 116 | - vvv 117 | - vvvv 118 | version: 119 | description: "Show ansible version number and exit" 120 | type: boolean 121 | -------------------------------------------------------------------------------- /actions/vault.decrypt.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: "vault.decrypt" 3 | description: "Decrypt ansible data files" 4 | runner_type: "local-shell-script" 5 | entry_point: "ansible_vault.py" 6 | enabled: true 7 | parameters: 8 | kwarg_op: 9 | description: "Lock operator type to '--'" 10 | type: string 11 | default: "--" 12 | immutable: true 13 | sudo: 14 | description: "Lock sudo, the behavior is controlled by ansible 'become_' options" 15 | type: boolean 16 | default: true 17 | cwd: 18 | description: "Working directory where the command will be executed in (default: current pack/workflow dir)" 19 | type: string 20 | default: "/opt/stackstorm/packs/{% if action_context.parent is defined %}{{ action_context.parent.pack }}{% else %}{{ action_context.pack }}{% endif %}" 21 | timeout: 22 | description: "Action timeout in seconds. Action will get killed if it doesn't finish in timeout seconds. Lock as uneeded" 23 | type: integer 24 | default: 60 25 | immutable: true 26 | action: 27 | description: "Action to use against playbook file(s)" 28 | type: string 29 | position: 0 30 | default: decrypt 31 | immutable: true 32 | files: 33 | description: "Data file(s) to encrypt (separated by space). Note that original file(s) will be modified" 34 | type: string 35 | position: 1 36 | required: true 37 | vault_password_file: 38 | description: "Vault file with password" 39 | type: string 40 | required: true 41 | debug: 42 | description: "Enable debug mode" 43 | type: boolean 44 | default: false 45 | -------------------------------------------------------------------------------- /actions/vault.encrypt.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: "vault.encrypt" 3 | description: "Encrypt ansible data files" 4 | runner_type: "local-shell-script" 5 | entry_point: "ansible_vault.py" 6 | enabled: true 7 | parameters: 8 | kwarg_op: 9 | description: "Lock operator type to '--'" 10 | type: string 11 | default: "--" 12 | immutable: true 13 | sudo: 14 | description: "Lock sudo, the behavior is controlled by ansible 'become_' options" 15 | type: boolean 16 | default: true 17 | cwd: 18 | description: "Working directory where the command will be executed in (default: current pack/workflow dir)" 19 | type: string 20 | default: "/opt/stackstorm/packs/{% if action_context.parent is defined %}{{ action_context.parent.pack }}{% else %}{{ action_context.pack }}{% endif %}" 21 | timeout: 22 | description: "Action timeout in seconds. Action will get killed if it doesn't finish in timeout seconds. Lock as uneeded" 23 | type: integer 24 | default: 60 25 | immutable: true 26 | action: 27 | description: "Action to use against playbook file(s)" 28 | type: string 29 | position: 0 30 | default: encrypt 31 | immutable: true 32 | files: 33 | description: "Data file(s) to encrypt (separated by space). Note that original file(s) will be modified" 34 | type: string 35 | position: 1 36 | required: true 37 | vault_password_file: 38 | description: "Vault file with password" 39 | type: string 40 | required: true 41 | debug: 42 | description: "Enable debug mode" 43 | type: boolean 44 | default: false 45 | -------------------------------------------------------------------------------- /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StackStorm-Exchange/stackstorm-ansible/9d505b4ba2ce1af70f42698e77508eea559a67af/icon.png -------------------------------------------------------------------------------- /pack.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | ref: ansible 3 | name : ansible 4 | description : ansible integrations 5 | keywords: 6 | - ansible 7 | - cfg management 8 | - configuration management 9 | version: 1.1.0 10 | author : StackStorm, Inc. 11 | email : info@stackstorm.com 12 | python_versions: 13 | - "3" 14 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | ansible>=1.9 2 | pywinrm[credssp,kerberos]>=0.2.2 3 | netaddr>=0.7.19 4 | -------------------------------------------------------------------------------- /tests/fixtures/extra_vars.yaml: -------------------------------------------------------------------------------- 1 | - 2 | name: key_value 3 | test: 4 | - 'key1=value1' 5 | - 'key2=value2' 6 | expected: 7 | - 'key1=value1 key2=value2' 8 | - 9 | name: at_file 10 | test: 11 | - '@path/to/vars.yaml' 12 | - '@path/to/vars.json' 13 | expected: 14 | - '@path/to/vars.yaml' 15 | - '@path/to/vars.json' 16 | - 17 | name: key_value_and_at_file 18 | test: 19 | - '@path/to/vars.yaml' 20 | - 'key1=value1' 21 | - '@path/to/vars.json' 22 | - 'key2=value2' 23 | - 'key3=value3' 24 | # before ordering 25 | # expected: 26 | # - '@path/to/vars.yaml' 27 | # - '@path/to/vars.json' 28 | # - 'key1=value1 key2=value2 key3=value3' 29 | # after ordering 30 | expected: 31 | - '@path/to/vars.yaml' 32 | - 'key1=value1' 33 | - '@path/to/vars.json' 34 | - 'key2=value2 key3=value3' 35 | 36 | -------------------------------------------------------------------------------- /tests/fixtures/extra_vars_complex.yaml: -------------------------------------------------------------------------------- 1 | - 2 | name: arbitrarily_complex 3 | test: 4 | - 'key1=value1' 5 | - 'key2=value2' 6 | - '@/path/to/file.yml' 7 | - 8 | key3: 'value3' 9 | key4: [ 'value4a', 'value4b' ] 10 | key5: 11 | - 'value5a' 12 | - { value5bkey: 'value5bvalue', value5bkey2: [1,2,3] } 13 | key6: 14 | key7: 'value7' 15 | - 'key8=value8' 16 | expected: 17 | - 'key1=value1 key2=value2' 18 | - '@/path/to/file.yml' 19 | - 20 | key3: 'value3' 21 | key4: [ 'value4a', 'value4b' ] 22 | key5: 23 | - 'value5a' 24 | - { value5bkey: 'value5bvalue', value5bkey2: [1,2,3] } 25 | key6: 26 | key7: 'value7' 27 | - 'key8=value8' 28 | 29 | -------------------------------------------------------------------------------- /tests/fixtures/extra_vars_json.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | - 3 | name: dict 4 | test: 5 | - 6 | key1: 'value1' 7 | key2: 'value2' 8 | - 9 | name: dict_with_list 10 | test: 11 | - 12 | key1: ['value1', 'value2', 'value3'] 13 | key2: 14 | - 'value4' 15 | - 'value5' 16 | - 'value6' 17 | - 18 | name: dict_dict 19 | test: 20 | - 21 | key1: {key1_1: 1, key1_2: 2} 22 | key2: 23 | key2_1: 1 24 | key2_2: 1 25 | - 26 | name: dict_multi 27 | test: 28 | - 29 | key1: 'value1' 30 | key2: 'value2' 31 | - 32 | key3: 'value3' 33 | key4: 'value4' 34 | - 35 | name: dict_with_list_multi 36 | test: 37 | - 38 | key1: ['value1', 'value2', 'value3'] 39 | key2: 40 | - 'value4' 41 | - 'value5' 42 | - 'value6' 43 | - 44 | key3: [1, 2, 3] 45 | key4: 46 | - 4 47 | - 5 48 | - 6 49 | - 50 | name: dict_dict_multi 51 | test: 52 | - 53 | key1: {key1_1: 1, key1_2: 2} 54 | key2: 55 | key2_1: 1 56 | key2_2: 2 57 | - 58 | key3: {key3_1: 1, key3_2: 2} 59 | key4: 60 | key4_1: 1 61 | key4_2: 2 62 | 63 | -------------------------------------------------------------------------------- /tests/test_actions_lib_ansiblebaserunner.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | 3 | import json 4 | 5 | import six 6 | import yaml 7 | import shlex 8 | 9 | from st2common.models.system.action import ShellScriptAction 10 | from st2tests.pack_resource import BasePackResourceTestCase 11 | 12 | from lib.ansible_base import AnsibleBaseRunner 13 | 14 | 15 | class TestActionsLibAnsibleBaseRunner(BasePackResourceTestCase): 16 | 17 | # from the ActiveDirectory test of a python action 18 | def load_yaml(self, filename): 19 | return yaml.safe_load(self.get_fixture_content(filename)) 20 | 21 | def setUp(self): 22 | super(TestActionsLibAnsibleBaseRunner, self).setUp() 23 | 24 | def test_init(self): 25 | args = ['ansible_base.py', '--arg1', '--arg2', 'value2', '--arg3=value3'] 26 | ansible_base_runner = AnsibleBaseRunner(args) 27 | self.assertEqual(ansible_base_runner.args, args[1:]) 28 | 29 | # AnsibleBaseRunner._parse_extra_vars() 30 | 31 | @staticmethod 32 | def generate_arg(st2_arg, value): 33 | dummy_action = ShellScriptAction('', '', '') 34 | 35 | # noinspection PyProtectedMember 36 | arg = dummy_action._get_script_arguments(named_args={st2_arg: value}) 37 | arg = ' '.join(shlex.split(arg)) 38 | 39 | return arg 40 | 41 | def check_arg_parse(self, arg_name, test_case, expected_ansible_args): 42 | args = ['ansible_base.py', self.generate_arg(arg_name, test_case)] 43 | ansible_base_runner = AnsibleBaseRunner(args) 44 | self.assertCountEqual(expected_ansible_args, ansible_base_runner.args) 45 | 46 | def test_parse_extra_vars_key_value(self): 47 | arg = '--extra_vars' 48 | test = ['key1=value1', 'key2=value2'] 49 | expected = ['--extra-vars=' + ' '.join(test)] 50 | 51 | self.check_arg_parse(arg, test, expected) 52 | 53 | def test_parse_extra_vars_at_file(self): 54 | arg = '--extra_vars' 55 | test = ['@/path/to/vars_file.yaml', '@/other/path/vars_file.json'] 56 | expected = ['--extra-vars=' + case for case in test] 57 | 58 | self.check_arg_parse(arg, test, expected) 59 | 60 | def test_parse_extra_vars_at_file_and_key_value(self): 61 | arg = '--extra_vars' 62 | test = ['@/path/to/vars_file.yaml', 'key1=value1'] 63 | expected = ['--extra-vars=' + case for case in test] 64 | 65 | self.check_arg_parse(arg, test, expected) 66 | 67 | def extra_vars_yaml_fixture(self, test_name): 68 | arg = '--extra_vars' 69 | test_yaml = self.load_yaml('extra_vars.yaml') 70 | test = next(t for t in test_yaml if t['name'] == test_name) 71 | case = test['test'] 72 | expected = ['--extra-vars={}'.format(e) for e in test['expected']] 73 | self.check_arg_parse(arg, case, expected) 74 | 75 | def test_parse_extra_vars_yaml_key_value(self): 76 | self.extra_vars_yaml_fixture('key_value') 77 | 78 | def test_parse_extra_vars_yaml_at_file(self): 79 | self.extra_vars_yaml_fixture('at_file') 80 | 81 | def test_parse_extra_vars_yaml_key_value_and_at_file(self): 82 | self.extra_vars_yaml_fixture('key_value_and_at_file') 83 | 84 | def extra_vars_json_yaml_fixture(self, test_name): 85 | arg = '--extra_vars' 86 | test_yaml = self.load_yaml('extra_vars_json.yaml') 87 | test = next(t for t in test_yaml if t['name'] == test_name) 88 | case = test['test'] 89 | expected = ['--extra-vars={}'.format(json.dumps(e)) for e in case] 90 | self.check_arg_parse(arg, case, expected) 91 | 92 | def test_parse_extra_vars_json_yaml_dict(self): 93 | self.extra_vars_json_yaml_fixture('dict') 94 | 95 | def test_parse_extra_vars_json_yaml_dict_with_list(self): 96 | self.extra_vars_json_yaml_fixture('dict_with_list') 97 | 98 | def test_parse_extra_vars_json_yaml_dict_dict(self): 99 | self.extra_vars_json_yaml_fixture('dict_dict') 100 | 101 | def test_parse_extra_vars_json_yaml_dict_multi(self): 102 | self.extra_vars_json_yaml_fixture('dict_multi') 103 | 104 | def test_parse_extra_vars_json_yaml_dict_with_list_multi(self): 105 | self.extra_vars_json_yaml_fixture('dict_with_list_multi') 106 | 107 | def test_parse_extra_vars_json_yaml_dict_dict_multi(self): 108 | self.extra_vars_json_yaml_fixture('dict_dict_multi') 109 | 110 | def extra_vars_complex_yaml_fixture(self, test_name): 111 | arg = '--extra_vars' 112 | test_yaml = self.load_yaml('extra_vars_complex.yaml') 113 | test = next(t for t in test_yaml if t['name'] == test_name) 114 | case = test['test'] 115 | # this does not preserve the order exactly, but it shows that elements are correctly parsed 116 | expected = ['--extra-vars={}'.format(e) 117 | for e in test['expected'] if isinstance(e, six.string_types)] 118 | expected.extend(['--extra-vars={}'.format(json.dumps(e)) 119 | for e in test['expected'] if isinstance(e, dict)]) 120 | self.check_arg_parse(arg, case, expected) 121 | 122 | def test_parse_extra_vars_complex_yaml_arbitrarily_complex(self): 123 | self.extra_vars_complex_yaml_fixture('arbitrarily_complex') 124 | --------------------------------------------------------------------------------