├── .gitignore ├── BUILDING.md ├── CHANGELOG.md ├── LICENSE ├── MANIFEST.in ├── Makefile ├── README.md ├── bin └── battle ├── etc ├── battleschool.yml └── hosts ├── install_local.sh ├── lib └── battleschool │ ├── __init__.py │ ├── constants.py │ ├── main.py │ ├── printing.py │ └── source │ ├── __init__.py │ ├── git.py │ ├── local.py │ └── url.py ├── pypi_upload.sh ├── setup.py ├── share ├── callback_plugins │ └── battleschool_callback.py ├── defaults │ ├── battleschool.yml │ └── hosts └── library │ └── mac_pkg └── test ├── ansible_module_dynamic.py ├── emptyconfig └── config.yml ├── test_app_dmg.sh ├── test_app_file.sh ├── test_app_tar.sh ├── test_pkg_file.sh ├── test_pkg_file_eula.sh ├── test_pkg_java.sh ├── test_pkg_macports.sh ├── test_pkg_vagrant.sh ├── test_pkg_zip.sh ├── test_script_brew.sh └── testconfig ├── config.yml └── playbooks ├── another_playbook.yml ├── library └── time-test └── playbook.yml /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | test/testconfig/cache 3 | test/test_cache 4 | MANIFEST 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Packages 10 | *.egg 11 | *.egg-info 12 | dist 13 | build 14 | eggs 15 | parts 16 | var 17 | sdist 18 | develop-eggs 19 | .installed.cfg 20 | lib64 21 | 22 | # Installer logs 23 | pip-log.txt 24 | 25 | # Unit test / coverage reports 26 | .coverage 27 | .tox 28 | nosetests.xml 29 | 30 | # Translations 31 | *.mo 32 | 33 | .idea 34 | *.iml 35 | out 36 | 37 | # Mr Developer 38 | .mr.developer.cfg 39 | .project 40 | .pydevproject 41 | -------------------------------------------------------------------------------- /BUILDING.md: -------------------------------------------------------------------------------- 1 | ### Building locally 2 | 3 | git checkout devel 4 | 5 | # make changes 6 | 7 | git commit 8 | 9 | git push 10 | 11 | git checkout master 12 | 13 | git merge devel 14 | 15 | make sdist 16 | 17 | git checkout devel #make sure to go back to devel to make changes 18 | 19 | ### deploying to pypi 20 | 21 | make sure `~/.pypirc` is setup correctly 22 | 23 | [pypirc] 24 | servers = pypi 25 | [server-login] 26 | username: 27 | password: 28 | 29 | then 30 | 31 | python setup.py sdist upload 32 | 33 | #### Tips for building on a blank osx vm 34 | 35 | * http://anadoxin.org/blog/creating-a-bootable-el-capitan-iso-image.html 36 | * http://ntk.me/2012/09/07/os-x-on-os-x/ 37 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ### Version 0.7.0 2 | 3 | - #46 & #37 fixed installs on OS X El Capitan. Moved battleschool files to `/usr/local` 4 | - #39 via #40 added `--use-default-callbacks` option to use ansible callbacks for things like prompting vars via @OldhamMade. 5 | - #38 fixed some error printing bugs via @OldhamMade. 6 | - #42 add remaining password options from updated ansible via @dochang. 7 | - #45 updated usage on readme via @vladdancer. 8 | 9 | ### Version 0.6.1 10 | 11 | - #33 extra vars parse error with ansible v1.9.1 via @subsetpark 12 | 13 | ### Version 0.6.0 14 | 15 | - #25 battleschool now supports ansible v1.9.1 and it is required (thanks to @eyadsibai for pointing me to @dochang's initial fix https://github.com/dochang/battleschool/commit/e54ded165dd80060741b844cb2f09877b1b4f6d6). The new short option for `--update-sources` is `-X`. 16 | 17 | ### Version 0.5.2 18 | 19 | - #28 added script_postfix and script_data params to mac_pkg (for homebrew install) 20 | 21 | ### Version 0.5.1 22 | 23 | - require ansible version <= 1.8.4 see #25 for support of ansible 1.9.x 24 | 25 | ### Version 0.5.0 26 | 27 | - merged #23 by @OldhamMade allow use of battleschool as a library 28 | 29 | ### Version 0.4.1 30 | 31 | - merged #22 by @OldhamMade allow overriding of `battleschool_dir` variable to be passed into `main()`, allowing easier extension of battleschool by external tools. 32 | - error messages that provide better debugging 33 | 34 | ### Version 0.4.0 35 | 36 | - merged #16 by @acaire to fix urls with special characters 37 | - merged #17 by @lndbrg to support newer versions of ansible 1.8+ 38 | 39 | ### Version 0.3.6 40 | 41 | - fixes #11 allowing incomplete or missing config.yml (noted by @AnneTheAgile) 42 | - doc fixes by @robyoung 5d8ddff03577146551f3f443202388522837abe3 43 | - fixed os.path.isfile call #9 by @graingert 44 | 45 | ### Version 0.3.5 46 | 47 | - allow the mac_pkg module to be run outside of battleschool (#7 courtesy of @vascoosx) 48 | - move respository to https://github.com/spencergibb/battleschool 49 | 50 | ### Version 0.3.4 51 | 52 | - added symlinks=True to copytree in AppPackage.install (fixes app installs that have symlinks) 53 | - added jinja2 and pyyaml as setup.py requires to fix installs using homebrew installed pip and python 54 | 55 | ### Version 0.3.3 56 | 57 | - serialize extra_vars to $TMPDIR/battleschool_extra_vars.json so mac_pkg can read options set in battle 5db798827a 58 | - added acquire-only option (useful to prep for demos) 59 | - removed --tags option since not all playbooks will have the same tags 60 | 61 | ### Version 0.3.2 62 | 63 | - added archive_type=tar support bc9180b5fbf 64 | 65 | ### Version 0.3.1 66 | 67 | - fixed bugs introduced if using ansible 1.5 68 | 69 | ### Version 0.2.2 70 | 71 | - Changed the `--update-source` option to `--update-sources` 72 | 73 | ### Version 0.2.1 74 | 75 | - remote config.yml (for first run on a machine) 76 | - url sources 77 | - configurable cache_dir 78 | 79 | ### Version 0.1.0 80 | 81 | - initial open source release -------------------------------------------------------------------------------- /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, and 10 | distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by the copyright 13 | owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all other entities 16 | that control, are controlled by, or are under common control with that entity. 17 | For the purposes of this definition, "control" means (i) the power, direct or 18 | indirect, to cause the direction or management of such entity, whether by 19 | contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the 20 | outstanding shares, or (iii) beneficial ownership of such entity. 21 | 22 | "You" (or "Your") shall mean an individual or Legal Entity exercising 23 | permissions granted by this License. 24 | 25 | "Source" form shall mean the preferred form for making modifications, including 26 | but not limited to software source code, documentation source, and configuration 27 | files. 28 | 29 | "Object" form shall mean any form resulting from mechanical transformation or 30 | translation of a Source form, including but not limited to compiled object code, 31 | generated documentation, and conversions to other media types. 32 | 33 | "Work" shall mean the work of authorship, whether in Source or Object form, made 34 | available under the License, as indicated by a copyright notice that is included 35 | in or attached to the work (an example is provided in the Appendix below). 36 | 37 | "Derivative Works" shall mean any work, whether in Source or Object form, that 38 | is based on (or derived from) the Work and for which the editorial revisions, 39 | annotations, elaborations, or other modifications represent, as a whole, an 40 | original work of authorship. For the purposes of this License, Derivative Works 41 | shall not include works that remain separable from, or merely link (or bind by 42 | name) to the interfaces of, the Work and Derivative Works thereof. 43 | 44 | "Contribution" shall mean any work of authorship, including the original version 45 | of the Work and any modifications or additions to that Work or Derivative Works 46 | thereof, that is intentionally submitted to Licensor for inclusion in the Work 47 | by the copyright owner or by an individual or Legal Entity authorized to submit 48 | on behalf of the copyright owner. For the purposes of this definition, 49 | "submitted" means any form of electronic, verbal, or written communication sent 50 | to the Licensor or its representatives, including but not limited to 51 | communication on electronic mailing lists, source code control systems, and 52 | issue tracking systems that are managed by, or on behalf of, the Licensor for 53 | the purpose of discussing and improving the Work, but excluding communication 54 | that is conspicuously marked or otherwise designated in writing by the copyright 55 | owner as "Not a Contribution." 56 | 57 | "Contributor" shall mean Licensor and any individual or Legal Entity on behalf 58 | of whom a Contribution has been received by Licensor and subsequently 59 | incorporated within the Work. 60 | 61 | 2. Grant of Copyright License. 62 | 63 | Subject to the terms and conditions of this License, each Contributor hereby 64 | grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, 65 | irrevocable copyright license to reproduce, prepare Derivative Works of, 66 | publicly display, publicly perform, sublicense, and distribute the Work and such 67 | Derivative Works in Source or Object form. 68 | 69 | 3. Grant of Patent License. 70 | 71 | Subject to the terms and conditions of this License, each Contributor hereby 72 | grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, 73 | irrevocable (except as stated in this section) patent license to make, have 74 | made, use, offer to sell, sell, import, and otherwise transfer the Work, where 75 | such license applies only to those patent claims licensable by such Contributor 76 | that are necessarily infringed by their Contribution(s) alone or by combination 77 | of their Contribution(s) with the Work to which such Contribution(s) was 78 | submitted. If You institute patent litigation against any entity (including a 79 | cross-claim or counterclaim in a lawsuit) alleging that the Work or a 80 | Contribution incorporated within the Work constitutes direct or contributory 81 | patent infringement, then any patent licenses granted to You under this License 82 | for that Work shall terminate as of the date such litigation is filed. 83 | 84 | 4. Redistribution. 85 | 86 | You may reproduce and distribute copies of the Work or Derivative Works thereof 87 | in any medium, with or without modifications, and in Source or Object form, 88 | provided that You meet the following conditions: 89 | 90 | You must give any other recipients of the Work or Derivative Works a copy of 91 | this License; and 92 | You must cause any modified files to carry prominent notices stating that You 93 | changed the files; and 94 | You must retain, in the Source form of any Derivative Works that You distribute, 95 | all copyright, patent, trademark, and attribution notices from the Source form 96 | of the Work, excluding those notices that do not pertain to any part of the 97 | Derivative Works; and 98 | If the Work includes a "NOTICE" text file as part of its distribution, then any 99 | Derivative Works that You distribute must include a readable copy of the 100 | attribution notices contained within such NOTICE file, excluding those notices 101 | that do not pertain to any part of the Derivative Works, in at least one of the 102 | following places: within a NOTICE text file distributed as part of the 103 | Derivative Works; within the Source form or documentation, if provided along 104 | with the Derivative Works; or, within a display generated by the Derivative 105 | Works, if and wherever such third-party notices normally appear. The contents of 106 | the NOTICE file are for informational purposes only and do not modify the 107 | License. You may add Your own attribution notices within Derivative Works that 108 | You distribute, alongside or as an addendum to the NOTICE text from the Work, 109 | provided that such additional attribution notices cannot be construed as 110 | modifying the License. 111 | You may add Your own copyright statement to Your modifications and may provide 112 | additional or different license terms and conditions for use, reproduction, or 113 | distribution of Your modifications, or for any such Derivative Works as a whole, 114 | provided Your use, reproduction, and distribution of the Work otherwise complies 115 | with the conditions stated in this License. 116 | 117 | 5. Submission of Contributions. 118 | 119 | Unless You explicitly state otherwise, any Contribution intentionally submitted 120 | for inclusion in the Work by You to the Licensor shall be under the terms and 121 | conditions of this License, without any additional terms or conditions. 122 | Notwithstanding the above, nothing herein shall supersede or modify the terms of 123 | any separate license agreement you may have executed with Licensor regarding 124 | such Contributions. 125 | 126 | 6. Trademarks. 127 | 128 | This License does not grant permission to use the trade names, trademarks, 129 | service marks, or product names of the Licensor, except as required for 130 | reasonable and customary use in describing the origin of the Work and 131 | reproducing the content of the NOTICE file. 132 | 133 | 7. Disclaimer of Warranty. 134 | 135 | Unless required by applicable law or agreed to in writing, Licensor provides the 136 | Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, 137 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, 138 | including, without limitation, any warranties or conditions of TITLE, 139 | NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are 140 | solely responsible for determining the appropriateness of using or 141 | redistributing the Work and assume any risks associated with Your exercise of 142 | permissions under this License. 143 | 144 | 8. Limitation of Liability. 145 | 146 | In no event and under no legal theory, whether in tort (including negligence), 147 | contract, or otherwise, unless required by applicable law (such as deliberate 148 | and grossly negligent acts) or agreed to in writing, shall any Contributor be 149 | liable to You for damages, including any direct, indirect, special, incidental, 150 | or consequential damages of any character arising as a result of this License or 151 | out of the use or inability to use the Work (including but not limited to 152 | damages for loss of goodwill, work stoppage, computer failure or malfunction, or 153 | any and all other commercial damages or losses), even if such Contributor has 154 | been advised of the possibility of such damages. 155 | 156 | 9. Accepting Warranty or Additional Liability. 157 | 158 | While redistributing the Work or Derivative Works thereof, You may choose to 159 | offer, and charge a fee for, acceptance of support, warranty, indemnity, or 160 | other liability obligations and/or rights consistent with this License. However, 161 | in accepting such obligations, You may act only on Your own behalf and on Your 162 | sole responsibility, not on behalf of any other Contributor, and only if You 163 | agree to indemnify, defend, and hold each Contributor harmless for any liability 164 | incurred by, or claims asserted against, such Contributor by reason of your 165 | accepting any such warranty or additional liability. 166 | 167 | END OF TERMS AND CONDITIONS 168 | 169 | APPENDIX: How to apply the Apache License to your work 170 | 171 | To apply the Apache License to your work, attach the following boilerplate 172 | notice, with the fields enclosed by brackets "[]" replaced with your own 173 | identifying information. (Don't include the brackets!) The text should be 174 | enclosed in the appropriate comment syntax for the file format. We also 175 | recommend that a file or class name and description of purpose be included on 176 | the same "printed page" as the copyright notice for easier identification within 177 | third-party archives. 178 | 179 | Copyright [yyyy] [name of copyright owner] 180 | 181 | Licensed under the Apache License, Version 2.0 (the "License"); 182 | you may not use this file except in compliance with the License. 183 | You may obtain a copy of the License at 184 | 185 | http://www.apache.org/licenses/LICENSE-2.0 186 | 187 | Unless required by applicable law or agreed to in writing, software 188 | distributed under the License is distributed on an "AS IS" BASIS, 189 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 190 | See the License for the specific language governing permissions and 191 | limitations under the License. 192 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.md COPYING 2 | include packaging/distutils/setup.py 3 | recursive-include share * 4 | include Makefile 5 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/make 2 | # WARN: gmake syntax 3 | ######################################################## 4 | # Makefile for Battleschool 5 | # 6 | # useful targets: 7 | # make sdist ---------------- produce a tarball 8 | 9 | ######################################################## 10 | # variable section 11 | 12 | PYTHON=python 13 | 14 | ######################################################## 15 | 16 | all: clean python 17 | 18 | clean: 19 | @echo "Cleaning up distutils stuff" 20 | rm -rf build 21 | rm -rf dist 22 | @echo "Cleaning up byte compiled python stuff" 23 | find . -type f -regex ".*\.py[co]$$" -delete 24 | @echo "Cleaning up editor backup files" 25 | find . -type f \( -name "*~" -or -name "#*" \) -delete 26 | find . -type f \( -name "*.swp" \) -delete 27 | 28 | python: 29 | $(PYTHON) setup.py build 30 | 31 | install: 32 | $(PYTHON) setup.py install 33 | 34 | sdist: clean 35 | $(PYTHON) setup.py sdist -t MANIFEST.in 36 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## battleschool 2 | 3 | Development environment provisioning using [ansible](http://www.ansibleworks.com/docs/), 4 | ala [boxen](http://boxen.github.com/) which uses [puppet](http://puppetlabs.com/puppet/what-is-puppet) and 5 | [kitchenplan](https://github.com/kitchenplan/kitchenplan) which uses [chef](http://docs.opscode.com/) 6 | Built on and for macs, but should be usable on Linux 7 | 8 | See this [blog post](http://spencer.gibb.us/blog/2014/02/03/introducing-battleschool) for some background. 9 | 10 | ### install 11 | 12 | #if needed 13 | sudo easy_install pip 14 | 15 | sudo pip install battleschool 16 | 17 | ### install preview releases 18 | 19 | sudo pip install https://github.com/spencergibb/battleschool/releases/download/v0.x.0/battleschool-0.x.0.tar.gz 20 | 21 | ### running battleschool for the first time 22 | 23 | battle --config-file http://somesite/path/to/your/config.yml 24 | 25 | ### installing on linux 26 | 27 | *NOTE*: You'll need to install libyaml-dev 28 | 29 | e.g. on ubuntu based systems 30 | 31 | sudo apt-get install libyaml-dev 32 | 33 | As long as your `config.yml` doesn't have a `source.local` section (see [configuration](#configuration) below), you don't need to download or create a configuration for the first time. 34 | 35 | ### running battleschool 36 | 37 | `battle` 38 | 39 | ### configuration 40 | 41 | *NOTE: in the future, a default empty configuration will be created if you do not create one* 42 | 43 | `mkdir ~/.battleschool` 44 | 45 | put the following in `~/.battleschool/config.yml` and uncomment the items you want intstalled (remove the #) 46 | 47 | --- 48 | sources: 49 | local: 50 | #- playbook.yml 51 | 52 | url: 53 | #- name: playbook.yml 54 | # url: https://db.tt/VcyI9dvr 55 | 56 | git: 57 | - name: 'osx' 58 | repo: 'https://github.com/spencergibb/ansible-osx' 59 | playbooks: 60 | #- adium.yml 61 | #- alfred.yml 62 | #- better-touch-tool.yml 63 | #- chrome-beta.yml 64 | #- dropbox.yml 65 | #- github.yml 66 | #- gitx.yml 67 | #- intellij-idea-ultimate.yml 68 | #- iterm2.yml 69 | #- java7.yml 70 | #- libreoffice.yml 71 | #- sequel-pro.yml 72 | #- skype.yml 73 | #- truecrypt.yml 74 | #- usb-overdrive.yml 75 | #- vagrant.yml 76 | #- virtualbox.yml 77 | #- xtra-finder.yml 78 | 79 | [Here is my config.yml](https://db.tt/aG2uyydU) 80 | 81 | [Here is my playbook.yml](https://db.tt/VcyI9dvr) 82 | 83 | ### explanation of ~/.battleschool/config.yml 84 | 85 | #### local sources 86 | 87 | sources: 88 | local: 89 | - playbook.yml 90 | 91 | Any [ansible playbooks](http://www.ansibleworks.com/docs/#playbooks) located in `~/.battleschool/playbooks` 92 | can be listed under local. Each playbook will be executed in order. This can be useful for custom 93 | configuration per workstation. (You could install apps with homebrew or macports if those are installed, for example) 94 | 95 | #### url sources 96 | 97 | url: 98 | - name: playbook.yml 99 | url: https://db.tt/VcyI9dvr 100 | 101 | Playbooks located at a url. Each playbook will be executed in order. Helpful for bootstrapping (ie, the first time 102 | you run battleschool. 103 | 104 | #### git sources 105 | 106 | git: 107 | - name: 'osx' 108 | repo: 'https://github.com/spencergibb/ansible-osx' 109 | playbooks: 110 | - adium.yml 111 | 112 | Any git repo that hosts ansible playbooks (specific to battleschool or not) will work here. Each item under 113 | playbooks is the relative location to a playbook in the specified git repository. In the example above, `adium.yml` 114 | is in the root of the ansible-osx repository. 115 | 116 | #### git repo sources 117 | 118 | Directory Layout 119 | 120 | The top level of the directory would contain files and directories like so: 121 | 122 | local.yml # master playbook, after ansible-pull, automatically run, no need to list under playbooks 123 | # NOT REQUIRED 124 | 125 | dev.yml # playbook for dev 126 | ux.yml # playbook for ux 127 | chrome.yml # playbook for chrome 128 | 129 | roles/ # standard ansible role hierarchy 130 | library/ # remote module definitions 131 | 132 | See the [roles docs](http://www.ansibleworks.com/docs/playbooks_roles.html) for information about ansible roles and 133 | library is the location for placing [custom ansible modules](http://www.ansibleworks.com/docs/developing_modules.html) 134 | 135 | 136 | #### the mac_pkg module 137 | 138 | if you look most of the playbooks in [this git repo](https://github.com/spencergibb/ansible-osx) you will see the use of 139 | the mac_pkg module. Mac apps are usually a pkg (or mpgk) installer, or the bare .app directory. They can be archived 140 | in a number of formats: DMG or zip commonly. Pkg files may not be archived at all. Less common formats (tar or 7zip) 141 | are not supported yet. 142 | 143 | Lets look at adium.yml 144 | 145 | --- 146 | - hosts: workstation 147 | 148 | tasks: 149 | - name: install Adium 150 | mac_pkg: pkg_type=app 151 | url=http://sourceforge.net/projects/adium/files/Adium_1.5.7.dmg/download 152 | archive_type=dmg archive_path=Adium.app 153 | sudo: yes 154 | 155 | `- hosts: workstation` this is required in each playbook as it targets the local workstation. Though this is generally 156 | arbitrary for most ansible users, it must be `workstation` in battleschool. 157 | 158 | `pkg_type=app` type must be pkg or app. Defaults to pkg 159 | 160 | `url=....` the url of the app to download, alternatively `src=/local/path/to/app.dmg` may be used instead. 161 | 162 | `archive_type=dmg` one of dmg, zip or none. Defaults to none. 163 | 164 | `archive_path=Adium.app` The path to the app or pkg in the archive. 165 | 166 | `sudo: yes` required for mac_pkg tasks (this will prompt you to enter you sudo password only once) 167 | 168 | *NOTE: battleschool, currently does not install apps from the Apple App Store.* 169 | 170 | 171 | ### common battle options 172 | 173 | I alias battle to `battle -K` 174 | 175 | -K, --ask-sudo-pass ask for sudo password 176 | 177 | Force update of the playbooks from a VCS such as git 178 | 179 | -X, --update-sources update playbooks from a version control system (vcs) 180 | 181 | 182 | ### battle USAGE 183 | $ battle -h 184 | Usage: battle 185 | 186 | Options: 187 | --acquire-only configure mac_pkg module to only aquire package (ie 188 | download only) 189 | --ask-become-pass ask for privilege escalation password 190 | -k, --ask-pass ask for SSH password 191 | --ask-su-pass ask for su password (deprecated, use become) 192 | -K, --ask-sudo-pass ask for sudo password (deprecated, use become) 193 | --ask-vault-pass ask for vault password 194 | -b, --become run operations with become (nopasswd implied) 195 | --become-method=BECOME_METHOD 196 | privilege escalation method to use (default=sudo), 197 | valid choices: [ sudo | su | pbrun | pfexec | runas ] 198 | --become-user=BECOME_USER 199 | run operations as this user (default=None) 200 | -C, --check don't make any changes; instead, try to predict some 201 | of the changes that may occur 202 | --config-dir=CONFIG_DIR 203 | config directory for battleschool 204 | (default=~/.battleschool) 205 | --config-file=CONFIG_FILE 206 | config file for battleschool 207 | (default=~/.battleschool/config.yml) 208 | -c CONNECTION, --connection=CONNECTION 209 | connection type to use (default=smart) 210 | -D, --diff when changing (small) files and templates, show the 211 | differences in those files; works great with --check 212 | -e EXTRA_VARS, --extra-vars=EXTRA_VARS 213 | set additional variables as key=value or YAML/JSON 214 | -f FORKS, --forks=FORKS 215 | specify number of parallel processes to use 216 | (default=5) 217 | -h, --help show this help message and exit 218 | -i INVENTORY, --inventory-file=INVENTORY 219 | specify inventory host file 220 | (default=/usr/local/share/battleschool/defaults/hosts) 221 | -l SUBSET, --limit=SUBSET 222 | further limit selected hosts to an additional pattern 223 | --list-hosts outputs a list of matching hosts; does not execute 224 | anything else 225 | --list-tasks do list all tasks that would be executed 226 | -M MODULE_PATH, --module-path=MODULE_PATH 227 | specify path(s) to module library (default=None) 228 | -o, --one-line condense output 229 | --private-key=PRIVATE_KEY_FILE 230 | use this file to authenticate the connection 231 | --step one-step-at-a-time: confirm each task before running 232 | -S, --su run operations with su (deprecated, use become) 233 | -R SU_USER, --su-user=SU_USER 234 | run operations with su as this user (default=root) 235 | (deprecated, use become) 236 | -s, --sudo run operations with sudo (nopasswd) (deprecated, use 237 | become) 238 | -U SUDO_USER, --sudo-user=SUDO_USER 239 | desired sudo user (default=root) (deprecated, use 240 | become) 241 | --syntax-check do a playbook syntax check on the playbook, do not 242 | execute the playbook 243 | -T TIMEOUT, --timeout=TIMEOUT 244 | override the SSH timeout in seconds (default=10) 245 | -t TREE, --tree=TREE log output to this directory 246 | -X, --update-sources update playbooks from sources(git, url, etc...) 247 | --use-default-callbacks 248 | use default ansible callbacks (to exec vars_prompt, 249 | etc.) 250 | -u REMOTE_USER, --user=REMOTE_USER 251 | connect as this user (default=sgibb) 252 | --vault-password-file=VAULT_PASSWORD_FILE 253 | vault password file 254 | -v, --verbose verbose mode (-vvv for more, -vvvv to enable 255 | connection debugging) 256 | --version show program's version number and exit 257 | 258 | For more options see `ansible-playbook -h` 259 | 260 | ================= 261 | 262 | TODO: cleanup cli output 263 | 264 | TODO: more docs 265 | 266 | TODO: default to ask sudo pass (simpler options). Only don't ask if --no-sudo-pass is true 267 | -------------------------------------------------------------------------------- /bin/battle: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import sys 4 | from battleschool.main import main 5 | 6 | if __name__ == '__main__': 7 | sys.exit(main(sys.argv[1:])) 8 | -------------------------------------------------------------------------------- /etc/battleschool.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - hosts: workstation 3 | vars_files: 4 | - ~/.battleschool/config.yml 5 | 6 | tasks: 7 | - name: print from playbook 8 | debug: msg="in battleschool.yml" 9 | 10 | #- include: {{item}} 11 | # with_items: playbooks -------------------------------------------------------------------------------- /etc/hosts: -------------------------------------------------------------------------------- 1 | [workstation] 2 | localhost ansible_connection=local -------------------------------------------------------------------------------- /install_local.sh: -------------------------------------------------------------------------------- 1 | sudo pip uninstall -y battleschool 2 | make sdist 3 | VER=`cat lib/battleschool/__init__.py | grep version | awk '{ print $3}' | tr -d "'"` 4 | sudo pip install dist/battleschool-${VER}.tar.gz 5 | -------------------------------------------------------------------------------- /lib/battleschool/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = '0.7.0' 2 | __author__ = 'Spencer Gibb' 3 | -------------------------------------------------------------------------------- /lib/battleschool/constants.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | 5 | def get_config(env_var, default): 6 | """ return a configuration variable with casting """ 7 | if env_var is not None: 8 | value = os.environ.get(env_var, None) 9 | if value is not None: 10 | return value 11 | return default 12 | 13 | 14 | # Needed so the RPM can call setup.py and have modules land in the 15 | # correct location. See #1277 for discussion 16 | if getattr(sys, "real_prefix", None): 17 | DIST_MODULE_PATH = os.path.join(sys.prefix, 'share/battleschool/') 18 | else: 19 | DIST_MODULE_PATH = '/usr/local/share/battleschool/' 20 | 21 | DEFAULT_MODULE_PATH = get_config('BATTLESCHOOL_LIBRARY', os.path.join(DIST_MODULE_PATH, 'library')) 22 | DEFAULT_HOST_LIST = os.path.expanduser(get_config('BATTLESCHOOL_HOSTS', os.path.join(DIST_MODULE_PATH, 'defaults', 'hosts'))) 23 | DEFAULT_PLAYBOOK = os.path.expanduser(get_config('BATTLESCHOOL_PLAYBOOK', os.path.join(DIST_MODULE_PATH, 'defaults', 'battleschool.yml'))) 24 | DEFAULT_CALLBACK_PLUGIN_PATH = os.path.expanduser(get_config('BATTLESCHOOL_CALLBACK_PLUGINS', os.path.join(DIST_MODULE_PATH, 'callback_plugins'))) 25 | DEFAULT_SUDO_FLAGS = get_config('BATTLESCHOOL_SUDO_FLAGS', '-E') 26 | 27 | -------------------------------------------------------------------------------- /lib/battleschool/main.py: -------------------------------------------------------------------------------- 1 | import ansible.constants as AC 2 | import battleschool.constants as C 3 | 4 | # before callbacks import 5 | AC.DEFAULT_CALLBACK_PLUGIN_PATH = C.DEFAULT_CALLBACK_PLUGIN_PATH 6 | 7 | import ansible.playbook 8 | import ansible.utils.template 9 | from ansible import callbacks 10 | from ansible import errors 11 | from ansible import utils 12 | 13 | from battleschool.__init__ import __version__ 14 | from battleschool.printing import * 15 | from battleschool.source.git import Git 16 | from battleschool.source.local import Local 17 | from battleschool.source.url import Url 18 | 19 | from copy import deepcopy 20 | from tempfile import gettempdir 21 | from sys import platform as _platform 22 | from urlparse import urlparse 23 | 24 | import json 25 | import os 26 | import platform 27 | import sys 28 | import tempfile 29 | 30 | # TODO: verify environment: ansible 31 | 32 | def getSourceHandlers(): 33 | handlers = [Git, Local, Url] 34 | # TODO: auto load sources 35 | # for name, obj in inspect.getmembers(sourcepkg): 36 | # if inspect.ismodule(obj): 37 | # package = obj.__dict__['__package__'] 38 | # if package is not None and package.startswith('battleschool.source'): 39 | # for srcName, srcObj in inspect.getmembers(obj): 40 | # if inspect.isclass(srcObj) and srcName != 'Source': 41 | # print "Loading %s/%s" % (srcName, srcObj) 42 | # handlers.append(srcObj) 43 | return handlers 44 | 45 | 46 | def load_config_path(options, inventory, sshpass, sudopass): 47 | if options.config_file: 48 | config_file = options.config_file 49 | parse_result = urlparse(config_file) 50 | if parse_result.scheme: 51 | url_options = deepcopy(options) 52 | url_options.update_sources = True 53 | url_options.cache_dir = gettempdir() 54 | name = 'downloaded_config.yml' 55 | sources = { 56 | 'url': [ 57 | {'name': name, 'url': config_file} 58 | ] 59 | } 60 | display(banner("Downloading config from url")) 61 | url = Url(url_options, sources) 62 | files = url.run(inventory, sshpass, sudopass) 63 | return files[0] 64 | else: 65 | return config_file 66 | 67 | return "%s/config.yml" % options.config_dir 68 | 69 | 70 | def main(args, battleschool_dir=None): 71 | if not battleschool_dir: 72 | battleschool_dir = "%s/.battleschool" % os.environ['HOME'] 73 | 74 | # TODO: make battle OO or more modular 75 | #----------------------------------------------------------- 76 | # make ansible defaults, battleschool defaults 77 | AC.DEFAULT_HOST_LIST = C.DEFAULT_HOST_LIST 78 | AC.DEFAULT_SUDO_FLAGS = C.DEFAULT_SUDO_FLAGS 79 | 80 | #----------------------------------------------------------- 81 | # create parser for CLI options 82 | usage = "%prog" 83 | parser = utils.base_parser( 84 | constants=AC, 85 | usage=usage, 86 | connect_opts=True, 87 | runas_opts=True, 88 | subset_opts=True, 89 | check_opts=True, 90 | diff_opts=True, 91 | output_opts=True 92 | ) 93 | parser.version = "%s %s" % ("battleschool", __version__) 94 | # parser.add_option('--tags', dest='tags', default='all', 95 | # help="only run plays and tasks tagged with these values") 96 | parser.add_option('--syntax-check', dest='syntax', action='store_true', 97 | help="do a playbook syntax check on the playbook, do not execute the playbook") 98 | parser.add_option('--list-tasks', dest='listtasks', action='store_true', 99 | help="do list all tasks that would be executed") 100 | parser.add_option('--step', dest='step', action='store_true', 101 | help="one-step-at-a-time: confirm each task before running") 102 | parser.add_option('--config-dir', dest='config_dir', default=None, 103 | help="config directory for battleschool (default=%s)" % battleschool_dir) 104 | parser.add_option('--config-file', dest='config_file', default=None, 105 | help="config file for battleschool (default=%s/%s)" % (battleschool_dir, "config.yml")) 106 | parser.add_option('-X', '--update-sources', dest='update_sources', default=False, action='store_true', 107 | help="update playbooks from sources(git, url, etc...)") 108 | parser.add_option('--acquire-only', dest='acquire_only', default=False, action='store_true', 109 | help="configure mac_pkg module to only aquire package (ie download only)") 110 | parser.add_option('--use-default-callbacks', dest='use_default_callbacks', 111 | default=False, action='store_true', 112 | help="use default ansible callbacks (to exec vars_prompt, etc.)") 113 | 114 | options, args = parser.parse_args(args) 115 | # options.connection = 'local' 116 | 117 | playbooks_to_run = [] #[C.DEFAULT_PLAYBOOK] 118 | 119 | #----------------------------------------------------------- 120 | # setup inventory 121 | inventory = ansible.inventory.Inventory(options.inventory) 122 | inventory.subset(options.subset) 123 | if len(inventory.list_hosts()) == 0: 124 | raise errors.AnsibleError("provided hosts list is empty") 125 | 126 | #----------------------------------------------------------- 127 | # setup default options 128 | sshpass = None 129 | sudopass = None 130 | vault_pass = None 131 | options.remote_user = AC.DEFAULT_REMOTE_USER 132 | if not options.listhosts and not options.syntax and not options.listtasks: 133 | options.ask_pass = AC.DEFAULT_ASK_PASS 134 | options.ask_sudo_pass = options.ask_sudo_pass or AC.DEFAULT_ASK_SUDO_PASS 135 | options.become_method = options.become_method or AC.DEFAULT_BECOME_METHOD 136 | passwds = utils.ask_passwords(ask_pass=options.ask_pass, become_ask_pass=options.ask_sudo_pass, ask_vault_pass=options.ask_vault_pass, become_method=options.become_method) 137 | sshpass = passwds[0] 138 | sudopass = passwds[1] 139 | vault_pass = passwds[2] 140 | # if options.sudo_user or options.ask_sudo_pass: 141 | # options.sudo = True 142 | options.sudo_user = AC.DEFAULT_SUDO_USER 143 | 144 | extra_vars = utils.parse_extra_vars(options.extra_vars, vault_pass) 145 | only_tags = None # options.tags.split(",") 146 | 147 | #----------------------------------------------------------- 148 | # setup config_dir and battleschool_dir 149 | if options.config_dir: 150 | battleschool_dir = options.config_dir 151 | else: 152 | options.config_dir = battleschool_dir 153 | 154 | #----------------------------------------------------------- 155 | # setup module_path 156 | if options.module_path is None: 157 | options.module_path = AC.DEFAULT_MODULE_PATH 158 | 159 | if options.module_path is None: 160 | options.module_path = C.DEFAULT_MODULE_PATH 161 | 162 | if C.DEFAULT_MODULE_PATH not in options.module_path: 163 | options.module_path = "%s:%s" % (C.DEFAULT_MODULE_PATH, options.module_path) 164 | 165 | #----------------------------------------------------------- 166 | # parse config data 167 | config_path = load_config_path(options, inventory, sshpass, sudopass) 168 | if os.path.exists(config_path) and os.path.isfile(config_path): 169 | config_data = utils.parse_yaml_from_file(config_path) 170 | else: 171 | config_data = {} 172 | 173 | #----------------------------------------------------------- 174 | # set config_dir 175 | if "cache_dir" in config_data: 176 | options.cache_dir = os.path.expanduser(config_data["cache_dir"]) 177 | elif _platform == "darwin": # OS X 178 | options.cache_dir = os.path.expanduser("~/Library/Caches/battleschool") 179 | else: 180 | options.cache_dir = "%s/cache" % battleschool_dir 181 | 182 | os.environ["BATTLESCHOOL_CACHE_DIR"] = options.cache_dir 183 | 184 | #----------------------------------------------------------- 185 | # setup extra_vars for later use 186 | if extra_vars is None: 187 | extra_vars = dict() 188 | 189 | extra_vars['battleschool_config_dir'] = battleschool_dir 190 | extra_vars['battleschool_cache_dir'] = options.cache_dir 191 | extra_vars['mac_pkg_acquire_only'] = options.acquire_only 192 | 193 | #----------------------------------------------------------- 194 | # set mac_version for extra_vars 195 | if _platform == "darwin": 196 | mac_version = platform.mac_ver()[0].split(".") 197 | extra_vars['mac_version'] = mac_version 198 | extra_vars['mac_major_minor_version'] = "%s.%s" % (mac_version[0], mac_version[1]) 199 | 200 | #----------------------------------------------------------- 201 | # serialize extra_vars since there is now way to pass data 202 | # to a module without modifying every playbook 203 | tempdir = tempfile.gettempdir() 204 | extra_vars_path = os.path.join(tempdir, "battleschool_extra_vars.json") 205 | with open(extra_vars_path, 'w') as f: 206 | f.write(json.dumps(extra_vars)) 207 | 208 | #----------------------------------------------------------- 209 | # setup and run source handlers 210 | handlers = getSourceHandlers() 211 | 212 | if 'sources' in config_data and config_data['sources']: 213 | sources = config_data['sources'] 214 | display(banner("Updating sources")) 215 | for handler in handlers: 216 | source = handler(options, sources) 217 | playbooks = source.run(inventory, sshpass, sudopass) 218 | for playbook in playbooks: 219 | playbooks_to_run.append(playbook) 220 | else: 221 | display(banner("No sources to update")) 222 | 223 | #----------------------------------------------------------- 224 | # validate playbooks 225 | for playbook in playbooks_to_run: 226 | if not os.path.exists(playbook): 227 | raise errors.AnsibleError("the playbook: %s could not be found" % playbook) 228 | if not os.path.isfile(playbook): 229 | raise errors.AnsibleError("the playbook: %s does not appear to be a file" % playbook) 230 | 231 | become = True 232 | #----------------------------------------------------------- 233 | # run all playbooks specified from config 234 | for playbook in playbooks_to_run: 235 | stats = callbacks.AggregateStats() 236 | 237 | # let inventory know which playbooks are using so it can know the basedirs 238 | inventory.set_playbook_basedir(os.path.dirname(playbook)) 239 | 240 | if options.use_default_callbacks: 241 | runner_cb = callbacks.PlaybookRunnerCallbacks(stats, verbose=utils.VERBOSITY) 242 | playbook_cb = callbacks.PlaybookCallbacks(verbose=utils.VERBOSITY) 243 | else: 244 | runner_cb = BattleschoolRunnerCallbacks() 245 | playbook_cb = BattleschoolCallbacks() 246 | 247 | if options.step: 248 | playbook_cb.step = options.step 249 | 250 | pb = ansible.playbook.PlayBook( 251 | playbook=playbook, 252 | module_path=options.module_path, 253 | inventory=inventory, 254 | forks=options.forks, 255 | remote_user=options.remote_user, 256 | remote_pass=sshpass, 257 | callbacks=playbook_cb, 258 | runner_callbacks=runner_cb, 259 | stats=stats, 260 | timeout=options.timeout, 261 | transport=options.connection, 262 | become=become, 263 | become_method="sudo", 264 | become_user=options.sudo_user, 265 | become_pass=sudopass, 266 | extra_vars=extra_vars, 267 | private_key_file=options.private_key_file, 268 | only_tags=only_tags, 269 | check=options.check, 270 | diff=options.diff 271 | ) 272 | 273 | if options.listhosts or options.listtasks: 274 | print '' 275 | print 'playbook: %s' % playbook 276 | print '' 277 | playnum = 0 278 | for (play_ds, play_basedir) in zip(pb.playbook, pb.play_basedirs): 279 | playnum += 1 280 | play = ansible.playbook.Play(pb, play_ds, play_basedir) 281 | label = play.name 282 | if options.listhosts: 283 | hosts = pb.inventory.list_hosts(play.hosts) 284 | print ' play #%d (%s): host count=%d' % (playnum, label, len(hosts)) 285 | for host in hosts: 286 | print ' %s' % host 287 | if options.listtasks: 288 | matched_tags, unmatched_tags = play.compare_tags(pb.only_tags) 289 | unmatched_tags.discard('all') 290 | unknown_tags = set(pb.only_tags) - (matched_tags | unmatched_tags) 291 | if unknown_tags: 292 | continue 293 | print ' play #%d (%s): task count=%d' % (playnum, label, len(play.tasks())) 294 | for task in play.tasks(): 295 | if set(task.tags).intersection(pb.only_tags): 296 | if getattr(task, 'name', None) is not None: 297 | # meta tasks have no names 298 | print ' %s' % task.name 299 | print '' 300 | continue 301 | 302 | if options.syntax: 303 | # if we've not exited by now then we are fine. 304 | print 'Playbook Syntax is fine' 305 | return 0 306 | 307 | failed_hosts = [] 308 | 309 | try: 310 | 311 | pb.run() 312 | 313 | hosts = sorted(pb.stats.processed.keys()) 314 | # display(callbacks.banner("PLAY RECAP")) 315 | playbook_cb.on_stats(pb.stats) 316 | 317 | for host in hosts: 318 | smry = pb.stats.summarize(host) 319 | if smry['unreachable'] > 0 or smry['failures'] > 0: 320 | failed_hosts.append(host) 321 | 322 | if len(failed_hosts) > 0: 323 | filename = pb.generate_retry_inventory(failed_hosts) 324 | if filename: 325 | display(" to retry, use: --limit @%s\n" % filename) 326 | 327 | for host in hosts: 328 | smry = pb.stats.summarize(host) 329 | print_stats(host, smry) 330 | 331 | # print "" 332 | if len(failed_hosts) > 0: 333 | return 2 334 | 335 | except errors.AnsibleError, e: 336 | display("ERROR: %s" % e, color='red') 337 | return 1 338 | 339 | if not playbooks_to_run: 340 | display("\tWARNING: no playbooks run!", color='yellow') 341 | 342 | os.remove(extra_vars_path) 343 | display(banner("Battleschool completed")) 344 | # TODO: aggregate stats across playbook runs 345 | -------------------------------------------------------------------------------- /lib/battleschool/printing.py: -------------------------------------------------------------------------------- 1 | from ansible.callbacks import call_callback_module 2 | from ansible.callbacks import display 3 | from ansible.callbacks import DefaultRunnerCallbacks 4 | from ansible.color import ANSIBLE_COLOR, stringc 5 | 6 | import pprint 7 | 8 | pp = pprint.PrettyPrinter() 9 | 10 | 11 | def colorize(lead, num, color): 12 | """ Print 'lead' = 'num' in 'color' """ 13 | if num != 0 and ANSIBLE_COLOR and color is not None: 14 | return "%s%s%s" % (stringc(lead, color), stringc("=", color), stringc(str(num), color)) 15 | else: 16 | return "%s=%s" % (lead, str(num)) 17 | 18 | 19 | def hostcolor(host, stats, color=True): 20 | if ANSIBLE_COLOR and color: 21 | if stats['failures'] != 0 or stats['unreachable'] != 0: 22 | return "%s" % stringc(host, 'red') 23 | elif stats['changed'] != 0: 24 | return "%s" % stringc(host, 'yellow') 25 | else: 26 | return "%s" % stringc(host, 'green') 27 | return "%s" % host 28 | 29 | 30 | def print_stats(host, smry): 31 | ok = smry['ok'] 32 | changed = smry['changed'] 33 | unreachable = smry['unreachable'] 34 | failures = smry['failures'] 35 | 36 | if unreachable > 0 or failures > 0: 37 | status = stringc("FAILED", "red") 38 | else: 39 | status = stringc("OK", "bright green") 40 | 41 | pattern = "\tPlaybook %s, %s, %s, %s, %s" 42 | 43 | display(pattern % (status, 44 | # hostcolor(host, smry, False), 45 | colorize('ok', ok, 'bright green'), 46 | colorize('changed', changed, 'yellow'), 47 | colorize('unreachable', unreachable, 'red'), 48 | colorize('failed', failures, 'red')), 49 | screen_only=True 50 | ) 51 | 52 | display(pattern % (status, 53 | # hostcolor(host, smry, False), 54 | colorize('ok', ok, None), 55 | colorize('changed', changed, None), 56 | colorize('unreachable', unreachable, None), 57 | colorize('failed', failures, None)), 58 | log_only=True 59 | ) 60 | 61 | 62 | def banner(msg): 63 | #TODO: configurable size orig 78 64 | width = 110 - len(msg) 65 | if width < 3: 66 | width = 3 67 | filler = "#" * width 68 | return "## %s %s " % (msg, filler) 69 | 70 | 71 | class BattleschoolRunnerCallbacks(DefaultRunnerCallbacks): 72 | """ callbacks for use by battles """ 73 | 74 | def get_play(self): 75 | if self.runner: 76 | return self.runner 77 | if self.task: 78 | return self.task 79 | return self.play 80 | 81 | def get_name(self): 82 | if self.runner: 83 | return self.get_play().module_name 84 | return self.get_play().name 85 | 86 | def on_failed(self, host, res, ignore_errors=False): 87 | if not ignore_errors: 88 | display("\tTask FAILED: %s %s" % (self.get_name(), res.get('msg')), color="red") 89 | super(BattleschoolRunnerCallbacks, self).on_failed(host, res, ignore_errors=ignore_errors) 90 | 91 | def on_ok(self, host, res): 92 | if 'msg' in res: 93 | msg = ": %s" % res.get('msg') 94 | 95 | if 'item' in res: 96 | msg = "%s => item=%s" % (msg, res.get('item')) 97 | else: 98 | msg = '' 99 | 100 | try: 101 | if self.get_play(): 102 | display("\tTask OK: %s%s" % (self.get_name(), msg)) 103 | except AttributeError: 104 | invocation = res.get('invocation') 105 | msg = invocation['module_args'] 106 | module_name = invocation['module_name'] 107 | 108 | #TODO: move this? 109 | if module_name == 'git': 110 | args = dict() 111 | tokens = invocation['module_args'].split() 112 | for token in tokens: 113 | pair = token.split("=")[:2] 114 | args[pair[0]] = pair[1] 115 | msg = args['repo'] 116 | 117 | display("\tTask OK: %s, changed=%s %s" % (module_name, res.get('changed'), msg)) 118 | super(BattleschoolRunnerCallbacks, self).on_ok(host, res) 119 | 120 | def on_unreachable(self, host, results): 121 | item = None 122 | if type(results) == dict: 123 | item = results.get('item', None) 124 | if item: 125 | msg = "\tFailed Task: %s => (item=%s) => %s" % (host, item, results) 126 | else: 127 | msg = "\tFatal Task: %s => %s" % (host, results) 128 | display(msg, color='red', runner=self.runner) 129 | super(BattleschoolRunnerCallbacks, self).on_unreachable(host, results) 130 | 131 | def on_skipped(self, host, item=None): 132 | display("\tTask skipped: %s" % self.get_name(), color="yellow") 133 | super(BattleschoolRunnerCallbacks, self).on_skipped(host, item) 134 | 135 | def on_error(self, host, err): 136 | display("\tTask ERROR: %s%s" % (self.get_name(), err), color="red") 137 | super(BattleschoolRunnerCallbacks, self).on_error(host, err) 138 | 139 | def on_no_hosts(self): 140 | display("\tTask NO HOSTS: %s" % self.get_name(), color="red") 141 | super(BattleschoolRunnerCallbacks, self).on_no_hosts() 142 | 143 | def on_async_poll(self, host, res, jid, clock): 144 | super(BattleschoolRunnerCallbacks, self).on_async_poll(host, res, jid, clock) 145 | 146 | def on_async_ok(self, host, res, jid): 147 | super(BattleschoolRunnerCallbacks, self).on_async_ok(host, res, jid) 148 | 149 | def on_async_failed(self, host, res, jid): 150 | super(BattleschoolRunnerCallbacks, self).on_async_failed(host,res,jid) 151 | 152 | def on_file_diff(self, host, diff): 153 | super(BattleschoolRunnerCallbacks, self).on_file_diff(host, diff) 154 | 155 | 156 | class BattleschoolCallbacks(object): 157 | """ used by battle for playbooks """ 158 | 159 | def __init__(self, verbose=False): 160 | 161 | self.verbose = verbose 162 | 163 | def on_start(self): 164 | call_callback_module('playbook_on_start') 165 | 166 | def on_notify(self, host, handler): 167 | call_callback_module('playbook_on_notify', host, handler) 168 | 169 | def on_no_hosts_matched(self): 170 | call_callback_module('playbook_on_no_hosts_matched') 171 | 172 | def on_no_hosts_remaining(self): 173 | display("\tFailed playbook: %s" % self.playbook.filename, color='bright red') 174 | call_callback_module('playbook_on_no_hosts_remaining') 175 | 176 | def on_task_start(self, name, is_conditional): 177 | call_callback_module('playbook_on_task_start', name, is_conditional) 178 | 179 | def on_vars_prompt(self, varname, private=True, prompt=None, encrypt=None, confirm=False, salt_size=None, salt=None, default=None): 180 | call_callback_module( 'playbook_on_vars_prompt', varname, private=private, prompt=prompt, 181 | encrypt=encrypt, confirm=confirm, salt_size=salt_size, salt=None, default=default ) 182 | 183 | def on_setup(self): 184 | call_callback_module('playbook_on_setup') 185 | 186 | def on_import_for_host(self, host, imported_file): 187 | call_callback_module('playbook_on_import_for_host', host, imported_file) 188 | 189 | def on_not_import_for_host(self, host, missing_file): 190 | call_callback_module('playbook_on_not_import_for_host', host, missing_file) 191 | 192 | def on_play_start(self, pattern): 193 | display(banner("Executing playbook %s" % self.playbook.filename), color="bright blue") 194 | call_callback_module('playbook_on_play_start', pattern) 195 | 196 | def on_stats(self, stats): 197 | call_callback_module('playbook_on_stats', stats) 198 | -------------------------------------------------------------------------------- /lib/battleschool/source/__init__.py: -------------------------------------------------------------------------------- 1 | from battleschool.printing import BattleschoolRunnerCallbacks 2 | 3 | __author__ = 'spencergibb' 4 | 5 | 6 | import abc 7 | import os 8 | import sys 9 | 10 | from ansible import errors 11 | from ansible.callbacks import display 12 | from ansible.runner import Runner 13 | 14 | 15 | class Source(object): 16 | """Base class for source handlers. 17 | """ 18 | __metaclass__ = abc.ABCMeta 19 | 20 | def __init__(self, options, sources): 21 | self.options = options 22 | if (sources): 23 | self.sources = sources 24 | else: 25 | self.sources = {} 26 | return 27 | 28 | @abc.abstractmethod 29 | def type(self): 30 | pass 31 | 32 | @abc.abstractmethod 33 | def module_args(self, source): 34 | """Override to do something useful. 35 | """ 36 | 37 | def has_source_type(self): 38 | return self.type() in self.sources and self.sources[self.type()] is not None 39 | 40 | def dest_dir(self, source): 41 | dest_dir = "%s/%s" % (self.options.cache_dir, source['name']) 42 | return dest_dir 43 | 44 | def module_name(self): 45 | return self.type() 46 | 47 | def add_playbook(self, playbooks, playbook): 48 | if os.path.exists(playbook) and os.path.isfile(playbook): 49 | playbooks.append(playbook) 50 | 51 | def run_module(self, inventory, source, sshpass, sudopass): 52 | runner_cb = BattleschoolRunnerCallbacks() 53 | runner_cb.options = self.options 54 | runner_cb.options.module_name = self.module_name() 55 | module_args = self.module_args(source) 56 | #TODO: get workstation from options 57 | runner = Runner( 58 | pattern='workstation', 59 | module_name=self.module_name(), 60 | module_path=self.options.module_path, 61 | module_args=module_args, 62 | inventory=inventory, 63 | callbacks=runner_cb, 64 | timeout=self.options.timeout, 65 | transport=self.options.connection, 66 | #sudo=self.options.sudo, 67 | become=False, 68 | become_method="sudo", 69 | become_user=self.options.sudo_user, 70 | become_pass=sudopass, 71 | check=self.options.check, 72 | diff=self.options.diff, 73 | private_key_file=self.options.private_key_file, 74 | remote_user=self.options.remote_user, 75 | remote_pass=sshpass, 76 | forks=self.options.forks 77 | ) 78 | try: 79 | results = runner.run() 80 | for result in results['contacted'].values(): 81 | if 'failed' in result or result.get('rc', 0) != 0: 82 | display("ERROR: failed source type (%s) '%s': %s" % (self.type(), module_args, result['msg']), 83 | stderr=True, color='red') 84 | sys.exit(2) 85 | if results['dark']: 86 | display("ERROR: failed source type (%s) '%s': DARK" % (self.type(), module_args), 87 | stderr=True, color='red') 88 | sys.exit(2) 89 | except errors.AnsibleError, e: 90 | # Generic handler for ansible specific errors 91 | display("ERROR: %s" % str(e), stderr=True, color='red') 92 | sys.exit(1) 93 | 94 | def run(self, inventory, sshpass, sudopass): 95 | playbooks = [] 96 | if self.has_source_type(): 97 | for source in self.sources[self.type()]: 98 | # print source 99 | self.run_module(inventory, source, sshpass, sudopass) 100 | 101 | source_playbooks = ["local.yml"] 102 | 103 | #add other playbooks relative to dest_dir from config.yml 104 | if "playbooks" in source and source['playbooks'] is not None: 105 | for playbook in source['playbooks']: 106 | # support for single level of directories in a playbook repo 107 | if type(playbook) is dict: 108 | for dir in playbook: 109 | names = playbook[dir] 110 | for name in names: 111 | playbook_name = "%s/%s" % (dir, name) 112 | if playbook_name not in source_playbooks: 113 | source_playbooks.append(playbook_name) 114 | elif playbook not in source_playbooks: 115 | source_playbooks.append(playbook) 116 | 117 | for playbook_name in source_playbooks: 118 | suffix = "" 119 | 120 | if not playbook_name.endswith(".yml") and not playbook_name.endswith(".yaml"): 121 | suffix = ".yml" 122 | 123 | playbook = "%s/%s%s" % (self.dest_dir(source), playbook_name, suffix) 124 | self.add_playbook(playbooks, playbook) 125 | 126 | return playbooks 127 | -------------------------------------------------------------------------------- /lib/battleschool/source/git.py: -------------------------------------------------------------------------------- 1 | __author__ = 'spencergibb' 2 | 3 | 4 | from . import Source 5 | 6 | 7 | class Git(Source): 8 | """git source handler. 9 | """ 10 | 11 | def type(self): 12 | return 'git' 13 | 14 | def dest_dir(self, source): 15 | dest_dir = "%s/%s" % (self.options.cache_dir, source['name']) 16 | return dest_dir 17 | 18 | def module_args(self, source): 19 | force = "no" 20 | update = "no" 21 | 22 | if self.options.update_sources: 23 | update = "yes" 24 | force = "yes" 25 | 26 | module_args = "repo=%s dest=%s force=%s update=%s " % \ 27 | (source['repo'], self.dest_dir(source), force, update) 28 | return module_args 29 | -------------------------------------------------------------------------------- /lib/battleschool/source/local.py: -------------------------------------------------------------------------------- 1 | __author__ = 'spencergibb' 2 | 3 | 4 | from . import Source 5 | 6 | 7 | class Local(Source): 8 | """git source handler. 9 | """ 10 | 11 | def type(self): 12 | return 'local' 13 | 14 | def run(self, inventory, sshpass, sudopass): 15 | playbooks = [] 16 | 17 | if self.has_source_type(): 18 | for source in self.sources[self.type()]: 19 | playbook = "%s/playbooks/%s" % (self.options.config_dir, source) 20 | self.add_playbook(playbooks, playbook) 21 | 22 | return playbooks 23 | 24 | def module_args(self, source): 25 | pass 26 | -------------------------------------------------------------------------------- /lib/battleschool/source/url.py: -------------------------------------------------------------------------------- 1 | __author__ = 'spencergibb' 2 | 3 | 4 | from . import Source 5 | 6 | 7 | class Url(Source): 8 | """url source handler. 9 | """ 10 | 11 | def type(self): 12 | return 'url' 13 | 14 | def module_name(self): 15 | return 'get_url' 16 | 17 | def dest_dir(self, source): 18 | return self.options.cache_dir 19 | 20 | def module_args(self, source): 21 | force = "no" 22 | 23 | if self.options.update_sources: 24 | force = "yes" 25 | 26 | module_args = "url=%s dest=%s/%s force=%s validate_certs=no " % \ 27 | (source["url"], self.dest_dir(source), source["name"], force) 28 | 29 | if "playbooks" not in source: 30 | source["playbooks"] = [] 31 | 32 | source["playbooks"].append(source["name"]) 33 | return module_args 34 | -------------------------------------------------------------------------------- /pypi_upload.sh: -------------------------------------------------------------------------------- 1 | python setup.py sdist upload 2 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import os 4 | import sys 5 | from glob import glob 6 | 7 | sys.path.insert(0, os.path.abspath('lib')) 8 | from battleschool import __version__, __author__ 9 | from distutils.core import setup 10 | 11 | # find library modules 12 | from battleschool.constants import DIST_MODULE_PATH 13 | 14 | long_description = """ 15 | Development environment provisioning using ansible (http://docs.ansible.com), 16 | ala boxen (http://boxen.github.com/) which uses puppet (http://puppetlabs.com/puppet/what-is-puppet) and 17 | kitchenplan (https://github.com/kitchenplan/kitchenplan) which uses chef (http://docs.opscode.com/) 18 | Built on and for macs, but should be usable on Linux 19 | """ 20 | 21 | share_path = "./share/" 22 | files = os.listdir(share_path) 23 | data_files = [] 24 | for i in files: 25 | if os.path.isdir(os.path.join(share_path, i)): 26 | data_files.append((DIST_MODULE_PATH + i, glob(share_path + i + '/*'))) 27 | 28 | #if os.path.isfile(os.path.join(share_path, i)): 29 | # data_files.append((DIST_MODULE_PATH, share_path + i)) 30 | 31 | setup(name='battleschool', 32 | version=__version__, 33 | description='simple dev box provisioning', 34 | long_description=long_description, 35 | author=__author__, 36 | author_email='spencer@gibb.us', 37 | url='http://spencer.gibb.us', 38 | download_url='https://github.com/spencergibb/battleschool/releases', 39 | license='Apache License, Version 2.0', 40 | # added jinja2 and pyyaml to fix installs under homebrew pip 41 | install_requires=[ 42 | 'ansible >= 1.9.1', 43 | 'jinja2', 44 | 'pyyaml' 45 | ], 46 | classifiers=[ 47 | "Development Status :: 3 - Alpha", 48 | "Environment :: Console", 49 | "Environment :: MacOS X", 50 | "Intended Audience :: Developers", 51 | "License :: OSI Approved :: Apache Software License", 52 | "Programming Language :: Python :: 2.7", 53 | "Topic :: System :: Installation/Setup" 54 | ], 55 | keywords="provisioning setup install", 56 | package_dir={'battleschool': 'lib/battleschool'}, 57 | packages=[ 58 | 'battleschool', 59 | 'battleschool.source', 60 | ], 61 | scripts=[ 62 | 'bin/battle' 63 | ], 64 | data_files=data_files 65 | ) 66 | -------------------------------------------------------------------------------- /share/callback_plugins/battleschool_callback.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | class CallbackModule(object): 4 | 5 | def display(self, event, *args): 6 | 7 | play = getattr(self, 'play', None) 8 | task = getattr(self, 'task', None) 9 | 10 | if play is not None: 11 | playbook = play.playbook.filename 12 | else: 13 | playbook = "" 14 | 15 | if task is not None: 16 | task_name = task.name 17 | else: 18 | task_name = "" 19 | 20 | print("%25s: play = %s, task = %s, args = %s" % (event, playbook, task_name, args)) 21 | 22 | def on_any(self, *args, **kwargs): 23 | # play = getattr(self, 'play', None) 24 | # task = getattr(self, 'task', None) 25 | # print "on_any: play = %s, task = %s, args = %s, kwargs = %s" % (play, task, args, kwargs) 26 | pass 27 | 28 | def runner_on_failed(self, host, res, ignore_errors=False): 29 | # self.display('runner_on_failed', host, res, ignore_errors) 30 | pass 31 | 32 | def runner_on_ok(self, host, res): 33 | # self.display('runner_on_ok', host, res) 34 | pass 35 | 36 | def runner_on_error(self, host, msg): 37 | # self.display('runner_on_error', host, msg) 38 | pass 39 | 40 | def runner_on_skipped(self, host, item=None): 41 | # self.display('runner_on_skipped', host, item) 42 | pass 43 | 44 | def runner_on_unreachable(self, host, res): 45 | # self.display('runner_on_unreachable', host, res) 46 | pass 47 | 48 | def runner_on_no_hosts(self): 49 | # self.display('runner_on_no_hosts') 50 | pass 51 | 52 | def runner_on_async_poll(self, host, res, jid, clock): 53 | self.display('runner_on_async_poll', host, res, jid, clock) 54 | 55 | def runner_on_async_ok(self, host, res, jid): 56 | self.display('runner_on_async_ok', host, res, jid) 57 | 58 | def runner_on_async_failed(self, host, res, jid): 59 | self.display('runner_on_async_failed', host, res, jid) 60 | 61 | def playbook_on_start(self): 62 | # self.display('playbook_on_start') 63 | pass 64 | 65 | def playbook_on_notify(self, host, handler): 66 | self.display('playbook_on_notify', host, handler) 67 | 68 | def playbook_on_no_hosts_matched(self): 69 | self.display('playbook_on_no_hosts_matched') 70 | 71 | def playbook_on_no_hosts_remaining(self): 72 | # self.display('playbook_on_no_hosts_remaining') 73 | pass 74 | 75 | def playbook_on_task_start(self, name, is_conditional): 76 | # self.display('playbook_on_task_start', name, is_conditional) 77 | pass 78 | 79 | def playbook_on_vars_prompt(self, varname, private=True, prompt=None, encrypt=None, confirm=False, salt_size=None, salt=None, default=None): 80 | self.display('playbook_on_vars_prompt', varname, private, prompt, encrypt, confirm, salt_size, salt, default) 81 | 82 | def playbook_on_setup(self): 83 | # self.display('playbook_on_setup') 84 | pass 85 | 86 | def playbook_on_import_for_host(self, host, imported_file): 87 | self.display('playbook_on_import_for_host', host, imported_file) 88 | 89 | def playbook_on_not_import_for_host(self, host, missing_file): 90 | self.display('playbook_on_not_import_for_host', host, missing_file) 91 | 92 | def playbook_on_play_start(self, pattern): 93 | # self.display('playbook_on_play_start', pattern) 94 | pass 95 | 96 | def playbook_on_stats(self, stats): 97 | # self.display('playbook_on_stats', stats) 98 | pass 99 | 100 | -------------------------------------------------------------------------------- /share/defaults/battleschool.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - hosts: workstation 3 | vars_files: 4 | - ~/.battleschool/config.yml 5 | 6 | tasks: 7 | - name: print from playbook 8 | debug: msg="in battleschool.yml" 9 | -------------------------------------------------------------------------------- /share/defaults/hosts: -------------------------------------------------------------------------------- 1 | [workstation] 2 | localhost ansible_connection=local -------------------------------------------------------------------------------- /share/library/mac_pkg: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | 4 | DOCUMENTATION = ''' 5 | --- 6 | module: mac_pkg 7 | author: Spencer Gibb 8 | short_description: Installs Mac packages 9 | description: 10 | - Installs Mac .pkg or .app files, optionally unarchiving the package first 11 | version_added: "1.1" 12 | options: 13 | state: 14 | description: 15 | - state of the package 16 | choices: [ 'present', 'absent' ] 17 | required: false 18 | default: present 19 | notes: [] 20 | ''' 21 | EXAMPLES = ''' 22 | - mac_pkg: name=foo state=present 23 | - mac_pkg: name=foo state=present update_cache=yes 24 | ''' 25 | 26 | import abc 27 | import hashlib 28 | import json 29 | import os 30 | import shutil 31 | import subprocess 32 | import tempfile 33 | import traceback 34 | 35 | from os import path 36 | 37 | 38 | def run_command(module, args, check_rc=False, close_fds=False, executable=None, data=None, cwd=None): 39 | ''' 40 | Execute a command, returns rc, stdout, and stderr. 41 | args is the command to run 42 | If args is a list, the command will be run with shell=False. 43 | Otherwise, the command will be run with shell=True when args is a string. 44 | Other arguments: 45 | - check_rc (boolean) Whether to call fail_json in case of 46 | non zero RC. Default is False. 47 | - close_fds (boolean) See documentation for subprocess.Popen(). 48 | Default is False. 49 | - executable (string) See documentation for subprocess.Popen(). 50 | Default is None. 51 | - cwd (string) See documentation for subprocess.Popen(). 52 | Default is None. 53 | ''' 54 | if isinstance(args, list): 55 | shell = False 56 | elif isinstance(args, basestring): 57 | shell = True 58 | else: 59 | msg = "Argument 'args' to run_command must be list or string" 60 | module.fail_json(rc=257, cmd=args, msg=msg) 61 | rc = 0 62 | msg = None 63 | st_in = None 64 | if data: 65 | st_in = subprocess.PIPE 66 | try: 67 | cmd = subprocess.Popen(args, 68 | executable=executable, 69 | shell=shell, 70 | close_fds=close_fds, 71 | stdin=st_in, 72 | stdout=subprocess.PIPE, 73 | stderr=subprocess.PIPE, 74 | cwd=cwd) 75 | if data: 76 | cmd.stdin.write(data) 77 | cmd.stdin.write('\n') 78 | out, err = cmd.communicate() 79 | rc = cmd.returncode 80 | except (OSError, IOError), e: 81 | module.fail_json(rc=e.errno, msg=str(e), cmd=args) 82 | except: 83 | module.fail_json(rc=257, msg=traceback.format_exc(), cmd=args) 84 | if rc != 0 and check_rc: 85 | msg = err.rstrip() 86 | module.fail_json(cmd=args, rc=rc, stdout=out, stderr=err, msg=msg) 87 | return rc, out, err 88 | 89 | 90 | class Archive(object): 91 | 92 | __metaclass__ = abc.ABCMeta 93 | 94 | def __init__(self, module, pkg): 95 | self.module = module 96 | self.params = module.params 97 | self.curl_path = module.get_bin_path('curl', True, ['/usr/bin']) 98 | self.chown_path = module.get_bin_path('chown', True) 99 | self.pkg = pkg 100 | self._pkg_path = None 101 | return 102 | 103 | @abc.abstractmethod 104 | def open(self): 105 | """ Open the archive """ 106 | 107 | def pkg_path(self): 108 | return self._pkg_path 109 | 110 | def clone(self): 111 | """ close the archive """ 112 | pass 113 | 114 | def source(self): 115 | if exists(self.params, 'src'): 116 | return self.params['src'] 117 | 118 | return self.params['url'] 119 | 120 | 121 | def chown_dir(self, chown_user, target_path): 122 | if chown_user is not None: 123 | rc, out, err = self.module.run_command("%s -R %s %s" % (self.chown_path, chown_user, target_path)) 124 | if rc > 0: 125 | self.module.fail_json(msg="failed to chown %s: %s\n\t%s" % (self.pkg.name(), out, err)) 126 | 127 | def acquire(self): 128 | acquired = False 129 | if exists(self.params, 'src'): 130 | src = self.params['src'] 131 | 132 | self._pkg_path = src 133 | # module.exit_json(changed=False, msg="src: '%s'" % src) 134 | acquired = True 135 | else: 136 | #get pkg from url 137 | #wget --no-cookies --no-check-certificate --directory-prefix=${WORK_DIR} -c --header "Cookie: $COOKIE" ${URL} 138 | #wget --no-cookies --no-check-certificate --directory-prefix=${WORK_DIR} -c ${URL} 139 | #curl --location --insecure --cookie gpw_e24=http%3A%2F%2Fwww.oracle.com 140 | #TODO: verify url 141 | url = self.params['url'] 142 | 143 | curl_opts = "" 144 | 145 | if exists(self.params, 'curl_opts'): 146 | curl_opts = self.params['curl_opts'] 147 | 148 | #TODO: make chown optional? 149 | chown_user = get_env('sudo_user', None) 150 | # self.module.exit_json(changed=False, msg="chown_user %s" % chown_user) 151 | 152 | #TODO: use environment variable to override cachedir 153 | cache_dir = path.expanduser(get_env("battleschool_cache_dir", "~/Library/Caches/battleschool")) 154 | download_dir = path.join(cache_dir, "downloads") 155 | if not path.exists(download_dir): 156 | os.makedirs(download_dir) 157 | #TODO: only chown downloaded item, only chown if just downloaded 158 | self.chown_dir(chown_user, download_dir) 159 | # self.module.exit_json(changed=False, msg="download_dir %s" % download_dir) 160 | # self.module.exit_json(changed=False, msg="cache_dir %s" % get_env('battleschool_cache_dir', "")) 161 | 162 | #TODO: verify write perms of dest parent 163 | if exists(self.params, 'dest'): 164 | dest = self.params['dest'] 165 | 166 | if not path.isabs(dest): 167 | dest = "%s/%s" % (download_dir, dest) 168 | 169 | cwd = None 170 | output_opts = "--output %s" % dest 171 | do_download = not path.exists(dest) 172 | else: 173 | sha1 = hashlib.sha1(url).hexdigest() 174 | dest = "%s/%s" % (download_dir, sha1) 175 | if not path.exists(dest): 176 | os.makedirs(dest) 177 | self.chown_dir(chown_user, dest) 178 | do_download = not os.listdir(dest) 179 | cwd = dest 180 | output_opts = "--remote-name --remote-header-name" 181 | 182 | # self.module.exit_json(changed=False, msg="pre_cmd %s, output_opts %s, curl_opts %s, do_download %s" 183 | # % (pre_cmd, output_opts, curl_opts, do_download)) 184 | 185 | #force delete 186 | if path.exists(dest) and self.params['force']: 187 | do_download = True 188 | if path.isfile(dest): 189 | os.unlink(dest) 190 | elif path.isdir(dest): 191 | files = os.listdir(dest) 192 | for file in files: 193 | file_path = "%s/%s" % (dest, file) 194 | os.unlink(file_path) 195 | 196 | if do_download: 197 | download_cmd = "%s --insecure --silent --location %s %s '%s'" % ( 198 | self.curl_path, output_opts, curl_opts, url) 199 | # self.module.exit_json(changed=False, msg="download_cmd %s" % download_cmd) 200 | rc, out, err = run_command(self.module, download_cmd, cwd=cwd) 201 | if rc > 0: 202 | self.module.fail_json(msg="failed to download %s: %s\n\t%s" % (self.pkg.name(), out, err)) 203 | acquired = True 204 | 205 | if not exists(self.params, 'dest'): 206 | # need to find out the path of the downloaded file 207 | files = os.listdir(dest) 208 | # self.module.exit_json(changed=False, msg="files %s" % files) 209 | num_files = len(files) 210 | 211 | if num_files != 1: 212 | self.module.fail_json(msg="failed to locate download for %s: %s file(s) found in %s" 213 | % (self.pkg.name(), num_files, dest)) 214 | 215 | dest = "%s/%s" % (dest, files[0]) 216 | # self.module.exit_json(changed=False, msg="dest %s" % dest) 217 | 218 | #TODO: only chown downloaded item 219 | if do_download and acquired: 220 | self.chown_dir(chown_user, download_dir) 221 | 222 | self._pkg_path = dest 223 | 224 | return acquired 225 | 226 | 227 | class ZipArchive(Archive): 228 | 229 | def __init__(self, module, pkg): 230 | super(ZipArchive, self).__init__(module, pkg) 231 | self.unzip_path = module.get_bin_path('unzip', True, ['/usr/bin']) 232 | #TODO: override target_dir 233 | self.target_dir = tempfile.mkdtemp(suffix='-zip-extract', prefix='mac_pkg-') 234 | 235 | def open(self): 236 | # self.module.exit_json(changed=False, msg="unzip_path %s, pkg_path %s, target_dir %s, archive_path %s" 237 | # % (self.unzip_path, self.pkg_path(), self.target_dir, self.params['archive_path'])) 238 | rc, out, err = self.module.run_command("%s %s -d %s" % (self.unzip_path, self.pkg_path(), self.target_dir)) 239 | if rc > 0: 240 | self.module.fail_json(msg="failed to unzip %s: %s\n\t%s" % (self.pkg_path(), out, err)) 241 | self._pkg_path = "%s/%s" % (self.target_dir, self.params['archive_path']) 242 | 243 | def close(self): 244 | shutil.rmtree(self.target_dir) 245 | 246 | 247 | class TarArchive(Archive): 248 | 249 | def __init__(self, module, pkg): 250 | super(TarArchive, self).__init__(module, pkg) 251 | self.tar_path = module.get_bin_path('tar', True, ['/usr/bin']) 252 | self.tar_opts = self.params['tar_opts'] 253 | #TODO: override target_dir 254 | self.target_dir = tempfile.mkdtemp(suffix='-tar-extract', prefix='mac_pkg-') 255 | 256 | def open(self): 257 | #self.module.exit_json(changed=False,msg="tar_path %s, tar_opts %s, pkg_path %s, target_dir %s, archive_path %s" 258 | # % (self.tar_path, self.tar_opts, self.pkg_path(), self.target_dir, 259 | # self.params['archive_path'])) 260 | rc, out, err = self.module.run_command("%s %s %s -C %s" % (self.tar_path, self.tar_opts, self.pkg_path(), 261 | self.target_dir)) 262 | if rc > 0: 263 | self.module.fail_json(msg="failed to extract tar %s: %s\n\t%s" % (self.pkg_path(), out, err)) 264 | self._pkg_path = "%s/%s" % (self.target_dir, self.params['archive_path']) 265 | 266 | def close(self): 267 | shutil.rmtree(self.target_dir) 268 | 269 | 270 | class DmgArchive(Archive): 271 | 272 | def __init__(self, module, pkg): 273 | super(DmgArchive, self).__init__(module, pkg) 274 | self.hdiutil_path = module.get_bin_path('hdiutil', True, ['/usr/bin']) 275 | self.dmg_volume = None 276 | self.dmg_license = self.params['dmg_license'] 277 | 278 | def open(self): 279 | hdi_pre = "" 280 | hdi_post = "| grep Volumes" 281 | 282 | if self.dmg_license: 283 | hdi_pre = "echo y |" 284 | hdi_post = "" 285 | 286 | #if dmg mount and record volume path 287 | #hdiutil attach Vagrant-1.3.0.dmg | grep Volumes | awk '{print $3}' 288 | command = "%s %s attach \"%s\" %s " % (hdi_pre, self.hdiutil_path, self._pkg_path, hdi_post) 289 | # self.module.exit_json(changed=False, msg="hdicmd: %s" % command) 290 | rc, out, err = run_command(self.module, command) 291 | if rc > 0: 292 | self.module.fail_json(msg="failed to attach %s: rc: %s, %s, err: %s" % (self.pkg.name(), rc, out, err)) 293 | 294 | idx = out.index("/Volumes/") 295 | self.dmg_volume = out[idx:].strip() 296 | archive_path = self.params['archive_path'] 297 | self._pkg_path = "%s/%s" % (self.dmg_volume, archive_path) 298 | # self.module.exit_json(changed=False, msg="pkg_path %s, archive_path %s" % (self._pkg_path, archive_path)) 299 | 300 | def close(self): 301 | rc, out, err = self.module.run_command("%s unmount \"%s\"" % (self.hdiutil_path, self.dmg_volume)) 302 | if rc > 0: 303 | self.module.fail_json(msg="failed to unmount %s: %s\n\t%s" % (self.pkg.name(), out, err)) 304 | 305 | 306 | class NoneArchive(Archive): 307 | 308 | def open(self): 309 | pass 310 | 311 | def close(self): 312 | pass 313 | 314 | 315 | class Package(object): 316 | 317 | __metaclass__ = abc.ABCMeta 318 | 319 | def __init__(self, module): 320 | self.module = module 321 | self.params = module.params 322 | return 323 | 324 | @abc.abstractmethod 325 | def install(self, pkg_path): 326 | """ Install the package """ 327 | 328 | @abc.abstractmethod 329 | def is_installed(self): 330 | """ Is the package installed """ 331 | 332 | @abc.abstractmethod 333 | def name(self): 334 | """ the name of the package """ 335 | 336 | @abc.abstractmethod 337 | def version(self): 338 | """ the version of the package """ 339 | 340 | 341 | class PkgPackage(Package): 342 | 343 | def __init__(self, module): 344 | super(PkgPackage, self).__init__(module) 345 | self.installer_path = module.get_bin_path('installer', True, ['/usr/sbin']) 346 | self.pkgutil_path = module.get_bin_path('pkgutil', True, ['/usr/sbin']) 347 | #TODO: add creates which would override pkg_version 348 | 349 | self._pkg_name = self.params['pkg_name'] 350 | #find installed version 351 | self._version = self.find_version(module) 352 | # self.module.exit_json(changed=False, msg="_version %s, _pkg_name %s" % (self._version, self._pkg_name)) 353 | 354 | def find_version(self, module): 355 | rc, out, err = run_command(self.module, "%s --pkg-info=%s 2>&1 |grep version| awk '{print $2}'" % 356 | (self.pkgutil_path, self._pkg_name)) 357 | if rc > 0: 358 | module.fail_json(msg="failed to find version via pkgutil %s: %s %s" % (self._pkg_name, out, err)) 359 | return out.strip() 360 | 361 | def install(self, pkg_path): 362 | #install package file 363 | #installer -pkg "${PGK_DIR}"/"${PKG_FILE}" -target / 364 | #TODO: support target param 365 | rc, out, err = self.module.run_command("%s -pkg \"%s\" -target /" % (self.installer_path, pkg_path)) 366 | if rc > 0: 367 | return out, err 368 | 369 | #no error, update the version from pkg_util 370 | self._version = self.find_version(self.module) 371 | return False 372 | 373 | def is_installed(self): 374 | pkg_version = None 375 | if exists(self.params, 'pkg_version'): 376 | pkg_version = self.params['pkg_version'] 377 | 378 | if pkg_version is not None: 379 | return self._version == pkg_version 380 | 381 | if self._version: 382 | return True 383 | 384 | return False 385 | 386 | def name(self): 387 | return self._pkg_name 388 | 389 | def version(self): 390 | return self._version 391 | 392 | 393 | class AppPackage(Package): 394 | 395 | def __init__(self, module): 396 | super(AppPackage, self).__init__(module) 397 | if exists(self.params, 'creates'): 398 | self._app_creates = self.params['creates'] 399 | if not self._app_creates.startswith("/"): 400 | #not an absolute path, prepend /Applications 401 | self.set_applications_path('creates') 402 | elif exists(self.params, 'archive_path'): 403 | self.set_applications_path('archive_path') 404 | else: 405 | self.module.fail_json(msg="for pkg_type=app one of app_creates or archive_path must not be empty") 406 | 407 | def set_applications_path(self, param_name): 408 | self._app_creates = '/Applications/%s' % self.params[param_name] 409 | 410 | def install(self, pkg_path): 411 | # self.module.fail_json(msg="pkg_path %s - _app_creates %s" % (pkg_path, self._app_creates)) 412 | # TODO: symlinks is an option? 413 | shutil.copytree(pkg_path, self._app_creates, symlinks=True) 414 | 415 | def is_installed(self): 416 | return path.exists(self._app_creates) 417 | 418 | def name(self): 419 | return self._app_creates 420 | 421 | def version(self): 422 | return "N/A" 423 | 424 | 425 | class ScriptPackage(Package): 426 | 427 | def __init__(self, module): 428 | super(ScriptPackage, self).__init__(module) 429 | self._creates = self.params['creates'] 430 | self._exe_path = module.get_bin_path(self.params['script_executable'], True, ['/usr/bin', '/usr/local/bin']) 431 | 432 | self._prefix = self.get_param('script_prefix') 433 | self._postfix = self.get_param('script_postfix') 434 | self._data = self.get_param('script_data', None) 435 | 436 | # self.module.exit_json(changed=False, msg="_prefix %s, _exe_path %s, _postfix %s, _data %s, _creates %s" % (self._prefix, self._exe_path, self._postfix, self._data, self._creates)) 437 | 438 | def install(self, pkg_path): 439 | _cmd = "%s%s \"%s\"%s" % (self._prefix, self._exe_path, pkg_path, self._postfix) 440 | # self.module.exit_json(changed=False, msg="_cmd %s" % _cmd) 441 | rc, out, err = self.module.run_command(_cmd, data=self._data) 442 | if rc > 0: 443 | return out, err 444 | # self.module.exit_json(changed=False, msg="rc %s, out %s, err %s" % (rc, out, err)) 445 | 446 | return False 447 | 448 | def is_installed(self): 449 | # self.module.exit_json(changed=False, msg="is_installed %s" % path.exists(self._creates)) 450 | return path.exists(self._creates) 451 | 452 | def name(self): 453 | return self._creates 454 | 455 | def get_param(self, key, default=''): 456 | if exists(self.params, key): 457 | return self.params[key] 458 | else: 459 | return default 460 | 461 | def version(self): 462 | return "N/A" 463 | 464 | 465 | def exists(dict, key): 466 | """ is key in dict and is dict[key] not none """ 467 | if key in dict and dict[key] is not None: 468 | return True 469 | 470 | return False 471 | 472 | 473 | class Installer(object): 474 | 475 | def __init__(self, module): 476 | self.module = module 477 | self.params = module.params 478 | 479 | def install(self, acquire_only): 480 | pkg = self._instantiate('pkg_type', 'Package') 481 | # self.module.exit_json(changed=False, msg="params %s" % self.params) 482 | 483 | if pkg.is_installed() and not acquire_only: 484 | self.module.exit_json(changed=False, version=pkg.version(), msg="package %s already present" % pkg.name()) 485 | else: 486 | archive = self._instantiate('archive_type', 'Archive', pkg) 487 | 488 | acquired = archive.acquire() 489 | 490 | if acquire_only: 491 | if acquired: 492 | state = "Acquired" 493 | else: 494 | state = "Skipped acquisition of" 495 | self.module.exit_json(changed=acquired, msg="%s pkg %s" % (state, archive.source())) 496 | return 497 | 498 | archive.open() 499 | 500 | failed = pkg.install(archive.pkg_path()) 501 | 502 | if failed: 503 | archive.close() 504 | self.module.fail_json(msg="failed to install package %s: %s %s" % (archive.pkg_path(), failed[0], failed[1])) 505 | 506 | archive.close() 507 | #self.module.exit_json(changed=False, msg="pkg %s, archive %s" % (pkg, archive)) 508 | self.module.exit_json(changed=True, version=pkg.version(), msg="installed package %s" % pkg.name()) 509 | 510 | def _instantiate(self, class_key, class_suffix, pkg=None): 511 | class_name = "%s%s" % (self.params[class_key].title(), class_suffix) 512 | m = __import__(self.__module__) 513 | klass = getattr(m, class_name) 514 | 515 | if pkg is None: 516 | instance = klass(self.module) 517 | else: 518 | instance = klass(self.module, pkg) 519 | 520 | return instance 521 | 522 | def validate(self): 523 | if self.params['pkg_type'] == "app" and self.params['archive_type'] == "none": 524 | self.module.fail_json(msg="pkg_type can not = 'app' with archive_type = 'none'") 525 | 526 | if not exists(self.params, 'src') and not exists(self.params, 'url'): 527 | self.module.fail_json(msg="one of src or url must not be empty") 528 | 529 | 530 | tempdir = tempfile.gettempdir() 531 | extra_vars_path = path.join(tempdir, "battleschool_extra_vars.json") 532 | if path.isfile(extra_vars_path): 533 | with open(extra_vars_path) as f: 534 | extra_vars = json.load(f) 535 | else: 536 | extra_vars = {} 537 | 538 | def get_env(name, default, converter = None): 539 | if name in extra_vars: 540 | val = extra_vars[name] 541 | elif name in os.environ: 542 | val = os.environ[name] 543 | elif name.upper() in os.environ: 544 | val = os.environ[name.upper()] 545 | else: 546 | val = default 547 | 548 | if converter: 549 | return converter(val) 550 | else: 551 | return val 552 | 553 | 554 | def main(): 555 | module = AnsibleModule( 556 | argument_spec=dict( 557 | state=dict(default="present", choices=["present", "installed"]), 558 | archive_type=dict(default="none", choices=["zip", "dmg", "tar", "none"]), 559 | src=dict(aliases=["source"], required=False), 560 | url=dict(aliases=[], required=False), 561 | dest=dict(aliases=["destination"], required=False), 562 | force=dict(default='no', type='bool'), 563 | curl_opts=dict(aliases=[], required=False), 564 | tar_opts=dict(aliases=[], default="xf", required=False), 565 | archive_path=dict(required=False), 566 | pkg_type=dict(default="pkg", choices=["pkg", "app", "script"]), 567 | pkg_name=dict(required=False), 568 | pkg_version=dict(required=False), 569 | creates=dict(aliases=["app_creates", "script_creates"], required=False), 570 | dmg_license=dict(default='no', type='bool'), 571 | script_executable=dict(aliases=["script_exe"], required=False), 572 | script_prefix=dict(aliases=["script_prefix"], required=False), 573 | script_postfix=dict(aliases=["script_postfix"], required=False), 574 | script_data=dict(aliases=["script_data"], required=False), 575 | ) 576 | ) 577 | 578 | p = module.params 579 | 580 | installer = Installer(module) 581 | 582 | installer.validate() 583 | 584 | acquire_only = get_env('mac_pkg_acquire_only', False, module.boolean) 585 | 586 | # module.exit_json(changed=False, msg="acquire_only %s, env %s" % (acquire_only, extra_vars)) 587 | 588 | if p["state"] in ["present", "installed"]: 589 | installer.install(acquire_only) 590 | 591 | #TODO: implement remove 592 | elif p["state"] in ["absent", "removed"]: 593 | #remove_package(module, pkgutil_path, pkg) 594 | module.fail_json(changed=False, msg="remove package NOT IMPLEMENTED") 595 | 596 | # this is magic, see lib/ansible/module_common.py 597 | #<> 598 | 599 | main() 600 | -------------------------------------------------------------------------------- /test/ansible_module_dynamic.py: -------------------------------------------------------------------------------- 1 | #This is documentation for when you write an ansible module in python 2 | 3 | MODULE_ARGS = "time='March 14 12:23'" 4 | MODULE_LANG = 'C' 5 | MODULE_COMPLEX_ARGS = '{}' 6 | 7 | BOOLEANS_TRUE = ['yes', 'on', '1', 'true', 1] 8 | BOOLEANS_FALSE = ['no', 'off', '0', 'false', 0] 9 | BOOLEANS = BOOLEANS_TRUE + BOOLEANS_FALSE 10 | 11 | # ansible modules can be written in any language. To simplify 12 | # development of Python modules, the functions available here 13 | # can be inserted in any module source automatically by including 14 | # #<> on a blank line by itself inside 15 | # of an ansible module. The source of this common code lives 16 | # in lib/ansible/module_common.py 17 | 18 | import os 19 | import re 20 | import shlex 21 | import subprocess 22 | import sys 23 | import syslog 24 | import types 25 | import time 26 | import shutil 27 | import stat 28 | import traceback 29 | import grp 30 | import pwd 31 | import platform 32 | import errno 33 | 34 | try: 35 | import json 36 | except ImportError: 37 | try: 38 | import simplejson as json 39 | except ImportError: 40 | sys.stderr.write('Error: ansible requires a json module, none found!') 41 | sys.exit(1) 42 | except SyntaxError: 43 | sys.stderr.write('SyntaxError: probably due to json and python being for different versions') 44 | sys.exit(1) 45 | 46 | HAVE_SELINUX=False 47 | try: 48 | import selinux 49 | HAVE_SELINUX=True 50 | except ImportError: 51 | pass 52 | 53 | try: 54 | from hashlib import md5 as _md5 55 | except ImportError: 56 | from md5 import md5 as _md5 57 | 58 | try: 59 | from systemd import journal 60 | has_journal = True 61 | except ImportError: 62 | import syslog 63 | has_journal = False 64 | 65 | FILE_COMMON_ARGUMENTS=dict( 66 | src = dict(), 67 | mode = dict(), 68 | owner = dict(), 69 | group = dict(), 70 | seuser = dict(), 71 | serole = dict(), 72 | selevel = dict(), 73 | setype = dict(), 74 | # not taken by the file module, but other modules call file so it must ignore them. 75 | content = dict(), 76 | backup = dict(), 77 | force = dict(), 78 | ) 79 | 80 | def get_platform(): 81 | ''' what's the platform? example: Linux is a platform. ''' 82 | return platform.system() 83 | 84 | def get_distribution(): 85 | ''' return the distribution name ''' 86 | if platform.system() == 'Linux': 87 | try: 88 | distribution = platform.linux_distribution()[0].capitalize() 89 | if distribution == 'NA': 90 | if os.path.isfile('/etc/system-release'): 91 | distribution = 'OtherLinux' 92 | except: 93 | # FIXME: MethodMissing, I assume? 94 | distribution = platform.dist()[0].capitalize() 95 | else: 96 | distribution = None 97 | return distribution 98 | 99 | def load_platform_subclass(cls, *args, **kwargs): 100 | ''' 101 | used by modules like User to have different implementations based on detected platform. See User 102 | module for an example. 103 | ''' 104 | 105 | this_platform = get_platform() 106 | distribution = get_distribution() 107 | subclass = None 108 | 109 | # get the most specific superclass for this platform 110 | if distribution is not None: 111 | for sc in cls.__subclasses__(): 112 | if sc.distribution is not None and sc.distribution == distribution and sc.platform == this_platform: 113 | subclass = sc 114 | if subclass is None: 115 | for sc in cls.__subclasses__(): 116 | if sc.platform == this_platform and sc.distribution is None: 117 | subclass = sc 118 | if subclass is None: 119 | subclass = cls 120 | 121 | return super(cls, subclass).__new__(subclass) 122 | 123 | 124 | class AnsibleModule(object): 125 | 126 | def __init__(self, argument_spec, bypass_checks=False, no_log=False, 127 | check_invalid_arguments=True, mutually_exclusive=None, required_together=None, 128 | required_one_of=None, add_file_common_args=False, supports_check_mode=False): 129 | 130 | ''' 131 | common code for quickly building an ansible module in Python 132 | (although you can write modules in anything that can return JSON) 133 | see library/* for examples 134 | ''' 135 | 136 | self.argument_spec = argument_spec 137 | self.supports_check_mode = supports_check_mode 138 | self.check_mode = False 139 | 140 | self.aliases = {} 141 | 142 | if add_file_common_args: 143 | for k, v in FILE_COMMON_ARGUMENTS.iteritems(): 144 | if k not in self.argument_spec: 145 | self.argument_spec[k] = v 146 | 147 | os.environ['LANG'] = MODULE_LANG 148 | (self.params, self.args) = self._load_params() 149 | 150 | self._legal_inputs = [ 'CHECKMODE' ] 151 | 152 | self.aliases = self._handle_aliases() 153 | 154 | if check_invalid_arguments: 155 | self._check_invalid_arguments() 156 | self._check_for_check_mode() 157 | 158 | self._set_defaults(pre=True) 159 | 160 | if not bypass_checks: 161 | self._check_required_arguments() 162 | self._check_argument_values() 163 | self._check_argument_types() 164 | self._check_mutually_exclusive(mutually_exclusive) 165 | self._check_required_together(required_together) 166 | self._check_required_one_of(required_one_of) 167 | 168 | self._set_defaults(pre=False) 169 | if not no_log: 170 | self._log_invocation() 171 | 172 | def load_file_common_arguments(self, params): 173 | ''' 174 | many modules deal with files, this encapsulates common 175 | options that the file module accepts such that it is directly 176 | available to all modules and they can share code. 177 | ''' 178 | 179 | path = params.get('path', params.get('dest', None)) 180 | if path is None: 181 | return {} 182 | 183 | mode = params.get('mode', None) 184 | owner = params.get('owner', None) 185 | group = params.get('group', None) 186 | 187 | # selinux related options 188 | seuser = params.get('seuser', None) 189 | serole = params.get('serole', None) 190 | setype = params.get('setype', None) 191 | selevel = params.get('serange', 's0') 192 | secontext = [seuser, serole, setype] 193 | 194 | if self.selinux_mls_enabled(): 195 | secontext.append(selevel) 196 | 197 | default_secontext = self.selinux_default_context(path) 198 | for i in range(len(default_secontext)): 199 | if i is not None and secontext[i] == '_default': 200 | secontext[i] = default_secontext[i] 201 | 202 | return dict( 203 | path=path, mode=mode, owner=owner, group=group, 204 | seuser=seuser, serole=serole, setype=setype, 205 | selevel=selevel, secontext=secontext, 206 | ) 207 | 208 | 209 | # Detect whether using selinux that is MLS-aware. 210 | # While this means you can set the level/range with 211 | # selinux.lsetfilecon(), it may or may not mean that you 212 | # will get the selevel as part of the context returned 213 | # by selinux.lgetfilecon(). 214 | 215 | def selinux_mls_enabled(self): 216 | if not HAVE_SELINUX: 217 | return False 218 | if selinux.is_selinux_mls_enabled() == 1: 219 | return True 220 | else: 221 | return False 222 | 223 | def selinux_enabled(self): 224 | if not HAVE_SELINUX: 225 | return False 226 | if selinux.is_selinux_enabled() == 1: 227 | return True 228 | else: 229 | return False 230 | 231 | # Determine whether we need a placeholder for selevel/mls 232 | def selinux_initial_context(self): 233 | context = [None, None, None] 234 | if self.selinux_mls_enabled(): 235 | context.append(None) 236 | return context 237 | 238 | def _to_filesystem_str(self, path): 239 | '''Returns filesystem path as a str, if it wasn't already. 240 | 241 | Used in selinux interactions because it cannot accept unicode 242 | instances, and specifying complex args in a playbook leaves 243 | you with unicode instances. This method currently assumes 244 | that your filesystem encoding is UTF-8. 245 | 246 | ''' 247 | if isinstance(path, unicode): 248 | path = path.encode("utf-8") 249 | return path 250 | 251 | # If selinux fails to find a default, return an array of None 252 | def selinux_default_context(self, path, mode=0): 253 | context = self.selinux_initial_context() 254 | if not HAVE_SELINUX or not self.selinux_enabled(): 255 | return context 256 | try: 257 | ret = selinux.matchpathcon(self._to_filesystem_str(path), mode) 258 | except OSError: 259 | return context 260 | if ret[0] == -1: 261 | return context 262 | context = ret[1].split(':') 263 | return context 264 | 265 | def selinux_context(self, path): 266 | context = self.selinux_initial_context() 267 | if not HAVE_SELINUX or not self.selinux_enabled(): 268 | return context 269 | try: 270 | ret = selinux.lgetfilecon(self._to_filesystem_str(path)) 271 | except OSError, e: 272 | if e.errno == errno.ENOENT: 273 | self.fail_json(path=path, msg='path %s does not exist' % path) 274 | else: 275 | self.fail_json(path=path, msg='failed to retrieve selinux context') 276 | if ret[0] == -1: 277 | return context 278 | context = ret[1].split(':') 279 | return context 280 | 281 | def user_and_group(self, filename): 282 | filename = os.path.expanduser(filename) 283 | st = os.lstat(filename) 284 | uid = st.st_uid 285 | gid = st.st_gid 286 | return (uid, gid) 287 | 288 | def set_default_selinux_context(self, path, changed): 289 | if not HAVE_SELINUX or not self.selinux_enabled(): 290 | return changed 291 | context = self.selinux_default_context(path) 292 | return self.set_context_if_different(path, context, False) 293 | 294 | def set_context_if_different(self, path, context, changed): 295 | 296 | if not HAVE_SELINUX or not self.selinux_enabled(): 297 | return changed 298 | cur_context = self.selinux_context(path) 299 | new_context = list(cur_context) 300 | # Iterate over the current context instead of the 301 | # argument context, which may have selevel. 302 | 303 | for i in range(len(cur_context)): 304 | if context[i] is not None and context[i] != cur_context[i]: 305 | new_context[i] = context[i] 306 | if context[i] is None: 307 | new_context[i] = cur_context[i] 308 | if cur_context != new_context: 309 | try: 310 | if self.check_mode: 311 | return True 312 | rc = selinux.lsetfilecon(self._to_filesystem_str(path), 313 | str(':'.join(new_context))) 314 | except OSError: 315 | self.fail_json(path=path, msg='invalid selinux context', new_context=new_context, cur_context=cur_context, input_was=context) 316 | if rc != 0: 317 | self.fail_json(path=path, msg='set selinux context failed') 318 | changed = True 319 | return changed 320 | 321 | def set_owner_if_different(self, path, owner, changed): 322 | path = os.path.expanduser(path) 323 | if owner is None: 324 | return changed 325 | orig_uid, orig_gid = self.user_and_group(path) 326 | try: 327 | uid = int(owner) 328 | except ValueError: 329 | try: 330 | uid = pwd.getpwnam(owner).pw_uid 331 | except KeyError: 332 | self.fail_json(path=path, msg='chown failed: failed to look up user %s' % owner) 333 | if orig_uid != uid: 334 | if self.check_mode: 335 | return True 336 | try: 337 | os.lchown(path, uid, -1) 338 | except OSError: 339 | self.fail_json(path=path, msg='chown failed') 340 | changed = True 341 | return changed 342 | 343 | def set_group_if_different(self, path, group, changed): 344 | path = os.path.expanduser(path) 345 | if group is None: 346 | return changed 347 | orig_uid, orig_gid = self.user_and_group(path) 348 | try: 349 | gid = int(group) 350 | except ValueError: 351 | try: 352 | gid = grp.getgrnam(group).gr_gid 353 | except KeyError: 354 | self.fail_json(path=path, msg='chgrp failed: failed to look up group %s' % group) 355 | if orig_gid != gid: 356 | if self.check_mode: 357 | return True 358 | try: 359 | os.lchown(path, -1, gid) 360 | except OSError: 361 | self.fail_json(path=path, msg='chgrp failed') 362 | changed = True 363 | return changed 364 | 365 | def set_mode_if_different(self, path, mode, changed): 366 | path = os.path.expanduser(path) 367 | if mode is None: 368 | return changed 369 | try: 370 | # FIXME: support English modes 371 | if not isinstance(mode, int): 372 | mode = int(mode, 8) 373 | except Exception, e: 374 | self.fail_json(path=path, msg='mode needs to be something octalish', details=str(e)) 375 | 376 | st = os.lstat(path) 377 | prev_mode = stat.S_IMODE(st[stat.ST_MODE]) 378 | 379 | if prev_mode != mode: 380 | if self.check_mode: 381 | return True 382 | # FIXME: comparison against string above will cause this to be executed 383 | # every time 384 | try: 385 | if 'lchmod' in dir(os): 386 | os.lchmod(path, mode) 387 | else: 388 | os.chmod(path, mode) 389 | except OSError, e: 390 | if e.errno == errno.ENOENT: # Can't set mode on broken symbolic links 391 | pass 392 | else: 393 | raise e 394 | except Exception, e: 395 | self.fail_json(path=path, msg='chmod failed', details=str(e)) 396 | 397 | st = os.lstat(path) 398 | new_mode = stat.S_IMODE(st[stat.ST_MODE]) 399 | 400 | if new_mode != prev_mode: 401 | changed = True 402 | return changed 403 | 404 | def set_file_attributes_if_different(self, file_args, changed): 405 | # set modes owners and context as needed 406 | changed = self.set_context_if_different( 407 | file_args['path'], file_args['secontext'], changed 408 | ) 409 | changed = self.set_owner_if_different( 410 | file_args['path'], file_args['owner'], changed 411 | ) 412 | changed = self.set_group_if_different( 413 | file_args['path'], file_args['group'], changed 414 | ) 415 | changed = self.set_mode_if_different( 416 | file_args['path'], file_args['mode'], changed 417 | ) 418 | return changed 419 | 420 | def set_directory_attributes_if_different(self, file_args, changed): 421 | changed = self.set_context_if_different( 422 | file_args['path'], file_args['secontext'], changed 423 | ) 424 | changed = self.set_owner_if_different( 425 | file_args['path'], file_args['owner'], changed 426 | ) 427 | changed = self.set_group_if_different( 428 | file_args['path'], file_args['group'], changed 429 | ) 430 | changed = self.set_mode_if_different( 431 | file_args['path'], file_args['mode'], changed 432 | ) 433 | return changed 434 | 435 | def add_path_info(self, kwargs): 436 | ''' 437 | for results that are files, supplement the info about the file 438 | in the return path with stats about the file path. 439 | ''' 440 | 441 | path = kwargs.get('path', kwargs.get('dest', None)) 442 | if path is None: 443 | return kwargs 444 | if os.path.exists(path): 445 | (uid, gid) = self.user_and_group(path) 446 | kwargs['uid'] = uid 447 | kwargs['gid'] = gid 448 | try: 449 | user = pwd.getpwuid(uid)[0] 450 | except KeyError: 451 | user = str(uid) 452 | try: 453 | group = grp.getgrgid(gid)[0] 454 | except KeyError: 455 | group = str(gid) 456 | kwargs['owner'] = user 457 | kwargs['group'] = group 458 | st = os.lstat(path) 459 | kwargs['mode'] = oct(stat.S_IMODE(st[stat.ST_MODE])) 460 | # secontext not yet supported 461 | if os.path.islink(path): 462 | kwargs['state'] = 'link' 463 | elif os.path.isdir(path): 464 | kwargs['state'] = 'directory' 465 | else: 466 | kwargs['state'] = 'file' 467 | if HAVE_SELINUX and self.selinux_enabled(): 468 | kwargs['secontext'] = ':'.join(self.selinux_context(path)) 469 | kwargs['size'] = st[stat.ST_SIZE] 470 | else: 471 | kwargs['state'] = 'absent' 472 | return kwargs 473 | 474 | 475 | def _handle_aliases(self): 476 | aliases_results = {} #alias:canon 477 | for (k,v) in self.argument_spec.iteritems(): 478 | self._legal_inputs.append(k) 479 | aliases = v.get('aliases', None) 480 | default = v.get('default', None) 481 | required = v.get('required', False) 482 | if default is not None and required: 483 | # not alias specific but this is a good place to check this 484 | self.fail_json(msg="internal error: required and default are mutally exclusive for %s" % k) 485 | if aliases is None: 486 | continue 487 | if type(aliases) != list: 488 | self.fail_json(msg='internal error: aliases must be a list') 489 | for alias in aliases: 490 | self._legal_inputs.append(alias) 491 | aliases_results[alias] = k 492 | if alias in self.params: 493 | self.params[k] = self.params[alias] 494 | 495 | return aliases_results 496 | 497 | def _check_for_check_mode(self): 498 | for (k,v) in self.params.iteritems(): 499 | if k == 'CHECKMODE': 500 | if not self.supports_check_mode: 501 | self.exit_json(skipped=True, msg="remote module does not support check mode") 502 | if self.supports_check_mode: 503 | self.check_mode = True 504 | 505 | def _check_invalid_arguments(self): 506 | for (k,v) in self.params.iteritems(): 507 | if k == 'CHECKMODE': 508 | continue 509 | if k not in self._legal_inputs: 510 | self.fail_json(msg="unsupported parameter for module: %s" % k) 511 | 512 | def _count_terms(self, check): 513 | count = 0 514 | for term in check: 515 | if term in self.params: 516 | count += 1 517 | return count 518 | 519 | def _check_mutually_exclusive(self, spec): 520 | if spec is None: 521 | return 522 | for check in spec: 523 | count = self._count_terms(check) 524 | if count > 1: 525 | self.fail_json(msg="parameters are mutually exclusive: %s" % check) 526 | 527 | def _check_required_one_of(self, spec): 528 | if spec is None: 529 | return 530 | for check in spec: 531 | count = self._count_terms(check) 532 | if count == 0: 533 | self.fail_json(msg="one of the following is required: %s" % ','.join(check)) 534 | 535 | def _check_required_together(self, spec): 536 | if spec is None: 537 | return 538 | for check in spec: 539 | counts = [ self._count_terms([field]) for field in check ] 540 | non_zero = [ c for c in counts if c > 0 ] 541 | if len(non_zero) > 0: 542 | if 0 in counts: 543 | self.fail_json(msg="parameters are required together: %s" % check) 544 | 545 | def _check_required_arguments(self): 546 | ''' ensure all required arguments are present ''' 547 | missing = [] 548 | for (k,v) in self.argument_spec.iteritems(): 549 | required = v.get('required', False) 550 | if required and k not in self.params: 551 | missing.append(k) 552 | if len(missing) > 0: 553 | self.fail_json(msg="missing required arguments: %s" % ",".join(missing)) 554 | 555 | def _check_argument_values(self): 556 | ''' ensure all arguments have the requested values, and there are no stray arguments ''' 557 | for (k,v) in self.argument_spec.iteritems(): 558 | choices = v.get('choices',None) 559 | if choices is None: 560 | continue 561 | if type(choices) == list: 562 | if k in self.params: 563 | if self.params[k] not in choices: 564 | choices_str=",".join([str(c) for c in choices]) 565 | msg="value of %s must be one of: %s, got: %s" % (k, choices_str, self.params[k]) 566 | self.fail_json(msg=msg) 567 | else: 568 | self.fail_json(msg="internal error: do not know how to interpret argument_spec") 569 | 570 | def _check_argument_types(self): 571 | ''' ensure all arguments have the requested type ''' 572 | for (k, v) in self.argument_spec.iteritems(): 573 | wanted = v.get('type', None) 574 | if wanted is None: 575 | continue 576 | if k not in self.params: 577 | continue 578 | 579 | value = self.params[k] 580 | is_invalid = False 581 | 582 | if wanted == 'str': 583 | if not isinstance(value, basestring): 584 | self.params[k] = str(value) 585 | elif wanted == 'list': 586 | if not isinstance(value, list): 587 | if isinstance(value, basestring): 588 | self.params[k] = value.split(",") 589 | else: 590 | is_invalid = True 591 | elif wanted == 'dict': 592 | if not isinstance(value, dict): 593 | if isinstance(value, basestring): 594 | self.params[k] = dict([x.split("=", 1) for x in value.split(",")]) 595 | else: 596 | is_invalid = True 597 | elif wanted == 'bool': 598 | if not isinstance(value, bool): 599 | if isinstance(value, basestring): 600 | self.params[k] = self.boolean(value) 601 | else: 602 | is_invalid = True 603 | elif wanted == 'int': 604 | if not isinstance(value, int): 605 | if isinstance(value, basestring): 606 | self.params[k] = int(value) 607 | else: 608 | is_invalid = True 609 | else: 610 | self.fail_json(msg="implementation error: unknown type %s requested for %s" % (wanted, k)) 611 | 612 | if is_invalid: 613 | self.fail_json(msg="argument %s is of invalid type: %s, required: %s" % (k, type(value), wanted)) 614 | 615 | def _set_defaults(self, pre=True): 616 | for (k,v) in self.argument_spec.iteritems(): 617 | default = v.get('default', None) 618 | if pre == True: 619 | # this prevents setting defaults on required items 620 | if default is not None and k not in self.params: 621 | self.params[k] = default 622 | else: 623 | # make sure things without a default still get set None 624 | if k not in self.params: 625 | self.params[k] = default 626 | 627 | def _load_params(self): 628 | ''' read the input and return a dictionary and the arguments string ''' 629 | args = MODULE_ARGS 630 | items = shlex.split(args) 631 | params = {} 632 | for x in items: 633 | try: 634 | (k, v) = x.split("=",1) 635 | except Exception, e: 636 | self.fail_json(msg="this module requires key=value arguments (%s)" % items) 637 | params[k] = v 638 | params2 = json.loads(MODULE_COMPLEX_ARGS) 639 | params2.update(params) 640 | return (params2, args) 641 | 642 | def _log_invocation(self): 643 | ''' log that ansible ran the module ''' 644 | # TODO: generalize a separate log function and make log_invocation use it 645 | # Sanitize possible password argument when logging. 646 | log_args = dict() 647 | passwd_keys = ['password', 'login_password'] 648 | 649 | for param in self.params: 650 | canon = self.aliases.get(param, param) 651 | arg_opts = self.argument_spec.get(canon, {}) 652 | no_log = arg_opts.get('no_log', False) 653 | 654 | if no_log: 655 | log_args[param] = 'NOT_LOGGING_PARAMETER' 656 | elif param in passwd_keys: 657 | log_args[param] = 'NOT_LOGGING_PASSWORD' 658 | else: 659 | log_args[param] = self.params[param] 660 | 661 | module = 'ansible-%s' % os.path.basename(__file__) 662 | msg = '' 663 | for arg in log_args: 664 | msg = msg + arg + '=' + str(log_args[arg]) + ' ' 665 | if msg: 666 | msg = 'Invoked with %s' % msg 667 | else: 668 | msg = 'Invoked' 669 | 670 | if (has_journal): 671 | journal_args = ["MESSAGE=%s %s" % (module, msg)] 672 | journal_args.append("MODULE=%s" % os.path.basename(__file__)) 673 | for arg in log_args: 674 | journal_args.append(arg.upper() + "=" + str(log_args[arg])) 675 | try: 676 | journal.sendv(*journal_args) 677 | except IOError, e: 678 | # fall back to syslog since logging to journal failed 679 | syslog.openlog(module, 0, syslog.LOG_USER) 680 | syslog.syslog(syslog.LOG_NOTICE, msg) 681 | else: 682 | syslog.openlog(module, 0, syslog.LOG_USER) 683 | syslog.syslog(syslog.LOG_NOTICE, msg) 684 | 685 | def get_bin_path(self, arg, required=False, opt_dirs=[]): 686 | ''' 687 | find system executable in PATH. 688 | Optional arguments: 689 | - required: if executable is not found and required is true, fail_json 690 | - opt_dirs: optional list of directories to search in addition to PATH 691 | if found return full path; otherwise return None 692 | ''' 693 | sbin_paths = ['/sbin', '/usr/sbin', '/usr/local/sbin'] 694 | paths = [] 695 | for d in opt_dirs: 696 | if d is not None and os.path.exists(d): 697 | paths.append(d) 698 | paths += os.environ.get('PATH', '').split(os.pathsep) 699 | bin_path = None 700 | # mangle PATH to include /sbin dirs 701 | for p in sbin_paths: 702 | if p not in paths and os.path.exists(p): 703 | paths.append(p) 704 | for d in paths: 705 | path = os.path.join(d, arg) 706 | if os.path.exists(path) and self.is_executable(path): 707 | bin_path = path 708 | break 709 | if required and bin_path is None: 710 | self.fail_json(msg='Failed to find required executable %s' % arg) 711 | return bin_path 712 | 713 | def boolean(self, arg): 714 | ''' return a bool for the arg ''' 715 | if arg is None or type(arg) == bool: 716 | return arg 717 | if type(arg) in types.StringTypes: 718 | arg = arg.lower() 719 | if arg in BOOLEANS_TRUE: 720 | return True 721 | elif arg in BOOLEANS_FALSE: 722 | return False 723 | else: 724 | self.fail_json(msg='Boolean %s not in either boolean list' % arg) 725 | 726 | def jsonify(self, data): 727 | return json.dumps(data) 728 | 729 | def from_json(self, data): 730 | return json.loads(data) 731 | 732 | def exit_json(self, **kwargs): 733 | ''' return from the module, without error ''' 734 | self.add_path_info(kwargs) 735 | if not kwargs.has_key('changed'): 736 | kwargs['changed'] = False 737 | print self.jsonify(kwargs) 738 | sys.exit(0) 739 | 740 | def fail_json(self, **kwargs): 741 | ''' return from the module, with an error message ''' 742 | self.add_path_info(kwargs) 743 | assert 'msg' in kwargs, "implementation error -- msg to explain the error is required" 744 | kwargs['failed'] = True 745 | print self.jsonify(kwargs) 746 | sys.exit(1) 747 | 748 | def is_executable(self, path): 749 | '''is the given path executable?''' 750 | return (stat.S_IXUSR & os.stat(path)[stat.ST_MODE] 751 | or stat.S_IXGRP & os.stat(path)[stat.ST_MODE] 752 | or stat.S_IXOTH & os.stat(path)[stat.ST_MODE]) 753 | 754 | def md5(self, filename): 755 | ''' Return MD5 hex digest of local file, or None if file is not present. ''' 756 | if not os.path.exists(filename): 757 | return None 758 | if os.path.isdir(filename): 759 | self.fail_json(msg="attempted to take md5sum of directory: %s" % filename) 760 | digest = _md5() 761 | blocksize = 64 * 1024 762 | infile = open(filename, 'rb') 763 | block = infile.read(blocksize) 764 | while block: 765 | digest.update(block) 766 | block = infile.read(blocksize) 767 | infile.close() 768 | return digest.hexdigest() 769 | 770 | def backup_local(self, fn): 771 | '''make a date-marked backup of the specified file, return True or False on success or failure''' 772 | # backups named basename-YYYY-MM-DD@HH:MM~ 773 | ext = time.strftime("%Y-%m-%d@%H:%M~", time.localtime(time.time())) 774 | backupdest = '%s.%s' % (fn, ext) 775 | 776 | try: 777 | shutil.copy2(fn, backupdest) 778 | except shutil.Error, e: 779 | self.fail_json(msg='Could not make backup of %s to %s: %s' % (fn, backupdest, e)) 780 | return backupdest 781 | 782 | def cleanup(self,tmpfile): 783 | if os.path.exists(tmpfile): 784 | try: 785 | os.unlink(tmpfile) 786 | except OSError, e: 787 | sys.stderr.write("could not cleanup %s: %s" % (tmpfile, e)) 788 | 789 | def atomic_move(self, src, dest): 790 | '''atomically move src to dest, copying attributes from dest, returns true on success''' 791 | context = None 792 | if os.path.exists(dest): 793 | try: 794 | st = os.stat(dest) 795 | os.chmod(src, st.st_mode & 07777) 796 | os.chown(src, st.st_uid, st.st_gid) 797 | except OSError, e: 798 | if e.errno != errno.EPERM: 799 | raise 800 | if self.selinux_enabled(): 801 | context = self.selinux_context(dest) 802 | else: 803 | if self.selinux_enabled(): 804 | context = self.selinux_default_context(dest) 805 | # Ensure file is on same partition to make replacement atomic 806 | dest_dir = os.path.dirname(dest) 807 | dest_file = os.path.basename(dest) 808 | tmp_dest = "%s/.%s.%s.%s" % (dest_dir,dest_file,os.getpid(),time.time()) 809 | 810 | try: # leaves tmp file behind when sudo and not root 811 | if os.getenv("SUDO_USER") and os.getuid() != 0: 812 | # cleanup will happen by 'rm' of tempdir 813 | shutil.copy(src, tmp_dest) 814 | else: 815 | shutil.move(src, tmp_dest) 816 | if self.selinux_enabled(): 817 | self.set_context_if_different(tmp_dest, context, False) 818 | os.rename(tmp_dest, dest) 819 | if self.selinux_enabled(): 820 | # rename might not preserve context 821 | self.set_context_if_different(dest, context, False) 822 | except (shutil.Error, OSError, IOError), e: 823 | self.cleanup(tmp_dest) 824 | self.fail_json(msg='Could not replace file: %s to %s: %s' % (src, dest, e)) 825 | 826 | def run_command(self, args, check_rc=False, close_fds=False, executable=None, data=None): 827 | ''' 828 | Execute a command, returns rc, stdout, and stderr. 829 | args is the command to run 830 | If args is a list, the command will be run with shell=False. 831 | Otherwise, the command will be run with shell=True when args is a string. 832 | Other arguments: 833 | - check_rc (boolean) Whether to call fail_json in case of 834 | non zero RC. Default is False. 835 | - close_fds (boolean) See documentation for subprocess.Popen(). 836 | Default is False. 837 | - executable (string) See documentation for subprocess.Popen(). 838 | Default is None. 839 | ''' 840 | if isinstance(args, list): 841 | shell = False 842 | elif isinstance(args, basestring): 843 | shell = True 844 | else: 845 | msg = "Argument 'args' to run_command must be list or string" 846 | self.fail_json(rc=257, cmd=args, msg=msg) 847 | rc = 0 848 | msg = None 849 | st_in = None 850 | if data: 851 | st_in = subprocess.PIPE 852 | try: 853 | cmd = subprocess.Popen(args, 854 | executable=executable, 855 | shell=shell, 856 | close_fds=close_fds, 857 | stdin=st_in, 858 | stdout=subprocess.PIPE, 859 | stderr=subprocess.PIPE) 860 | if data: 861 | cmd.stdin.write(data) 862 | cmd.stdin.write('\n') 863 | out, err = cmd.communicate() 864 | rc = cmd.returncode 865 | except (OSError, IOError), e: 866 | self.fail_json(rc=e.errno, msg=str(e), cmd=args) 867 | except: 868 | self.fail_json(rc=257, msg=traceback.format_exc(), cmd=args) 869 | if rc != 0 and check_rc: 870 | msg = err.rstrip() 871 | self.fail_json(cmd=args, rc=rc, stdout=out, stderr=err, msg=msg) 872 | return (rc, out, err) 873 | 874 | def pretty_bytes(self,size): 875 | ranges = ( 876 | (1<<50L, 'ZB'), 877 | (1<<50L, 'EB'), 878 | (1<<50L, 'PB'), 879 | (1<<40L, 'TB'), 880 | (1<<30L, 'GB'), 881 | (1<<20L, 'MB'), 882 | (1<<10L, 'KB'), 883 | (1, 'Bytes') 884 | ) 885 | for limit, suffix in ranges: 886 | if size >= limit: 887 | break 888 | return '%.2f %s' % (float(size)/ limit, suffix) 889 | 890 | # == END DYNAMICALLY INSERTED CODE === 891 | 892 | 893 | -------------------------------------------------------------------------------- /test/emptyconfig/config.yml: -------------------------------------------------------------------------------- 1 | # Empty Battleschool config.yml. 2 | tasks: 3 | - name: 'Say whereiam' 4 | command: 'echo inside emptyconfig/config.yml' 5 | 6 | sources: 7 | # local: 8 | # - playbook.yml 9 | 10 | # url: 11 | # - name: playbook.yml 12 | # url: https://db.tt/VcyI9dvr 13 | 14 | # git: 15 | # - name: 'osx' 16 | # repo: 'https://github.com/spencergibb/ansible-osx' 17 | # playbooks: 18 | # - adium.yml -------------------------------------------------------------------------------- /test/test_app_dmg.sh: -------------------------------------------------------------------------------- 1 | MODULE_PARAMS="pkg_type=app" 2 | MODULE_PARAMS="$MODULE_PARAMS archive_type=dmg" 3 | MODULE_PARAMS="$MODULE_PARAMS archive_path=Adium.app" 4 | MODULE_PARAMS="$MODULE_PARAMS url=http://sourceforge.net/projects/adium/files/Adium_1.5.9.dmg/download" 5 | #echo $MODULE_PARAMS 6 | $ANSIBLE_SRC_PATH/hacking/test-module -m share/library/mac_pkg -a "$MODULE_PARAMS" 7 | -------------------------------------------------------------------------------- /test/test_app_file.sh: -------------------------------------------------------------------------------- 1 | MODULE_PARAMS="src=/tmp/Alfred.zip" 2 | MODULE_PARAMS="$MODULE_PARAMS pkg_type=app" 3 | MODULE_PARAMS="$MODULE_PARAMS archive_type=zip" 4 | MODULE_PARAMS="$MODULE_PARAMS archive_path='Alfred 2.app'" 5 | #echo $MODULE_PARAMS 6 | $ANSIBLE_SRC_PATH/hacking/test-module -m share/library/mac_pkg -a "$MODULE_PARAMS" 7 | -------------------------------------------------------------------------------- /test/test_app_tar.sh: -------------------------------------------------------------------------------- 1 | MODULE_PARAMS="pkg_type=app" 2 | MODULE_PARAMS="$MODULE_PARAMS url=https://github.com/b4winckler/macvim/releases/download/snapshot-72/MacVim-snapshot-72-Mavericks.tbz" 3 | MODULE_PARAMS="$MODULE_PARAMS archive_type=tar" 4 | MODULE_PARAMS="$MODULE_PARAMS archive_path=MacVim-snapshot-72/MacVim.app" 5 | MODULE_PARAMS="$MODULE_PARAMS creates=MacVim.app" 6 | 7 | #echo $MODULE_PARAMS 8 | $ANSIBLE_SRC_PATH/hacking/test-module -m share/library/mac_pkg -a "$MODULE_PARAMS" 9 | -------------------------------------------------------------------------------- /test/test_pkg_file.sh: -------------------------------------------------------------------------------- 1 | MODULE_PARAMS="pkg_name=com.oracle.jdk7u25" 2 | MODULE_PARAMS="$MODULE_PARAMS pkg_version=1.1x" 3 | MODULE_PARAMS="$MODULE_PARAMS archive_type=dmg" 4 | MODULE_PARAMS="$MODULE_PARAMS src=/tmp/jdk7.dmg" 5 | MODULE_PARAMS="$MODULE_PARAMS archive_path='JDK 7 Update 25.pkg'" 6 | #echo $MODULE_PARAMS 7 | $ANSIBLE_SRC_PATH/hacking/test-module -m share/library/mac_pkg -a "$MODULE_PARAMS" 8 | -------------------------------------------------------------------------------- /test/test_pkg_file_eula.sh: -------------------------------------------------------------------------------- 1 | MODULE_PARAMS="pkg_name=org.TrueCryptFoundation.TrueCrypt" 2 | MODULE_PARAMS="$MODULE_PARAMS pkg_version=7.1.1x" 3 | MODULE_PARAMS="$MODULE_PARAMS archive_type=dmg" 4 | MODULE_PARAMS="$MODULE_PARAMS src=/tmp/truecrypt.dmg" 5 | MODULE_PARAMS="$MODULE_PARAMS archive_path='TrueCrypt 7.1a.mpkg'" 6 | MODULE_PARAMS="$MODULE_PARAMS dmg_license=yes" 7 | #echo $MODULE_PARAMS 8 | $ANSIBLE_SRC_PATH/hacking/test-module -m share/library/mac_pkg -a "$MODULE_PARAMS" 9 | -------------------------------------------------------------------------------- /test/test_pkg_java.sh: -------------------------------------------------------------------------------- 1 | MODULE_PARAMS="pkg_name=com.oracle.jdk7u51" 2 | MODULE_PARAMS="$MODULE_PARAMS pkg_version=1.1x" 3 | MODULE_PARAMS="$MODULE_PARAMS url=http://download.oracle.com/otn-pub/java/jdk/7u51-b13/jdk-7u51-macosx-x64.dmg" 4 | MODULE_PARAMS="$MODULE_PARAMS curl_opts='-L --cookie oraclelicense=accept-securebackup-cookie'" 5 | #MODULE_PARAMS="$MODULE_PARAMS dest=jdk7.dmg" 6 | #MODULE_PARAMS="$MODULE_PARAMS force=yes" 7 | MODULE_PARAMS="$MODULE_PARAMS archive_type=dmg" 8 | MODULE_PARAMS="$MODULE_PARAMS archive_path='JDK 7 Update 51.pkg'" 9 | #echo $MODULE_PARAMS 10 | $ANSIBLE_SRC_PATH/hacking/test-module -m share/library/mac_pkg -a "$MODULE_PARAMS" 11 | -------------------------------------------------------------------------------- /test/test_pkg_macports.sh: -------------------------------------------------------------------------------- 1 | MODULE_PARAMS="pkg_name=org.macports.MacPorts" 2 | MODULE_PARAMS="$MODULE_PARAMS pkg_version=0.2.2.0.0.0.0.0.0x" 3 | #MODULE_PARAMS="$MODULE_PARAMS dest=/tmp/macports.pkg" 4 | MODULE_PARAMS="$MODULE_PARAMS force=true" 5 | MODULE_PARAMS="$MODULE_PARAMS url=https://distfiles.macports.org/MacPorts/MacPorts-2.2.0-10.8-MountainLion.pkg" 6 | #echo $MODULE_PARAMS 7 | $ANSIBLE_SRC_PATH/hacking/test-module -m share/library/mac_pkg -a "$MODULE_PARAMS" 8 | -------------------------------------------------------------------------------- /test/test_pkg_vagrant.sh: -------------------------------------------------------------------------------- 1 | MODULE_PARAMS="pkg_name=com.vagrant.vagrant" 2 | MODULE_PARAMS="$MODULE_PARAMS pkg_version=1.5.0" 3 | MODULE_PARAMS="$MODULE_PARAMS archive_type=dmg" 4 | MODULE_PARAMS="$MODULE_PARAMS archive_path=Vagrant.pkg" 5 | #MODULE_PARAMS="$MODULE_PARAMS force=true" 6 | MODULE_PARAMS="$MODULE_PARAMS url=https://dl.bintray.com/mitchellh/vagrant/vagrant_1.5.0.dmg" 7 | #echo $MODULE_PARAMS 8 | $ANSIBLE_SRC_PATH/hacking/test-module -m share/library/mac_pkg -a "$MODULE_PARAMS" 9 | -------------------------------------------------------------------------------- /test/test_pkg_zip.sh: -------------------------------------------------------------------------------- 1 | MODULE_PARAMS="pkg_name=org.macports.MacPorts" 2 | MODULE_PARAMS="$MODULE_PARAMS pkg_version=1.0" 3 | MODULE_PARAMS="$MODULE_PARAMS src=/tmp/macports.zip" 4 | MODULE_PARAMS="$MODULE_PARAMS archive_type=zip" 5 | MODULE_PARAMS="$MODULE_PARAMS archive_path=MacPorts-2.1.3-10.8-MountainLion.pkg" 6 | 7 | #echo $MODULE_PARAMS 8 | $ANSIBLE_SRC_PATH/hacking/test-module -m share/library/mac_pkg -a "$MODULE_PARAMS" 9 | -------------------------------------------------------------------------------- /test/test_script_brew.sh: -------------------------------------------------------------------------------- 1 | MODULE_PARAMS="pkg_type=script" 2 | MODULE_PARAMS="$MODULE_PARAMS script_creates=/usr/local/bin/brew" 3 | #MODULE_PARAMS="$MODULE_PARAMS script_prefix='yes | '" 4 | MODULE_PARAMS="$MODULE_PARAMS script_exe=/usr/bin/ruby" 5 | MODULE_PARAMS="$MODULE_PARAMS url=https://raw.githubusercontent.com/Homebrew/install/master/install" 6 | #MODULE_PARAMS="$MODULE_PARAMS script_postfix=' < /dev/null'" 7 | MODULE_PARAMS="$MODULE_PARAMS script_data='\n'" 8 | #echo $MODULE_PARAMS 9 | $ANSIBLE_SRC_PATH/hacking/test-module -m share/library/mac_pkg -a "$MODULE_PARAMS" 10 | -------------------------------------------------------------------------------- /test/testconfig/config.yml: -------------------------------------------------------------------------------- 1 | --- 2 | cache_dir: test/test_cache 3 | 4 | sources: 5 | local: 6 | - playbook.yml 7 | #- another_playbook.yml 8 | 9 | url: 10 | #- https://db.tt/JVnWh3vI 11 | - name: another_playbook.yml 12 | url: https://db.tt/JVnWh3vI 13 | 14 | git: 15 | #- { name: 'osx', repo: 'https://github.com/spencergibb/ansible-osx' } 16 | - name: 'osx' 17 | repo: 'https://github.com/spencergibb/ansible-osx' 18 | playbooks: 19 | - test 20 | - apps: 21 | - test2 22 | -------------------------------------------------------------------------------- /test/testconfig/playbooks/another_playbook.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - hosts: workstation 3 | 4 | tasks: 5 | - name: print from playbook 6 | debug: msg="in playbooks/another_playbook.yml" 7 | 8 | - name: print os version 9 | debug: msg="mac_major_minor_version = {{mac_major_minor_version}}, ansible_distribution = {{ansible_distribution}}, ansible_distribution_version = {{ansible_distribution_version}}" 10 | 11 | - name: print mavericks 12 | debug: msg="Mavericks" 13 | when: mac_major_minor_version == "10.9" 14 | 15 | - name: print mountain lion 16 | debug: msg="Mountain Lion" 17 | when: mac_major_minor_version == "10.8" 18 | 19 | - name: print lion 20 | debug: msg="Lion" 21 | when: mac_major_minor_version == "10.7" 22 | -------------------------------------------------------------------------------- /test/testconfig/playbooks/library/time-test: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | DOCUMENTATION = ''' 4 | --- 5 | module: time 6 | author: Spencer Gibb 7 | version_added: "1.2.2" 8 | short_description: Print the time 9 | description: 10 | - Print the time 11 | options: 12 | time: 13 | required: false 14 | description: 15 | - Optionally sets the time to print 16 | ''' 17 | 18 | # import some python modules that we'll use. These are all 19 | # available in Python's core 20 | 21 | import datetime 22 | import sys 23 | import json 24 | import os 25 | 26 | 27 | def main(): 28 | 29 | module = AnsibleModule( 30 | argument_spec=dict( 31 | time=dict(default=None, type='str'), 32 | ), 33 | supports_check_mode=False 34 | ) 35 | 36 | time = module.params['time'] 37 | 38 | if time: 39 | # now we'll affect the change. Many modules 40 | # will strive to be 'idempotent', meaning they 41 | # will only make changes when the desired state 42 | # expressed to the module does not match 43 | # the current state. Look at 'service' 44 | # or 'yum' in the main git tree for an example 45 | # of how that might look. 46 | 47 | #rc = os.system("date -s \"%s\"" % time) 48 | rc = os.system("gdate -s \"%s\"" % time) 49 | 50 | # always handle all possible errors 51 | # 52 | # when returning a failure, include 'failed' 53 | # in the return data, and explain the failure 54 | # in 'msg'. Both of these conventions are 55 | # required however additional keys and values 56 | # can be added. 57 | 58 | if rc != 0: 59 | print json.dumps({ 60 | "failed": True, 61 | "msg": "failed setting the time" 62 | }) 63 | sys.exit(1) 64 | 65 | # when things do not fail, we do not 66 | # have any restrictions on what kinds of 67 | # data are returned, but it's always a 68 | # good idea to include whether or not 69 | # a change was made, as that will allow 70 | # notifiers to be used in playbooks. 71 | 72 | date = str(datetime.datetime.now()) 73 | print json.dumps({ 74 | "time": date, 75 | "changed": True 76 | }) 77 | sys.exit(0) 78 | 79 | else: 80 | # if no parameters are sent, the module may or 81 | # may not error out, this one will just 82 | # return the time 83 | 84 | date = str(datetime.datetime.now()) 85 | print json.dumps({ 86 | "time": date 87 | }) 88 | 89 | # include magic from lib/ansible/module_common.py 90 | #<> 91 | main() -------------------------------------------------------------------------------- /test/testconfig/playbooks/playbook.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - hosts: workstation 3 | 4 | tasks: 5 | - name: print from playbook 6 | debug: msg="in playbooks/playbook.yml" 7 | 8 | - name: get time from user module 9 | action: time-test 10 | register: playbook_time 11 | 12 | - name: print time from user module 13 | debug: msg="time {{playbook_time.time}}" 14 | 15 | - name: install java 7 16 | mac_pkg: pkg_name=com.oracle.jdk7u25 17 | pkg_version=1.1 18 | state=present 19 | url=https://edelivery.oracle.com/otn-pub/java/jdk/7u25-b15/jdk-7u25-macosx-x64.dmg 20 | curl_opts='--cookie gpw_e24=http%3A%2F%2Fwww.oracle.com' 21 | archive_path='JDK 7 Update 25.pkg' 22 | sudo: yes 23 | 24 | --------------------------------------------------------------------------------