Compare commits

...

246 Commits
0.3 ... 0.4

Author SHA1 Message Date
9551afc061 Update changelog for release. 2012-05-23 19:38:59 -04:00
e03a724dc0 Merge pull request #413 from emgee/kv-equals
Allow "=" in k-v values.
2012-05-23 03:59:53 -07:00
8babac4856 Allow "=" in k-v values. 2012-05-23 00:58:05 +01:00
90c70d7c05 Merge pull request #412 from mgwilliams/bug-include-handlers-templating
template path for handlers: include
2012-05-22 04:23:46 -07:00
0844db4462 template path for handlers: include 2012-05-21 21:40:07 -07:00
0de0a999ab Merge pull request #411 from mgwilliams/bug-fetch-module
Fetch module: run dwim on dest after templating, not before
2012-05-20 07:01:11 -07:00
a8b494bab5 dim fetch dest after templating 2012-05-19 18:59:23 -07:00
5dc05ef556 Merge pull request #391 from cocoy/reload-feature
Feature: Add state=reloaded
2012-05-19 14:26:20 -07:00
d4cc8dc242 Fix hosts being in multiple groups, and duplicate host references in the same inventory file. 2012-05-19 17:24:54 -04:00
5672d56189 Merge pull request #402 from denen99/devel
updated connection.py to have the same interface for exec_command on both LocalConnection and ParamikoConnection classes
2012-05-17 04:06:02 -07:00
03a3d74954 updated connection.py to have the same interface for exec_command on both LocalConnection and ParamikoConnection classes 2012-05-17 00:11:50 -04:00
5ab77b8239 Merge pull request #393 from bradobro/fix_usergroup
0.4 fix, Remove call to undefined debug() in module 'user'
2012-05-15 10:04:28 -07:00
c6699ad557 Merge pull request #392 from jhoekx/fix-large-output-test
If /usr/share/dict/words does not exist, use cracklib-small to test larg...
2012-05-15 10:04:06 -07:00
8975fe5a8e Merge pull request #396 from jhoekx/setup-json
Arguments to setup module should be json.
2012-05-15 10:03:11 -07:00
4f70245eb6 Arguments to setup module should be json. 2012-05-15 17:43:01 +02:00
4d08940223 Remove call to undefined debug() in module 'user'.
I think when we stopped using stderr for debugging modules because
paramiko didn't like it, many modules used the idiom of defining
a debug function that used standard error. The def's and calls were
removed.

This looks like a stray debug() that didn't get removed and didn't
show up unless you alter a user's groups. If it's hit, 'user' fails
with a global undefined function error.
2012-05-15 09:19:43 -04:00
3768095645 If /usr/share/dict/words does not exist, use cracklib-small to test large output. 2012-05-15 14:26:43 +02:00
fa9462d687 Add state=reloaded 2012-05-15 17:28:49 +08:00
404c3acbbf Merge pull request #387 from skvidal/devel
implement --private-key for ansible - to take pem files as an optional arg
2012-05-14 15:56:27 -07:00
87076e29e7 Merge pull request #388 from wgjohnson/devel
Devel: created get_selinux_facts
2012-05-14 15:55:42 -07:00
119d7cd8fd Merge pull request #1 from wgjohnson/get_selinux_facts
starting fresh, doing it 'right'
2012-05-14 14:34:36 -07:00
d67f996f8d starting fresh, doing it 'right'
Lost commit history, but this way I'm not introducing crappy commit
history in RP
2012-05-14 14:31:53 -07:00
f5c6457de8 make sure private_key_file is hooked up in playbooks, too 2012-05-14 16:52:48 -04:00
622405c03d hook up --private-key to the runner 2012-05-14 16:22:05 -04:00
94d7ceb21a add --private-key option and related infrastructure to make paramiko work
with a private key file - not just an agent or pw
2012-05-14 16:14:38 -04:00
776fe43d12 Merge pull request #384 from dne/template-fixes
Template fixes
2012-05-14 05:17:54 -07:00
5338fabf20 In the template module, use utils.template_from_file instead of reimplementing it inline 2012-05-13 20:57:01 +02:00
a5dc23b537 For utils.template_from_file, change 'no_engine' default to True
This now follows the other template functions, and implicitly also
fixes that playbook handler includes were still processed by Jinja2.
2012-05-13 20:42:11 +02:00
c33cba3746 Set 'hostvars' before performing variable replacements 2012-05-13 20:36:08 +02:00
ada9033acd Squashed commit of the following, plus some streamlining (MPD).
commit e00368e7c65c65bed11fcaaf83fe8b093dbf492e
Merge: 2ea7110 c039aa0
Author: Michael DeHaan <michael.dehaan@gmail.com>
Date:   Thu May 10 01:43:10 2012 -0400

    Merge branch 'devel' of https://github.com/weaselkeeper/ansible into weaselkeeper-devel

commit c039aa0915
Author: Jim Richardson <weaselkeeper@gmail.com>
Date:   Fri May 11 17:55:13 2012 -0700

    cleanup and simplification of ANSIBLE_REMOTE_TMP feature

commit d87f15b796
Merge: 5917aba 4c2fd25
Author: Jim Richardson <weaselkeeper@gmail.com>
Date:   Fri May 11 17:30:16 2012 -0700

    Merge branch 'devel' of github.com:weaselkeeper/ansible into devel

commit 5917aba761
Author: Jim Richardson <jrichardson@classmates.com>
Date:   Wed May 9 11:25:45 2012 -0700

     ANSIBLE_REMOTE_TMP environment variable sets where ansible will stuf tmp files on remote host.  Default is /var/tmp for root, and $HOME/.ansible/tmp for non-root

commit 4c2fd25777
Author: Jim Richardson <jrichardson@classmates.com>
Date:   Wed May 9 11:25:45 2012 -0700

     ANSIBLE_REMOTE_TMP environment variable sets where ansible will stuf tmp files on remote host.  Default is /var/tmp for root, and $HOME/.ansible/tmp for non-root
2012-05-10 01:50:22 -04:00
2ea7110c83 Reinstate raw module 2012-05-10 01:14:30 -04:00
f5bd60c87c add fake module for 'raw' 2012-05-10 01:08:01 -04:00
c8b298dc56 Default git module to master branch so branch= is not required. 2012-05-10 00:41:44 -04:00
733677362d update comment about branch support, now that supported 2012-05-10 00:39:15 -04:00
1864a480ba Do not read user's SSH config file because only reading it partially is confusing to everyone :) 2012-05-09 23:45:37 -04:00
d19588b00c Update development changelogs 2012-05-09 23:31:58 -04:00
454e74620b Allow hosts not in inventory to override inventory with -i "host1,host2:port" etc, which is the original intent
of --override-hosts.
2012-05-09 23:26:45 -04:00
84995367a5 Do not run unit tests against the EVENTS code because it is subject to change and that does not mean anything is broken.
If we want to test playbooks, it's best to test the result of a playbook to make sure it does the correct thing.  Also
remove playbook2/3 yml which were not used.
2012-05-09 23:21:44 -04:00
81b9240979 Merge pull request #363 from jhoekx/template-only-if
Fix correct variable expansion in includes only_if.
2012-05-11 15:55:09 -07:00
b1da61ae92 Fix correct variable expansion in includes only_if.
There's not need to run the complete include through the templating engine.
Several variables were not included before the conditional was evaluated.
2012-05-11 15:55:33 +02:00
4e65b537da Merge pull request #356 from jkleint/singleton-notify
Support single handlers for notify.
2012-05-10 17:04:54 -07:00
5904682fdd Merge pull request #355 from jkleint/devel
Fix YAML hosts list.
2012-05-10 13:48:57 -07:00
03b887a53d Support single handlers for notify. 2012-05-10 15:37:30 -04:00
38a03aef6b Fix YAML hosts list. 2012-05-10 15:13:32 -04:00
174e4c1340 Merge pull request #354 from jhoekx/varreplace-list
Support list indexing in varReplace.
2012-05-10 11:12:55 -07:00
b8573ab7d1 Support list indexing in varReplace. 2012-05-10 19:25:28 +02:00
b90c2356c3 Merge pull request #349 from jhoekx/template-unicode-fixes
Fixup unicode varReplace templating.
2012-05-10 05:05:50 -07:00
461ba57277 Merge pull request #351 from cocoy/bug-264
Fix for bug #264
2012-05-10 05:04:57 -07:00
f8244f3168 Fix for bug #264 2012-05-10 18:53:24 +08:00
c362a2e581 Fixup unicode varReplace templating.
The original patches should have conflicted?
53bde0bf51 vs efde61e537
2012-05-10 10:11:14 +02:00
85438bf1c0 Allow service 'enable' parameter be 'enabled', which is what I'd personally try to use. 2012-05-09 23:13:13 -04:00
480614ee91 Merge pull request #371 from lcosmin/devel
Added the network address of an interface as an ansible variable
2012-05-11 15:21:37 -07:00
63bf10bab0 Merge pull request #370 from sfromm/issue353
Fix for issue 353
2012-05-11 11:55:18 -07:00
5e5b4a90dc Fix for sudo to another non-root user (issue #353)
mktemp creates the temp directory 700 only.  If the sudo-user is not
root, the other user will not be able to run the command (Permission
denied error).  This adds the executable bit for all on the temp
directory.
2012-05-11 11:48:31 -07:00
abb391f3fd Merge pull request #362 from jhoekx/uppercase-vars
Allow camelCase variables in varReplace.
2012-05-11 10:06:21 -07:00
277ff4f516 Merge pull request #364 from lcosmin/a5b13fe4ce7e3d3846999ae35127fbe4095ee6c3
Added parameter to the apt-get install command to prevent interactive conflict resolution
2012-05-11 10:05:44 -07:00
38ad3da63b Merge pull request #365 from jkleint/devel
Fix hang on large command output.
2012-05-11 09:52:35 -07:00
64673cc932 Add test for large output; fix indentation. 2012-05-11 11:38:15 -04:00
0956aa96f4 Don't read from Paramiko's stderr since there isn't one. 2012-05-11 10:53:35 -04:00
2fae38fffb Added a new fact: network address for an interface 2012-05-11 17:17:59 +03:00
0b5b44858e Allow camelCase variables in varreplace. 2012-05-11 14:14:53 +02:00
a5b13fe4ce Added parameter to the apt-get install command to prevent interactive conflict resolution 2012-05-11 10:54:55 +03:00
6119988934 override-hosts is gone since we can't load inventory for hosts (so it's not an whitelist), but we can
still support hosts outside of inventory if you pass in an array of hosts.  Ports are supported,
but not much other info.
2012-05-09 21:53:09 -04:00
e0317a35d0 Don't use template syntax in non-templates, it's not the default anymore 2012-05-08 00:09:04 -04:00
7f6add670e Merge pull request #346 from jhoekx/improve-varreplace
Support nested variables in varReplace
2012-05-09 17:25:07 -07:00
a7599954a3 Make no_engine the default templating action. 2012-05-09 16:15:43 +02:00
53bde0bf51 Support nested variables in varReplace. 2012-05-09 16:15:43 +02:00
c7c38e9a61 Merge pull request #342 from jhoekx/fix-jinja-newline
Preserve trailing newline after templating.
2012-05-09 05:33:10 -07:00
55fbc95243 Preserve trailing newline after templating.
Jinja seems to eat trailing whitespace. A lot of tools complain about this: iptables-restore, iscsid...
2012-05-09 10:31:32 +02:00
1dbea82350 Merge pull request #331 from mgwilliams/bug-fetch-module
Template dest and src parameters
2012-05-08 11:11:13 -07:00
1c652492e1 Merge pull request #335 from sfromm/user
User module fix
2012-05-08 11:10:37 -07:00
5506147033 Move import of spwd under a try block
Resolves issue #333.  If spwd is not available, the password will
be set regardless.
2012-05-08 10:40:44 -07:00
a07f69cd54 Template dest and src parameters 2012-05-08 07:12:30 -07:00
1cb278f50d changelog update 2012-05-07 23:48:18 -04:00
8cb22cf858 Merge pull request #336 from sfromm/issue122
Issue122 - have all modules log to syslog
2012-05-09 17:19:24 -07:00
37b394fd6e Merge pull request #337 from ordrin/devel
Support for branches in git module
2012-05-09 17:18:32 -07:00
9b587d4458 Merge pull request #339 from mgwilliams/feature-unicode
Allow unicode (utf8) in templates
2012-05-09 17:16:59 -07:00
1144a21668 Apply logging of arguments to modules (issue #122)
The ohai and facter modules use /usr/bin/logger to log the fact that
they have been invoked.  I added 'import os' to the ping module
so that it could have the same syslog statements as the other modules.
I separated the condensed:
    shlex.split(open(argfile, 'r').read())
into two separate statements similar to the other modules.
2012-05-09 15:19:20 -07:00
b89d8db7ce removed debug code 2012-05-08 16:05:43 -07:00
efde61e537 allow unicode (utf8) characters in jinja templates 2012-05-08 16:03:51 -07:00
5f8e2a3651 removed unecessary branch addition from switchver 2012-05-08 16:24:29 -04:00
325b397ac0 added in support for branches in git module (now takes optional "branch") 2012-05-08 16:20:08 -04:00
dd5f535925 Be able to use --extra-vars to set the hosts variable 2012-05-07 23:37:09 -04:00
5e84a05db9 override hosts gone 2012-05-07 23:18:06 -04:00
0abca68998 Since host variables are becoming important, it did not make sense to sustain --override-hosts, with the ability
to create hosts that didn't have inventory information, but also existed, in various groups.
2012-05-07 23:16:20 -04:00
b2485850bf Revert "fix indentation"
This reverts commit 42b55a07cd.
2012-05-07 21:46:45 -04:00
893bf90627 Revert "fix the get_hosts() error in get_groups returning a dict not a list of group objects"
This reverts commit f41fb90d78.
2012-05-07 21:46:36 -04:00
a54d9f66e6 Merge pull request #329 from skvidal/devel
inventory and indentation
2012-05-08 03:54:41 -07:00
6c0511a2d0 Merge pull request #330 from jhoekx/yaml-inventory-list
Yaml inventory variable list
2012-05-08 03:53:46 -07:00
1d28212f86 Inventory: undefined group -> ungrouped.
Also change test name collision.
2012-05-08 09:44:59 +02:00
3b40158961 Inventory: support list of vars in host. 2012-05-08 09:44:29 +02:00
b5f6558872 Merge pull request #327 from mrproper/devel
Devel - fix issue #315
2012-05-07 18:18:00 -07:00
f09ec78fbd Merge remote-tracking branch 'ansible-origin/devel' into devel
Conflicts:
	library/user
2012-05-08 10:33:18 +10:00
f2e8836c94 Merge pull request #324 from lcosmin/devel
Prevent facter stderr messages from confusing ansible
2012-05-07 17:07:49 -07:00
a589ee4d15 Reverting to apply correct attribution. 2012-05-07 20:07:12 -04:00
de00dacff9 Don't let facter spew things to stderr and confuse Ansible, which now blends stdout/stderr due to paramiko
pty usage.

(This commit from lcosmin, manually applied due to github crossup on my part -- MPD)
2012-05-07 20:05:10 -04:00
95069b7ee3 Merge pull request #326 from pas256/patch-2
Fixing append when user already in desired groups
2012-05-07 17:01:50 -07:00
db65cf5a90 Fix non-sudo execution, add --sudo-user to ansible-playbook. 2012-05-07 19:58:16 -04:00
5d63c6b204 remove now useless append check in user_mod 2012-05-08 09:49:37 +10:00
96c0ee0efc Fixes issue #315
This adds some logic when usings groups possibly in combination with append

  if just specifying groups and the current groups do not match the list
     set groups
  if specifying groups with append and any group thats not in the current groups
     set groups with -a
2012-05-08 09:43:51 +10:00
42b55a07cd fix indentation 2012-05-07 18:10:52 -04:00
f41fb90d78 fix the get_hosts() error in get_groups returning a dict not a list of group objects 2012-05-07 18:09:57 -04:00
6a4e8d7f4c Code was adding '-a' even when the user was already in the desired groups causing usermod to fail since there was no '-G' option. This is duplicate code (3 lines) from the 'group' section, so could be improved, but it works. 2012-05-07 11:35:45 -07:00
472395bd77 Prevent facter stderr messages from confusing ansible 2012-05-07 18:08:50 +03:00
d6e56dee87 Update development changelog. 2012-05-06 18:34:07 -04:00
66ba6b023c Update manpage to mention -U (--sudo-user). 2012-05-06 18:32:43 -04:00
4694c3fbba Fixup the previous sudo_user support patch to work with playbooks. 2012-05-06 18:24:45 -04:00
a1ed9fc546 sudo -u works now. Needs testing and maybe a bit of cleanup 2012-05-06 18:24:45 -04:00
abcfdbeeaf first bit of getting sudo -u functionality 2012-05-06 18:24:45 -04:00
4d623d0356 sudo -u works now. Needs testing and maybe a bit of cleanup 2012-05-06 18:24:44 -04:00
98dda72be1 first bit of getting sudo -u functionality 2012-05-06 18:24:44 -04:00
cbbd9d8f3a Merge pull request #316 from skvidal/devel
fix up service module to make it start processes that are listed to be
2012-05-06 16:43:20 -07:00
85a04a2517 Add missing file 2012-05-06 18:02:53 -04:00
a7b9bf958d Add tests for new advanced inventory features (groups of groups, group variables) in the default INI format file. 2012-05-06 18:01:11 -04:00
46c661b3e3 Further work on making the YAML inventory parser use the new inventory objects. 2012-05-06 17:03:17 -04:00
c804752835 Fix playbooks such that they work with external inventory scripts. We really want to find out why the 'all'
group isn't available, but group vars don't make sense for external inventory anyway.
2012-05-06 15:00:17 -04:00
3fb2d38514 Reinstate external inventory script support this time using the new more OO-ey inventory system.
Next up: YAML format.
2012-05-06 14:47:05 -04:00
d9bec842ef missing files 2012-05-05 16:37:28 -04:00
4011d15faa Refactored inventory to make it object oriented, need to make YAML format and executable script
format compatible with this still, and add some tests for INI-style groups of groups
and variables.
2012-05-05 16:31:03 -04:00
958832fb1f Log last execution of ansible-pull to /var/log/ansible-pull.log
TODO: logrotate script and append to log instead.
2012-05-05 12:18:04 -04:00
23c6a0985c Update devel branch version in RPM spec. 2012-05-05 12:16:19 -04:00
2c416b8f06 fix up service module to make it start processes that are listed to be
running and to report errors from starting them
2012-05-04 01:20:51 -04:00
81b8190fc5 Include files are no longer to be considered Jinja2 templates, but individual THINGS
in included files can still be templated just like top level playbooks.  Resolves
some issues about statements being evaluated before facts were available
2012-05-02 00:28:19 -04:00
aec8995fd9 Change defaults so people have to make conscious decisions about pull setup. 2012-05-01 23:25:19 -04:00
00ea71d8de Add some comments to ansible-pull and the example playbook 2012-05-01 23:22:23 -04:00
6be85f4fc5 Merge branch 'local' of https://github.com/sfromm/ansible into devel 2012-05-01 23:06:50 -04:00
84c9caa805 Simplify ansible-pull to just invoke ansible and ansible-playbook
This eliminates the creation of runner and playbook instances and just
invokes the processes ansible and ansible-playbook.
2012-05-02 09:51:19 -07:00
23ff967f38 Copy inventory file to client 2012-05-02 09:51:19 -07:00
672794f586 Add ansible-pull
A first stab at a pull-based model for ansible.  This does two things:
    1. Invoke the git module via Runner to set up a git repository on the
       localhost.  It sets up Runner to use transport='local' and forces
       the inventory to just 'localhost'.
    2. Run any playbooks provided.  By default, this wants to run the
       playbook local.yml.  This also sets transport='local' and sets
       the host_list to a list: localhost, fqdn, and hostname.
The reason for setting the host_list and not using override_hosts is
because there may be plays in the playbook that are not meant for a
specific host.  That is, if the git repository is for the entire site
and not host-specific, you don't want to override hosts and apply all
plays to any given host.  This has the downside of potentially running a
play three times if the play is defined for 'hosts: all'.
2012-05-02 09:51:19 -07:00
3803b27f6c Add playbook and template to set up ansible-pull
Playbook will install ansible, create directory where git checkout goes,
and set up a cron job to run ansible-pull.
2012-05-02 09:51:19 -07:00
df61a65344 Merge pull request #294 from jhoekx/inventory-hostname
Define 'inventory_hostname' variable for hosts.
2012-05-02 04:37:45 -07:00
fe90650311 Merge pull request #295 from jhoekx/user-existing-groups
Group order in user module should not matter.
2012-05-02 04:36:47 -07:00
2c8fbdbd20 Group order in user module should not matter.
Groups are not necessarily returned in the defined order, especially when the user already existed before ansible started managing the system.
2012-05-02 11:54:27 +02:00
03541baba7 Define 'inventory_hostname' variable for hosts. 2012-05-02 09:56:58 +02:00
bcd713a9f3 changelog update 2012-05-01 23:05:40 -04:00
8ec60e0b4e update development changelog 2012-05-01 23:03:00 -04:00
3eb00ec1e7 Merge branch 'devel' of https://github.com/skvidal/ansible into skvidal-devel 2012-05-01 22:58:47 -04:00
38ea61054f if a playbook has no vars - still include the global vars 2012-05-03 15:52:02 -04:00
dc35dd99c0 inventory group 'all' variables are global variables
applied to every host and available all over
they are read in so that ones defined first can be used to define
the later ones.
2012-05-02 18:30:58 -04:00
2b7828e118 Fix: file module does not remove dangling symlinks 2012-05-01 22:56:37 -04:00
192c0c641c Trivial fix: make PyDev happy (imports). 2012-05-01 22:56:21 -04:00
12873b9bb6 Merge pull request #303 from jkleint/devel
Remove command timeout.
2012-05-03 15:45:40 -07:00
d6bbf288cd Merge pull request #306 from leucos/devel
Adds support for Ubuntu style update-rc.d
2012-05-03 05:37:11 -07:00
fc61141ebf Fixes _find_binaries not using globals
_find_binaries now sets the right globals
Binaries are now properly populated (reverse path/binary for loops)
2012-05-03 12:36:06 +02:00
572868c9e1 Adds support for Ubuntu style update-rc.d
Checks if update-rc.d (Ubuntu) or chkconfig (RHEL) should be used.
Adds basic bin path search for those binaries
Adds 'enable' and 'disable' options for 'enable' command since it's the
arguments that update-rc.d uses (this might be somewhat confusing to
have a command line with 'enable=enable', but probably mkes sense for
Ubuntu users).
Allows use of mixed case for 'list' and 'state' commands.
2012-05-03 08:34:36 +02:00
9ad4e7d149 Merge pull request #300 from sfromm/git
Updates for git module
2012-05-02 19:57:50 -07:00
5fe677645d Merge pull request #302 from sfromm/selinux
Set default selinux context with '_default'
2012-05-02 19:56:33 -07:00
52efc90326 Merge pull request #299 from pas256/patch-1
test-module arguments file location
2012-05-02 17:58:01 -07:00
cbbaee8a9f Remove command timeout. 2012-05-02 16:25:04 -04:00
f1b7149fcf Update how to set default selinux context for file
This removes the 'context' option and replaces it with checks for
'_default' value for seuser, serole, setype, or (maybe) selevel.
If '_default' is provided *and* there is a default context for the given
file, this will set the file context to the available default.
2012-05-02 11:38:44 -07:00
887d293a3b Add git reset --hard support to git module
Resets working tree to what is in HEAD and discards any
uncommitted changes.
2012-05-02 09:50:36 -07:00
40dbd9b3d7 Add exit_json and fail_json to git module
This adds exit_json() and fail_json() to git module.  It also sets
version to 'HEAD', if not provided.
2012-05-02 09:50:36 -07:00
f9a14cfec1 Adding a missing '~' to use the user's home directory instead of the root file system for the module arguments 2012-05-02 09:50:20 -07:00
594084bc9f Merge pull request #287 from dne/user-group-add-system
Add 'system' option for user and group modules
2012-05-01 15:54:45 -07:00
af74c741ef Merge pull request #285 from tbielawa/fixLicenseInDevel
Fix license in devel
2012-05-01 15:42:13 -07:00
27582ad313 Merge pull request #281 from dne/file-link-relative
File module fix for symlink from relative source path
2012-05-01 15:41:46 -07:00
dd380f67e6 Merge pull request #289 from skvidal/devel
service enable=on/off
2012-05-01 15:40:59 -07:00
f14c1e3e91 Async: print one poll message per polling cycle.
Instead of one per host per polling cycle.
2012-05-01 18:38:26 -04:00
029fe1273c Modify /usr/bin/ansible and playbooks to use the new async API. 2012-05-01 18:38:26 -04:00
ce9a8c9ffc Introduce Async API in Runner. 2012-05-01 18:38:26 -04:00
d96763346f Make async wrapper kill the process it supervises.
It wasn't killing the job at all before.
Indentation was 3 spaces.
2012-05-01 18:38:26 -04:00
cc13d34f91 changelog update 2012-05-01 18:38:26 -04:00
dc30b01c87 new service module handles enable=on/off for chkconfigging it on 2012-05-01 17:46:45 -04:00
a837009c5d Add 'system' option for user and group modules
Creates system accounts/groups; corresponds to the '-r' option for {user,group}add.

The option is only honored when users/groups are added, not when modified.
2012-05-01 21:38:55 +02:00
029d86be2f Include license file in MANIFEST.in. Fixes #284 2012-05-01 10:34:36 -04:00
82d3fcfd53 File module fix for symlink from relative source path 2012-05-01 03:12:13 +02:00
cfb7c94c49 Merge pull request #271 from jhoekx/group-names
Introduce group_names in template variables.
2012-04-28 13:55:08 -07:00
b87710a1df Introduce group_names in template variables.
This is a list of all the groups a host is in.
2012-04-28 18:23:44 +02:00
6341361a5b Clarify that stderr WILL crash your module. Can't redirect because if we do we lose tracebacks
from modules remotely when they fail to parse, and this is VERY useful data.
2012-04-27 01:36:31 -04:00
88a7f771cb update changelog 2012-04-27 01:27:47 -04:00
36e454c52f Because paramiko using a pty can't distinguish stderr and stdout, remove handling that
treated -D as a way to show stderr, and make sure modules don't include things on stderr.
Update docs and test module script to come into line.
2012-04-27 01:25:38 -04:00
076f1bc169 Revert "Revert "Unify normal and sudo remote command execution. Breaks stderr/stdout handling"
This reverts commit 40f603539c.
2012-04-27 00:46:26 -04:00
0d5bee2dfa Revert "Revert "Actually wait for password prompt in remote sudo execution. Totally breaks stderr debugging"
This reverts commit e16114034c.

Conflicts:

	lib/ansible/connection.py
2012-04-27 00:46:17 -04:00
40f603539c Revert "Unify normal and sudo remote command execution. Breaks stderr/stdout handling
This reverts commit 44486223ed.
2012-04-27 00:33:48 -04:00
e16114034c Revert "Actually wait for password prompt in remote sudo execution. Totally breaks stderr debugging
and various modules.

This reverts commit 6341a9547f.
2012-04-27 00:31:18 -04:00
f939f3fdaf Playbook can now take a 'name' which it will show when starting the play 2012-04-27 00:08:06 -04:00
67d1169674 fix code handling if module_vars is None 2012-04-26 23:18:29 -04:00
803672a769 Update development changelog 2012-04-26 23:12:09 -04:00
aa8ae85070 Add playbook example for first_available_file 2012-04-26 23:03:14 -04:00
3804910187 Some tweaks to first_available_file mostly in terms of error handling and not trying src= to be present
as it is not used here.
2012-04-26 23:00:33 -04:00
b56efa7809 Fix handling of list=status such that it will actually work in absense of the state command, to
make it useful from /usr/bin/ansible and the API.  Also some minor style fixes.
2012-04-26 22:43:36 -04:00
ea296e9edb Merge branch 'devel' of https://github.com/jkleint/ansible into jkleint-devel 2012-04-26 22:23:09 -04:00
47ec93b32f Merge pull request #270 from skvidal/devel
first_if_available and yum fixes
2012-04-27 18:44:48 -07:00
a3ab793ba8 fixes for yum module for rhel5 and issue 269 2012-04-27 17:38:46 -04:00
c701e59592 very minor indentation issue in librar/copy 2012-04-27 16:45:28 -04:00
72fd971822 fall through file source list: first_available_file support
add first_available_file look up to _execute_template and _execute_copy
to runner.

add this data to playbook handler so it can be included into module_vars
2012-04-27 16:43:55 -04:00
c6db4e8fa3 Get service module working with sudo, add list=status, better error messages.
When running the service module via sudo, `$PATH` didn't contain `/sbin`,
so the service binary couldn't be found.  This just runs `/sbin/service`
directly.  Output is spewed to stderr on error.

Added `list=status` to include the output of `service <cmd> status`.
2012-04-27 13:35:24 -04:00
40f5469869 minor personal style preference on 'not' 2012-04-26 20:42:20 -04:00
bee0e8db5b Merge pull request #266 from RWJMurphy/devel
[devel] ansible-playbook hangs if not passed --extra-vars
2012-04-26 19:22:22 -07:00
abf524405a shlex.split() tries to read from stdin if passed None 2012-04-27 11:25:43 +10:00
b9982fc17b Reinstate --extra-vars, which can do things in playbooks like:
ansible-playbook release-my-app.yml --extra-vars="version=123"

And make $version available in the playbook without re-editing the file
2012-04-26 19:57:07 -04:00
a0ac936a55 Merge pull request #249 from jhoekx/octal-file-mode
Return the octal mode of a file instead of decimal.
2012-04-26 16:25:39 -07:00
5aa5a48f7f Merge pull request #250 from jkleint/devel
Service module crashes if args has no "=".
2012-04-26 16:22:56 -07:00
b6bdb22341 Merge pull request #251 from sfromm/selinux
Selinux -- more fun
2012-04-26 16:20:49 -07:00
08b2f147e3 Merge pull request #252 from sfromm/facts
Use socket.getfqdn() to fix issue #243
2012-04-26 16:20:05 -07:00
a4b3b7a2cf Local connection doesn't need a port. 2012-04-26 19:19:34 -04:00
6341a9547f Actually wait for password prompt in remote sudo execution.
When running on lots of hosts with a large login banner on a slow network, it was still possible that the first recv() didn't to pull in the sudo password prompt, and sudo would fail intermittently.  This patch tells sudo to use a specific, randomly-generated prompt and then reads until it finds that prompt (or times out).  Only then is the password sent.  It also catches `socket.timeout` and thunks it to a more useful `AnsbileError` with the output of sudo so if something goes wrong you can see what's up.
2012-04-26 15:01:20 -03:00
7b41e22e56 Change to socket.getfqdn() for fqdn fact 2012-04-26 08:39:47 -07:00
5be20f8748 Update file module selinux-awareness
This adds selinux_mls_enabled() and selinux_enabled() to detect a)
whether selinux is MLS aware (ie supports selevel) and b) whether
selinux is enabled.  If selinux is not enabled, all selinux operations
are punted on -- same as if python's selinux module were not available.
In set_context_if_different(), I now iterate over the current context
instead of the context argument.  Even if the system supports MLS, it
may not return the selevel from selinux.lgetfilecon().  Lastly, this
drops selinux_has_selevel() in lieu of the current approach.
2012-04-26 07:31:25 -07:00
30ce430363 Service module crashes if args has no "=". 2012-04-26 10:48:50 -03:00
3594d1ce80 Return the octal mode of a file instead of decimal. 2012-04-26 14:55:31 +02:00
49c3ee9050 Merge pull request #247 from cocoy/devel
Do not override the value of user and hostname.
2012-04-26 05:20:04 -07:00
1220a46e3a Bugfix for issue #245.
Do not override the value of user and hostname.
Get port and identityfile only.
2012-04-26 14:46:32 +08:00
7773a62291 Development branch changelog update 2012-04-25 21:32:08 -04:00
66869c39b0 changelog and readme 2012-04-25 21:32:08 -04:00
acc6c9a9a5 Merge pull request #238 from sfromm/selinux
Fix issue #227
2012-04-25 10:33:04 -07:00
d9ee5db15a Merge pull request #237 from jkleint/devel
Unify normal and sudo remote exeuction.
2012-04-25 10:32:36 -07:00
44486223ed Unify normal and sudo remote command execution.
Commit SHA: 87b1cf45 that put temp files in `$HOME/.ansible` instead of `/home/<user>/.ansible` was producing a directory literally called `$HOME` (no expansion) with non-sudo remote execution.  I'll take the blame for this one, as `ParamikoConnection.exec_command()` was not using the shell for non-sudo commands.  This does sudo and non-sudo execution the same way, using the shell, so environment variables should get expanded.
2012-04-25 13:40:21 -03:00
5c74a524b9 Fix for issue #227
Older versions of selinux, such as that deployed on rhel5, only return a
context of user:role:type instead of user:role:type:level.  This detects
whether the tuple has three elements (old-style) or four.  If the
old-style, it keeps the secontext list at three elements.
2012-04-25 09:39:11 -07:00
bb1d72f2b4 Merge pull request #235 from skvidal/devel
fix for issue #230 - handle template taking 3  args
2012-04-25 09:05:55 -07:00
fa2aebc8a6 fix for issue #230 - handle template taking 3 args 2012-04-25 11:59:19 -04:00
283b897a1f Merge pull request #233 from cocoy/devel
Bugfix for issue #229
2012-04-25 06:17:51 -07:00
7a19a46e24 Bugfix for issue no. 229 2012-04-25 21:02:44 +08:00
87b1cf45a4 Merge pull request #226 from cread/make-osx-tests-pass
Use the $HOME env var instead of hard coding /home/<username>
2012-04-24 15:22:43 -07:00
decc46ad17 Merge pull request #224 from jkleint/devel
Support YAML list of hosts in playbook.
2012-04-24 13:58:18 -07:00
4e1bc43645 Support YAML lists of hosts in playbooks.
Reading the docs, I was a bit confused as to how to specify multiple hosts/groups in a playbook.  Being YAML, I assumed a normal YAML list would work:

    ---
    - hosts: [host1, host2]

But this crashes when inventory._matches() assumes hosts is a string.  This patch just checks if hosts is a list, and turns it into a string joined by ';'.
2012-04-24 17:54:00 -03:00
2999b6b2c4 Merge pull request #223 from jkleint/devel
runner._chain_file_module() calls .get() on error string
2012-04-24 13:52:58 -07:00
dbb4afff50 Use the /Users/cread env var instead of hard coding /home/<username> 2012-04-24 21:45:54 +01:00
b50c50748e _chain_file_module() calls .get() on error string
runner._return_from_module() normally returns a list (?) of `[str,bool,dict,str]`, but on error it returns `[str,bool,str,str]`.  runner._chain_file_module() then tries to call .get() on the third item (`data2`), which fails when it's a string.  This patch only accesses `data2` if the return value was `ok`.  It might be better to return consistent types in both cases, but I'm not sure where/how else the return value is used.
2012-04-24 17:21:01 -03:00
90a8893418 Merge pull request #220 from skvidal/devel
handle issues when the hostlist is inadvertently set executable
2012-04-24 08:19:17 -07:00
730cc89c5b Merge branch 'devel' of git://github.com/ansible/ansible into devel
* 'devel' of git://github.com/ansible/ansible: (2 commits)
  Fix tests for facter and the ansible_facts API.
  ...
2012-04-24 11:15:24 -04:00
5f1bbb1de2 Merge pull request #219 from jhoekx/revert-facts-modules
Ohai and facter modules should not use the ansible_facts API.
2012-04-24 08:05:32 -07:00
41619278e5 handle issues when the hostlist is inadvertently set executable
and/or executing it fails. This produces a nicer error message than
a traceback
2012-04-24 11:03:14 -04:00
cc579a8aa2 Fix tests for facter and the ansible_facts API. 2012-04-24 17:01:48 +02:00
d3d26d0cd6 Ohai and facter modules should not use the ansible_facts API. 2012-04-24 16:57:04 +02:00
02abb5a83b Merge pull request #217 from jhoekx/connection-fixes
Expand user in ssh identity file
2012-04-24 07:40:33 -07:00
46b3dc744e Merge pull request #218 from jhoekx/ansible-facts-fixes
Fix incorrect merge of custom-facts branch.
2012-04-24 07:38:49 -07:00
973b1fe02e Fix incorrect merge of custom-facts branch.
This fixes #216.
2012-04-24 16:11:56 +02:00
c2f1aefaf1 Expand user in SSH identity file. 2012-04-24 15:56:46 +02:00
1804df0bae Whitespace fixes in connection.py 2012-04-24 15:56:04 +02:00
80e6d83660 Fix bug in library/file 2012-04-24 08:10:50 -04:00
66ba31aeb0 Fix bug in library/file 2012-04-24 08:06:32 -04:00
3311763225 Merge pull request #213 from mcodd/devel
bugfix for ssh_config patch
2012-04-24 04:55:08 -07:00
d34160ed26 cast ssh port number as integer 2012-04-24 00:38:24 -04:00
89c013035e Merge branch 'integration' of https://github.com/cocoy/ansible into cocoy-integration 2012-04-23 21:30:44 -04:00
bced4c9db1 Merge branch 'jhoekx-custom-facts' into devel 2012-04-23 21:25:26 -04:00
49cca98f1e Merge branch 'custom-facts' of https://github.com/jhoekx/ansible into jhoekx-custom-facts
Conflicts:
	lib/ansible/runner.py
2012-04-23 21:24:52 -04:00
d60efc235d Merge branch 'jkleint-integration' into devel 2012-04-23 21:22:27 -04:00
346df537b4 Merge branch 'integration' of https://github.com/jkleint/ansible into jkleint-integration 2012-04-23 21:21:43 -04:00
d37a7115a8 Merge branch 'master' into devel 2012-04-23 21:19:10 -04:00
6ba91a715e Update README to indicate new branch name 2012-04-23 21:18:57 -04:00
7de90c4e64 Version bump for integration branch (soon to be renamed 'devel'), didn't update package
changelogs yet since this isn't released yet (but 0.3 is)
2012-04-23 21:14:48 -04:00
e69e078569 More robust remote sudo.
The basic idea is sudo /bin/sh -c 'quoted_command'.  We use Paramiko's low-level API to set a timeout, get a pseudo tty, execute sudo and the (shell quoted) command atomically, wait just until sudo is ready to accept the password before sending it down the pipe, and then return the command's stdout and stderr.

This should be faster, as there are no unneeded sleeps.  There are no permissions issues reading the output.  It will raise socket.timeout if the command takes too long.  However, this is a per-read timeout, not a total execution timeout, so as long as the command is writing output and you are reading it, it will not time out.

Local and non-sudo commands remain unchanged, but should probably adopt a similar approach.

Since this is a significant change, it needs a lot of testing.  Also, someone smarter than I should double-check the quoting and execution, since it is a security issue.
2012-04-23 17:32:08 -03:00
2dc9a563ef Allow modules to return facts.
If the module result contains "ansible_facts", that will be added to the setup
cache.
2012-04-23 21:28:12 +02:00
c844a2d072 Fix to skip /.ssh/config if don't exist rather than raise an error. 2012-04-23 14:32:57 +08:00
645b7a2dff Add .ssh/config support 2012-04-23 09:48:42 +08:00
59 changed files with 2590 additions and 1286 deletions

93
CHANGELOG.md Normal file
View File

@ -0,0 +1,93 @@
Ansible Changes By Release
==========================
0.4 "Unchained" ------- May 23, 2012
Internals/Core
* internal inventory API now more object oriented, parsers decoupled
* async handling improvements
* misc fixes for running ansible on OS X (overlord only)
* sudo improvements, now works much more smoothly
* sudo to a particular user with -U/--sudo-user, or using 'sudo_user: foo' in a playbook
* --private-key CLI option to work with pem files
Inventory
* can use -i host1,host2,host3:port to specify hosts not in inventory (replaces --override-hosts)
* ansible INI style format can do groups of groups [groupname:children] and group vars [groupname:vars]
* groups and users module takes an optional system=yes|no on creation (default no)
* list of hosts in playbooks can be expressed as a YAML list in addition to ; delimited
Playbooks
* variables can be replaced like ${foo.nested_hash_key.nested_subkey[array_index]}
* unicode now ok in templates (assumes utf8)
* able to pass host specifier or group name in to "hosts:" with --extra-vars
* ansible-pull script and example playbook (extreme scaling, remediation)
* inventory_hostname variable available that contains the value of the host as ansible knows it
* variables in the 'all' section can be used to define other variables based on those values
* 'group_names' is now a variable made available to templates
* first_available_file feature, see selective_file_sources.yml in examples/playbooks for info
* --extra-vars="a=2 b=3" etc, now available to inject parameters into playbooks from CLI
Incompatible Changes
* jinja2 is only usable in templates, not playbooks, use $foo instead
* --override-hosts removed, can use -i with comma notation (-i "ahost,bhost")
* modules can no longer include stderr output (paramiko limitation from sudo)
Module Changes
* tweaks to SELinux implementation for file module
* fixes for yum module corner cases on EL5
* file module now correctly returns the mode in octal
* fix for symlink handling in the file module
* service takes an enable=yes|no which works with chkconfig or updates-rc.d as appropriate
* service module works better on Ubuntu
* git module now does resets and such to work more smoothly on updates
* modules all now log to syslog
* enabled=yes|no on a service can be used to toggle chkconfig & updates-rc.d states
* git module supports branch=
* service fixes to better detect status using return codes of the service script
* custom facts provided by the setup module mean no dependency on Ruby, facter, or ohai
* service now has a state=reloaded
* raw module for bootstrapping and talking to routers w/o Python, etc
Misc Bugfixes
* fixes for variable parsing in only_if lines
* misc fixes to key=value parsing
* variables with mixed case now legal
* fix to internals of hacking/test-module development script
0.3 "Baluchitherium" -- April 23, 2012
* Packaging for Debian, Gentoo, and Arch
* Improvements to the apt and yum modules
* A virt module
* SELinux support for the file module
* Ability to use facts from other systems in templates (aka exported
resources like support)
* Built in Ansible facts so you don't need ohai, facter, or Ruby
* tempdir selections that work with noexec mounted /tmp
* templates happen locally, not remotely, so no dependency on
python-jinja2 for remote computers
* advanced inventory format in YAML allows more control over variables
per host and per group
* variables in playbooks can be structured/nested versus just a flat namespace
* manpage upgrades (docs)
* various bugfixes
* can specify a default --user for playbooks rather than specifying it
in the playbook file
* able to specify ansible port in ansible host file (see docs)
* refactored Inventory API to make it easier to write scripts using Ansible
* looping capability for playbooks (with_items)
* support for using sudo with a password
* module arguments can be unicode
* A local connection type, --connection=local, for use with cron or
in kickstarts
* better module debugging with -D
* fetch module for pulling in files from remote hosts
* command task supports creates=foo for idempotent semantics, won't
run if file foo already exists
0.0.2 and 0.0.1
* Initial stages of project

View File

@ -1,4 +1,4 @@
include README.md packaging/rpm/ansible.spec
include README.md packaging/rpm/ansible.spec COPYING
include examples/hosts
include packaging/distutils/setup.py
recursive-include docs *

View File

@ -24,14 +24,23 @@ Get Involved
* [ansible-project mailing list](http://groups.google.com/group/ansible-project)
* irc.freenode.net: #ansible
Branch Info
===========
* The master branch corresponds to release 0.3 "Baluchitherium".
* Bug fixes WILL be periodically made to the master branch.
* The devel branch corresponds to release 0.4 "Unchained".
* Note: sudo support works much better in 0.4/devel, consider trying it.
* See CHANGELOG.md for release notes to track each release.
Patch Instructions
==================
Contributions to the core and modules are greatly welcome.
* Required Process:
* Submit github pull requests to the "ansible/integration" branch
* (This branch will be renamed 'devel' in the very near future)
* Submit github pull requests to the "ansible/devel" branch for features
* Fixes for bugs may be submitted to "ansible/master"
* Make sure "make tests" passes before submitting any requests.
* Bonus points:
* Joining the mailing list
@ -40,7 +49,7 @@ Contributions to the core and modules are greatly welcome.
* Adding more unit tests
* Avoid:
* Sending patches to the mailing list directly.
* Sending pull requests to the 'master' branch instead of the integration branch.
* Sending feature pull requests to the 'master' branch instead of the devel branch
* Sending pull requests to mpdehaan's personal ansible fork.
Author

View File

@ -1 +1 @@
0.3
0.4

View File

@ -21,9 +21,8 @@
import sys
import getpass
import time
import ansible.runner
from ansible.runner import Runner
import ansible.constants as C
from ansible import utils
from ansible import errors
@ -40,7 +39,6 @@ class Cli(object):
def __init__(self):
self.stats = callbacks.AggregateStats()
self.callbacks = callbacks.CliRunnerCallbacks()
self.silent_callbacks = callbacks.DefaultRunnerCallbacks()
# ----------------------------------------------
@ -54,7 +52,6 @@ class Cli(object):
parser.add_option('-m', '--module-name', dest='module_name',
help="module name to execute (default=%s)" % C.DEFAULT_MODULE_NAME,
default=C.DEFAULT_MODULE_NAME)
options, args = parser.parse_args()
self.callbacks.options = options
@ -73,8 +70,8 @@ class Cli(object):
inventory_manager = inventory.Inventory(options.inventory)
hosts = inventory_manager.list_hosts(pattern)
if len(hosts) == 0:
print >>sys.stderr, "No hosts matched"
sys.exit(1)
print >>sys.stderr, "No hosts matched"
sys.exit(1)
sshpass = None
sudopass = None
@ -82,84 +79,46 @@ class Cli(object):
sshpass = getpass.getpass(prompt="SSH password: ")
if options.ask_sudo_pass:
sudopass = getpass.getpass(prompt="sudo password: ")
options.sudo = True
if options.sudo_user:
options.sudo = True
options.sudo_user = options.sudo_user or C.DEFAULT_SUDO_USER
if options.tree:
utils.prepare_writeable_dir(options.tree)
if options.seconds:
print "background launch...\n\n"
runner = ansible.runner.Runner(
runner = Runner(
module_name=options.module_name, module_path=options.module_path,
module_args=options.module_args,
remote_user=options.remote_user, remote_pass=sshpass,
inventory=inventory_manager, timeout=options.timeout,
private_key_file=options.private_key_file,
forks=options.forks,
background=options.seconds, pattern=pattern,
pattern=pattern,
callbacks=self.callbacks, sudo=options.sudo,
sudo_pass=sudopass,
sudo_pass=sudopass,sudo_user=options.sudo_user,
transport=options.connection, debug=options.debug
)
return (runner, runner.run())
if options.seconds:
print "background launch...\n\n"
results, poller = runner.runAsync(options.seconds)
results = self.poll_while_needed(poller, options)
else:
results = runner.run()
# ----------------------------------------------
def get_polling_runner(self, old_runner, jid):
return ansible.runner.Runner(
module_name='async_status', module_path=old_runner.module_path,
module_args="jid=%s" % jid, remote_user=old_runner.remote_user,
remote_pass=old_runner.remote_pass, inventory=old_runner.inventory,
timeout=old_runner.timeout, forks=old_runner.forks,
pattern='*', callbacks=self.silent_callbacks,
)
# ----------------------------------------------
def hosts_to_poll(self, results):
hosts = []
for (host, res) in results['contacted'].iteritems():
if res.get('started',False):
hosts.append(host)
return hosts
return (runner, results)
# ----------------------------------------------
def poll_if_needed(self, runner, results, options, args):
def poll_while_needed(self, poller, options):
''' summarize results from Runner '''
if results is None:
exit("No hosts matched")
# BACKGROUND POLL LOGIC when -B and -P are specified
# FIXME: refactor
if options.seconds and options.poll_interval > 0:
poll_hosts = results['contacted'].keys()
if len(poll_hosts) == 0:
exit("no jobs were launched successfully")
ahost = poll_hosts[0]
jid = results['contacted'][ahost].get('ansible_job_id', None)
if jid is None:
exit("unexpected error: unable to determine jid")
poller.wait(options.seconds, options.poll_interval)
return poller.results
clock = options.seconds
while (clock >= 0):
runner.inventory.restrict_to(poll_hosts)
polling_runner = self.get_polling_runner(runner, jid)
poll_results = polling_runner.run()
runner.inventory.lift_restriction()
if poll_results is None:
break
for (host, host_result) in poll_results['contacted'].iteritems():
# override last result with current status result for report
results['contacted'][host] = host_result
print utils.async_poll_status(jid, host, clock, host_result)
for (host, host_result) in poll_results['dark'].iteritems():
print "FAILED: %s => %s" % (host, host_result)
clock = clock - options.poll_interval
time.sleep(options.poll_interval)
poll_hosts = self.hosts_to_poll(poll_results)
if len(poll_hosts)==0:
break
########################################################
@ -172,6 +131,4 @@ if __name__ == '__main__':
# Generic handler for ansible specific errors
print "ERROR: %s" % str(e)
sys.exit(1)
else:
cli.poll_if_needed(runner, results, options, args)

View File

@ -33,8 +33,8 @@ def main(args):
# create parser for CLI options
usage = "%prog playbook.yml"
parser = utils.base_parser(constants=C, usage=usage, connect_opts=True, runas_opts=True)
parser.add_option('-O', '--override-hosts', dest="override_hosts", default=None,
help="run playbook against these hosts regardless of inventory settings")
parser.add_option('-e', '--extra-vars', dest="extra_vars", default=None,
help="set additional key=value variables from the CLI")
options, args = parser.parse_args(args)
@ -48,9 +48,11 @@ def main(args):
sshpass = getpass.getpass(prompt="SSH password: ")
if options.ask_sudo_pass:
sudopass = getpass.getpass(prompt="sudo password: ")
override_hosts = None
if options.override_hosts:
override_hosts = options.override_hosts.split(",")
options.sudo = True
if options.sudo_user:
options.sudo = True
options.sudo_user = options.sudo_user or C.DEFAULT_SUDO_USER
extra_vars = utils.parse_kv(options.extra_vars)
# run all playbooks specified on the command line
for playbook in args:
@ -63,7 +65,6 @@ def main(args):
playbook=playbook,
module_path=options.module_path,
host_list=options.inventory,
override_hosts=override_hosts,
forks=options.forks,
debug=options.debug,
remote_user=options.remote_user,
@ -74,7 +75,10 @@ def main(args):
timeout=options.timeout,
transport=options.connection,
sudo=options.sudo,
sudo_pass=sudopass
sudo_user=options.sudo_user,
sudo_pass=sudopass,
extra_vars=extra_vars,
private_key_file=options.private_key_file
)
try:

75
bin/ansible-pull Executable file
View File

@ -0,0 +1,75 @@
#!/usr/bin/env python
# ansible-pull is a script that runs ansible in local mode
# after checking out a playbooks directory from git. There is an
# example playbook to bootstrap this script in the examples/ dir which
# installs ansible and sets it up to run on cron.
#
# usage:
# ansible-pull -d /var/ansible/local -U http://wherever/content.git -C production
#
# the git repo must contain a playbook named 'local.yml'
# (c) 2012, Stephen Fromm <sfromm@gmail.com>
#
# Ansible is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Ansible is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
import os
import subprocess
import sys
from optparse import OptionParser
DEFAULT_PLAYBOOK = 'local.yml'
def _run(cmd):
cmd = subprocess.Popen(cmd, shell=True,
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
(out, err) = cmd.communicate()
print out
if cmd.returncode != 0:
print err
return cmd.returncode
def main(args):
""" Set up and run a local playbook """
usage = "%prog [options]"
parser = OptionParser()
parser.add_option('-d', '--directory', dest='dest', default=None,
help='Directory to checkout git repository')
parser.add_option('-U', '--url', dest='url',
default=None,
help='URL of git repository')
parser.add_option('-C', '--checkout', dest='checkout',
default="HEAD",
help='Branch/Tag/Commit to checkout. Defaults to HEAD.')
options, args = parser.parse_args(args)
git_opts = "repo=%s dest=%s version=%s" % (options.url, options.dest, options.checkout)
cmd = 'ansible all -c local -m git -a "%s"' % git_opts
print "cmd=%s" % cmd
rc = _run(cmd)
if rc != 0:
return rc
os.chdir(options.dest)
cmd = 'ansible-playbook -c local %s' % DEFAULT_PLAYBOOK
rc = _run(cmd)
return rc
if __name__ == '__main__':
try:
sys.exit(main(sys.argv[1:]))
except KeyboardInterrupt, e:
print >>sys.stderr, "Exit on user request.\n"
sys.exit(1)

View File

@ -1,22 +1,13 @@
'\" t
.\" Title: ansible-playbook
.\" Author: [see the "AUTHOR" section]
.\" Generator: DocBook XSL Stylesheets v1.76.1 <http://docbook.sf.net/>
.\" Date: 04/17/2012
.\" Generator: DocBook XSL Stylesheets v1.75.2 <http://docbook.sf.net/>
.\" Date: 04/26/2012
.\" Manual: System administration commands
.\" Source: Ansible 0.0.2
.\" Source: Ansible 0.4
.\" Language: English
.\"
.TH "ANSIBLE\-PLAYBOOK" "1" "04/17/2012" "Ansible 0\&.0\&.2" "System administration commands"
.\" -----------------------------------------------------------------
.\" * Define some portability stuff
.\" -----------------------------------------------------------------
.\" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.\" http://bugs.debian.org/507673
.\" http://lists.gnu.org/archive/html/groff/2009-02/msg00013.html
.\" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.ie \n(.g .ds Aq \(aq
.el .ds Aq '
.TH "ANSIBLE\-PLAYBOOK" "1" "04/26/2012" "Ansible 0\&.4" "System administration commands"
.\" -----------------------------------------------------------------
.\" * set default formatting
.\" -----------------------------------------------------------------
@ -63,6 +54,11 @@ to load modules from\&. The default is
\fI/usr/share/ansible\fR\&.
.RE
.PP
\fB\-e\fR \fIVARS\fR, \fB\-\-extra\-vars=\fR\fIVARS\fR
.RS 4
Extra variables to inject into a playbook, in key=value key=value format\&.
.RE
.PP
\fB\-f\fR \fINUM\fR, \fB\-\-forks=\fR\fINUM\fR
.RS 4
Level of parallelism\&.

View File

@ -36,8 +36,7 @@ OPTIONS
*-D*, *--debug*
Print any messages the remote module sends to standard error to the console
Debug mode
*-i* 'PATH', *--inventory=*'PATH'::
@ -48,6 +47,9 @@ The 'PATH' to the inventory hosts file, which defaults to '/etc/ansible/hosts'.
The 'DIRECTORY' to load modules from. The default is '/usr/share/ansible'.
*-e* 'VARS', *--extra-vars=*'VARS'::
Extra variables to inject into a playbook, in key=value key=value format.
*-f* 'NUM', *--forks=*'NUM'::
@ -69,12 +71,6 @@ Prompt for the password to use for playbook plays that request sudo access, if a
Connection timeout to use when trying to talk to hosts, in 'SECONDS'.
*-O* 'OVERRIDE_HOSTS', *--override-hosts=*'OVERRIDE_HOSTS'::
Ignore the inventory file and run the playbook against only these hosts. "hosts:" line
in playbook should be set to 'all' when using this option.
*-s*, *--sudo*::
Force all plays to use sudo, even if not marked as such.

View File

@ -1,22 +1,13 @@
'\" t
.\" Title: ansible
.\" Author: [see the "AUTHOR" section]
.\" Generator: DocBook XSL Stylesheets v1.76.1 <http://docbook.sf.net/>
.\" Date: 04/17/2012
.\" Generator: DocBook XSL Stylesheets v1.75.2 <http://docbook.sf.net/>
.\" Date: 05/06/2012
.\" Manual: System administration commands
.\" Source: Ansible 0.0.2
.\" Source: Ansible 0.4
.\" Language: English
.\"
.TH "ANSIBLE" "1" "04/17/2012" "Ansible 0\&.0\&.2" "System administration commands"
.\" -----------------------------------------------------------------
.\" * Define some portability stuff
.\" -----------------------------------------------------------------
.\" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.\" http://bugs.debian.org/507673
.\" http://lists.gnu.org/archive/html/groff/2009-02/msg00013.html
.\" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.ie \n(.g .ds Aq \(aq
.el .ds Aq '
.TH "ANSIBLE" "1" "05/06/2012" "Ansible 0\&.4" "System administration commands"
.\" -----------------------------------------------------------------
.\" * set default formatting
.\" -----------------------------------------------------------------
@ -34,7 +25,7 @@ ansible \- run a command somewhere else
ansible <host\-pattern> [\-f forks] [\-m module_name] [\-a args]
.SH "DESCRIPTION"
.sp
\fBAnsible\fR is an extra\-simple tool/framework/API for doing \*(Aqremote things\*(Aq over SSH\&.
\fBAnsible\fR is an extra\-simple tool/framework/API for doing \'remote things\' over SSH\&.
.SH "ARGUMENTS"
.PP
\fBhost\-pattern\fR
@ -72,7 +63,7 @@ to load modules from\&. The default is
\fI/usr/share/ansible\fR\&.
.RE
.PP
\fB\-a\fR \*(Aq\fIARGUMENTS\fR\*(Aq, \fB\-\-args=\fR\*(Aq\fIARGUMENTS\fR\*(Aq
\fB\-a\fR \'\fIARGUMENTS\fR\', \fB\-\-args=\fR\'\fIARGUMENTS\fR\'
.RS 4
The
\fIARGUMENTS\fR
@ -81,7 +72,7 @@ to pass to the module\&.
.PP
\fB\-D\fR, \fB\-\-debug\fR
.RS 4
Print any messages the remote module sends to standard error to the console
Debug mode
.RE
.PP
\fB\-k\fR, \fB\-\-ask\-pass\fR
@ -138,6 +129,13 @@ Use this remote
instead of root\&.
.RE
.PP
\fB\-U\fR \fISUDO_USERNAME\fR, \fB\-\-sudo\-user=\fR\fISUDO_USERNAME\fR
.RS 4
Sudo to
\fISUDO_USERNAME\fR
instead of root\&. Implies \-\-sudo\&.
.RE
.PP
\fB\-c\fR \fICONNECTION\fR, \fB\-\-connection=\fR\fICONNECTION\fR
.RS 4
Connection type to use\&. Possible options are

View File

@ -62,7 +62,7 @@ The 'ARGUMENTS' to pass to the module.
*-D*, *--debug*::
Print any messages the remote module sends to standard error to the console
Debug mode
*-k*, *--ask-pass*::
@ -101,6 +101,10 @@ Poll a background job every 'NUM' seconds. Requires *-B*.
Use this remote 'USERNAME' instead of root.
*-U* 'SUDO_USERNAME', *--sudo-user=*'SUDO_USERNAME'::
Sudo to 'SUDO_USERNAME' instead of root. Implies --sudo.
*-c* 'CONNECTION', *--connection=*'CONNECTION'::
Connection type to use. Possible options are 'paramiko' (SSH) and 'local'.

View File

@ -0,0 +1,51 @@
# ansibple-pull setup
#
# on remote hosts, set up ansible to run periodically using the latest code
# from a particular checkout, in pull based fashion, inverting Ansible's
# usual push-based operating mode.
#
# This particular pull based mode is ideal for:
#
# (A) massive scale out
# (B) continual system remediation
#
# DO NOT RUN THIS AGAINST YOUR HOSTS WITHOUT CHANGING THE repo_url
# TO SOMETHING YOU HAVE PERSONALLY VERIFIED
#
#
---
- hosts: pull_mode_hosts
user: root
vars:
# schdule is fed directly to cron
schedule: '*/15 * * * *'
# User to run ansible-pull as from cron
cron_user: root
# Directory to where repository will be cloned
workdir: /var/lib/ansible/local
# Repository to check out -- YOU MUST CHANGE THIS
# repo must contain a local.yml file at top level
#repo_url: git://github.com/sfromm/ansible-playbooks.git
repo_url: SUPPLY_YOUR_OWN_GIT_URL_HERE
tasks:
- name: Install ansible
action: yum pkg=ansible state=installed
- name: Create local directory to work from
action: file path=$workdir state=directory owner=root group=root mode=0751
- name: Copy ansible inventory file to client
action: copy src=/etc/ansible/hosts dest=/etc/ansible/hosts
owner=root group=root mode=0644
- name: Create crontab entry to clone/pull git repository
action: template src=templates/ansible-pull.j2 dest=/etc/cron.d/ansible-pull owner=root group=root mode=0644

View File

@ -3,6 +3,7 @@
- hosts: all
user: root
sudo: True
tasks:

View File

@ -34,7 +34,7 @@
# we could also have done something like:
# - include: wordpress.yml user=timmy
# and had access to the template variable {{ user }} in the
# and had access to the template variable $user in the
# included file, if we wanted to. Variables from vars
# and vars_files are also available inside include files

View File

@ -7,7 +7,8 @@
# on all hosts, run as the user root...
- hosts: all
- name: example play
hosts: all
user: root
# could have also have done:

View File

@ -0,0 +1,28 @@
---
# this is an example of how to template a file over using some variables derived
# from the system. For instance, if you wanted to have different configuration
# templates by OS version, this is a neat way to do it. Any Ansible facts, facter facts,
# or ohai facts could be used to do this.
- hosts: all
tasks:
- name: template a config file
action: template dest=/etc/imaginary_file.conf
first_available_file:
# first see if we have a file for this specific host
- /srv/whatever/${ansible_hostname}.conf
# next try to load something like CentOS6.2.conf
- /srv/whatever/${ansible_distribution}${ansible_distribution_version}.conf
# next see if there's a CentOS.conf
- /srv/whatever/${ansible_distribution}.conf
# finally give up and just use something generic
- /srv/whatever/default

View File

@ -0,0 +1,2 @@
# Cron job to git clone/pull a repo and then run locally
{{ schedule }} {{ cron_user }} ansible-pull -d {{ workdir }} -U {{ repo_url }} >/var/log/ansible-pull.log 2>&1

View File

@ -49,7 +49,7 @@ if len(sys.argv) > 1:
else:
args = ""
argspath = os.path.expanduser("/.ansible_test_module_arguments")
argspath = os.path.expanduser("~/.ansible_test_module_arguments")
argsfile = open(argspath, 'w')
argsfile.write(args)
argsfile.close()
@ -63,8 +63,9 @@ cmd = subprocess.Popen("%s %s" % (modfile, argspath),
if err and err != '':
print "***********************************"
print "RECIEVED DATA ON STDOUT, WILL IGNORE THIS:"
print "RECIEVED DATA ON STDERR, THIS WILL CRASH YOUR MODULE"
print err
sys.exit(1)
try:
print "***********************************"
@ -87,3 +88,4 @@ print utils.bigjson(results)
sys.exit(0)

View File

@ -14,5 +14,5 @@
#
# You should have received a copy of the GNU General Public License
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
__version__ = '0.3'
__version__ = '0.4'
__author__ = 'Michael DeHaan'

View File

@ -50,7 +50,7 @@ class AggregateStats(object):
elif 'skipped' in value and bool(value['skipped']):
self._increment('skipped', host)
elif 'changed' in value and bool(value['changed']):
if not setup:
if not setup and not poll:
self._increment('changed', host)
self._increment('ok', host)
else:
@ -98,6 +98,15 @@ class DefaultRunnerCallbacks(object):
def on_no_hosts(self):
pass
def on_async_poll(self, host, res, jid, clock):
pass
def on_async_ok(self, host, res, jid):
pass
def on_async_failed(self, host, res, jid):
pass
########################################################################
class CliRunnerCallbacks(DefaultRunnerCallbacks):
@ -106,12 +115,17 @@ class CliRunnerCallbacks(DefaultRunnerCallbacks):
def __init__(self):
# set by /usr/bin/ansible later
self.options = None
self._async_notified = {}
def on_failed(self, host, res):
self._on_any(host,res)
invocation = res.get('invocation','')
if not invocation.startswith('async_status'):
self._on_any(host,res)
def on_ok(self, host, res):
self._on_any(host,res)
invocation = res.get('invocation','')
if not invocation.startswith('async_status'):
self._on_any(host,res)
def on_unreachable(self, host, res):
print "%s | FAILED => %s" % (host, res)
@ -122,11 +136,24 @@ class CliRunnerCallbacks(DefaultRunnerCallbacks):
pass
def on_error(self, host, err):
print >>sys.stderr, "stderr: [%s] => %s\n" % (host, err)
print >>sys.stderr, "err: [%s] => %s\n" % (host, err)
def on_no_hosts(self):
print >>sys.stderr, "no hosts matched\n"
def on_async_poll(self, host, res, jid, clock):
if jid not in self._async_notified:
self._async_notified[jid] = clock + 1
if self._async_notified[jid] > clock:
self._async_notified[jid] = clock
print "<job %s> polling, %ss remaining"%(jid, clock)
def on_async_ok(self, host, res, jid):
print "<job %s> finished on %s => %s"%(jid, host, utils.bigjson(res))
def on_async_failed(self, host, res, jid):
print "<job %s> FAILED on %s => %s"%(jid, host, utils.bigjson(res))
def _on_any(self, host, result):
print utils.host_report_msg(host, self.options.module_name, result, self.options.one_line)
if self.options.tree:
@ -139,9 +166,10 @@ class PlaybookRunnerCallbacks(DefaultRunnerCallbacks):
def __init__(self, stats):
self.stats = stats
self._async_notified = {}
def on_unreachable(self, host, msg):
print "unreachable: [%s] => %s" % (host, msg)
print "fatal: [%s] => %s" % (host, msg)
def on_failed(self, host, results):
invocation = results.get('invocation',None)
@ -160,7 +188,7 @@ class PlaybookRunnerCallbacks(DefaultRunnerCallbacks):
print "ok: [%s] => %s\n" % (host, invocation)
def on_error(self, host, err):
print >>sys.stderr, "stderr: [%s] => %s\n" % (host, err)
print >>sys.stderr, "err: [%s] => %s\n" % (host, err)
def on_skipped(self, host):
print "skipping: [%s]\n" % host
@ -168,6 +196,19 @@ class PlaybookRunnerCallbacks(DefaultRunnerCallbacks):
def on_no_hosts(self):
print "no hosts matched or remaining\n"
def on_async_poll(self, host, res, jid, clock):
if jid not in self._async_notified:
self._async_notified[jid] = clock + 1
if self._async_notified[jid] > clock:
self._async_notified[jid] = clock
print "<job %s> polling, %ss remaining"%(jid, clock)
def on_async_ok(self, host, res, jid):
print "<job %s> finished on %s"%(jid, host)
def on_async_failed(self, host, res, jid):
print "<job %s> FAILED on %s"%(jid, host)
########################################################################
class PlaybookCallbacks(object):
@ -205,10 +246,3 @@ class PlaybookCallbacks(object):
def on_play_start(self, pattern):
print "PLAY [%s] ****************************\n" % pattern
def on_async_confused(self, msg):
print msg
def on_async_poll(self, jid, host, clock, host_result):
print utils.async_poll_status(jid, host, clock, host_result)

View File

@ -19,20 +19,23 @@
################################################
import warnings
import traceback
import os
import time
import re
import shutil
import subprocess
import pipes
import socket
import random
from ansible import errors
# prevent paramiko warning noise
# see http://stackoverflow.com/questions/3920502/
with warnings.catch_warnings():
warnings.simplefilter("ignore")
import paramiko
import traceback
import os
import time
import random
import re
import shutil
import subprocess
from ansible import errors
################################################
@ -41,14 +44,14 @@ class Connection(object):
_LOCALHOSTRE = re.compile(r"^(127.0.0.1|localhost|%s)$" % os.uname()[1])
def __init__(self, runner, transport):
def __init__(self, runner, transport,sudo_user):
self.runner = runner
self.transport = transport
self.sudo_user = sudo_user
def connect(self, host, port=None):
conn = None
if self.transport == 'local' and self._LOCALHOSTRE.search(host):
conn = LocalConnection(self.runner, host, None)
conn = LocalConnection(self.runner, host)
elif self.transport == 'paramiko':
conn = ParamikoConnection(self.runner, host, port)
if conn is None:
@ -73,17 +76,20 @@ class ParamikoConnection(object):
self.port = self.runner.remote_port
def _get_conn(self):
user = self.runner.remote_user
ssh = paramiko.SSHClient()
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
try:
ssh.connect(
self.host,
username=self.runner.remote_user,
allow_agent=True,
look_for_keys=True,
self.host,
username=user,
allow_agent=True,
look_for_keys=True,
key_filename=self.runner.private_key_file,
password=self.runner.remote_pass,
timeout=self.runner.timeout,
timeout=self.runner.timeout,
port=self.port
)
except Exception, e:
@ -94,65 +100,52 @@ class ParamikoConnection(object):
return ssh
def connect(self):
''' connect to the remote host '''
self.ssh = self._get_conn()
return self
def exec_command(self, cmd, tmp_path, sudoable=False):
def exec_command(self, cmd, tmp_path,sudo_user,sudoable=False):
''' run a command on the remote host '''
if not self.runner.sudo or not sudoable:
stdin, stdout, stderr = self.ssh.exec_command(cmd)
return (stdin, stdout, stderr)
bufsize = 4096
chan = self.ssh.get_transport().open_session()
chan.get_pty()
if not self.runner.sudo or not sudoable:
quoted_command = '"$SHELL" -c ' + pipes.quote(cmd)
chan.exec_command(quoted_command)
else:
# percalculated tmp_path is ONLY required for sudo usage
if tmp_path is None:
raise Exception("expecting tmp_path")
r = random.randint(0,99999)
# invoke command using a new connection over sudo
result_file = os.path.join(tmp_path, "sudo_result.%s" % r)
self.ssh.close()
ssh_sudo = self._get_conn()
sudo_chan = ssh_sudo.invoke_shell()
sudo_chan.send("sudo -s\n")
# Rather than detect if sudo wants a password this time, -k makes
# sudo always ask for a password if one is required. The "--"
# tells sudo that this is the end of sudo options and the command
# follows. Passing a quoted compound command to sudo (or sudo -s)
# directly doesn't work, so we shellquote it with pipes.quote()
# and pass the quoted string to the user's shell. We loop reading
# output until we see the randomly-generated sudo prompt set with
# the -p option.
randbits = ''.join(chr(random.randint(ord('a'), ord('z'))) for x in xrange(32))
prompt = '[sudo via ansible, key=%s] password: ' % randbits
sudocmd = 'sudo -k -p "%s" -u %s -- "$SHELL" -c %s' % (prompt,
sudo_user, pipes.quote(cmd))
sudo_output = ''
try:
chan.exec_command(sudocmd)
if self.runner.sudo_pass:
while not sudo_output.endswith(prompt):
chunk = chan.recv(bufsize)
if not chunk:
raise errors.AnsibleError('ssh connection closed waiting for sudo password prompt')
sudo_output += chunk
chan.sendall(self.runner.sudo_pass + '\n')
except socket.timeout:
raise errors.AnsibleError('ssh timed out waiting for sudo.\n' + sudo_output)
# FIXME: using sudo with a password adds more delay, someone may wish
# to optimize to see when the channel is actually ready
if self.runner.sudo_pass:
time.sleep(0.1) # this is conservative
sudo_chan.send("%s\n" % self.runner.sudo_pass)
time.sleep(0.1)
# to avoid ssh expect logic, redirect output to file and move the
# file when we are done with it...
sudo_chan.send("(%s >%s_pre 2>/dev/null ; mv %s_pre %s) &\n" % (cmd, result_file, result_file, result_file))
# FIXME: someone may wish to optimize to not background the launch, and tell when the command
# returns, removing the time.sleep(1) here
time.sleep(1)
sudo_chan.close()
self.ssh = self._get_conn()
# now load the results of the JSON execution...
# FIXME: really need some timeout logic here
# though it doesn't make since to use the SSH timeout or impose any particular
# limit. Upgrades welcome.
sftp = self.ssh.open_sftp()
while True:
# print "waiting on %s" % result_file
time.sleep(1)
try:
sftp.stat(result_file)
break
except IOError:
pass
sftp.close()
# TODO: see if there's a SFTP way to just get the file contents w/o saving
# to disk vs this hack...
stdin, stdout, stderr = self.ssh.exec_command("cat %s" % result_file)
return (stdin, stdout, stderr)
stdin = chan.makefile('wb', bufsize)
stdout = chan.makefile('rb', bufsize)
stderr = '' # stderr goes to stdout when using a pty, so this will never output anything.
return stdin, stdout, stderr
def put_file(self, in_path, out_path):
''' transfer a file from local to remote '''
@ -195,7 +188,7 @@ class LocalConnection(object):
return self
def exec_command(self, cmd, tmp_path, sudoable=False):
def exec_command(self, cmd, tmp_path,sudo_user,sudoable=False):
''' run a command on the local host '''
if self.runner.sudo and sudoable:
cmd = "sudo -s %s" % cmd
@ -231,4 +224,3 @@ class LocalConnection(object):
''' terminate the connection; nothing to do here '''
pass

View File

@ -23,6 +23,8 @@ DEFAULT_HOST_LIST = os.environ.get('ANSIBLE_HOSTS',
'/etc/ansible/hosts')
DEFAULT_MODULE_PATH = os.environ.get('ANSIBLE_LIBRARY',
'/usr/share/ansible')
DEFAULT_REMOTE_TMP = os.environ.get('ANSIBLE_REMOTE_TMP',
'/$HOME/.ansible/tmp')
DEFAULT_MODULE_NAME = 'command'
DEFAULT_PATTERN = '*'
@ -32,7 +34,9 @@ DEFAULT_TIMEOUT = 10
DEFAULT_POLL_INTERVAL = 15
DEFAULT_REMOTE_USER = 'root'
DEFAULT_REMOTE_PASS = None
DEFAULT_PRIVATE_KEY_FILE = None
DEFAULT_SUDO_PASS = None
DEFAULT_SUDO_USER = 'root'
DEFAULT_REMOTE_PORT = 22
DEFAULT_TRANSPORT = 'paramiko'
DEFAULT_TRANSPORT_OPTS = ['local', 'paramiko']

76
lib/ansible/group.py Normal file
View File

@ -0,0 +1,76 @@
# (c) 2012, Michael DeHaan <michael.dehaan@gmail.com>
#
# This file is part of Ansible
#
# Ansible is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Ansible is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
#############################################
# from ansible import errors
class Group(object):
"""
Group of ansible hosts
"""
def __init__(self, name=None):
self.name = name
self.hosts = []
self.vars = {}
self.child_groups = []
self.parent_groups = []
if self.name is None:
raise Exception("group name is required")
def add_child_group(self, group):
if self == group:
raise Exception("can't add group to itself")
self.child_groups.append(group)
group.parent_groups.append(self)
def add_host(self, host):
self.hosts.append(host)
host.add_group(self)
def set_variable(self, key, value):
self.vars[key] = value
def get_hosts(self):
hosts = []
for kid in self.child_groups:
hosts.extend(kid.get_hosts())
hosts.extend(self.hosts)
return hosts
def get_variables(self):
vars = {}
# FIXME: verify this variable override order is what we want
for ancestor in self.get_ancestors():
vars.update(ancestor.get_variables())
vars.update(self.vars)
return vars
def _get_ancestors(self):
results = {}
for g in self.parent_groups:
results[g.name] = g
results.update(g._get_ancestors())
return results
def get_ancestors(self):
return self._get_ancestors().values()

62
lib/ansible/host.py Normal file
View File

@ -0,0 +1,62 @@
# (c) 2012, Michael DeHaan <michael.dehaan@gmail.com>
#
# This file is part of Ansible
#
# Ansible is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Ansible is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
#############################################
from ansible import errors
import ansible.constants as C
class Host(object):
"""
Group of ansible hosts
"""
def __init__(self, name=None, port=None):
self.name = name
self.vars = {}
self.groups = []
if port and port != C.DEFAULT_REMOTE_PORT:
self.set_variable('ansible_ssh_port', int(port))
if self.name is None:
raise Exception("host name is required")
def add_group(self, group):
self.groups.append(group)
def set_variable(self, key, value):
self.vars[key]=value;
def get_groups(self):
groups = {}
for g in self.groups:
groups[g.name] = g
ancestors = g.get_ancestors()
for a in ancestors:
groups[a.name] = a
return groups.values()
def get_variables(self):
results = {}
for group in self.groups:
results.update(group.get_variables())
results.update(self.vars)
results['inventory_hostname'] = self.name
groups = self.get_groups()
results['group_names'] = sorted([ g.name for g in groups if g.name != 'all'])
return results

View File

@ -19,274 +19,146 @@
import fnmatch
import os
import subprocess
import constants as C
import subprocess
from ansible.inventory_parser import InventoryParser
from ansible.inventory_parser_yaml import InventoryParserYaml
from ansible.inventory_script import InventoryScript
from ansible.group import Group
from ansible.host import Host
from ansible import errors
from ansible import utils
class Inventory(object):
""" Host inventory for ansible.
The inventory is either a simple text file with systems and [groups] of
systems, or a script that will be called with --list or --host.
"""
Host inventory for ansible.
"""
def __init__(self, host_list=C.DEFAULT_HOST_LIST):
# the host file file, or script path, or list of hosts
# if a list, inventory data will NOT be loaded
self.host_list = host_list
# the inventory object holds a list of groups
self.groups = []
# a list of host(names) to contain current inquiries to
self._restriction = None
self._variables = {}
# whether the inventory file is a script
self._is_script = False
if type(host_list) in [ str, unicode ]:
if host_list.find(",") != -1:
host_list = host_list.split(",")
if type(host_list) == list:
self.host_list = host_list
self.groups = dict(ungrouped=host_list)
self._is_script = False
return
inventory_file = os.path.expanduser(host_list)
if not os.path.exists(inventory_file):
raise errors.AnsibleFileNotFound("inventory file not found: %s" % host_list)
self.inventory_file = os.path.abspath(inventory_file)
if os.access(self.inventory_file, os.X_OK):
self.host_list, self.groups = self._parse_from_script()
all = Group('all')
self.groups = [ all ]
for x in host_list:
if x.find(":") != -1:
tokens = x.split(":",1)
all.add_host(Host(tokens[0], tokens[1]))
else:
all.add_host(Host(x))
elif os.access(host_list, os.X_OK):
self._is_script = True
self.parser = InventoryScript(filename=host_list)
self.groups = self.parser.groups.values()
else:
self.host_list, self.groups = self._parse_from_file()
self._is_script = False
data = file(host_list).read()
if not data.startswith("---"):
self.parser = InventoryParser(filename=host_list)
self.groups = self.parser.groups.values()
else:
self.parser = InventoryParserYaml(filename=host_list)
self.groups = self.parser.groups.values()
def _match(self, str, pattern_str):
return fnmatch.fnmatch(str, pattern_str)
# *****************************************************
# Public API
def get_hosts(self, pattern="all"):
""" Get all host objects matching the pattern """
hosts = {}
patterns = pattern.replace(";",":").split(":")
groups = self.get_groups()
for group in groups:
for host in group.get_hosts():
for pat in patterns:
if group.name == pat or pat == 'all' or self._match(host.name, pat):
if not self._restriction:
hosts[host.name] = host
if self._restriction and host.name in self._restriction:
hosts[host.name] = host
return sorted(hosts.values(), key=lambda x: x.name)
def get_groups(self):
return self.groups
def get_host(self, hostname):
for group in self.groups:
for host in group.get_hosts():
if hostname == host.name:
return host
return None
def get_group(self, groupname):
for group in self.groups:
if group.name == groupname:
return group
return None
def get_group_variables(self, groupname):
group = self.get_group(groupname)
if group is None:
raise Exception("group not found: %s" % groupname)
return group.get_variables()
def get_variables(self, hostname):
if self._is_script:
# TODO: move this to inventory_script.py
host = self.get_host(hostname)
cmd = subprocess.Popen(
[self.host_list,"--host",hostname],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE
)
(out, err) = cmd.communicate()
results = utils.parse_json(out)
results['inventory_hostname'] = hostname
groups = [ g.name for g in host.get_groups() if g.name != 'all' ]
results['group_names'] = sorted(groups)
return results
host = self.get_host(hostname)
if host is None:
raise Exception("host not found: %s" % hostname)
return host.get_variables()
def add_group(self, group):
self.groups.append(group)
def list_hosts(self, pattern="all"):
""" Return a list of hosts [matching the pattern] """
if self._restriction is None:
host_list = self.host_list
else:
host_list = [ h for h in self.host_list if h in self._restriction ]
return [ h for h in host_list if self._matches(h, pattern) ]
return [ h.name for h in self.get_hosts(pattern) ]
def restrict_to(self, restriction):
def list_groups(self):
return [ g.name for g in self.groups ]
def get_restriction(self):
return self._restriction
def restrict_to(self, restriction, append_missing=False):
""" Restrict list operations to the hosts given in restriction """
if type(restriction)!=list:
restriction = [ restriction ]
self._restriction = restriction
if type(restriction) != list:
restriction = [ restriction ]
self._restriction = restriction
def lift_restriction(self):
""" Do not restrict list operations """
self._restriction = None
def get_variables(self, host):
""" Return the variables associated with this host. """
if host in self._variables:
return self._variables[host].copy()
if not self._is_script:
return {}
return self._get_variables_from_script(host)
# *****************************************************
def _parse_from_file(self):
''' parse a textual host file '''
results = []
groups = dict(ungrouped=[])
lines = file(self.inventory_file).read().split("\n")
if "---" in lines:
return self._parse_yaml()
group_name = 'ungrouped'
for item in lines:
item = item.lstrip().rstrip()
if item.startswith("#"):
# ignore commented out lines
pass
elif item.startswith("["):
# looks like a group
group_name = item.replace("[","").replace("]","").lstrip().rstrip()
groups[group_name] = []
elif item != "":
# looks like a regular host
if ":" in item:
# a port was specified
item, port = item.split(":")
try:
port = int(port)
except ValueError:
raise errors.AnsibleError("SSH port for %s in inventory (%s) should be numerical."%(item, port))
self._set_variable(item, "ansible_ssh_port", port)
groups[group_name].append(item)
if not item in results:
results.append(item)
return (results, groups)
# *****************************************************
def _parse_from_script(self):
''' evaluate a script that returns list of hosts by groups '''
results = []
groups = dict(ungrouped=[])
cmd = [self.inventory_file, '--list']
cmd = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=False)
out, err = cmd.communicate()
rc = cmd.returncode
if rc:
raise errors.AnsibleError("%s: %s" % self.inventory_file, err)
try:
groups = utils.json_loads(out)
except:
raise errors.AnsibleError("invalid JSON response from script: %s" % self.inventory_file)
for (groupname, hostlist) in groups.iteritems():
for host in hostlist:
if host not in results:
results.append(host)
return (results, groups)
# *****************************************************
def _parse_yaml(self):
""" Load the inventory from a yaml file.
returns hosts and groups"""
data = utils.parse_yaml_from_file(self.inventory_file)
if type(data) != list:
raise errors.AnsibleError("YAML inventory should be a list.")
hosts = []
groups = {}
ungrouped = []
for item in data:
if type(item) == dict:
if "group" in item:
group_name = item["group"]
group_vars = []
if "vars" in item:
group_vars = item["vars"]
group_hosts = []
if "hosts" in item:
for host in item["hosts"]:
host_name = self._parse_yaml_host(host, group_vars)
group_hosts.append(host_name)
groups[group_name] = group_hosts
hosts.extend(group_hosts)
elif "host" in item:
host_name = self._parse_yaml_host(item)
hosts.append(host_name)
ungrouped.append(host_name)
else:
host_name = self._parse_yaml_host(item)
hosts.append(host_name)
ungrouped.append(host_name)
# filter duplicate hosts
output_hosts = []
for host in hosts:
if host not in output_hosts:
output_hosts.append(host)
if len(ungrouped) > 0 :
# hosts can be defined top-level, but also in a group
really_ungrouped = []
for host in ungrouped:
already_grouped = False
for name, group_hosts in groups.items():
if host in group_hosts:
already_grouped = True
if not already_grouped:
really_ungrouped.append(host)
groups["ungrouped"] = really_ungrouped
return output_hosts, groups
def _parse_yaml_host(self, item, variables=[]):
def set_variables(host, variables):
if type(variables) == list:
for variable in variables:
if len(variable) != 1:
raise errors.AnsibleError("Only one item expected in %s"%(variable))
k, v = variable.items()[0]
self._set_variable(host, k, v)
elif type(variables) == dict:
for k, v in variables.iteritems():
self._set_variable(host, k, v)
if type(item) in [str, unicode]:
set_variables(item, variables)
return item
elif type(item) == dict:
if "host" in item:
host_name = item["host"]
set_variables(host_name, variables)
if "vars" in item:
set_variables(host_name, item["vars"])
return host_name
else:
raise errors.AnsibleError("Unknown item in inventory: %s"%(item))
def _get_variables_from_script(self, host):
''' support per system variabes from external variable scripts, see web docs '''
cmd = [self.inventory_file, '--host', host]
cmd = subprocess.Popen(cmd,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
shell=False
)
out, err = cmd.communicate()
variables = {}
try:
variables = utils.json_loads(out)
except:
raise errors.AnsibleError("%s returned invalid result when called with hostname %s" % (
self.inventory_file,
host
))
return variables
def _set_variable(self, host, key, value):
if not host in self._variables:
self._variables[host] = {}
self._variables[host][key] = value
def _matches(self, host_name, pattern):
''' returns if a hostname is matched by the pattern '''
# a pattern is in fnmatch format but more than one pattern
# can be strung together with semicolons. ex:
# atlanta-web*.example.com;dc-web*.example.com
if host_name == '':
return False
pattern = pattern.replace(";",":")
subpatterns = pattern.split(":")
for subpattern in subpatterns:
if subpattern == 'all':
return True
if fnmatch.fnmatch(host_name, subpattern):
return True
elif subpattern in self.groups:
if host_name in self.groups[subpattern]:
return True
return False

View File

@ -0,0 +1,151 @@
# (c) 2012, Michael DeHaan <michael.dehaan@gmail.com>
#
# This file is part of Ansible
#
# Ansible is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Ansible is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
#############################################
import fnmatch
import os
import subprocess
import constants as C
from ansible.host import Host
from ansible.group import Group
from ansible import errors
from ansible import utils
class InventoryParser(object):
"""
Host inventory for ansible.
"""
def __init__(self, filename=C.DEFAULT_HOST_LIST):
fh = open(filename)
self.lines = fh.readlines()
self.groups = {}
self.hosts = {}
self._parse()
def _parse(self):
self._parse_base_groups()
self._parse_group_children()
self._parse_group_variables()
return self.groups
# [webservers]
# alpha
# beta:2345
# gamma sudo=True user=root
# delta asdf=jkl favcolor=red
def _parse_base_groups(self):
ungrouped = Group(name='ungrouped')
all = Group(name='all')
all.add_child_group(ungrouped)
self.groups = dict(all=all, ungrouped=ungrouped)
active_group_name = 'ungrouped'
for line in self.lines:
if line.startswith("["):
active_group_name = line.replace("[","").replace("]","").strip()
if line.find(":vars") != -1 or line.find(":children") != -1:
active_group_name = None
else:
new_group = self.groups[active_group_name] = Group(name=active_group_name)
all.add_child_group(new_group)
elif line.startswith("#") or line == '':
pass
elif active_group_name:
tokens = line.split()
if len(tokens) == 0:
continue
hostname = tokens[0]
port = C.DEFAULT_REMOTE_PORT
if hostname.find(":") != -1:
tokens2 = hostname.split(":")
hostname = tokens2[0]
port = tokens2[1]
host = None
if hostname in self.hosts:
host = self.hosts[hostname]
else:
host = Host(name=hostname, port=port)
self.hosts[hostname] = host
if len(tokens) > 1:
for t in tokens[1:]:
(k,v) = t.split("=")
host.set_variable(k,v)
self.groups[active_group_name].add_host(host)
# [southeast:children]
# atlanta
# raleigh
def _parse_group_children(self):
group = None
for line in self.lines:
line = line.strip()
if line is None or line == '':
continue
if line.startswith("[") and line.find(":children]") != -1:
line = line.replace("[","").replace(":children]","")
group = self.groups.get(line, None)
if group is None:
group = self.groups[line] = Group(name=line)
elif line.startswith("#"):
pass
elif line.startswith("["):
group = None
elif group:
kid_group = self.groups.get(line, None)
if kid_group is None:
raise errors.AnsibleError("child group is not defined: (%s)" % line)
else:
group.add_child_group(kid_group)
# [webservers:vars]
# http_port=1234
# maxRequestsPerChild=200
def _parse_group_variables(self):
group = None
for line in self.lines:
line = line.strip()
if line.startswith("[") and line.find(":vars]") != -1:
line = line.replace("[","").replace(":vars]","")
group = self.groups.get(line, None)
if group is None:
raise errors.AnsibleError("can't add vars to undefined group: %s" % line)
elif line.startswith("#"):
pass
elif line.startswith("["):
group = None
elif line == '':
pass
elif group:
if line.find("=") == -1:
raise errors.AnsibleError("variables assigned to group must be in key=value form")
else:
(k,v) = line.split("=")
group.set_variable(k,v)

View File

@ -0,0 +1,108 @@
# (c) 2012, Michael DeHaan <michael.dehaan@gmail.com>
#
# This file is part of Ansible
#
# Ansible is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Ansible is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
#############################################
import constants as C
from ansible.host import Host
from ansible.group import Group
from ansible import errors
from ansible import utils
class InventoryParserYaml(object):
"""
Host inventory for ansible.
"""
def __init__(self, filename=C.DEFAULT_HOST_LIST):
fh = open(filename)
data = fh.read()
fh.close()
self._hosts = {}
self._parse(data)
def _make_host(self, hostname):
if hostname in self._hosts:
return self._hosts[hostname]
else:
host = Host(hostname)
self._hosts[hostname] = host
return host
# see file 'test/yaml_hosts' for syntax
def _parse(self, data):
all = Group('all')
ungrouped = Group('ungrouped')
all.add_child_group(ungrouped)
self.groups = dict(all=all, ungrouped=ungrouped)
yaml = utils.parse_yaml(data)
for item in yaml:
if type(item) in [ str, unicode ]:
host = self._make_host(item)
ungrouped.add_host(host)
elif type(item) == dict and 'host' in item:
host = self._make_host(item['host'])
vars = item.get('vars', {})
if type(vars)==list:
varlist, vars = vars, {}
for subitem in varlist:
vars.update(subitem)
for (k,v) in vars.items():
host.set_variable(k,v)
elif type(item) == dict and 'group' in item:
group = Group(item['group'])
for subresult in item.get('hosts',[]):
if type(subresult) in [ str, unicode ]:
host = self._make_host(subresult)
group.add_host(host)
elif type(subresult) == dict:
host = self._make_host(subresult['host'])
vars = subresult.get('vars',{})
if type(vars) == list:
for subitem in vars:
for (k,v) in subitem.items():
host.set_variable(k,v)
elif type(vars) == dict:
for (k,v) in subresult.get('vars',{}).items():
host.set_variable(k,v)
else:
raise errors.AnsibleError("unexpected type for variable")
group.add_host(host)
vars = item.get('vars',{})
if type(vars) == dict:
for (k,v) in item.get('vars',{}).items():
group.set_variable(k,v)
elif type(vars) == list:
for subitem in vars:
if type(subitem) != dict:
raise errors.AnsibleError("expected a dictionary")
for (k,v) in subitem.items():
group.set_variable(k,v)
self.groups[group.name] = group
all.add_child_group(group)

View File

@ -0,0 +1,59 @@
# (c) 2012, Michael DeHaan <michael.dehaan@gmail.com>
#
# This file is part of Ansible
#
# Ansible is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Ansible is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
#############################################
import os
import subprocess
import constants as C
import os
from ansible.host import Host
from ansible.group import Group
from ansible import errors
from ansible import utils
class InventoryScript(object):
"""
Host inventory parser for ansible using external inventory scripts.
"""
def __init__(self, filename=C.DEFAULT_HOST_LIST):
cmd = [ filename, "--list" ]
sp = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
(stdout, stderr) = sp.communicate()
self.data = stdout
self.groups = self._parse()
def _parse(self):
groups = {}
self.raw = utils.parse_json(self.data)
all=Group('all')
self.groups = dict(all=all)
group = None
for (group_name, hosts) in self.raw.items():
group = groups[group_name] = Group(group_name)
host = None
for hostname in hosts:
host = Host(hostname)
group.add_host(host)
# FIXME: hack shouldn't be needed
all.add_host(host)
all.add_child_group(group)
return groups

View File

@ -23,7 +23,6 @@ import ansible.constants as C
from ansible import utils
from ansible import errors
import os
import time
# used to transfer variables to Runner
SETUP_CACHE={ }
@ -57,12 +56,14 @@ class PlayBook(object):
sudo_pass = C.DEFAULT_SUDO_PASS,
remote_port = C.DEFAULT_REMOTE_PORT,
transport = C.DEFAULT_TRANSPORT,
override_hosts = None,
private_key_file = C.DEFAULT_PRIVATE_KEY_FILE,
debug = False,
callbacks = None,
runner_callbacks = None,
stats = None,
sudo = False):
sudo = False,
sudo_user = C.DEFAULT_SUDO_USER,
extra_vars = None):
"""
playbook: path to a playbook file
@ -75,7 +76,6 @@ class PlayBook(object):
sudo_pass: if sudo==True, and a password is required, this is the sudo password
remote_port: default remote port to use if not specified with the host or play
transport: how to connect to hosts that don't specify a transport (local, paramiko, etc)
override_hosts: skip the inventory file, just talk to these hosts
callbacks output callbacks for the playbook
runner_callbacks: more callbacks, this time for the runner API
stats: holds aggregrate data about events occuring to each host
@ -85,6 +85,9 @@ class PlayBook(object):
if playbook is None or callbacks is None or runner_callbacks is None or stats is None:
raise Exception('missing required arguments')
if extra_vars is None:
extra_vars = {}
self.module_path = module_path
self.forks = forks
self.timeout = timeout
@ -95,48 +98,53 @@ class PlayBook(object):
self.debug = debug
self.callbacks = callbacks
self.runner_callbacks = runner_callbacks
self.override_hosts = override_hosts
self.stats = stats
self.sudo = sudo
self.sudo_pass = sudo_pass
self.sudo_user = sudo_user
self.extra_vars = extra_vars
self.global_vars = {}
self.private_key_file = private_key_file
self.inventory = ansible.inventory.Inventory(host_list)
if not self.inventory._is_script:
self.global_vars.update(self.inventory.get_group_variables('all'))
self.basedir = os.path.dirname(playbook)
self.playbook = self._parse_playbook(playbook)
if override_hosts is not None:
if type(override_hosts) != list:
raise errors.AnsibleError("override hosts must be a list")
self.inventory = ansible.inventory.Inventory(override_hosts)
else:
self.inventory = ansible.inventory.Inventory(host_list)
# *****************************************************
def _get_vars(self, play, dirname):
''' load the vars section from a play '''
if play.get('vars') is None:
play['vars'] = {}
vars = play['vars']
if type(vars) not in [dict, list]:
if type(play['vars']) not in [dict, list]:
raise errors.AnsibleError("'vars' section must contain only key/value pairs")
vars = self.global_vars
# translate a list of vars into a dict
if type(vars) == list:
varlist = vars
vars = {}
for item in varlist:
if type(play['vars']) == list:
for item in play['vars']:
k, v = item.items()[0]
vars[k] = v
play['vars'] = vars
else:
vars.update(play['vars'])
vars_prompt = play.get('vars_prompt', {})
if type(vars_prompt) != dict:
raise errors.AnsibleError("'vars_prompt' section must contain only key/value pairs")
for vname in vars_prompt:
# TODO: make this prompt one line and consider double entry
print vars_prompt[vname]
# FIXME - need some way to know that this prompt should be getpass or raw_input
vars[vname] = self.callbacks.on_vars_prompt(vname)
return vars
results = self.extra_vars.copy()
results.update(vars)
return results
# *****************************************************
@ -144,9 +152,9 @@ class PlayBook(object):
''' load tasks included from external files. '''
# include: some.yml a=2 b=3 c=4
include_tokens = task['include'].split()
path = utils.path_dwim(dirname, include_tokens[0])
play_vars = self._get_vars(play, dirname)
include_tokens = utils.template(task['include'], play_vars, SETUP_CACHE).split()
path = utils.path_dwim(dirname, include_tokens[0])
include_vars = {}
for i,x in enumerate(include_tokens):
if x.find("=") != -1:
@ -154,8 +162,7 @@ class PlayBook(object):
include_vars[k] = v
inject_vars = play_vars.copy()
inject_vars.update(include_vars)
included = utils.template_from_file(path, inject_vars)
included = utils.parse_yaml(included)
included = utils.parse_yaml_from_file(path)
for x in included:
if len(include_vars):
x["vars"] = include_vars
@ -165,10 +172,10 @@ class PlayBook(object):
def _include_handlers(self, play, handler, dirname, new_handlers):
''' load handlers from external files '''
path = utils.path_dwim(dirname, handler['include'])
inject_vars = self._get_vars(play, dirname)
included = utils.template_from_file(path, inject_vars)
path = utils.template(handler['include'], inject_vars, SETUP_CACHE)
path = utils.path_dwim(dirname, path)
included = utils.template_from_file(path, inject_vars, SETUP_CACHE)
included = utils.parse_yaml(included)
for x in included:
new_handlers.append(x)
@ -246,90 +253,24 @@ class PlayBook(object):
# *****************************************************
def hosts_to_poll(self, results):
''' which hosts need more polling? '''
hosts = []
for (host, res) in results['contacted'].iteritems():
if (host in self.stats.failures) or (host in self.stats.dark):
continue
if not 'finished' in res and not 'skipped' in res and 'started' in res:
hosts.append(host)
return hosts
# *****************************************************
def _async_poll(self, runner, hosts, async_seconds, async_poll_interval, only_if):
def _async_poll(self, poller, async_seconds, async_poll_interval):
''' launch an async job, if poll_interval is set, wait for completion '''
runner.background = async_seconds
results = runner.run()
self.stats.compute(results, poll=True)
if async_poll_interval <= 0:
# if not polling, playbook requested fire and forget
# trust the user wanted that and return immediately
return results
poll_hosts = results['contacted'].keys()
if len(poll_hosts) == 0:
# no hosts launched ok, return that.
return results
ahost = poll_hosts[0]
jid = results['contacted'][ahost].get('ansible_job_id', None)
if jid is None:
# note: this really shouldn't happen, ever
self.callbacks.on_async_confused("unexpected error: unable to determine jid")
return results
clock = async_seconds
host_list = self.hosts_to_poll(results)
poll_results = results
while (clock >= 0):
# poll/loop until polling duration complete
runner.module_args = "jid=%s" % jid
runner.module_name = 'async_status'
runner.background = 0
runner.pattern = '*'
self.inventory.restrict_to(host_list)
poll_results = runner.run()
self.stats.compute(poll_results, poll=True)
host_list = self.hosts_to_poll(poll_results)
self.inventory.lift_restriction()
if len(host_list) == 0:
break
if poll_results is None:
break
# mention which hosts we're going to poll again...
for (host, host_result) in poll_results['contacted'].iteritems():
results['contacted'][host] = host_result
if not host in self.stats.dark and not host in self.stats.failures:
self.callbacks.on_async_poll(jid, host, clock, host_result)
# run down the clock
clock = clock - async_poll_interval
time.sleep(async_poll_interval)
results = poller.wait(async_seconds, async_poll_interval)
# mark any hosts that are still listed as started as failed
# since these likely got killed by async_wrapper
for (host, host_result) in poll_results['contacted'].iteritems():
if 'started' in host_result:
reason = { 'failed' : 1, 'rc' : None, 'msg' : 'timed out' }
self.runner_callbacks.on_failed(host, reason)
results['contacted'][host] = reason
for host in poller.hosts_to_poll:
reason = { 'failed' : 1, 'rc' : None, 'msg' : 'timed out' }
self.runner_callbacks.on_failed(host, reason)
results['contacted'][host] = reason
return results
# *****************************************************
def _run_module(self, pattern, module, args, vars, remote_user,
async_seconds, async_poll_interval, only_if, sudo, transport, port):
async_seconds, async_poll_interval, only_if, sudo, sudo_user, transport, port):
''' run a particular module step in a playbook '''
hosts = [ h for h in self.inventory.list_hosts() if (h not in self.stats.failures) and (h not in self.stats.dark)]
@ -344,16 +285,22 @@ class PlayBook(object):
remote_pass=self.remote_pass, module_path=self.module_path,
timeout=self.timeout, remote_user=remote_user,
remote_port=port, module_vars=vars,
private_key_file=self.private_key_file,
setup_cache=SETUP_CACHE, basedir=self.basedir,
conditional=only_if, callbacks=self.runner_callbacks,
debug=self.debug, sudo=sudo,
debug=self.debug, sudo=sudo, sudo_user=sudo_user,
transport=transport, sudo_pass=self.sudo_pass, is_playbook=True
)
if async_seconds == 0:
results = runner.run()
else:
results = self._async_poll(runner, hosts, async_seconds, async_poll_interval, only_if)
results, poller = runner.runAsync(async_seconds)
self.stats.compute(results)
if async_poll_interval > 0:
# if not polling, playbook requested fire and forget
# trust the user wanted that and return immediately
results = self._async_poll(poller, async_seconds, async_poll_interval)
self.inventory.lift_restriction()
return results
@ -361,7 +308,8 @@ class PlayBook(object):
# *****************************************************
def _run_task(self, pattern=None, task=None,
remote_user=None, handlers=None, conditional=False, sudo=False, transport=None, port=None):
remote_user=None, handlers=None, conditional=False,
sudo=False, sudo_user=None, transport=None, port=None):
''' run a single task in the playbook and recursively run any subtasks. '''
# load the module name and parameters from the task entry
@ -382,8 +330,10 @@ class PlayBook(object):
module_args = tokens[1]
# include task specific vars
module_vars = task.get('vars')
module_vars = task.get('vars', {})
if 'first_available_file' in task:
module_vars['first_available_file'] = task.get('first_available_file')
# tasks can be direct (run on all nodes matching
# the pattern) or conditional, where they ran
# as the result of a change handler on a subset
@ -395,7 +345,13 @@ class PlayBook(object):
# run the task in parallel
results = self._run_module(pattern, module_name,
module_args, module_vars, remote_user, async_seconds,
async_poll_interval, only_if, sudo, transport, port)
async_poll_interval, only_if, sudo, sudo_user, transport, port)
# add facts to the global setup cache
for host, result in results['contacted'].iteritems():
if "ansible_facts" in result:
for k,v in result['ansible_facts'].iteritems():
SETUP_CACHE[host][k]=v
self.stats.compute(results)
@ -410,6 +366,8 @@ class PlayBook(object):
# we would only trigger restarting Apache on half of the nodes
subtasks = task.get('notify', [])
if isinstance(subtasks, basestring):
subtasks = [subtasks]
if len(subtasks) > 0:
for host, results in results.get('contacted',{}).iteritems():
if results.get('changed', False):
@ -445,19 +403,23 @@ class PlayBook(object):
# *****************************************************
def _do_conditional_imports(self, vars_files):
def _do_conditional_imports(self, vars_files, pattern=None):
''' handle the vars_files section, which can contain variables '''
# FIXME: save parsed variable results in memory to avoid excessive re-reading/parsing
# FIXME: currently parses imports for hosts not in the pattern, that is not wrong, but it's
# not super optimized yet either, because we wouldn't have hit them, ergo
# it will raise false errors if there is no defaults variable file without any $vars
# in it, which could happen on uncontacted hosts.
if type(vars_files) != list:
raise errors.AnsibleError("vars_files must be a list")
for host in self.inventory.list_hosts():
cache_vars = SETUP_CACHE.get(host,{})
host_list = [ h for h in self.inventory.list_hosts(pattern)
if not (h in self.stats.failures or h in self.stats.dark) ]
for host in host_list:
cache_vars = SETUP_CACHE.get(host,{})
SETUP_CACHE[host] = cache_vars
for filename in vars_files:
if type(filename) == list:
@ -490,7 +452,7 @@ class PlayBook(object):
# *****************************************************
def _do_setup_step(self, pattern, vars, user, port, sudo, transport, vars_files=None):
def _do_setup_step(self, pattern, vars, user, port, sudo, sudo_user, transport, vars_files=None):
''' push variables down to the systems and get variables+facts back up '''
# this enables conditional includes like $facter_os.yml and is only done
@ -499,12 +461,13 @@ class PlayBook(object):
if vars_files is not None:
self.callbacks.on_setup_secondary()
self._do_conditional_imports(vars_files)
self._do_conditional_imports(vars_files, pattern)
else:
self.callbacks.on_setup_primary()
host_list = [ h for h in self.inventory.list_hosts(pattern)
if not (h in self.stats.failures or h in self.stats.dark) ]
self.inventory.restrict_to(host_list)
# push any variables down to the system
@ -514,9 +477,11 @@ class PlayBook(object):
forks=self.forks, module_path=self.module_path,
timeout=self.timeout, remote_user=user,
remote_pass=self.remote_pass, remote_port=port,
private_key_file=self.private_key_file,
setup_cache=SETUP_CACHE,
callbacks=self.runner_callbacks, sudo=sudo, debug=self.debug,
transport=transport, sudo_pass=self.sudo_pass, is_playbook=True
callbacks=self.runner_callbacks, sudo=sudo, sudo_user=sudo_user,
debug=self.debug, transport=transport,
sudo_pass=self.sudo_pass, is_playbook=True
).run()
self.stats.compute(setup_results, setup=True)
@ -528,7 +493,8 @@ class PlayBook(object):
if vars_files is None:
# first pass only or we'll erase good work
for (host, result) in setup_ok.iteritems():
SETUP_CACHE[host] = result
if 'ansible_facts' in result:
SETUP_CACHE[host] = result['ansible_facts']
# *****************************************************
@ -536,11 +502,13 @@ class PlayBook(object):
''' run a list of tasks for a given pattern, in order '''
# get configuration information about the pattern
pattern = pg.get('hosts',None)
if self.override_hosts:
pattern = 'all'
pattern = pg.get('hosts')
if pattern is None:
raise errors.AnsibleError('hosts declaration is required')
if isinstance(pattern, list):
pattern = ';'.join(pattern)
pattern = utils.template(pattern, self.extra_vars, {})
name = pg.get('name', pattern)
vars = self._get_vars(pg, self.basedir)
vars_files = pg.get('vars_files', {})
@ -549,16 +517,21 @@ class PlayBook(object):
user = pg.get('user', self.remote_user)
port = pg.get('port', self.remote_port)
sudo = pg.get('sudo', self.sudo)
sudo_user = pg.get('sudo_user', self.sudo_user)
transport = pg.get('connection', self.transport)
self.callbacks.on_play_start(pattern)
# the default sudo user is root, so if you change it, sudo is implied
if sudo_user != 'root':
sudo = True
self.callbacks.on_play_start(name)
# push any variables down to the system # and get facts/ohai/other data back up
self._do_setup_step(pattern, vars, user, port, sudo, transport, None)
self._do_setup_step(pattern, vars, user, port, sudo, sudo_user, transport, None)
# now with that data, handle contentional variable file imports!
if len(vars_files) > 0:
self._do_setup_step(pattern, vars, user, port, sudo, transport, vars_files)
self._do_setup_step(pattern, vars, user, port, sudo, sudo_user, transport, vars_files)
# run all the top level tasks, these get run on every node
for task in tasks:
@ -568,6 +541,7 @@ class PlayBook(object):
handlers=handlers,
remote_user=user,
sudo=sudo,
sudo_user=sudo_user,
transport=transport,
port=port
)
@ -589,6 +563,7 @@ class PlayBook(object):
conditional=True,
remote_user=user,
sudo=sudo,
sudo_user=sudo_user,
transport=transport,
port=port
)

View File

@ -26,8 +26,10 @@ import Queue
import random
import traceback
import tempfile
import time
import base64
import getpass
import codecs
import ansible.constants as C
import ansible.connection
@ -74,10 +76,11 @@ class Runner(object):
forks=C.DEFAULT_FORKS, timeout=C.DEFAULT_TIMEOUT,
pattern=C.DEFAULT_PATTERN, remote_user=C.DEFAULT_REMOTE_USER,
remote_pass=C.DEFAULT_REMOTE_PASS, remote_port=C.DEFAULT_REMOTE_PORT,
sudo_pass=C.DEFAULT_SUDO_PASS, background=0, basedir=None,
setup_cache=None, transport=C.DEFAULT_TRANSPORT, conditional='True',
callbacks=None, debug=False, sudo=False, module_vars=None,
is_playbook=False, inventory=None):
private_key_file=C.DEFAULT_PRIVATE_KEY_FILE, sudo_pass=C.DEFAULT_SUDO_PASS,
background=0, basedir=None, setup_cache=None,
transport=C.DEFAULT_TRANSPORT, conditional='True', callbacks=None,
debug=False, sudo=False, sudo_user=C.DEFAULT_SUDO_USER,
module_vars=None, is_playbook=False, inventory=None):
"""
host_list : path to a host list file, like /etc/ansible/hosts
@ -90,6 +93,8 @@ class Runner(object):
remote_user : connect as this remote username
remote_pass : supply this password (if not using keys)
remote_port : use this default remote port (if not set by the inventory system)
private_key_file : use this private key as your auth key
sudo_user : If you want to sudo to a user other than root.
sudo_pass : sudo password if using sudo and sudo requires a password
background : run asynchronously with a cap of this many # of seconds (if not 0)
basedir : paths used by modules if not absolute are relative to here
@ -114,14 +119,18 @@ class Runner(object):
self.generated_jid = str(random.randint(0, 999999999999))
self.sudo_user = sudo_user
self.transport = transport
self.connector = ansible.connection.Connection(self, self.transport)
self.connector = ansible.connection.Connection(self, self.transport, self.sudo_user)
if inventory is None:
self.inventory = ansible.inventory.Inventory(host_list)
else:
self.inventory = inventory
if module_vars is None:
module_vars = {}
self.setup_cache = setup_cache
self.conditional = conditional
self.module_path = module_path
@ -135,6 +144,7 @@ class Runner(object):
self.remote_user = remote_user
self.remote_pass = remote_pass
self.remote_port = remote_port
self.private_key_file = private_key_file
self.background = background
self.basedir = basedir
self.sudo = sudo
@ -152,19 +162,6 @@ class Runner(object):
# *****************************************************
@classmethod
def parse_hosts(cls, host_list, override_hosts=None):
''' parse the host inventory file, returns (hosts, groups) '''
if override_hosts is None:
inventory = ansible.inventory.Inventory(host_list)
else:
inventory = ansible.inventory.Inventory(override_hosts)
return inventory.host_list, inventory.groups
# *****************************************************
def _return_from_module(self, conn, host, result, err, executed=None):
''' helper function to handle JSON parsing of results '''
@ -209,7 +206,7 @@ class Runner(object):
afd, afile = tempfile.mkstemp()
afo = os.fdopen(afd, 'w')
afo.write(data)
afo.write(data.encode("utf8"))
afo.flush()
afo.close()
@ -253,13 +250,13 @@ class Runner(object):
if self.remote_user == 'root':
args = "%s metadata=/etc/ansible/setup" % args
else:
args = "%s metadata=/home/%s/.ansible/setup" % (args, self.remote_user)
args = "%s metadata=%s/.ansible/setup" % (args, C.DEFAULT_REMOTE_TMP)
else:
if not 'metadata' in args:
if self.remote_user == 'root':
args['metadata'] = '/etc/ansible/setup'
else:
args['metadata'] = "/home/%s/.ansible/setup" % (self.remote_user)
args['metadata'] = "%s/.ansible/setup" % C.DEFAULT_REMOTE_TMP
return args
# *****************************************************
@ -268,15 +265,18 @@ class Runner(object):
async_jid=None, async_module=None, async_limit=None):
''' runs a module that has already been transferred '''
inject = self.setup_cache.get(conn.host,{})
inject = self.setup_cache.get(conn.host,{}).copy()
host_variables = self.inventory.get_variables(conn.host)
inject.update(host_variables)
inject.update(self.module_vars)
conditional = utils.double_template(self.conditional, inject, self.setup_cache)
if not eval(conditional):
return [ utils.smjson(dict(skipped=True)), None, 'skipped' ]
host_variables = self.inventory.get_variables(conn.host)
inject.update(host_variables)
if self.module_name == 'setup':
if not args:
args = {}
args = self._add_setup_vars(inject, args)
args = self._add_setup_metadata(args)
@ -320,9 +320,9 @@ class Runner(object):
''' allows discovered variables to be used in templates and action statements '''
host = conn.host
try:
var_result = utils.parse_json(result)
except:
if 'ansible_facts' in result:
var_result = result['ansible_facts']
else:
var_result = {}
# note: do not allow variables from playbook to be stomped on
@ -336,6 +336,17 @@ class Runner(object):
# *****************************************************
def _execute_raw(self, conn, host, tmp):
''' execute a non-module command for bootstrapping, or if there's no python on a device '''
stdout, stderr = self._exec_command( conn, self.module_args, tmp, sudoable = True )
data = dict(stdout=stdout)
if stderr:
data['stderr'] = stderr
return (host, True, data, '')
# ***************************************************
def _execute_normal_module(self, conn, host, tmp, module_name):
''' transfer & execute a module that is not 'copy' or 'template' '''
@ -347,12 +358,12 @@ class Runner(object):
module = self._transfer_module(conn, tmp, module_name)
(result, err, executed) = self._execute_module(conn, tmp, module, self.module_args)
if module_name == 'setup':
self._add_result_to_setup_cache(conn, result)
if self.is_playbook:
self._save_setup_result_to_disk(conn, result)
(host, ok, data, err) = self._return_from_module(conn, host, result, err, executed)
return self._return_from_module(conn, host, result, err, executed)
if ok:
self._add_result_to_setup_cache(conn, data)
return (host, ok, data, err)
# *****************************************************
@ -385,11 +396,26 @@ class Runner(object):
options = utils.parse_kv(self.module_args)
source = options.get('src', None)
dest = options.get('dest', None)
if source is None or dest is None:
if (source is None and not 'first_available_file' in self.module_vars) or dest is None:
return (host, True, dict(failed=True, msg="src and dest are required"), '')
# apply templating to source argument
inject = self.setup_cache.get(conn.host,{})
# FIXME: break duplicate code up into subfunction
# if we have first_available_file in our vars
# look up the files and use the first one we find as src
if 'first_available_file' in self.module_vars:
found = False
for fn in self.module_vars.get('first_available_file'):
fn = utils.template(fn, inject, self.setup_cache)
if os.path.exists(fn):
source = fn
found = True
break
if not found:
return (host, True, dict(failed=True, msg="could not find src in first_available_file list"), '')
source = utils.template(source, inject, self.setup_cache)
# transfer the file to a remote tmp location
@ -422,6 +448,13 @@ class Runner(object):
if source is None or dest is None:
return (host, True, dict(failed=True, msg="src and dest are required"), '')
# apply templating to source argument
inject = self.setup_cache.get(conn.host,{})
source = utils.template(source, inject, self.setup_cache)
# apply templating to dest argument
dest = utils.template(dest, inject, self.setup_cache)
# files are saved in dest dir, with a subdir for each host, then the filename
dest = "%s/%s/%s" % (utils.path_dwim(self.basedir, dest), host, source)
dest = dest.replace("//","/")
@ -435,6 +468,7 @@ class Runner(object):
if remote_md5 != local_md5:
# create the containing directories, if needed
os.makedirs(os.path.dirname(dest))
# fetch the file and check for changes
conn.fetch_file(source, dest)
new_md5 = os.popen("md5sum %s" % dest).read().split()[0]
@ -456,8 +490,11 @@ class Runner(object):
(result2, err2, executed2) = self._execute_module(conn, tmp, module, args)
results2 = self._return_from_module(conn, conn.host, result2, err2, executed)
(host, ok, data2, err2) = results2
new_changed = data2.get('changed', False)
data.update(data2)
if ok:
new_changed = data2.get('changed', False)
data.update(data2)
else:
new_changed = False
if old_changed or new_changed:
data['changed'] = True
return (host, ok, data, "%s%s"%(err,err2))
@ -472,11 +509,25 @@ class Runner(object):
source = options.get('src', None)
dest = options.get('dest', None)
metadata = options.get('metadata', None)
if source is None or dest is None:
if (source is None and 'first_available_file' not in self.module_vars) or dest is None:
return (host, True, dict(failed=True, msg="src and dest are required"), '')
# apply templating to source argument so vars can be used in the path
inject = self.setup_cache.get(conn.host,{})
# if we have first_available_file in our vars
# look up the files and use the first one we find as src
if 'first_available_file' in self.module_vars:
found = False
for fn in self.module_vars.get('first_available_file'):
fn = utils.template(fn, inject, self.setup_cache)
if os.path.exists(fn):
source = fn
found = True
break
if not found:
return (host, True, dict(failed=True, msg="could not find src in first_available_file list"), '')
source = utils.template(source, inject, self.setup_cache)
(host, ok, data, err) = (None, None, None, None)
@ -509,10 +560,9 @@ class Runner(object):
copy_module = self._transfer_module(conn, tmp, 'copy')
# template the source data locally
source_data = file(utils.path_dwim(self.basedir, source)).read()
resultant = ''
try:
resultant = utils.template(source_data, inject, self.setup_cache)
resultant = utils.template_from_file(utils.path_dwim(self.basedir, source),
inject, self.setup_cache, no_engine=False)
except Exception, e:
return (host, False, dict(failed=True, msg=str(e)), '')
xfered = self._transfer_str(conn, tmp, 'source', resultant)
@ -524,6 +574,7 @@ class Runner(object):
# modify file attribs if needed
if ok:
executed = executed.replace("copy","template",1)
return self._chain_file_module(conn, tmp, data, err, options, executed)
else:
return (host, ok, data, err)
@ -569,6 +620,8 @@ class Runner(object):
result = self._execute_fetch(conn, host, tmp)
elif self.module_name == 'template':
result = self._execute_template(conn, host, tmp)
elif self.module_name == 'raw':
result = self._execute_raw(conn, host, tmp)
else:
if self.background == 0:
result = self._execute_normal_module(conn, host, tmp, module_name)
@ -599,33 +652,38 @@ class Runner(object):
def _exec_command(self, conn, cmd, tmp, sudoable=False):
''' execute a command string over SSH, return the output '''
stdin, stdout, stderr = conn.exec_command(cmd, tmp, sudoable=sudoable)
sudo_user = self.sudo_user
stdin, stdout, stderr = conn.exec_command(cmd, tmp, sudo_user, sudoable=sudoable)
err=None
out=None
if type(stderr) != str:
err="\n".join(stderr.readlines())
else:
err=stderr
if type(stdout) != str:
return "\n".join(stdout.readlines()), err
out="\n".join(stdout.readlines())
else:
return stdout, err
out=stdout
return (out,err)
# *****************************************************
def _get_tmp_path(self, conn):
''' gets a temporary path on a remote box '''
basetmp = "/var/tmp"
if self.remote_user != 'root':
basetmp = "/home/%s/.ansible/tmp" % self.remote_user
cmd = "mktemp -d %s/ansible.XXXXXX" % basetmp
basetmp = C.DEFAULT_REMOTE_TMP
if self.remote_user == 'root':
basetmp ="/var/tmp"
cmd = "mktemp -d %s/ansible.XXXXXX" % basetmp
if self.remote_user != 'root':
cmd = "mkdir -p %s && %s" % (basetmp, cmd)
result, err = self._exec_command(conn, cmd, None, sudoable=False)
cleaned = result.split("\n")[0].strip() + '/'
if self.remote_user != 'root':
cmd = 'chmod a+x %s' % cleaned
result, err = self._exec_command(conn, cmd, None, sudoable=False)
return cleaned
@ -717,4 +775,89 @@ class Runner(object):
results = [ self._executor(h[1]) for h in hosts ]
return self._partition_results(results)
def runAsync(self, time_limit):
''' Run this module asynchronously and return a poller. '''
self.background = time_limit
results = self.run()
return results, AsyncPoller(results, self)
class AsyncPoller(object):
""" Manage asynchronous jobs. """
def __init__(self, results, runner):
self.runner = runner
self.results = { 'contacted': {}, 'dark': {}}
self.hosts_to_poll = []
self.completed = False
# Get job id and which hosts to poll again in the future
jid = None
for (host, res) in results['contacted'].iteritems():
if res.get('started', False):
self.hosts_to_poll.append(host)
jid = res.get('ansible_job_id', None)
else:
self.results['contacted'][host] = res
for (host, res) in results['dark'].iteritems():
self.results['dark'][host] = res
if jid is None:
raise errors.AnsibleError("unexpected error: unable to determine jid")
if len(self.hosts_to_poll)==0:
raise errors.AnsibleErrot("unexpected error: no hosts to poll")
self.jid = jid
def poll(self):
""" Poll the job status.
Returns the changes in this iteration."""
self.runner.module_name = 'async_status'
self.runner.module_args = "jid=%s" % self.jid
self.runner.pattern = "*"
self.runner.background = 0
self.runner.inventory.restrict_to(self.hosts_to_poll)
results = self.runner.run()
self.runner.inventory.lift_restriction()
hosts = []
poll_results = { 'contacted': {}, 'dark': {}, 'polled': {}}
for (host, res) in results['contacted'].iteritems():
if res.get('started',False):
hosts.append(host)
poll_results['polled'][host] = res
else:
self.results['contacted'][host] = res
poll_results['contacted'][host] = res
if 'failed' in res:
self.runner.callbacks.on_async_failed(host, res, self.jid)
else:
self.runner.callbacks.on_async_ok(host, res, self.jid)
for (host, res) in results['dark'].iteritems():
self.results['dark'][host] = res
poll_results['dark'][host] = res
self.runner.callbacks.on_async_failed(host, res, self.jid)
self.hosts_to_poll = hosts
if len(hosts)==0:
self.completed = True
return poll_results
def wait(self, seconds, poll_interval):
""" Wait a certain time for job completion, check status every poll_interval. """
clock = seconds - poll_interval
while (clock >= 0 and not self.completed):
time.sleep(poll_interval)
poll_results = self.poll()
for (host, res) in poll_results['polled'].iteritems():
if res.get('started'):
self.runner.callbacks.on_async_poll(host, res, self.jid, clock)
clock = clock - poll_interval
return self.results

View File

@ -21,6 +21,7 @@ import sys
import os
import shlex
import re
import codecs
import jinja2
import yaml
import optparse
@ -170,14 +171,6 @@ def path_dwim(basedir, given):
else:
return os.path.join(basedir, given)
def async_poll_status(jid, host, clock, result):
if 'finished' in result:
return "<job %s> finished on %s" % (jid, host)
elif 'failed' in result:
return "<job %s> FAILED on %s" % (jid, host)
else:
return "<job %s> polling on %s, %s remaining" % (jid, host, clock)
def json_loads(data):
return json.loads(data)
@ -206,7 +199,29 @@ def parse_json(data):
return { "failed" : True, "parsed" : False, "msg" : data }
return results
_KEYCRE = re.compile(r"\$(\w+)")
_LISTRE = re.compile(r"(\w+)\[(\d+)\]")
def varLookup(name, vars):
''' find the contents of a possibly complex variable in vars. '''
path = name.split('.')
space = vars
for part in path:
if part in space:
space = space[part]
elif "[" in part:
m = _LISTRE.search(part)
if not m:
return
try:
space = space[m.group(1)][int(m.group(2))]
except (KeyError, IndexError):
return
else:
return
return space
_KEYCRE = re.compile(r"\$(?P<complex>\{){0,1}((?(complex)[\w\.\[\]]+|\w+))(?(complex)\})")
# if { -> complex if complex, allow . and need trailing }
def varReplace(raw, vars):
'''Perform variable replacement of $vars
@ -228,8 +243,9 @@ def varReplace(raw, vars):
# Determine replacement value (if unknown variable then preserve
# original)
varname = m.group(1).lower()
replacement = str(vars.get(varname, m.group()))
varname = m.group(2)
replacement = unicode(varLookup(varname, vars) or m.group())
start, end = m.span()
done.append(raw[:start]) # Keep stuff leading up to token
@ -238,21 +254,29 @@ def varReplace(raw, vars):
return ''.join(done)
def template(text, vars, setup_cache):
def template(text, vars, setup_cache, no_engine=True):
''' run a text buffer through the templating engine '''
vars = vars.copy()
text = varReplace(str(text), vars)
vars['hostvars'] = setup_cache
template = jinja2.Template(text)
return template.render(vars)
text = varReplace(unicode(text), vars)
if no_engine:
# used when processing include: directives so that Jinja is evaluated
# in a later context when more variables are available
return text
else:
template = jinja2.Template(text)
res = template.render(vars)
if text.endswith('\n') and not res.endswith('\n'):
res = res + '\n'
return res
def double_template(text, vars, setup_cache):
return template(template(text, vars, setup_cache), vars, setup_cache)
def template_from_file(path, vars):
def template_from_file(path, vars, setup_cache, no_engine=True):
''' run a file through the templating engine '''
data = file(path).read()
return template(data, vars)
data = codecs.open(path, encoding="utf8").read()
return template(data, vars, setup_cache, no_engine=no_engine)
def parse_yaml(data):
return yaml.load(data)
@ -267,11 +291,12 @@ def parse_yaml_from_file(path):
def parse_kv(args):
''' convert a string of key/value items to a dict '''
options = {}
vargs = shlex.split(args, posix=True)
for x in vargs:
if x.find("=") != -1:
k, v = x.split("=")
options[k]=v
if args is not None:
vargs = shlex.split(args, posix=True)
for x in vargs:
if x.find("=") != -1:
k, v = x.split("=", 1)
options[k]=v
return options
class SortedOptParser(optparse.OptionParser):
@ -285,7 +310,7 @@ def base_parser(constants=C, usage="", output_opts=False, runas_opts=False, asyn
parser = SortedOptParser(usage)
parser.add_option('-D','--debug', default=False, action="store_true",
help='debug standard error output of remote modules')
help='debug mode')
parser.add_option('-f','--forks', dest='forks', default=constants.DEFAULT_FORKS, type='int',
help="specify number of parallel processes to use (default=%s)" % constants.DEFAULT_FORKS)
parser.add_option('-i', '--inventory-file', dest='inventory',
@ -293,6 +318,8 @@ def base_parser(constants=C, usage="", output_opts=False, runas_opts=False, asyn
default=constants.DEFAULT_HOST_LIST)
parser.add_option('-k', '--ask-pass', default=False, dest='ask_pass', action='store_true',
help='ask for SSH password')
parser.add_option('--private-key', default=None, dest='private_key_file',
help='use this file to authenticate the connection')
parser.add_option('-K', '--ask-sudo-pass', default=False, dest='ask_sudo_pass', action='store_true',
help='ask for sudo password')
parser.add_option('-M', '--module-path', dest='module_path',
@ -311,6 +338,8 @@ def base_parser(constants=C, usage="", output_opts=False, runas_opts=False, asyn
if runas_opts:
parser.add_option("-s", "--sudo", default=False, action="store_true",
dest='sudo', help="run operations with sudo (nopasswd)")
parser.add_option('-U', '--sudo-user', dest='sudo_user', help='desired sudo user (default=root)',
default=None) # Can't default to root because we need to detect when this option was given
parser.add_option('-u', '--user', default=constants.DEFAULT_REMOTE_USER,
dest='remote_user',
help='connect as this user (default=%s)' % constants.DEFAULT_REMOTE_USER)

View File

@ -25,14 +25,12 @@ import os
import sys
import shlex
import subprocess
import syslog
import traceback
APT_PATH = "/usr/bin/apt-get"
APT = "DEBIAN_PRIORITY=critical %s" % APT_PATH
def debug(msg):
print >>sys.stderr, msg
def exit_json(rc=0, **kwargs):
print json.dumps(kwargs)
sys.exit(rc)
@ -84,7 +82,7 @@ def install(pkgspec, cache, upgrade=False, default_release=None):
name, version = package_split(pkgspec)
installed, upgradable = package_status(name, version, cache)
if not installed or (upgrade and upgradable):
cmd = "%s -q -y install '%s'" % (APT, pkgspec)
cmd = "%s --option Dpkg::Options::=--force-confold -q -y install '%s'" % (APT, pkgspec)
if default_release:
cmd += " -t '%s'" % (default_release,)
rc, out, err = run_apt(cmd)
@ -116,6 +114,8 @@ if not os.path.exists(APT_PATH):
argfile = sys.argv[1]
args = open(argfile, 'r').read()
items = shlex.split(args)
syslog.openlog('ansible-%s' % os.path.basename(__file__))
syslog.syslog(syslog.LOG_NOTICE, 'Invoked with %s' % args)
if not len(items):
fail_json(msg='the module requires arguments -a')

View File

@ -28,13 +28,18 @@ import subprocess
import sys
import datetime
import traceback
import syslog
# ===========================================
# FIXME: better error handling
argsfile = sys.argv[1]
items = shlex.split(file(argsfile).read())
args = open(argsfile, 'r').read()
items = shlex.split(args)
syslog.openlog('ansible-%s' % os.path.basename(__file__))
syslog.syslog(syslog.LOG_NOTICE, 'Invoked with %s' % args)
params = {}
for x in items:

View File

@ -30,6 +30,7 @@ import datetime
import traceback
import signal
import time
import syslog
def daemonize_self():
# daemonizing code: http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/66012
@ -76,6 +77,9 @@ wrapped_module = sys.argv[3]
argsfile = sys.argv[4]
cmd = "%s %s" % (wrapped_module, argsfile)
syslog.openlog('ansible-%s' % os.path.basename(__file__))
syslog.syslog(syslog.LOG_NOTICE, 'Invoked with %s' % " ".join(sys.argv[1:]))
# setup logging directory
logdir = os.path.expanduser("~/.ansible_async")
log_path = os.path.join(logdir, jid)
@ -128,52 +132,68 @@ def _run_command(wrapped_cmd, jid, log_path):
# immediately exit this process, leaving an orphaned process
# running which immediately forks a supervisory timing process
pid = os.fork()
if pid != 0:
# the parent indicates the job has started
# print "RETURNING SUCCESS IN PARENT"
print json.dumps({ "started" : 1, "ansible_job_id" : jid, "results_file" : log_path })
sys.stdout.flush()
# we need to not return immmediately such that the launched command has an attempt
# to initialize PRIOR to ansible trying to clean up the launch directory (and argsfile)
# this probably could be done with some IPC later. Modules should always read
# the argsfile at the very first start of their execution anyway
time.sleep(1)
sys.exit(0)
else:
# the kid manages the job
# WARNING: the following call may be total overkill
daemonize_self()
#import logging
#import logging.handlers
# we are now daemonized in this other fork but still
# want to create a supervisory process
#logger = logging.getLogger("ansible_async")
#logger.setLevel(logging.WARNING)
#logger.addHandler( logging.handlers.SysLogHandler("/dev/log") )
def debug(msg):
#logger.warning(msg)
pass
#print "DAEMONIZED KID MAKING MORE KIDS"
sub_pid = os.fork()
if sub_pid == 0:
#print "RUNNING IN KID A"
_run_command(cmd, jid, log_path)
#print "KID A COMPLETE"
sys.stdout.flush()
sys.exit(0)
else:
#print "WATCHING IN KID B"
remaining = int(time_limit)
if os.path.exists("/proc/%s" % sub_pid):
#print "STILL RUNNING"
time.sleep(1)
remaining = remaining - 1
else:
#print "DONE IN KID B"
sys.stdout.flush()
sys.exit(0)
if remaining == 0:
#print "SLAYING IN KID B"
os.kill(sub_pid, signals.SIGKILL)
sys.stdout.flush()
sys.exit(1)
try:
pid = os.fork()
if pid:
# Notify the overlord that the async process started
sys.stdout.flush()
sys.exit(0)
# we need to not return immmediately such that the launched command has an attempt
# to initialize PRIOR to ansible trying to clean up the launch directory (and argsfile)
# this probably could be done with some IPC later. Modules should always read
# the argsfile at the very first start of their execution anyway
time.sleep(1)
debug("Return async_wrapper task started.")
print json.dumps({ "started" : 1, "ansible_job_id" : jid, "results_file" : log_path })
sys.stdout.flush()
sys.exit(0)
else:
# The actual wrapper process
# Daemonize, so we keep on running
daemonize_self()
# we are now daemonized, create a supervisory process
debug("Starting module and watcher")
sub_pid = os.fork()
if sub_pid:
# the parent stops the process after the time limit
remaining = int(time_limit)
# set the child process group id to kill all children
os.setpgid(sub_pid, sub_pid)
debug("Start watching %s (%s)"%(sub_pid, remaining))
time.sleep(5)
while os.waitpid(sub_pid, os.WNOHANG) == (0, 0):
debug("%s still running (%s)"%(sub_pid, remaining))
time.sleep(5)
remaining = remaining - 5
if remaining == 0:
debug("Now killing %s"%(sub_pid))
os.killpg(sub_pid, signal.SIGKILL)
debug("Sent kill to group %s"%sub_pid)
time.sleep(1)
sys.exit(0)
debug("Done in kid B.")
os._exit(0)
else:
# the child process runs the actual module
debug("Start module (%s)"%os.getpid())
_run_command(cmd, jid, log_path)
debug("Module complete (%s)"%os.getpid())
sys.exit(0)
except Exception, err:
debug("error: %s"%(err))
raise err

View File

@ -29,9 +29,12 @@ import datetime
import traceback
import shlex
import os
import syslog
argfile = sys.argv[1]
args = open(argfile, 'r').read()
syslog.openlog('ansible-%s' % os.path.basename(__file__))
syslog.syslog(syslog.LOG_NOTICE, 'Invoked with %s' % args)
shell = False

View File

@ -21,6 +21,7 @@
import sys
import os
import shlex
import syslog
# ===========================================
# convert arguments of form a=b c=d
@ -32,7 +33,11 @@ if len(sys.argv) == 1:
argfile = sys.argv[1]
if not os.path.exists(argfile):
sys.exit(1)
items = shlex.split(open(argfile, 'r').read())
args = open(argfile, 'r').read()
items = shlex.split(args)
syslog.openlog('ansible-%s' % os.path.basename(__file__))
syslog.syslog(syslog.LOG_NOTICE, 'Invoked with %s' % args)
params = {}
@ -43,9 +48,9 @@ for x in items:
src = params['src']
dest = params['dest']
if src:
src = os.path.expanduser(src)
src = os.path.expanduser(src)
if dest:
dest = os.path.expanduser(dest)
dest = os.path.expanduser(dest)
# raise an error if there is no src file
if not os.path.exists(src):

View File

@ -22,4 +22,5 @@
# facter
# ruby-json
/usr/bin/facter --json
/usr/bin/logger -t ansible-facter Invoked as-is
/usr/bin/facter --json 2>/dev/null

View File

@ -17,10 +17,6 @@
# You should have received a copy of the GNU General Public License
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
try:
import json
except ImportError:
import simplejson as json
import os
import sys
import shlex
@ -29,27 +25,25 @@ import shutil
import stat
import grp
import pwd
import syslog
try:
import selinux
HAVE_SELINUX=True
except ImportError:
HAVE_SELINUX=False
def debug(msg):
# ansible ignores stderr, so it's safe to use for debug
# print >>sys.stderr, msg
pass
def dump_kv(vars):
return " ".join("%s=%s" % (k,v) for (k,v) in vars.items())
def exit_json(rc=0, **kwargs):
def exit_kv(rc=0, **kwargs):
if 'path' in kwargs:
debug("adding path info")
add_path_info(kwargs)
print json.dumps(kwargs)
print dump_kv(kwargs)
sys.exit(rc)
def fail_json(**kwargs):
def fail_kv(**kwargs):
kwargs['failed'] = True
exit_json(rc=1, **kwargs)
exit_kv(rc=1, **kwargs)
def add_path_info(kwargs):
path = kwargs['path']
@ -58,7 +52,7 @@ def add_path_info(kwargs):
kwargs['user'] = user
kwargs['group'] = group
st = os.stat(path)
kwargs['mode'] = stat.S_IMODE(st[stat.ST_MODE])
kwargs['mode'] = oct(stat.S_IMODE(st[stat.ST_MODE]))
# secontext not yet supported
if os.path.islink(path):
kwargs['state'] = 'link'
@ -66,16 +60,44 @@ def add_path_info(kwargs):
kwargs['state'] = 'file'
else:
kwargs['state'] = 'directory'
if HAVE_SELINUX:
if HAVE_SELINUX and selinux_enabled():
kwargs['secontext'] = ':'.join(selinux_context(path))
else:
kwargs['state'] = 'absent'
return kwargs
# Detect whether using selinux that is MLS-aware.
# While this means you can set the level/range with
# selinux.lsetfilecon(), it may or may not mean that you
# will get the selevel as part of the context returned
# by selinux.lgetfilecon().
def selinux_mls_enabled():
if not HAVE_SELINUX:
return False
if selinux.is_selinux_mls_enabled() == 1:
return True
else:
return False
def selinux_enabled():
if not HAVE_SELINUX:
return False
if selinux.is_selinux_enabled() == 1:
return True
else:
return False
# Determine whether we need a placeholder for selevel/mls
def selinux_initial_context():
context = [None, None, None]
if selinux_mls_enabled():
context.append(None)
return context
# If selinux fails to find a default, return an array of None
def selinux_default_context(path, mode=0):
context = [None, None, None, None]
if not HAVE_SELINUX:
context = selinux_initial_context()
if not HAVE_SELINUX or not selinux_enabled():
return context
try:
ret = selinux.matchpathcon(path, mode)
@ -84,7 +106,19 @@ def selinux_default_context(path, mode=0):
if ret[0] == -1:
return context
context = ret[1].split(':')
debug("got default secontext=%s" % ret[1])
return context
def selinux_context(path):
context = selinux_initial_context()
if not HAVE_SELINUX or not selinux_enabled():
return context
try:
ret = selinux.lgetfilecon(path)
except:
fail_kv(path=path, msg='failed to retrieve selinux context')
if ret[0] == -1:
return context
context = ret[1].split(':')
return context
# ===========================================
@ -92,9 +126,11 @@ def selinux_default_context(path, mode=0):
argfile = sys.argv[1]
args = open(argfile, 'r').read()
items = shlex.split(args)
syslog.openlog('ansible-%s' % os.path.basename(__file__))
syslog.syslog(syslog.LOG_NOTICE, 'Invoked with %s' % args)
if not len(items):
fail_json(msg='the module requires arguments -a')
fail_kv(msg='the module requires arguments -a')
sys.exit(1)
params = {}
@ -122,23 +158,22 @@ seuser = params.get('seuser', None)
serole = params.get('serole', None)
setype = params.get('setype', None)
selevel = params.get('serange', 's0')
context = params.get('context', None)
secontext = [seuser, serole, setype, selevel]
secontext = [seuser, serole, setype]
if selinux_mls_enabled():
secontext.append(selevel)
if context is not None:
if context != 'default':
fail_json(msg='invalid context: %s' % context)
if seuser is not None or serole is not None or setype is not None:
fail_json(msg='cannot define context=default and seuser, serole or setype')
secontext = selinux_default_context(path)
default_secontext = selinux_default_context(path)
for i in range(len(default_secontext)):
if i is not None and secontext[i] == '_default':
secontext[i] = default_secontext[i]
if state not in [ 'file', 'directory', 'link', 'absent']:
fail_json(msg='invalid state: %s' % state)
fail_kv(msg='invalid state: %s' % state)
if state == 'link' and (src is None or dest is None):
fail_json(msg='src and dest are required for "link" state')
fail_kv(msg='src and dest are required for "link" state')
elif path is None:
fail_json(msg='path is required')
fail_kv(msg='path is required')
changed = False
@ -154,79 +189,59 @@ def user_and_group(filename):
gid = st.st_gid
user = pwd.getpwuid(uid)[0]
group = grp.getgrgid(gid)[0]
debug("got user=%s and group=%s" % (user, group))
return (user, group)
def selinux_context(path):
context = [None, None, None, None]
if not HAVE_SELINUX:
return context
try:
ret = selinux.lgetfilecon(path)
except:
fail_json(path=path, msg='failed to retrieve selinux context')
if ret[0] == -1:
return context
context = ret[1].split(':')
debug("got current secontext=%s" % ret[1])
return context
def set_context_if_different(path, context, changed):
if not HAVE_SELINUX:
if not HAVE_SELINUX or not selinux_enabled():
return changed
cur_context = selinux_context(path)
new_context = list(cur_context)
for i in range(len(context)):
# Iterate over the current context instead of the
# argument context, which may have selevel.
for i in range(len(cur_context)):
if context[i] is not None and context[i] != cur_context[i]:
new_context[i] = context[i]
debug("current secontext is %s" % ':'.join(cur_context))
debug("new secontext is %s" % ':'.join(new_context))
if cur_context != new_context:
try:
rc = selinux.lsetfilecon(path, ':'.join(new_context))
except OSError:
fail_json(path=path, msg='invalid selinux context')
fail_kv(path=path, msg='invalid selinux context')
if rc != 0:
fail_json(path=path, msg='set selinux context failed')
fail_kv(path=path, msg='set selinux context failed')
changed = True
return changed
def set_owner_if_different(path, owner, changed):
if owner is None:
debug('not tweaking owner')
return changed
user, group = user_and_group(path)
if owner != user:
debug('setting owner')
rc = os.system("/bin/chown -R %s %s" % (owner, path))
if rc != 0:
fail_json(path=path, msg='chown failed')
fail_kv(path=path, msg='chown failed')
return True
return changed
def set_group_if_different(path, group, changed):
if group is None:
debug('not tweaking group')
return changed
old_user, old_group = user_and_group(path)
if old_group != group:
debug('setting group')
rc = os.system("/bin/chgrp -R %s %s" % (group, path))
if rc != 0:
fail_json(path=path, msg='chgrp failed')
fail_kv(path=path, msg='chgrp failed')
return True
return changed
def set_mode_if_different(path, mode, changed):
if mode is None:
debug('not tweaking mode')
return changed
try:
# FIXME: support English modes
mode = int(mode, 8)
except Exception, e:
fail_json(path=path, msg='mode needs to be something octalish', details=str(e))
fail_kv(path=path, msg='mode needs to be something octalish', details=str(e))
st = os.stat(path)
prev_mode = stat.S_IMODE(st[stat.ST_MODE])
@ -235,10 +250,9 @@ def set_mode_if_different(path, mode, changed):
# FIXME: comparison against string above will cause this to be executed
# every time
try:
debug('setting mode')
os.chmod(path, mode)
except Exception, e:
fail_json(path=path, msg='chmod failed', details=str(e))
fail_kv(path=path, msg='chmod failed', details=str(e))
st = os.stat(path)
new_mode = stat.S_IMODE(st[stat.ST_MODE])
@ -249,13 +263,13 @@ def set_mode_if_different(path, mode, changed):
def rmtree_error(func, path, exc_info):
fail_json(path=path, msg='failed to remove directory')
fail_kv(path=path, msg='failed to remove directory')
# ===========================================
# go...
prev_state = 'absent'
if os.path.exists(path):
if os.path.lexists(path):
if os.path.islink(path):
prev_state = 'link'
elif os.path.isfile(path):
@ -264,7 +278,6 @@ if os.path.exists(path):
prev_state = 'directory'
if prev_state != 'absent' and state == 'absent':
debug('requesting absent')
try:
if prev_state == 'directory':
if os.path.islink(path):
@ -274,21 +287,20 @@ if prev_state != 'absent' and state == 'absent':
else:
os.unlink(path)
except Exception, e:
fail_json(path=path, msg=str(e))
exit_json(path=path, changed=True)
fail_kv(path=path, msg=str(e))
exit_kv(path=path, changed=True)
sys.exit(0)
if prev_state != 'absent' and prev_state != state:
fail_json(path=path, msg='refusing to convert between %s and %s' % (prev_state, state))
fail_kv(path=path, msg='refusing to convert between %s and %s' % (prev_state, state))
if prev_state == 'absent' and state == 'absent':
exit_json(path=path, changed=False)
exit_kv(path=path, changed=False)
if state == 'file':
debug('requesting file')
if prev_state == 'absent':
fail_json(path=path, msg='file does not exist, use copy or template module to create')
fail_kv(path=path, msg='file does not exist, use copy or template module to create')
# set modes owners and context as needed
changed = set_context_if_different(path, secontext, changed)
@ -296,11 +308,10 @@ if state == 'file':
changed = set_group_if_different(path, group, changed)
changed = set_mode_if_different(path, mode, changed)
exit_json(path=path, changed=changed)
exit_kv(path=path, changed=changed)
elif state == 'directory':
debug('requesting directory')
if prev_state == 'absent':
os.makedirs(path)
changed = True
@ -311,16 +322,16 @@ elif state == 'directory':
changed = set_group_if_different(path, group, changed)
changed = set_mode_if_different(path, mode, changed)
exit_json(path=path, changed=changed)
exit_kv(path=path, changed=changed)
elif state == 'link':
if os.path.isabs(src):
abs_src = src
else:
abs_src = os.path.join(os.path.dirname(dest))
if not os.path.exists(abssrc):
fail_json(dest=dest, src=src, msg='src file does not exist')
abs_src = os.path.join(os.path.dirname(dest), src)
if not os.path.exists(abs_src):
fail_kv(dest=dest, src=src, msg='src file does not exist')
if prev_state == 'absent':
os.symlink(src, dest)
@ -333,7 +344,7 @@ elif state == 'link':
os.unlink(dest)
os.symlink(src, dest)
else:
fail_json(dest=dest, src=src, msg='unexpected position reached')
fail_kv(dest=dest, src=src, msg='unexpected position reached')
# set modes owners and context as needed
changed = set_context_if_different(dest, secontext, changed)
@ -341,9 +352,9 @@ elif state == 'link':
changed = set_group_if_different(dest, group, changed)
changed = set_mode_if_different(dest, mode, changed)
exit_json(dest=dest, src=src, changed=changed)
exit_kv(dest=dest, src=src, changed=changed)
fail_json(path=path, msg='unexpected position reached')
fail_kv(path=path, msg='unexpected position reached')
sys.exit(0)

View File

@ -18,19 +18,31 @@
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
# I wanted to keep this simple at first, so for now this checks out
# from the MASTER branch of a repo at a particular SHA or
# from the given branch of a repo at a particular SHA or
# tag. Latest is not supported, you should not be doing
# that. Branch checkouts are not supported. Contribs
# welcome! -- MPD
# that. Contribs welcome! -- MPD
try:
import json
except ImportError:
import simplejson as json
import os
import re
import sys
import shlex
import subprocess
import syslog
# ===========================================
# Basic support methods
def exit_json(rc=0, **kwargs):
print json.dumps(kwargs)
sys.exit(rc)
def fail_json(**kwargs):
kwargs['failed'] = True
exit_json(rc=1, **kwargs)
# ===========================================
# convert arguments of form a=b c=d
@ -38,29 +50,19 @@ import subprocess
# FIXME: make more idiomatic
if len(sys.argv) == 1:
print json.dumps({
"failed" : True,
"msg" : "the command module requires arguments (-a)"
})
sys.exit(1)
fail_json(msg="the command module requires arguments (-a)")
argfile = sys.argv[1]
if not os.path.exists(argfile):
print json.dumps({
"failed" : True,
"msg" : "Argument file not found"
})
sys.exit(1)
fail_json(msg="Argument file not found")
args = open(argfile, 'r').read()
items = shlex.split(args)
syslog.openlog('ansible-%s' % os.path.basename(__file__))
syslog.syslog(syslog.LOG_NOTICE, 'Invoked with %s' % args)
if not len(items):
print json.dumps({
"failed" : True,
"msg" : "the command module requires arguments (-a)"
})
sys.exit(1)
fail_json(msg="the command module requires arguments (-a)")
params = {}
for x in items:
@ -69,7 +71,8 @@ for x in items:
dest = params['dest']
repo = params['repo']
version = params['version']
branch = params.get('branch', 'master')
version = params.get('version', 'HEAD')
# ===========================================
@ -81,7 +84,7 @@ def get_version(dest):
sha = sha[0].split()[1]
return sha
def clone(repo, dest):
def clone(repo, dest, branch):
''' makes a new git repo if it does not already exist '''
try:
os.makedirs(dest)
@ -89,12 +92,68 @@ def clone(repo, dest):
pass
cmd = "git clone %s %s" % (repo, dest)
cmd = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
(out, err) = cmd.communicate()
rc = cmd.returncode
if branch is None or rc != 0:
return (out, err)
os.chdir(dest)
cmd = "git checkout -b %s origin/%s" % (branch, branch)
cmd = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
return cmd.communicate()
def pull(repo, dest):
def reset(dest):
'''
Resets the index and working tree to HEAD.
Discards any changes to tracked files in working
tree since that commit.
'''
os.chdir(dest)
cmd = "git reset --hard HEAD"
cmd = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
(out, err) = cmd.communicate()
rc = cmd.returncode
return (rc, out, err)
def switchLocalBranch( branch ):
cmd = "git checkout %s" % branch
cmd = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
return cmd.communicate()
def pull(repo, dest, branch):
''' updates repo from remote sources '''
os.chdir(dest)
cmd = "git pull -u origin"
cmd = "git branch -a"
cmd = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
(gbranch_out, gbranch_err) = cmd.communicate()
try:
m = re.search( '^\* (\S+|\(no branch\))$', gbranch_out, flags=re.M )
cur_branch = m.group(1)
m = re.search( '\s+remotes/origin/HEAD -> origin/(\S+)', gbranch_out, flags=re.M )
default_branch = m.group(1)
except:
fail_json(msg="could not determine branch data - received: %s" % gbranch_out)
if branch is None:
if cur_branch != default_branch:
(out, err) = switchLocalBranch( default_branch )
cmd = "git pull -u origin"
elif branch == cur_branch:
cmd = "git pull -u origin"
else:
m = re.search( '^\s+%s$' % branch, gbranch_out, flags=re.M ) #see if we've already checked it out
if m is None:
cmd = "git checkout -b %s origin/%s" % (branch, branch)
else:
(out, err) = switchLocalBranch( branch )
cmd = "git pull -u origin"
cmd = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
return cmd.communicate()
@ -120,33 +179,26 @@ out, err, status = (None, None, None)
before = None
if not os.path.exists(gitconfig):
(out, err) = clone(repo, dest)
(out, err) = clone(repo, dest, branch)
else:
# else do a pull
before = get_version(dest)
(out, err) = pull(repo, dest)
(rc, out, err) = reset(dest)
if rc != 0:
fail_json(out=out, err=err)
(out, err) = pull(repo, dest, branch)
# handle errors from clone or pull
if out.find('error') != -1:
print json.dumps({
"failed" : True,
"out" : out,
"err" : err
})
sys.exit(1)
fail_json(out=out, err=err)
# switch to version specified regardless of whether
# we cloned or pulled
(out, err) = switchver(version, dest)
if err.find('error') != -1:
print json.dumps({
"failed" : True,
"out" : out,
"err" : err
})
sys.exit(1)
fail_json(out=out, err=err)
# determine if we changed anything
@ -156,9 +208,4 @@ changed = False
if before != after:
changed = True
print json.dumps({
"changed" : changed,
"before" : before,
"after" : after
})
exit_json(changed=changed, before=before, after=after)

View File

@ -26,19 +26,14 @@ import grp
import shlex
import subprocess
import sys
import syslog
GROUPADD = "/usr/sbin/groupadd"
GROUPDEL = "/usr/sbin/groupdel"
GROUPMOD = "/usr/sbin/groupmod"
def debug(msg):
# ansible ignores stderr, so it's safe to use for debug
print >>sys.stderr, msg
#pass
def exit_json(rc=0, **kwargs):
if 'name' in kwargs:
debug("add group info to exit_json")
add_group_info(kwargs)
print json.dumps(kwargs)
sys.exit(rc)
@ -59,7 +54,6 @@ def add_group_info(kwargs):
def group_del(group):
cmd = [GROUPDEL, group]
debug("Arguments to groupdel: %s" % (" ".join(cmd)))
rc = subprocess.call(cmd, shell=False, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
if rc == 0:
return True
@ -72,8 +66,9 @@ def group_add(group, **kwargs):
if key == 'gid' and kwargs[key] is not None:
cmd.append('-g')
cmd.append(kwargs[key])
elif key == 'system' and kwargs[key] == 'yes':
cmd.append('-r')
cmd.append(group)
debug("Arguments to groupadd: %s" % (" ".join(cmd)))
rc = subprocess.call(cmd, shell=False, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
if rc == 0:
return True
@ -91,7 +86,6 @@ def group_mod(group, **kwargs):
if len(cmd) == 1:
return False
cmd.append(group)
debug("Arguments to groupmod: %s" % (" ".join(cmd)))
rc = subprocess.call(cmd, shell=False, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
if rc == 0:
return True
@ -138,6 +132,8 @@ if len(sys.argv) == 2 and os.path.exists(sys.argv[1]):
else:
args = ' '.join(sys.argv[1:])
items = shlex.split(args)
syslog.openlog('ansible-%s' % os.path.basename(__file__))
syslog.syslog(syslog.LOG_NOTICE, 'Invoked with %s' % args)
if not len(items):
fail_json(msg='the module requires arguments -a')
@ -151,9 +147,12 @@ for x in items:
state = params.get('state','present')
name = params.get('name', None)
gid = params.get('gid', None)
system = params.get('system', 'no')
if state not in [ 'present', 'absent' ]:
fail_json(msg='invalid state')
if system not in ['yes', 'no']:
fail_json(msg='invalid system')
if name is None:
fail_json(msg='name is required')
@ -165,7 +164,7 @@ if state == 'absent':
exit_json(name=name, changed=changed)
elif state == 'present':
if not group_exists(name):
changed = group_add(name, gid=gid)
changed = group_add(name, gid=gid, system=system)
else:
changed = group_mod(name, gid=gid)

View File

@ -18,4 +18,5 @@
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
#
/usr/bin/logger -t ansible-ohai Invoked as-is
/usr/bin/ohai

View File

@ -22,4 +22,10 @@ try:
except ImportError:
import simplejson as json
import os
import syslog
syslog.openlog('ansible-%s' % os.path.basename(__file__))
syslog.syslog(syslog.LOG_NOTICE, 'Invoked as-is')
print json.dumps({ "ping" : "pong" })

23
library/raw Executable file
View File

@ -0,0 +1,23 @@
#!/usr/bin/python
# (c) 2012, Michael DeHaan <michael.dehaan@gmail.com>
#
# This file is part of Ansible
#
# Ansible is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Ansible is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
# hey the Ansible raw module isn't really a remote transferred
# module. All the magic happens in Runner.py, see the web docs
# for more details.

View File

@ -21,89 +21,216 @@ try:
import json
except ImportError:
import simplejson as json
import os
import sys
import shlex
import subprocess
import os.path
import syslog
# TODO: switch to fail_json and other helper functions
# like other modules are using
# ===========================================
SERVICE = None
CHKCONFIG = None
def fail_json(d):
print json.dumps(d)
sys.exit(1)
def _find_binaries():
# list of possible paths for service/chkconfig binaries
# with the most probable first
global CHKCONFIG
global SERVICE
paths = ['/sbin', '/usr/sbin', '/bin', '/usr/bin']
binaries = [ 'service', 'chkconfig', 'update-rc.d' ]
location = dict()
for binary in binaries:
location[binary] = None
for binary in binaries:
for path in paths:
if os.path.exists(path + '/' + binary):
location[binary] = path + '/' + binary
break
if location.get('chkconfig', None):
CHKCONFIG = location['chkconfig']
elif location.get('update-rc.d', None):
CHKCONFIG = location['update-rc.d']
else:
fail_json(dict(failed=True, msg='unable to find chkconfig or update-rc.d binary'))
if location.get('service', None):
SERVICE = location['service']
else:
fail_json(dict(failed=True, msg='unable to find service binary'))
def _run(cmd):
# returns (rc, stdout, stderr) from shell command
process = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True)
stdout, stderr = process.communicate()
return (process.returncode, stdout, stderr)
def _do_enable(name, enable):
# we change argument depending on real binary used
# update-rc.d wants enable/disable while
# chkconfig wants on/off
valid_argument = dict({'on' : 'on', 'off' : 'off'})
if CHKCONFIG.endswith("update-rc.d"):
valid_argument['on'] = "enable"
valid_argument['off'] = "disable"
if enable.lower() in ['on', 'true', 'yes', 'enable']:
rc, stdout, stderr = _run("%s %s %s" % (CHKCONFIG, name, valid_argument['on']))
elif enable.lower() in ['off', 'false', 'no', 'disable']:
rc, stdout, stderr = _run("%s %s %s" % (CHKCONFIG, name, valid_argument['off']))
return rc, stdout, stderr
argfile = sys.argv[1]
args = open(argfile, 'r').read()
items = shlex.split(args)
syslog.openlog('ansible-%s' % os.path.basename(__file__))
syslog.syslog(syslog.LOG_NOTICE, 'Invoked with %s' % args)
if not len(items):
print json.dumps(dict(failed=True, msg='this module requires arguments (-a)'))
sys.exit(1)
fail_json(dict(failed=True, msg='this module requires arguments (-a)'))
params = {}
for x in items:
(k, v) = x.split("=")
params[k] = v
for arg in items:
if "=" not in arg:
fail_json(dict(failed=True, msg='expected key=value format arguments'))
name = params['name']
state = params.get('state','unknown')
(name, value) = arg.split("=")
params[name] = value
name = params.get('name', None)
if name is None:
fail_json(dict(failed=True, msg='missing name'))
state = params.get('state', None)
list_items = params.get('list', None)
enable = params.get('enabled', params.get('enable', None))
# running and started are the same
if state not in [ 'running', 'started', 'stopped', 'restarted' ]:
print json.dumps(dict(failed=True, msg='invalid state'))
sys.exit(1)
if state and state.lower() not in [ 'running', 'started', 'stopped', 'restarted','reloaded' ]:
fail_json(dict(failed=True, msg='invalid value for state'))
if list_items and list_items.lower() not in [ 'status' ]:
fail_json(dict(failed=True, msg='invalid value for list'))
if enable and enable.lower() not in [ 'on', 'off', 'true', 'false', 'yes', 'no', 'enable', 'disable' ]:
fail_json(dict(failed=True, msg='invalid value for enable'))
# ===========================================
# find binaries locations on minion
_find_binaries()
# ===========================================
# get service status
status = os.popen("service %s status" % name).read()
rc, status_stdout, status_stderr = _run("%s %s status" % (SERVICE, name))
status = status_stdout + status_stderr
# ===========================================
# determine if we are going to change anything
running = False
if status.find("not running") != -1:
if status_stdout.find("stopped") != -1 or rc == 3:
running = False
elif status.find("running") != -1:
elif status_stdout.find("running") != -1 or rc == 0:
running = True
elif name == 'iptables' and status.find("ACCEPT") != -1:
elif name == 'iptables' and status_stdout.find("ACCEPT") != -1:
# iptables status command output is lame
# TODO: lookup if we can use a return code for this instead?
running = True
changed = False
if not running and state == "started":
changed = True
elif running and state == "stopped":
changed = True
elif state == "restarted":
changed = True
if state or enable:
rc = 0
out = ''
err = ''
changed = False
if enable:
rc_enable, out_enable, err_enable = _do_enable(name, enable)
rc += rc_enable
out += out_enable
err += err_enable
# ===========================================
# run change commands if we need to
if state:
# a state change command has been requested
def _run(cmd):
return subprocess.call(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True)
# ===========================================
# determine if we are going to change anything
rc = 0
if changed:
if state in ('started', 'running'):
rc = _run("service %s start" % name)
elif state == 'stopped':
rc = _run("service %s stop" % name)
elif state == 'restarted':
rc1 = _run("service %s stop" % name)
rc2 = _run("service %s start" % name)
rc = rc1 and rc2
if not running and state in ("started", "running"):
changed = True
elif running and state in ("stopped","reloaded"):
changed = True
elif state == "restarted":
changed = True
# ===========================================
# run change commands if we need to
if changed:
if state in ('started', 'running'):
rc_state, stdout, stderr = _run("%s %s start" % (SERVICE, name))
elif state == 'stopped':
rc_state, stdout, stderr = _run("%s %s stop" % (SERVICE, name))
elif state == 'reloaded':
rc_state, stdout, stderr = _run("%s %s reload" % (SERVICE, name))
elif state == 'restarted':
rc1, stdout1, stderr1 = _run("%s %s stop" % (SERVICE, name))
rc2, stdout2, stderr2 = _run("%s %s start" % (SERVICE, name))
rc_state = rc + rc1 + rc2
stdout = stdout1 + stdout2
stderr = stderr1 + stderr2
out += stdout
err += stderr
rc = rc + rc_state
if rc != 0:
print json.dumps({
"failed" : 1,
"rc" : rc,
})
print >> sys.stderr, out + err
sys.exit(1)
# ===============================================
# success
result = {"changed": changed}
rc, stdout, stderr = _run("%s %s status" % (SERVICE, name))
if list_items and list_items in [ 'status' ]:
result['status'] = stdout
print json.dumps(result)
elif list_items is not None:
# solo list=status mode, don't change anything, just return
# suitable for /usr/bin/ansible usage or API, playbooks
# not so much
if rc != 0:
# yeah, should probably include output of failure...
print json.dumps({
"failed" : 1,
"rc" : rc
"status" : status
})
sys.exit(1)
# ===============================================
# success
else:
print json.dumps(dict(failed=True, msg="expected state or list parameters"))
print json.dumps({
"changed" : changed
})
sys.exit(0)

View File

@ -31,6 +31,7 @@ import socket
import struct
import subprocess
import traceback
import syslog
try:
import json
@ -224,7 +225,7 @@ def get_iface_hwaddr(iface):
return ''.join(['%02x:' % ord(char) for char in info[18:24]])[:-1]
def get_network_facts(facts):
facts['fqdn'] = socket.gethostname()
facts['fqdn'] = socket.getfqdn()
facts['hostname'] = facts['fqdn'].split('.')[0]
facts['interfaces'] = get_interfaces()
for iface in facts['interfaces']:
@ -241,6 +242,9 @@ def get_network_facts(facts):
facts[iface]['ipv4'] = {}
facts[iface]['ipv4'] = { 'address': data[1].split(':')[1],
'netmask': data[-1].split(':')[1] }
ip = struct.unpack("!L", socket.inet_aton(facts[iface]['ipv4']['address']))[0]
mask = struct.unpack("!L", socket.inet_aton(facts[iface]['ipv4']['netmask']))[0]
facts[iface]['ipv4']['network'] = socket.inet_ntoa(struct.pack("!L", ip & mask))
if 'inet6 addr' in line:
(ip, prefix) = data[2].split('/')
scope = data[3].split(':')[1].lower()
@ -263,8 +267,32 @@ def get_public_ssh_host_keys(facts):
else:
facts['ssh_host_key_rsa_public'] = rsa.split()[1]
def get_selinux_facts(facts):
if os.path.exists("/usr/sbin/sestatus"):
cmd = subprocess.Popen("/usr/sbin/sestatus", shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
out, err = cmd.communicate()
if err == '':
facts['selinux'] = {}
list = out.split("\n")
status = re.search("(enabled|disabled)", list[0])
if status.group() == "enabled":
mode = re.search("(enforcing|disabled|permissive)", list[2])
config_mode = re.search("(enforcing|disabled|permissive)", list[3])
policyvers = re.search("\d+", list[4])
type = re.search("(targeted|strict|mls)", list[5])
facts['selinux']['status'] = status.group()
facts['selinux']['mode'] = mode.group()
facts['selinux']['config_mode'] = config_mode.group()
facts['selinux']['policyvers'] = policyvers.group()
facts['selinux']['type'] = type.group()
elif status.group() == "disabled":
facts['selinux']['status'] = status.group()
else:
facts['selinux'] = False
def get_service_facts(facts):
get_public_ssh_host_keys(facts)
get_selinux_facts(facts)
def ansible_facts():
facts = {}
@ -295,7 +323,10 @@ except:
(k,v) = opt.split("=")
setup_options[k]=v
ansible_file = setup_options.get('metadata', DEFAULT_ANSIBLE_SETUP)
syslog.openlog('ansible-%s' % os.path.basename(__file__))
syslog.syslog(syslog.LOG_NOTICE, 'Invoked with %s' % setup_options)
ansible_file = os.path.expandvars(setup_options.get('metadata', DEFAULT_ANSIBLE_SETUP))
ansible_dir = os.path.dirname(ansible_file)
# create the config dir if it doesn't exist
@ -362,9 +393,11 @@ md5sum2 = os.popen("md5sum %s" % ansible_file).read().split()[0]
if md5sum != md5sum2:
changed = True
setup_options['written'] = ansible_file
setup_options['changed'] = changed
setup_options['md5sum'] = md5sum2
setup_result = {}
setup_result['written'] = ansible_file
setup_result['changed'] = changed
setup_result['md5sum'] = md5sum2
setup_result['ansible_facts'] = setup_options
print json.dumps(setup_options)
print json.dumps(setup_result)

View File

@ -21,6 +21,7 @@ import sys
import os
import shlex
import base64
import syslog
try:
import json
@ -36,7 +37,11 @@ if len(sys.argv) == 1:
argfile = sys.argv[1]
if not os.path.exists(argfile):
sys.exit(1)
items = shlex.split(open(argfile, 'r').read())
args = open(argfile, 'r').read()
items = shlex.split(args)
syslog.openlog('ansible-%s' % os.path.basename(__file__))
syslog.syslog(syslog.LOG_NOTICE, 'Invoked with %s' % args)
params = {}
for x in items:

View File

@ -25,22 +25,21 @@ import os
import pwd
import grp
import shlex
import spwd
import subprocess
import sys
import syslog
try:
import spwd
HAVE_SPWD=True
except:
HAVE_SPWD=False
USERADD = "/usr/sbin/useradd"
USERMOD = "/usr/sbin/usermod"
USERDEL = "/usr/sbin/userdel"
def debug(msg):
# ansible ignores stderr, so it's safe to use for debug
print >>sys.stderr, msg
#pass
def exit_json(rc=0, **kwargs):
if 'name' in kwargs:
debug("add user info to exit_json")
add_user_info(kwargs)
print json.dumps(kwargs)
sys.exit(rc)
@ -75,7 +74,6 @@ def user_del(user, **kwargs):
elif key == 'remove' and kwargs[key]:
cmd.append('-r')
cmd.append(user)
debug("Arguments to userdel: %s" % (" ".join(cmd)))
rc = subprocess.call(cmd, shell=False, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
if rc == 0:
return True
@ -117,14 +115,19 @@ def user_add(user, **kwargs):
cmd.append('-m')
else:
cmd.append('-M')
elif key == 'system' and kwargs[key] == 'yes':
cmd.append('-r')
cmd.append(user)
debug("Arguments to useradd: %s" % (" ".join(cmd)))
rc = subprocess.call(cmd, shell=False, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
if rc == 0:
return True
else:
return False
"""
Without spwd, we would have to resort to reading /etc/shadow
to get the encrypted string. For now, punt on idempotent password changes.
"""
def user_mod(user, **kwargs):
cmd = [USERMOD]
info = user_info(user)
@ -141,13 +144,27 @@ def user_mod(user, **kwargs):
cmd.append('-g')
cmd.append(kwargs[key])
elif key == 'groups' and kwargs[key] is not None:
for g in kwargs[key].split(','):
current_groups = user_group_membership(user)
groups = kwargs[key].split(',')
for g in groups:
if not group_exists(g):
fail_json(msg="Group %s does not exist" % (g))
groups = ",".join(user_group_membership(user))
if groups != kwargs[key]:
group_diff = set(sorted(current_groups)).symmetric_difference(set(sorted(groups)))
groups_need_mod = False
if group_diff:
if kwargs['append'] is not None and kwargs['append'] == 'yes':
for g in groups:
if g in group_diff:
cmd.append('-a')
groups_need_mod = True
else:
groups_need_mod = True
if groups_need_mod:
cmd.append('-G')
cmd.append(kwargs[key])
cmd.append(','.join(groups))
elif key == 'comment':
if kwargs[key] is not None and info[4] != kwargs[key]:
cmd.append('-c')
@ -164,15 +181,10 @@ def user_mod(user, **kwargs):
if kwargs[key] is not None and info[1] != kwargs[key]:
cmd.append('-p')
cmd.append(kwargs[key])
elif key == 'append':
if kwargs[key] is not None and kwargs[key] == 'yes':
if 'groups' in kwargs and kwargs['groups'] is not None:
cmd.append('-a')
# skip if no changes to be made
if len(cmd) == 1:
return False
cmd.append(user)
debug("Arguments to usermod: %s" % (" ".join(cmd)))
rc = subprocess.call(cmd, shell=False, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
if rc == 0:
return True
@ -223,10 +235,12 @@ def user_info(user):
return False
try:
info = get_pwd_info(user)
sinfo = spwd.getspnam(user)
if HAVE_SPWD:
sinfo = spwd.getspnam(user)
except KeyError:
return False
info[1] = sinfo[1]
if HAVE_SPWD:
info[1] = sinfo[1]
return info
# ===========================================
@ -250,6 +264,8 @@ if not os.path.exists(USERDEL):
argfile = sys.argv[1]
args = open(argfile, 'r').read()
items = shlex.split(args)
syslog.openlog('ansible-%s' % os.path.basename(__file__))
syslog.syslog(syslog.LOG_NOTICE, 'Invoked with %s' % args)
if not len(items):
fail_json(msg='the module requires arguments -a')
@ -278,6 +294,7 @@ remove = params.get('remove', False)
# ===========================================
# following options are specific to useradd
createhome = params.get('createhome', 'yes')
system = params.get('system', 'no')
# ===========================================
# following options are specific to usermod
@ -287,6 +304,8 @@ if state not in [ 'present', 'absent' ]:
fail_json(msg='invalid state')
if createhome not in [ 'yes', 'no' ]:
fail_json(msg='invalid createhome')
if system not in ['yes', 'no']:
fail_json(msg='invalid system')
if append not in [ 'yes', 'no' ]:
fail_json(msg='invalid append')
if name is None:
@ -302,7 +321,8 @@ elif state == 'present':
if not user_exists(name):
changed = user_add(name, uid=uid, group=group, groups=groups,
comment=comment, home=home, shell=shell,
password=password, createhome=createhome)
password=password, createhome=createhome,
system=system)
else:
changed = user_mod(name, uid=uid, group=group, groups=groups,
comment=comment, home=home, shell=shell,

View File

@ -27,6 +27,7 @@ except ImportError:
import os
import sys
import subprocess
import syslog
try:
import libvirt
except ImportError:
@ -366,6 +367,8 @@ def main():
args = open(argfile, 'r').read()
items = shlex.split(args)
syslog.openlog('ansible-%s' % os.path.basename(__file__))
syslog.syslog(syslog.LOG_NOTICE, 'Invoked with %s' % args)
if not len(items):
return VIRT_FAILED, msg

View File

@ -27,6 +27,7 @@ import datetime
import shlex
import re
import traceback
import syslog
try:
@ -57,13 +58,24 @@ def pkg_to_dict(po):
'epoch':po.epoch,
'release':po.release,
'version':po.version,
'repo':po.ui_from_repo,
'_nevra':po.ui_nevra,
}
if type(po) == yum.rpmsack.RPMInstalledPackage:
d['yumstate'] = 'installed'
d['repo'] = 'installed'
else:
d['yumstate'] = 'available'
d['repo'] = po.repoid
if hasattr(po, 'ui_from_repo'):
d['repo'] = po.ui_from_repo
if hasattr(po, 'ui_nevra'):
d['_nevra'] = po.ui_nevra
else:
d['_nevra'] = '%s-%s-%s.%s' % (po.name, po.version, po.release, po.arch)
return d
@ -215,6 +227,9 @@ def ensure(my, state, pkgspec):
if state == 'latest':
updates = my.doPackageLists(pkgnarrow='updates', patterns=[pkgspec]).updates
# sucks but this is for rhel5 - won't matter for rhel6 or fedora or whatnot
e,m,u = yum.parsePackages(updates, [pkgspec], casematch=True)
updates = e + m
avail = my.doPackageLists(pkgnarrow='available', patterns=[pkgspec]).available
if not updates and not avail:
if not my.doPackageLists(pkgnarrow='installed', patterns=[pkgspec]).installed:
@ -285,6 +300,8 @@ def main():
args = open(argfile, 'r').read()
items = shlex.split(args)
syslog.openlog('ansible-%s' % os.path.basename(__file__))
syslog.syslog(syslog.LOG_NOTICE, 'Invoked with %s' % args)
if not len(items):
msg = "the yum module requires arguments (-a)"

View File

@ -6,7 +6,7 @@ BuildRoot: %{_tmppath}/%{name}-%{version}-%{release}-buildroot
Name: ansible
Release: 1%{?dist}
Summary: Minimal SSH command and control
Version: 0.3
Version: 0.4
Group: Development/Libraries
License: GPLv3

View File

@ -3,6 +3,7 @@ import unittest
from ansible.inventory import Inventory
from ansible.runner import Runner
# from nose.plugins.skip import SkipTest
class TestInventory(unittest.TestCase):
@ -10,60 +11,73 @@ class TestInventory(unittest.TestCase):
self.cwd = os.getcwd()
self.test_dir = os.path.join(self.cwd, 'test')
self.inventory_file = os.path.join(self.test_dir, 'simple_hosts')
self.inventory_script = os.path.join(self.test_dir, 'inventory_api.py')
self.inventory_yaml = os.path.join(self.test_dir, 'yaml_hosts')
self.inventory_file = os.path.join(self.test_dir, 'simple_hosts')
self.complex_inventory_file = os.path.join(self.test_dir, 'complex_hosts')
self.inventory_script = os.path.join(self.test_dir, 'inventory_api.py')
self.inventory_yaml = os.path.join(self.test_dir, 'yaml_hosts')
os.chmod(self.inventory_script, 0755)
def tearDown(self):
os.chmod(self.inventory_script, 0644)
### Simple inventory format tests
def compare(self, left, right):
left = sorted(left)
right = sorted(right)
print left
print right
assert left == right
def simple_inventory(self):
return Inventory( self.inventory_file )
return Inventory(self.inventory_file)
def script_inventory(self):
return Inventory( self.inventory_script )
return Inventory(self.inventory_script)
def yaml_inventory(self):
return Inventory( self.inventory_yaml )
return Inventory(self.inventory_yaml)
def complex_inventory(self):
return Inventory(self.complex_inventory_file)
#####################################
### Simple inventory format tests
def test_simple(self):
inventory = self.simple_inventory()
hosts = inventory.list_hosts()
expected_hosts=['jupiter', 'saturn', 'zeus', 'hera', 'poseidon', 'thor', 'odin', 'loki']
assert hosts == expected_hosts
assert sorted(hosts) == sorted(expected_hosts)
def test_simple_all(self):
inventory = self.simple_inventory()
hosts = inventory.list_hosts('all')
expected_hosts=['jupiter', 'saturn', 'zeus', 'hera', 'poseidon', 'thor', 'odin', 'loki']
assert hosts == expected_hosts
assert sorted(hosts) == sorted(expected_hosts)
def test_simple_norse(self):
inventory = self.simple_inventory()
hosts = inventory.list_hosts("norse")
expected_hosts=['thor', 'odin', 'loki']
assert hosts == expected_hosts
assert sorted(hosts) == sorted(expected_hosts)
def test_simple_ungrouped(self):
inventory = self.simple_inventory()
hosts = inventory.list_hosts("ungrouped")
expected_hosts=['jupiter', 'saturn']
assert hosts == expected_hosts
assert sorted(hosts) == sorted(expected_hosts)
def test_simple_combined(self):
inventory = self.simple_inventory()
hosts = inventory.list_hosts("norse:greek")
expected_hosts=['zeus', 'hera', 'poseidon', 'thor', 'odin', 'loki']
assert hosts == expected_hosts
assert sorted(hosts) == sorted(expected_hosts)
def test_simple_restrict(self):
inventory = self.simple_inventory()
@ -74,25 +88,57 @@ class TestInventory(unittest.TestCase):
inventory.restrict_to(restricted_hosts)
hosts = inventory.list_hosts("norse:greek")
assert hosts == restricted_hosts
print "Hosts=%s" % hosts
print "Restricted=%s" % restricted_hosts
assert sorted(hosts) == sorted(restricted_hosts)
inventory.lift_restriction()
hosts = inventory.list_hosts("norse:greek")
assert hosts == expected_hosts
print hosts
print expected_hosts
assert sorted(hosts) == sorted(expected_hosts)
def test_simple_vars(self):
inventory = self.simple_inventory()
vars = inventory.get_variables('thor')
assert vars == {}
print vars
assert vars == {'group_names': ['norse'],
'inventory_hostname': 'thor'}
def test_simple_port(self):
inventory = self.simple_inventory()
vars = inventory.get_variables('hera')
assert vars == {'ansible_ssh_port': 3000}
print vars
expected = {'ansible_ssh_port': 3000,
'group_names': ['greek'],
'inventory_hostname': 'hera'}
print expected
assert vars == expected
###################################################
### INI file advanced tests
def test_complex_vars(self):
inventory = self.complex_inventory()
vars = inventory.get_variables('rtp_a')
print vars
expected = dict(
a='1', b='2', c='3', d='100002', rga='1', rgb='2', rgc='3',
inventory_hostname='rtp_a',
group_names=[ 'eastcoast', 'nc', 'redundantgroup', 'redundantgroup2', 'redundantgroup3', 'rtp', 'us' ]
)
print vars
print expected
assert vars == expected
###################################################
### Inventory API tests
def test_script(self):
@ -146,7 +192,11 @@ class TestInventory(unittest.TestCase):
inventory = self.script_inventory()
vars = inventory.get_variables('thor')
assert vars == {"hammer":True}
print "VARS=%s" % vars
assert vars == {'hammer':True,
'group_names': ['norse'],
'inventory_hostname': 'thor'}
### Tests for yaml inventory file
@ -155,35 +205,35 @@ class TestInventory(unittest.TestCase):
hosts = inventory.list_hosts()
print hosts
expected_hosts=['jupiter', 'saturn', 'zeus', 'hera', 'poseidon', 'thor', 'odin', 'loki']
assert hosts == expected_hosts
self.compare(hosts, expected_hosts)
def test_yaml_all(self):
inventory = self.yaml_inventory()
hosts = inventory.list_hosts('all')
expected_hosts=['jupiter', 'saturn', 'zeus', 'hera', 'poseidon', 'thor', 'odin', 'loki']
assert hosts == expected_hosts
self.compare(hosts, expected_hosts)
def test_yaml_norse(self):
inventory = self.yaml_inventory()
hosts = inventory.list_hosts("norse")
expected_hosts=['thor', 'odin', 'loki']
assert hosts == expected_hosts
self.compare(hosts, expected_hosts)
def test_simple_ungrouped(self):
def test_yaml_ungrouped(self):
inventory = self.yaml_inventory()
hosts = inventory.list_hosts("ungrouped")
expected_hosts=['jupiter']
assert hosts == expected_hosts
self.compare(hosts, expected_hosts)
def test_yaml_combined(self):
inventory = self.yaml_inventory()
hosts = inventory.list_hosts("norse:greek")
expected_hosts=['zeus', 'hera', 'poseidon', 'thor', 'odin', 'loki']
assert hosts == expected_hosts
self.compare(hosts, expected_hosts)
def test_yaml_restrict(self):
inventory = self.yaml_inventory()
@ -194,18 +244,27 @@ class TestInventory(unittest.TestCase):
inventory.restrict_to(restricted_hosts)
hosts = inventory.list_hosts("norse:greek")
assert hosts == restricted_hosts
self.compare(hosts, restricted_hosts)
inventory.lift_restriction()
hosts = inventory.list_hosts("norse:greek")
assert hosts == expected_hosts
self.compare(hosts, expected_hosts)
def test_yaml_vars(self):
inventory = self.yaml_inventory()
vars = inventory.get_variables('thor')
assert vars == {'group_names': ['norse'],
'hammer':True,
'inventory_hostname': 'thor'}
assert vars == {"hammer":True}
def test_yaml_list_vars(self):
inventory = self.yaml_inventory()
vars = inventory.get_variables('zeus')
assert vars == {'ansible_ssh_port': 3001,
'group_names': ['greek', 'ruler'],
'inventory_hostname': 'zeus',
'ntp_server': 'olympus.example.com'}
def test_yaml_change_vars(self):
inventory = self.yaml_inventory()
@ -214,39 +273,35 @@ class TestInventory(unittest.TestCase):
vars["hammer"] = False
vars = inventory.get_variables('thor')
assert vars == {"hammer":True}
print vars
assert vars == {'hammer':True,
'inventory_hostname': 'thor',
'group_names': ['norse']}
def test_yaml_host_vars(self):
inventory = self.yaml_inventory()
vars = inventory.get_variables('saturn')
assert vars == {"moon":"titan", "moon2":"enceladus"}
print vars
assert vars == {'inventory_hostname': 'saturn',
'moon': 'titan',
'moon2': 'enceladus',
'group_names': ['multiple']}
def test_yaml_port(self):
inventory = self.yaml_inventory()
vars = inventory.get_variables('hera')
assert vars == {'ansible_ssh_port': 3000, 'ntp_server': 'olympus.example.com'}
print vars
assert vars == {'ansible_ssh_port': 3000,
'inventory_hostname': 'hera',
'ntp_server': 'olympus.example.com',
'group_names': ['greek']}
### Test Runner class method
def test_yaml_multiple_groups(self):
inventory = self.yaml_inventory()
vars = inventory.get_variables('odin')
def test_class_method(self):
hosts, groups = Runner.parse_hosts(self.inventory_file)
assert 'group_names' in vars
assert sorted(vars['group_names']) == [ 'norse', 'ruler' ]
expected_hosts = ['jupiter', 'saturn', 'zeus', 'hera', 'poseidon', 'thor', 'odin', 'loki']
assert hosts == expected_hosts
expected_groups= {
'ungrouped': ['jupiter', 'saturn'],
'greek': ['zeus', 'hera', 'poseidon'],
'norse': ['thor', 'odin', 'loki']
}
assert groups == expected_groups
def test_class_override(self):
override_hosts = ['thor', 'odin']
hosts, groups = Runner.parse_hosts(self.inventory_file, override_hosts)
assert hosts == override_hosts
assert groups == { 'ungrouped': override_hosts }

View File

@ -74,12 +74,15 @@ class TestCallbacks(object):
def on_play_start(self, pattern):
EVENTS.append([ 'play start', [ pattern ]])
def on_async_confused(self, msg):
EVENTS.append([ 'async confused', [ msg ]])
def on_async_ok(self, host, res, jid):
EVENTS.append([ 'async ok', [ host ]])
def on_async_poll(self, jid, host, clock, host_result):
def on_async_poll(self, host, res, jid, clock):
EVENTS.append([ 'async poll', [ host ]])
def on_async_failed(self, host, res, jid):
EVENTS.append([ 'async failed', [ host ]])
def on_unreachable(self, host, msg):
EVENTS.append([ 'failed/dark', [ host, msg ]])
@ -141,7 +144,7 @@ class TestPlaybook(unittest.TestCase):
runner_callbacks = self.test_callbacks
)
result = self.playbook.run()
print utils.bigjson(dict(events=EVENTS))
# print utils.bigjson(dict(events=EVENTS))
return result
def test_one(self):
@ -166,5 +169,6 @@ class TestPlaybook(unittest.TestCase):
# make sure the template module took options from the vars section
data = file('/tmp/ansible_test_data_template.out').read()
print data
assert data.find("ears") != -1, "template success"

View File

@ -10,9 +10,9 @@ import os
import shutil
import time
try:
import json
import json
except:
import simplejson as json
import simplejson as json
from nose.plugins.skip import SkipTest
@ -25,200 +25,211 @@ def get_binary(name):
class TestRunner(unittest.TestCase):
def setUp(self):
self.user = getpass.getuser()
self.runner = ansible.runner.Runner(
module_name='ping',
module_path='library/',
module_args='',
remote_user=self.user,
remote_pass=None,
host_list='test/ansible_hosts',
timeout=5,
forks=1,
background=0,
pattern='all',
)
self.cwd = os.getcwd()
self.test_dir = os.path.join(self.cwd, 'test')
self.stage_dir = self._prepare_stage_dir()
def setUp(self):
self.user = getpass.getuser()
self.runner = ansible.runner.Runner(
module_name='ping',
module_path='library/',
module_args='',
remote_user=self.user,
remote_pass=None,
host_list='test/ansible_hosts',
timeout=5,
forks=1,
background=0,
pattern='all',
)
self.cwd = os.getcwd()
self.test_dir = os.path.join(self.cwd, 'test')
self.stage_dir = self._prepare_stage_dir()
def _prepare_stage_dir(self):
stage_path = os.path.join(self.test_dir, 'test_data')
if os.path.exists(stage_path):
shutil.rmtree(stage_path, ignore_errors=False)
assert not os.path.exists(stage_path)
os.makedirs(stage_path)
assert os.path.exists(stage_path)
return stage_path
def _prepare_stage_dir(self):
stage_path = os.path.join(self.test_dir, 'test_data')
if os.path.exists(stage_path):
shutil.rmtree(stage_path, ignore_errors=False)
assert not os.path.exists(stage_path)
os.makedirs(stage_path)
assert os.path.exists(stage_path)
return stage_path
def _get_test_file(self, filename):
# get a file inside the test input directory
filename = os.path.join(self.test_dir, filename)
assert os.path.exists(filename)
return filename
def _get_stage_file(self, filename):
# get a file inside the test output directory
filename = os.path.join(self.stage_dir, filename)
return filename
def _get_test_file(self, filename):
# get a file inside the test input directory
filename = os.path.join(self.test_dir, filename)
assert os.path.exists(filename)
return filename
def _run(self, module_name, module_args, background=0):
''' run a module and get the localhost results '''
self.runner.module_name = module_name
args = ' '.join(module_args)
print "DEBUG: using args=%s" % args
self.runner.module_args = args
self.runner.background = background
results = self.runner.run()
# when using nosetests this will only show up on failure
# which is pretty useful
print "RESULTS=%s" % results
assert "127.0.0.2" in results['contacted']
return results['contacted']['127.0.0.2']
def _get_stage_file(self, filename):
# get a file inside the test output directory
filename = os.path.join(self.stage_dir, filename)
return filename
def test_ping(self):
result = self._run('ping',[])
assert "ping" in result
def _run(self, module_name, module_args, background=0):
''' run a module and get the localhost results '''
self.runner.module_name = module_name
args = ' '.join(module_args)
print "DEBUG: using args=%s" % args
self.runner.module_args = args
self.runner.background = background
results = self.runner.run()
# when using nosetests this will only show up on failure
# which is pretty useful
print "RESULTS=%s" % results
assert "127.0.0.2" in results['contacted']
return results['contacted']['127.0.0.2']
def test_facter(self):
if not get_binary("facter"):
raise SkipTest
result = self._run('facter',[])
assert "hostname" in result
def test_ping(self):
result = self._run('ping', [])
assert "ping" in result
# temporarily disbabled since it occasionally hangs
# ohai's fault, setup module doesn't actually run this
# to get ohai's "facts" anyway
#
#def test_ohai(self):
# result = self._run('ohai',[])
# assert "hostname" in result
def test_facter(self):
if not get_binary("facter"):
raise SkipTest
result = self._run('facter', [])
assert "hostname" in result
def test_copy(self):
# test copy module, change trigger, etc
pass
# temporarily disbabled since it occasionally hangs
# ohai's fault, setup module doesn't actually run this
# to get ohai's "facts" anyway
#
#def test_ohai(self):
# if not get_binary("facter"):
# raise SkipTest
# result = self._run('ohai',[])
# assert "hostname" in result
def test_copy(self):
input = self._get_test_file('sample.j2')
output = self._get_stage_file('sample.out')
assert not os.path.exists(output)
result = self._run('copy', [
"src=%s" % input,
"dest=%s" % output,
])
assert os.path.exists(output)
data_in = file(input).read()
data_out = file(output).read()
assert data_in == data_out
assert 'failed' not in result
assert result['changed'] == True
assert 'md5sum' in result
result = self._run('copy', [
"src=%s" % input,
"dest=%s" % output,
])
assert result['changed'] == False
def test_copy(self):
# test copy module, change trigger, etc
input_ = self._get_test_file('sample.j2')
output = self._get_stage_file('sample.out')
assert not os.path.exists(output)
result = self._run('copy', [
"src=%s" % input_,
"dest=%s" % output,
])
assert os.path.exists(output)
data_in = file(input_).read()
data_out = file(output).read()
assert data_in == data_out
assert 'failed' not in result
assert result['changed'] == True
assert 'md5sum' in result
result = self._run('copy', [
"src=%s" % input_,
"dest=%s" % output,
])
assert result['changed'] == False
def test_template(self):
input = self._get_test_file('sample.j2')
metadata = self._get_test_file('metadata.json')
output = self._get_stage_file('sample.out')
result = self._run('template', [
"src=%s" % input,
"dest=%s" % output,
"metadata=%s" % metadata
])
assert os.path.exists(output)
out = file(output).read()
assert out.find("duck") != -1
assert result['changed'] == True
assert 'md5sum' in result
assert 'failed' not in result
result = self._run('template', [
"src=%s" % input,
"dest=%s" % output,
"metadata=%s" % metadata
])
assert result['changed'] == False
def test_template(self):
input_ = self._get_test_file('sample.j2')
metadata = self._get_test_file('metadata.json')
output = self._get_stage_file('sample.out')
result = self._run('template', [
"src=%s" % input_,
"dest=%s" % output,
"metadata=%s" % metadata
])
assert os.path.exists(output)
out = file(output).read()
assert out.find("duck") != -1
assert result['changed'] == True
assert 'md5sum' in result
assert 'failed' not in result
result = self._run('template', [
"src=%s" % input_,
"dest=%s" % output,
"metadata=%s" % metadata
])
assert result['changed'] == False
def test_command(self):
# test command module, change trigger, etc
result = self._run('command', [ "/bin/echo", "hi" ])
assert "failed" not in result
assert "msg" not in result
assert result['rc'] == 0
assert result['stdout'] == 'hi'
assert result['stderr'] == ''
result = self._run('command', [ "/bin/false" ])
assert result['rc'] == 1
assert 'failed' not in result
result = self._run('command', [ "/usr/bin/this_does_not_exist", "splat" ])
assert 'msg' in result
assert 'failed' in result
assert 'rc' not in result
def test_command(self):
# test command module, change trigger, etc
result = self._run('command', [ "/bin/echo", "hi" ])
assert "failed" not in result
assert "msg" not in result
assert result['rc'] == 0
assert result['stdout'] == 'hi'
assert result['stderr'] == ''
result = self._run('shell', [ "/bin/echo", "$HOME" ])
assert 'failed' not in result
assert result['rc'] == 0
result = self._run('command', [ "/bin/false" ])
assert result['rc'] == 1
assert 'failed' not in result
def test_setup(self):
output = self._get_stage_file('output.json')
result = self._run('setup', [ "metadata=%s" % output, "a=2", "b=3", "c=4" ])
assert 'failed' not in result
assert 'md5sum' in result
assert result['changed'] == True
outds = json.loads(file(output).read())
assert outds['c'] == '4'
# not bothering to test change hooks here since ohai/facter results change
# almost every time so changed is always true, this just tests that
# rewriting the file is ok
result = self._run('setup', [ "metadata=%s" % output, "a=2", "b=3", "c=4" ])
print "RAW RESULT=%s" % result
assert 'md5sum' in result
result = self._run('command', [ "/usr/bin/this_does_not_exist", "splat" ])
assert 'msg' in result
assert 'failed' in result
assert 'rc' not in result
def test_async(self):
# test async launch and job status
# of any particular module
result = self._run('command', [ get_binary("sleep"), "3" ], background=20)
assert 'ansible_job_id' in result
assert 'started' in result
jid = result['ansible_job_id']
# no real chance of this op taking a while, but whatever
time.sleep(5)
# CLI will abstract this (when polling), but this is how it works internally
result = self._run('async_status', [ "jid=%s" % jid ])
# TODO: would be nice to have tests for supervisory process
# killing job after X seconds
assert 'finished' in result
assert 'failed' not in result
assert 'rc' in result
assert 'stdout' in result
assert result['ansible_job_id'] == jid
result = self._run('shell', [ "/bin/echo", "$HOME" ])
assert 'failed' not in result
assert result['rc'] == 0
def test_fetch(self):
input = self._get_test_file('sample.j2')
output = os.path.join(self.stage_dir, '127.0.0.2', input)
result = self._run('fetch', [ "src=%s" % input, "dest=%s" % self.stage_dir ])
assert os.path.exists(output)
assert open(input).read() == open(output).read()
def test_large_output(self):
large_path = "/usr/share/dict/words"
if not os.path.exists(large_path):
large_path = "/usr/share/dict/cracklib-small"
if not os.path.exists(large_path):
raise SkipTest
# Ensure reading a large amount of output from a command doesn't hang.
result = self._run('command', [ "/bin/cat", large_path ])
assert "failed" not in result
assert "msg" not in result
assert result['rc'] == 0
assert len(result['stdout']) > 100000
assert result['stderr'] == ''
def test_yum(self):
if not get_binary("yum"):
raise SkipTest
result = self._run('yum', [ "list=repos" ])
assert 'failed' not in result
def test_setup(self):
output = self._get_stage_file('output.json')
result = self._run('setup', [ "metadata=%s" % output, "a=2", "b=3", "c=4" ])
assert 'failed' not in result
assert 'md5sum' in result
assert result['changed'] == True
outds = json.loads(file(output).read())
assert outds['c'] == '4'
# not bothering to test change hooks here since ohai/facter results change
# almost every time so changed is always true, this just tests that
# rewriting the file is ok
result = self._run('setup', [ "metadata=%s" % output, "a=2", "b=3", "c=4" ])
print "RAW RESULT=%s" % result
assert 'md5sum' in result
def test_git(self):
# TODO: tests for the git module
pass
def test_async(self):
# test async launch and job status
# of any particular module
result = self._run('command', [ get_binary("sleep"), "3" ], background=20)
assert 'ansible_job_id' in result
assert 'started' in result
jid = result['ansible_job_id']
# no real chance of this op taking a while, but whatever
time.sleep(5)
# CLI will abstract this (when polling), but this is how it works internally
result = self._run('async_status', [ "jid=%s" % jid ])
# TODO: would be nice to have tests for supervisory process
# killing job after X seconds
assert 'finished' in result
assert 'failed' not in result
assert 'rc' in result
assert 'stdout' in result
assert result['ansible_job_id'] == jid
def test_service(self):
# TODO: tests for the service module
pass
def test_fetch(self):
input_ = self._get_test_file('sample.j2')
output = os.path.join(self.stage_dir, '127.0.0.2', input_)
result = self._run('fetch', [ "src=%s" % input_, "dest=%s" % self.stage_dir ])
assert os.path.exists(output)
assert open(input_).read() == open(output).read()
def test_yum(self):
if not get_binary("yum"):
raise SkipTest
result = self._run('yum', [ "list=repos" ])
assert 'failed' not in result
def test_git(self):
# TODO: tests for the git module
pass
def test_service(self):
# TODO: tests for the service module
pass

244
test/TestUtils.py Normal file
View File

@ -0,0 +1,244 @@
# -*- coding: utf-8 -*-
import os
import unittest
import ansible.utils
class TestUtils(unittest.TestCase):
#####################################
### varLookup function tests
def test_varLookup_list(self):
vars = {
'data': {
'who': ['joe', 'jack', 'jeff']
}
}
res = ansible.utils.varLookup('data.who', vars)
assert sorted(res) == sorted(vars['data']['who'])
#####################################
### varReplace function tests
def test_varReplace_simple(self):
template = 'hello $who'
vars = {
'who': 'world',
}
res = ansible.utils.varReplace(template, vars)
assert res == 'hello world'
def test_varReplace_multiple(self):
template = '$what $who'
vars = {
'what': 'hello',
'who': 'world',
}
res = ansible.utils.varReplace(template, vars)
assert res == 'hello world'
def test_varReplace_caps(self):
template = 'hello $whoVar'
vars = {
'whoVar': 'world',
}
res = ansible.utils.varReplace(template, vars)
print res
assert res == 'hello world'
def test_varReplace_middle(self):
template = 'hello $who!'
vars = {
'who': 'world',
}
res = ansible.utils.varReplace(template, vars)
assert res == 'hello world!'
def test_varReplace_alternative(self):
template = 'hello ${who}'
vars = {
'who': 'world',
}
res = ansible.utils.varReplace(template, vars)
assert res == 'hello world'
def test_varReplace_almost_alternative(self):
template = 'hello $who}'
vars = {
'who': 'world',
}
res = ansible.utils.varReplace(template, vars)
assert res == 'hello world}'
def test_varReplace_almost_alternative2(self):
template = 'hello ${who'
vars = {
'who': 'world',
}
res = ansible.utils.varReplace(template, vars)
assert res == template
def test_varReplace_alternative_greed(self):
template = 'hello ${who} }'
vars = {
'who': 'world',
}
res = ansible.utils.varReplace(template, vars)
assert res == 'hello world }'
def test_varReplace_notcomplex(self):
template = 'hello $mydata.who'
vars = {
'data': {
'who': 'world',
},
}
res = ansible.utils.varReplace(template, vars)
print res
assert res == template
def test_varReplace_nested(self):
template = 'hello ${data.who}'
vars = {
'data': {
'who': 'world'
},
}
res = ansible.utils.varReplace(template, vars)
assert res == 'hello world'
def test_varReplace_nested_int(self):
template = '$what ${data.who}'
vars = {
'data': {
'who': 2
},
'what': 'hello',
}
res = ansible.utils.varReplace(template, vars)
assert res == 'hello 2'
def test_varReplace_unicode(self):
template = 'hello $who'
vars = {
'who': u'wórld',
}
res = ansible.utils.varReplace(template, vars)
assert res == u'hello wórld'
def test_varReplace_list(self):
template = 'hello ${data[1]}'
vars = {
'data': [ 'no-one', 'world' ]
}
res = ansible.utils.varReplace(template, vars)
assert res == 'hello world'
def test_varReplace_invalid_list(self):
template = 'hello ${data[1}'
vars = {
'data': [ 'no-one', 'world' ]
}
res = ansible.utils.varReplace(template, vars)
assert res == template
def test_varReplace_list_oob(self):
template = 'hello ${data[2]}'
vars = {
'data': [ 'no-one', 'world' ]
}
res = ansible.utils.varReplace(template, vars)
assert res == template
def test_varReplace_list_nolist(self):
template = 'hello ${data[1]}'
vars = {
'data': { 'no-one': 0, 'world': 1 }
}
res = ansible.utils.varReplace(template, vars)
assert res == template
def test_varReplace_nested_list(self):
template = 'hello ${data[1].msg[0]}'
vars = {
'data': [ 'no-one', {'msg': [ 'world'] } ]
}
res = ansible.utils.varReplace(template, vars)
assert res == 'hello world'
#####################################
### Template function tests
def test_template_basic(self):
template = 'hello {{ who }}'
vars = {
'who': 'world',
}
res = ansible.utils.template(template, vars, {}, no_engine=False)
assert res == 'hello world'
def test_template_whitespace(self):
template = 'hello {{ who }}\n'
vars = {
'who': 'world',
}
res = ansible.utils.template(template, vars, {}, no_engine=False)
assert res == 'hello world\n'
def test_template_unicode(self):
template = 'hello {{ who }}'
vars = {
'who': u'wórld',
}
res = ansible.utils.template(template, vars, {}, no_engine=False)
assert res == u'hello wórld'
#####################################
### key-value parsing
def test_parse_kv_basic(self):
assert (ansible.utils.parse_kv('a=simple b="with space" c="this=that"') ==
{'a': 'simple', 'b': 'with space', 'c': 'this=that'})

View File

@ -1,3 +1,4 @@
---
duck: quack
cow: moo
extguard: " '$favcolor' == 'blue' "

76
test/complex_hosts Normal file
View File

@ -0,0 +1,76 @@
# order of groups, children, and vars is not signficant
# so this example mixes them up for maximum testing
[nc:children]
rtp
triangle
[eastcoast:children]
nc
florida
[us:children]
eastcoast
[redundantgroup]
rtp_a
[redundantgroup2]
rtp_a
[redundantgroup3:children]
rtp
[redundantgroup:vars]
rga=1
[redundantgroup2:vars]
rgb=2
[redundantgroup3:vars]
rgc=3
[nc:vars]
b=10000
c=10001
d=10002
[rtp]
rtp_a
rtp_b
rtb_c
[rtp:vars]
a=1
b=2
c=3
[triangle]
tri_a
tri_b
tri_c
[triangle:vars]
a=11
b=12
c=13
[florida]
orlando
miami
[florida:vars]
a=100
b=101
c=102
[eastcoast:vars]
b=100000
c=100001
d=100002
[us:vars]
c=1000000

View File

@ -1,71 +0,0 @@
---
# this is an annotated example of some features available in playbooks
# it shows how to make sure packages are updated, how to make sure
# services are running, and how to template files. It also demos
# change handlers that can restart things (or trigger other actions)
# when resources change. For more advanced examples, see example2.yml
# on all hosts, run as the user root...
- hosts: all
user: root
# make these variables available inside of templates
# for when we use the 'template' action/module later on...
vars:
http_port: 80
max_clients: 200
# define the tasks that are part of this play...
tasks:
# task #1 is to run an arbitrary command
# we'll simulate a long running task, wait for up to 45 seconds, poll every 5
# obviously this does nothing useful but you get the idea
- name: longrunner
action: command /bin/sleep 15
async: 45
poll: 5
# let's demo file operations.
#
# We can 'copy' files or 'template' them instead, using jinja2
# as the templating engine. This is done using the variables
# from the vars section above mixed in with variables bubbled up
# automatically from tools like facter and ohai. 'copy'
# works just like 'template' but does not do variable subsitution.
#
# If and only if the file changes, restart apache at the very
# end of the playbook run
- name: write some_random_foo configuration
action: template src=templates/foo.j2 dest=/etc/some_random_foo.conf
notify:
- restart apache
# make sure httpd is installed at the latest version
- name: install httpd
action: yum pkg=httpd state=latest
# make sure httpd is running
- name: httpd start
action: service name=httpd state=running
# handlers are only run when things change, at the very end of each
# play. Let's define some. The names are significant and must
# match the 'notify' sections above
handlers:
# this particular handler is run when some_random_foo.conf
# is changed, and only then
- name: restart apache
action: service name=httpd state=restarted

View File

@ -1,71 +0,0 @@
---
# this is an annotated example of some features available in playbooks
# it shows how to make sure packages are updated, how to make sure
# services are running, and how to template files. It also demos
# change handlers that can restart things (or trigger other actions)
# when resources change. For more advanced examples, see example2.yml
# on all hosts, run as the user root...
- hosts: all
user: root
# make these variables available inside of templates
# for when we use the 'template' action/module later on...
vars:
http_port: 80
max_clients: 200
# define the tasks that are part of this play...
tasks:
# task #1 is to run an arbitrary command
# we'll simulate a long running task, wait for up to 45 seconds, poll every 5
# obviously this does nothing useful but you get the idea
- name: longrunner
action: command /bin/sleep 15
async: 45
poll: 5
# let's demo file operations.
#
# We can 'copy' files or 'template' them instead, using jinja2
# as the templating engine. This is done using the variables
# from the vars section above mixed in with variables bubbled up
# automatically from tools like facter and ohai. 'copy'
# works just like 'template' but does not do variable subsitution.
#
# If and only if the file changes, restart apache at the very
# end of the playbook run
- name: write some_random_foo configuration
action: template src=templates/foo.j2 dest=/etc/some_random_foo.conf
notify:
- restart apache
# make sure httpd is installed at the latest version
- name: install httpd
action: yum pkg=httpd state=latest
# make sure httpd is running
- name: httpd start
action: service name=httpd state=running
# handlers are only run when things change, at the very end of each
# play. Let's define some. The names are significant and must
# match the 'notify' sections above
handlers:
# this particular handler is run when some_random_foo.conf
# is changed, and only then
- name: restart apache
action: service name=httpd state=restarted

View File

@ -6,7 +6,9 @@
moon: titan
moon2: enceladus
- zeus
- host: zeus
vars:
- ansible_ssh_port: 3001
- group: greek
hosts:
@ -25,6 +27,11 @@
- odin
- loki
- group: ruler
hosts:
- zeus
- odin
- group: multiple
hosts:
- saturn