Compare commits

..

287 Commits
0.6 ... 0.5

Author SHA1 Message Date
22f3967f2c Bump dates in packaging files for release. 2012-07-04 13:37:46 -04:00
25504e1ae3 Merge pull request #532 from davehatton/allow_copy_if_file_doesnt_exist
md5 allow copy if file is missing
2012-07-03 12:41:20 -07:00
3350868093 md5 allow copy if file is missing 2012-07-03 20:07:08 +01:00
c5d10330f0 Merge pull request #531 from jeremysmitherman/devel
Added IndexError catch for copy module, IndexError is thrown if a destin...
2012-07-03 11:37:21 -07:00
3b3720287c Added IndexError catch for copy module, IndexError is thrown if a destination file doesn't exist 2012-07-03 14:12:19 -04:00
035b9535db Merge pull request #529 from davehatton/error_trapping_md5
improve error trapping for md5
2012-07-03 10:07:59 -07:00
05121bf380 Merge pull request #530 from dhozac/templates-vars
Allow variables in variables in templates
2012-07-03 10:06:04 -07:00
aba1fe8731 Allow variables in variables in templates 2012-07-03 16:42:00 +02:00
345499c002 improve error trapping for md5 2012-07-03 12:44:37 +01:00
12bb0859f3 Merge pull request #526 from setsuna-/devel
Fixed: executing templates outside playbooks
2012-07-02 15:55:49 -07:00
d3b2b2f6f7 Fixed: pathname for tmp 2012-07-02 22:04:56 +02:00
5bf780737a Merge pull request #524 from bradobro/authorized_key
Authorized key fixes
2012-07-02 12:39:42 -07:00
fa1c0e3ceb Fixed: executing templates outside playbooks 2012-07-02 21:34:11 +02:00
5e88673afe Fixes ansible/ansible#522, no longer creates comment in ~/.ssh/authorized_keys, does not create directory or file if state==absent. 2012-07-02 19:16:57 +00:00
84aabe25d3 Fixes ansible/ansible#523. Removed 'with:' blocks so module works with Python 2.4 for CentOS 5 support, courtesy of mcodd/ansible@29af24b732. 2012-07-02 17:57:38 +00:00
9fe39be90d Merge pull request #517 from goozbach/devel
Updated the file module to handle the case where the UID for the file doesn't exist on the system.
2012-07-02 05:18:29 -07:00
aff0403a0e remove UID/GID from output 2012-07-01 17:38:17 -04:00
c532a1c978 removed print from debug code of file module 2012-06-29 21:38:31 -04:00
f5259eead3 file when UID or GID doesn't exist 2012-06-29 20:05:23 -04:00
f70885afb0 Add some recent merges to the changelog 2012-06-29 18:56:49 -04:00
5f1c7c8e24 Fix up the output of the raw module when run within /usr/bin/ansible 2012-06-29 18:48:37 -04:00
834a56ffd7 update changelog 2012-06-29 18:44:09 -04:00
81adae619a Remove the -D module debug flag, which no longer is functional due to sudo pty requirements, and replace with -v/--verbose.
This flag will show playbook output from non-failing commands.  -v is also added to /usr/bin/ansible, but not  yet used.

I also gutted some internals code dealing with 'invocations' which allowed the callback to know what module invoked
it.  This is not something 0.5 does or needed, so callbacks have been simplified.
2012-06-19 21:57:43 -04:00
3b83a87bcc Merge pull request #512 from jhoekx/yaml-inventory-one-host
Correctly add ungrouped hosts to 'ungrouped' in YAML inventory.
2012-06-27 05:03:31 -07:00
b7cb00073b Correctly add ungrouped hosts to 'ungrouped' in YAML inventory. 2012-06-27 08:26:01 +02:00
06773dafb4 Merge pull request #511 from dsummersl/devel
added ability to ovverride all ssh settings via ANSIBLE_SSH_ARGS
2012-06-26 15:28:49 -07:00
4eb7206fed added ability to ovverride all ssh settings via ANSIBLE_SSH_ARGS 2012-06-26 14:59:42 -04:00
081808c34e Merge pull request #510 from ludovicc/devel
Add basic support for Solaris
2012-06-26 11:07:29 -07:00
608b6d9f3e Add basic support for Solaris 2012-06-26 19:49:26 +02:00
5362f18e53 Merge pull request #507 from jhoekx/convention-library-path
Add ./library to module search path.
2012-06-25 12:20:56 -07:00
4de2816208 Add ./library to module search path.
It allows for custom modules in the best practices directory structure.
Bundling custom modules along with playbooks in packages is useful.
2012-06-25 21:08:43 +02:00
a713226115 Merge pull request #506 from jhoekx/import-errors-poller
Import ansible errors in Poller.
2012-06-25 11:58:16 -07:00
62a36b67ca Merge pull request #504 from gottwald/devel-copyfix
Copy module: Error handling for missing permissions on the copy destination.
2012-06-25 11:57:50 -07:00
edaf0312b3 Import ansible errors in Poller. 2012-06-25 20:48:35 +02:00
e83b5db724 Copy module: Extended error handling and fixed error messages 2012-06-25 20:45:05 +02:00
3d5e517007 Added to copy module: Error handling for missing permissions on the copy destination 2012-06-23 21:39:37 +02:00
53360d6c10 Merge pull request #497 from davehatton/change_constants
shouldn't need a (slash) before /home/daveh
2012-06-22 12:01:15 -07:00
1b5d5feef3 Merge pull request #500 from davehatton/change_to_use_stdout
change to STDOUT from PIPE to get ssh connection module working
2012-06-22 04:41:11 -07:00
b72af8d0b9 change to STDOUT from PIPE to get ssh connection module working 2012-06-22 12:37:23 +01:00
6a201a3463 Merge pull request #498 from weaselkeeper/devel
fix syntax error in changelog that was preventing building of deb packag...
2012-06-21 11:50:34 -07:00
622deda575 fix syntax error in changelog that was preventing building of deb packages 2012-06-21 10:40:52 -07:00
61af403cf1 shouldn't need a (slash) before /home/daveh 2012-06-21 15:37:43 +01:00
6c37cbdae1 Update devel 2012-06-21 10:03:01 -03:00
eea0fc2cbd Update devel 2012-06-21 10:01:31 -03:00
78b59bc031 Update devel 2012-06-21 10:01:09 -03:00
b32b5a0620 Merge pull request #496 from davehatton/fix_stderr
change STDOUT to PIPE  to get ssh connection module working
2012-06-21 06:00:07 -07:00
5ed34b9b42 change STDOUT to PIPE to get ssh connection module working 2012-06-21 11:01:08 +01:00
deac1cdb6f Merge pull request #495 from nafeger/devel
make copy module implemented closer to other newer modules when returning results
2012-06-20 13:12:24 -07:00
97875d3d76 Merge pull request #494 from dhozac/ssh-conn-helpful
Make the SSH transport helpful
2012-06-20 13:09:50 -07:00
638f5fb0af attempt to make copy more idiomatic. 2012-06-20 14:17:15 -05:00
92dd59f641 Merge stdout and stderr for non-sudo 2012-06-20 20:49:52 +02:00
0bbea3e684 Wait for process to finish and then parse output 2012-06-20 20:49:52 +02:00
7e73d6c604 add -q to handle stdout/stderr being combined, add catch for ControlPersist not existing in ssh for rhel6, etc 2012-06-20 14:06:24 -04:00
67fc362fc6 Merge pull request #492 from dhozac/unnecessary-parse_json
Remove unnecessary parse_json invocation
2012-06-20 10:26:17 -07:00
3152fc977c Merge pull request #493 from dhozac/ssh-merge-stdouterr
Merge stdout and stderr as that is what is expected
2012-06-20 10:24:32 -07:00
705430267b Remove unnecessary parse_json invocation 2012-06-20 14:54:36 +02:00
e02070f655 update the changelog for the currently ongoing development release. 2012-06-19 21:20:17 -04:00
c0b3924c47 Merge pull request #483 from gottwald/devel-service-enhancement
Enhanced the service state recognition in the service module
2012-06-19 17:34:02 -07:00
07f5ab04ea Merge pull request #488 from dhozac/jinja2-env
Create a Jinja2 environment allowing includes
2012-06-19 13:09:02 -07:00
b067ea5a7f Merge pull request #489 from dhozac/ssh-args-default
Default to using a ControlMaster connection
2012-06-19 13:01:09 -07:00
c84e11e9d5 Default to using a ControlMaster connection 2012-06-19 21:58:24 +02:00
14c61c6f8f Merge stdout and stderr as that is what is expected
Should also fix problems where something might output an error
before the sudo prompt.
2012-06-19 21:55:00 +02:00
62997b3156 Merge pull request #487 from davehatton/fix_changelog_date
The changelog entry requires a valid date
2012-06-19 05:20:45 -07:00
6eca275695 The changelog entry requires a valid date 2012-06-19 10:51:22 +01:00
72760dd51f Merge pull request #486 from skvidal/devel
make Runner options conflict errors raise AnsibleErrors not traceback in...
2012-06-18 13:42:40 -07:00
00798736a3 make Runner options conflict errors raise AnsibleErrors not traceback in general
add catch if -c ssh and -k are passed at the same time since the ssh connection type doesn't
support passwords
2012-06-18 15:48:19 -04:00
c8f416a2c2 Merge pull request #485 from dhozac/ssh-conn
Add an ssh command wrapper transport
2012-06-18 11:17:53 -07:00
32422e9405 Update tests to reflect new API 2012-06-18 00:13:12 +02:00
fb7716bdea Create a Jinja2 environment allowing includes 2012-06-17 22:18:08 +02:00
88f2940914 Add an ssh command wrapper transport 2012-06-17 20:34:17 +02:00
d17dbc801b Enhanced the service state recognition in the service module:
- Added Upstart support
- Added an initial unknown state
- Prevented state changes when the current state is not recognized
- Changed the keyword recognition to a safer method
2012-06-17 15:55:26 +02:00
09901f41ac Merge pull request #482 from dhozac/iterative-template
Iterative template
2012-06-16 11:13:11 -07:00
682e728ebf Add tests of iterative templating 2012-06-16 13:13:01 +02:00
545b4b63d8 Run templating function until the text doesn't change
This allows variables to contain other variables.
2012-06-16 12:51:19 +02:00
ce838e0755 Merge pull request #478 from dhozac/not-host
Allow exclusion of hosts/groups
2012-06-15 17:39:47 -07:00
b4ebd75dbc Merge pull request #481 from jonten/devel
Fixed typo from results to result in _execute_raw method
2012-06-15 17:39:19 -07:00
8acc5cfc13 Fixed typo from results to result in _execute_raw method 2012-06-15 22:45:47 +02:00
e30d931062 Add tests of host exclusions 2012-06-15 19:09:04 +02:00
dfc70a9852 Merge pull request #477 from phred/md5sum-on-bsd
md5sum on bsd
2012-06-15 04:40:51 -07:00
42aecd33f8 Allow exclusion of hosts/groups 2012-06-15 11:01:30 +02:00
eda543f4a5 Remove unused md5sum function from file module 2012-06-15 00:13:35 -04:00
e6b76a59c5 Make shell outs to md5sum work on FreeBSD and OS X
Tested with OS X local connection and Linux remote. The paths to the
md5sum and md5 commands are hardcoded to the most common location. This
will definitely fail if the commands are elsewhere, or if the md5
command doesn't support the -q 'quiet' option.
2012-06-15 00:04:01 -04:00
0fb59f0bdd Merge pull request #475 from nafeger/devel
Better error messaging in utils
2012-06-14 12:15:36 -07:00
8aa26400ea Better error messaging in utils
When a command responds with json that is unparseable, dump that unpareseable response instead of swallowing it.
2012-06-14 13:17:38 -05:00
5bd95a9504 Merge pull request #474 from sfromm/issue471
Issue471 - import hashlib to sidestep deprecation warnings
2012-06-14 09:40:20 -07:00
14dc12cdaf Conditionally import hashlib (issue #471)
Import hashlib if it is there, otherwise import md5.  Adds method
file_digest that wraps the logic on which module to invoke.
2012-06-14 07:18:46 -07:00
2b1b8d219a Merge branch 'mktemp-vanish' of https://github.com/dagwieers/ansible into devel
Conflicts:
	lib/ansible/runner/__init__.py
2012-06-13 22:39:58 -04:00
4d74c770db Merge pull request #469 from nafeger/devel
Add support for older version of python-apt
2012-06-13 19:21:31 -07:00
2d6caa46fc Merge pull request #462 from tima/more-env-defaults
Most constants can be overriden with environment variables.
2012-06-13 19:20:33 -07:00
8c961e2003 Merge pull request #461 from dhozac/only_if-early
Evaluate only_if early
2012-06-13 19:19:25 -07:00
d95e8846f8 Merge pull request #459 from dhozac/with_items-variable
Allow a variable in with_items
2012-06-13 19:18:51 -07:00
509f657ac8 Merge pull request #463 from sfromm/assemble
Assemble module
2012-06-13 19:17:47 -07:00
9b7e7092a6 Add support for older version of python-apt 2012-06-13 13:57:47 -05:00
86d03713cb Add assemble module
This adds a module that concatenates (ie. assembles) a file from
fragments in a directory in alphabetical order.  It chains the file
module afterward to fix up ownership and permission.  This also adds
tests for the assemble module with fragments in assemble.d.
2012-06-11 23:09:54 -07:00
db2f47f6ae Most constants can be overriden with environment variables. Reserved doing anything with the connection options for further review. 2012-06-11 18:07:44 -04:00
2371bf9aaf Fix on_unreachable invocation with non-existing variable 2012-06-11 15:15:29 +02:00
1244054dfb Evaluate only_if early to do as little work as possible 2012-06-11 15:15:29 +02:00
e88d62634f Merge pull request #460 from emgee/module-paths-2
Search multiple paths for modules.
2012-06-10 09:56:21 -07:00
5fe75238fa Search multiple paths for modules.
Minimal change to allow a list of paths (separated by the typical path
separator) to be searched in sequence for the named module.
2012-06-10 16:22:59 +01:00
ba1e373034 Allow with_items to specify a variable 2012-06-09 21:59:22 +02:00
4e254f23d9 Reduce code duplication 2012-06-09 21:50:46 +02:00
c1b02d7805 Merge pull request #455 from dhozac/vars_files-non-list
Expand vars_files identically for lists and non-lists
2012-06-08 12:19:47 -07:00
ef074f3b61 Merge pull request #451 from phred/fix-fetch-module
Make 'fetch' test for local directories before creating.  Fixes issue #450
2012-06-08 12:06:02 -07:00
146f592eae Merge pull request #454 from jhoekx/configure-python-interpreter
Introduce ansible_python_interpreter variable.
2012-06-08 12:03:40 -07:00
4cf057a39e Merge pull request #456 from dhozac/notify-variable
Allow variables in notify list
2012-06-08 11:59:39 -07:00
0d4aa85852 Merge pull request #457 from dhozac/module_vars-consistent
Expand $item for copy and fetch
2012-06-08 08:55:20 -07:00
053007ef4f Expand $item for copy and fetch 2012-06-08 01:36:52 +02:00
3cf749ea95 Allow variables in notify list 2012-06-08 01:35:23 +02:00
b21171c08d Expand vars_files identically for lists and non-lists 2012-06-08 01:32:56 +02:00
2023da889d Introduce ansible_python_interpreter variable.
This allows configuration of the correct python interpreter on the managed system.
2012-06-07 09:50:38 +02:00
9187f654fe Get rid of mktemp dependency to support AIX
The function call has been renamed to better reflect what it does, and we
reduced the number of calls from two to one in case the remote user is not
root.

This patch also fixes a string concatenation in _copy_module() that
should use os.path.join() instead.

This closes #436
2012-06-06 14:47:47 +02:00
9a234ff9a2 Fix exception in fetch module when src or dest parameter omitted. 2012-06-05 11:35:19 -04:00
859510ae00 Fix remote md5 in fetch module, related to Issue #450 2012-06-05 11:35:19 -04:00
a6dccd63a1 Make 'fetch' test for local directories before creating. Fixes issue #450 2012-06-05 10:38:12 -04:00
2b43a99927 Merge pull request #448 from jkleint/better-ssh-error
More helpful message when ssh fails.
2012-06-01 14:49:36 -07:00
56de49b725 More helpful message when ssh fails. 2012-06-01 17:16:02 -04:00
0c0d8f5db2 capture possible chown stderr output 2012-05-31 22:19:05 -04:00
56b78eb70f Fix parsing error in file module when returning errors. 2012-05-31 22:14:33 -04:00
484adca241 To allow plays using different user accounts in the same playbook to function, clear the setup cache
between plays.
2012-05-31 22:08:00 -04:00
8b38b19b6d Revert "Get rid of mktemp dependency to support AIX"
This reverts commit 4ad1b57b52.
2012-05-31 21:59:31 -04:00
08112eca7b Merge pull request #426 from dagwieers/facts
Add BIOS information and fix VMware detection (supports both IDE and SCSI)
2012-05-31 18:45:52 -07:00
78d269d038 Fix for older version of sudo + some changes to imports so tests are happy 2012-05-31 21:44:56 -04:00
bc715d065f Merge pull request #437 from dagwieers/mktemp-vanish
Get rid of mktemp dependency to support AIX
2012-05-31 18:37:28 -07:00
7fa93c5041 Merge pull request #441 from jkleint/devel
Properly template list of hosts in playbooks.
2012-05-31 18:34:25 -07:00
05f5734053 Properly template list of hosts in playbooks.
In playbooks, hosts can be a YAML list. We templated the list before
converting it to a semicolon-separated string, which actually templated its
repr. This converts to a string first. A basic unit test is included.
2012-05-31 16:06:21 -04:00
4ad1b57b52 Get rid of mktemp dependency to support AIX
The function call has been renamed to better reflect what it does, and we
reduced the number of calls from two to one in case the remote user is not
root.

This patch also fixes a string concatenation in _copy_module() that
should use os.path.join()

This closes #436
2012-05-31 17:02:10 +02:00
a272a8cc02 Split connection code into submodules. 2012-05-30 20:16:31 -04:00
80852f8626 Leverage cowsay if installed. 2012-05-30 19:44:00 -04:00
a604463f32 Merge pull request #433 from bradobro/authorized_key
Renamed module, fixed idempotency, removed debug param.
2012-05-30 13:59:13 -07:00
e7f9d6adf4 Merge pull request #429 from sfromm/facts
Fix for get_selinux_facts
2012-05-30 13:58:03 -07:00
7492ee151f Renamed module, fixed idempotency, removed debug param. 2012-05-30 16:41:38 -04:00
da6f453c6f Merge pull request #431 from bradobro/fix_tasksafterfailure
Fix to make 0.5 hosts with failures not be included in future task executions
2012-05-30 13:08:09 -07:00
e90a022b98 Removed legacy comment. 2012-05-30 15:37:14 -04:00
26c95cc280 Fixed Inventory.get_hosts() ignoring restriction when there are no hosts left.
get_hosts() was treating [] (meaning complete restriction, no hosts allowed)
the same as None (meaning no restriction, all hosts allowed). Fixed logic.
2012-05-30 15:34:21 -04:00
493ec14ccb Update get_selinux_facts in setup module
This switches to using selinux library calls instead of parsing the
output of sestatus.  This fixes issue #428 where the output was slightly
different than expected on F17.  Tested against debian (non-selinux),
centos5, centos6, and fedora17.
2012-05-30 08:42:05 -07:00
cb07728109 Add BIOS information and fix VMware detection (supports both IDE and SCSI disks) 2012-05-30 14:23:52 +02:00
c92ddad517 Merge pull request #420 from bradobro/fix_no_varfiles
Test for play.vars_files == None, avoiding nasty traceback.
2012-05-29 08:32:47 -07:00
84787f4583 Merge pull request #421 from bradobro/fix_ignore_no_vars
Avoid traceback when vars file has no vars items. Could warn or fail instead.
2012-05-29 08:31:37 -07:00
6927097051 Avoid traceback when vars file has no vars items. Could warn or fail instead. 2012-05-29 10:23:09 -04:00
67ddf704ba Test for play.vars_files == None, avoiding nasty traceback. Ignores for now. Could warn.
Playbook test for no vars_files with len(), but that excepts if play.vars_files==None, as can happen when there's a vars_files section with no vars files listed. What is the ansible way: ignore, warn, or fail with message (instead of traceback)?
2012-05-29 10:09:38 -04:00
87c0555602 Merge pull request #419 from jtacoma/devel
Listed python sub-packages in setup.py.
2012-05-29 04:45:29 -07:00
4f4e90cbd8 Listed python sub-packages in setup.py. 2012-05-28 22:06:38 -04:00
b1cf36087d Merge pull request #415 from lcosmin/0.5-fixes
Removed line that caused stacktrace
2012-05-27 10:45:39 -07:00
f38aae6346 Removed line that caused stacktrace 2012-05-27 14:15:30 +03:00
49f90d8740 Get with_items to work with new play/task architecture. 2012-05-26 01:49:23 -04:00
f6961e9364 Tweaks on previous refactoring of playbook, version bump a 0.4 reference, remove some debug, etc 2012-05-26 01:20:53 -04:00
fb261f94b7 Playbook refactoring -- work in progress. 2012-05-26 00:37:34 -04:00
0f6a18f2e2 Reorganizing file structure. Not done. 2012-05-25 19:34:13 -04:00
ba24210564 Delete poller code from runner.py now that it is moved. 2012-05-25 19:20:01 -04:00
e87e872bd3 Fix casing/underscore convention in method name, split polling logic away from runner. 2012-05-25 19:18:02 -04:00
e806f7ec4c As part of a precursor to other refactoring, make returns less list-like throughout runner. 2012-05-25 18:44:29 -04:00
6446bad069 Move version files to 0.5 for devel branch 2012-05-25 16:43:16 -04:00
56c6268384 changelog updates 2012-05-23 20:23:46 -04:00
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
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
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
87 changed files with 3326 additions and 5711 deletions

View File

@ -1,67 +1,7 @@
Ansible Changes By Release
==========================
0.6 "Cabo" ------------ pending
playbooks:
* support to tag tasks and includes and use --tags in playbook CLI
* playbooks can now include other playbooks (example/playbooks/nested_playbooks.yml)
* vars_files now usable with with_items, provided file paths don't contain host specific facts
* error reporting if with_items value is unbound
* with_items no longer creates lots of tasks, creates one task that makes multiple calls
* can use host_specific facts inside with_items (see above)
* at the top level of a playbook, set 'gather_facts: False' to skip fact gathering
* first_available_file and with_items used together will now raise an error
* to catch typos, like 'var' for 'vars', playbooks and tasks now yell on invalid parameters
* automatically load (directory_of_inventory_file)/group_vars/groupname and /host_vars/hostname in vars_files
* playbook is now colorized, set ANSIBLE_NOCOLOR=1 if you do not like this, does not colorize if not a TTY
* hostvars now preserved between plays (regression in 0.5 from 0.4), useful for sharing vars in multinode configs
* ignore_errors: True on a task can be used to allow a task to fail and not stop the play
* with_items with the apt/yum module will install/remove/update everything in a single command
inventory:
* groups variable available as a hash to return the hosts in each group name
* in YAML inventory, hosts can list their groups in inverted order now also (see tests/yaml_hosts)
* YAML inventory is deprecated and will be removed in 0.7
* ec2 inventory script
* support ranges of hosts in the host file, like www[001-100].example.com (supports leading zeros and also not)
modules:
* fetch module now does not fail a system when requesting file paths (ex: logs) that don't exist
* apt module now takes an optional install-recommends=yes|no (default yes)
* fixes to the return codes of the copy module
* copy module takes a remote md5sum to avoid large file transfer
* various user and group module fixes (error handling, etc)
* apt module now takes an optional force parameter
* slightly better psychic service status handling for the service module
* fetch module fixes for SSH connection type
* modules now consistently all take yes/no for boolean parameters (and DWIM on true/false/1/0/y/n/etc)
* setup module no longer saves to disk, template module now only used in playbooks
* setup module no longer needs to run twice per playbook
* apt module now passes DEBIAN_FRONTEND=noninteractive
* mount module (manages active mounts + fstab)
* setup module fixes if no ipv6 support
* internals: template in common module boilerplate, also causes less SSH operations when used
* git module fixes
* setup module overhaul, more modular
* minor caching logic added to inventory to reduce hammering of inventory scripts.
* MySQL and PostgreSQL modules for user and db management
* vars_prompt now supports private password entry (see examples/playbooks/prompts.yml)
* yum module modified to be more tolerant of plugins spewing random console messages (ex: RHN)
internals:
* when sudoing to root, still use /etc/ansible/setup as the metadata path, as if root
* paramiko is now only imported if needed when running from source checkout
* cowsay support on Ubuntu
* various ssh connection fixes for old Ubuntu clients
* ./hacking/test-module now supports options like ansible takes and has a debugger mode
* sudoing to a user other than root now works more seamlessly (uses /tmp, avoids umask issues)
0.5 "Amsterdam" ------- July 04, 2012
0.5 "Amsterdam" ------- July XX, 2012
* Service module gets more accurate service states when running with upstart
* Jinja2 usage in playbooks (not templates), reinstated, supports %include directive

View File

@ -6,7 +6,7 @@
# useful targets:
# make sdist ---------------- produce a tarball
# make rpm ----------------- produce RPMs
# make deb ------------------ produce a DEB
# make debian --------------- produce a dpkg (FIXME?)
# make docs ----------------- rebuild the manpages (results are checked in)
# make tests ---------------- run the tests
# make pyflakes, make pep8 -- source code checks
@ -28,21 +28,13 @@ SITELIB = $(shell python -c "from distutils.sysconfig import get_python_lib; pri
# VERSION file provides one place to update the software version
VERSION := $(shell cat VERSION)
### Get the branch information from git
ifneq ($(shell which git),)
GIT_DATE := $(shell git log -n 1 --format="%ai")
endif
DATE := $(shell date --date="$(GIT_DATE)" +%Y%m%d%H%M)
# RPM build parameters
RPMSPECDIR= packaging/rpm
RPMSPEC = $(RPMSPECDIR)/ansible.spec
RPMVERSION := $(shell awk '/Version/{print $$2; exit}' < $(RPMSPEC) | cut -d "%" -f1)
RPMRELEASE := $(shell awk '/Release/{print $$2; exit}' < $(RPMSPEC) | cut -d "%" -f1)
RPMDIST = $(shell rpm --eval '%dist')
RPMRELEASE = 1
ifeq ($(OFFICIAL),)
RPMRELEASE = 0.git$(DATE)
endif
RPMNVR = "$(NAME)-$(VERSION)-$(RPMRELEASE)$(RPMDIST)"
RPMNVR = "$(NAME)-$(RPMVERSION)-$(RPMRELEASE)$(RPMDIST)"
########################################################
@ -110,7 +102,6 @@ sdist: clean
rpmcommon: sdist
@mkdir -p rpm-build
@cp dist/*.gz rpm-build/
@sed -e 's#^Version:.*#Version: $(VERSION)#' -e 's#^Release:.*#Release: $(RPMRELEASE)%{?dist}#' $(RPMSPEC) >rpm-build/$(NAME).spec
srpm: rpmcommon
@rpmbuild --define "_topdir %(pwd)/rpm-build" \
@ -119,8 +110,7 @@ srpm: rpmcommon
--define "_srcrpmdir %{_topdir}" \
--define "_specdir $(RPMSPECDIR)" \
--define "_sourcedir %{_topdir}" \
-bs rpm-build/$(NAME).spec
@rm -f rpm-build/$(NAME).spec
-bs $(RPMSPEC)
@echo "#############################################"
@echo "Ansible SRPM is built:"
@echo " rpm-build/$(RPMNVR).src.rpm"
@ -133,12 +123,10 @@ rpm: rpmcommon
--define "_srcrpmdir %{_topdir}" \
--define "_specdir $(RPMSPECDIR)" \
--define "_sourcedir %{_topdir}" \
--define "_rpmfilename $(RPMNVR).%%{ARCH}.rpm" \
-ba rpm-build/$(NAME).spec
@rm -f rpm-build/$(NAME).spec
-ba $(RPMSPEC)
@echo "#############################################"
@echo "Ansible RPM is built:"
@echo " rpm-build/$(RPMNVR).noarch.rpm"
@echo " rpm-build/noarch/$(RPMNVR).noarch.rpm"
@echo "#############################################"
debian: sdist

View File

@ -4,7 +4,7 @@ Ansible
Ansible is a radically simple configuration-management, deployment, task-execution, and
multinode orchestration framework.
Read the documentation at http://ansible.github.com
Read all about at it at (http://ansible.github.com)
Design Principles
=================
@ -27,11 +27,9 @@ Get Involved
Branch Info
===========
* Releases are named after Van Halen songs.
* The master branch corresponds to release 0.5 "Amsterdam".
* The devel branch corresponds to release 0.6 "Cabo".
* All feature work happens on the development branch.
* Major bug fixes will be made to the master branch, but not minor ones.
* The master branch corresponds to release 0.4 "Unchained".
* Major bug fixes will be periodically made to the master branch.
* The devel branch corresponds to release 0.5 "Amsterdam".
* See CHANGELOG.md for release notes to track each release.
Patch Instructions
@ -45,7 +43,6 @@ Contributions to the core and modules are greatly welcome.
* Make sure "make tests" passes before submitting any requests.
* Bonus points:
* Joining the mailing list
* Fixing bugs instead of sending bug reports.
* Using squash merges
* Updating the "rst/*" files in the docs project and "docs/" manpage content
* Adding more unit tests
@ -54,7 +51,6 @@ Contributions to the core and modules are greatly welcome.
* 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.6
0.5

View File

@ -18,7 +18,6 @@
#######################################################
import os
import sys
import getpass
@ -27,25 +26,6 @@ import ansible.constants as C
from ansible import errors
from ansible import callbacks
from ansible import utils
from ansible.color import ANSIBLE_COLOR, stringc
def colorize(lead, num, color):
""" Print 'lead' = 'num' in 'color' """
if num != 0 and ANSIBLE_COLOR:
return "%s%s%-15s" % (stringc(lead, color), stringc("=", color), stringc(str(num), color))
else:
return "%s=%-4s" % (lead, str(num))
def hostcolor(host, stats):
if ANSIBLE_COLOR:
if stats['failures'] != 0 or stats['unreachable'] != 0:
return "%-41s" % stringc(host, 'red')
elif stats['changed'] != 0:
return "%-41s" % stringc(host, 'yellow')
else:
return "%-41s" % stringc(host, 'green')
return "%-30s" % host
def main(args):
''' run ansible-playbook operations '''
@ -55,8 +35,6 @@ def main(args):
parser = utils.base_parser(constants=C, usage=usage, connect_opts=True, runas_opts=True)
parser.add_option('-e', '--extra-vars', dest="extra_vars", default=None,
help="set additional key=value variables from the CLI")
parser.add_option('-t', '--tags', dest='tags', default='all',
help="only run plays and tasks tagged with these values")
options, args = parser.parse_args(args)
@ -75,7 +53,6 @@ def main(args):
options.sudo = True
options.sudo_user = options.sudo_user or C.DEFAULT_SUDO_USER
extra_vars = utils.parse_kv(options.extra_vars)
only_tags = options.tags.split(",")
# run all playbooks specified on the command line
for playbook in args:
@ -101,8 +78,7 @@ def main(args):
sudo_user=options.sudo_user,
sudo_pass=sudopass,
extra_vars=extra_vars,
private_key_file=options.private_key_file,
only_tags=only_tags,
private_key_file=options.private_key_file
)
try:
@ -111,13 +87,9 @@ def main(args):
print callbacks.banner("PLAY RECAP")
for h in hosts:
t = pb.stats.summarize(h)
print "%-30s : %s %s %s %s " % (
hostcolor(h, t),
colorize('ok', t['ok'], 'green'),
colorize('changed', t['changed'], 'yellow'),
colorize('unreachable', t['unreachable'], 'red'),
colorize('failed', t['failures'], 'red'))
print "%-30s : ok=%4s changed=%4s unreachable=%4s failed=%4s " % (h,
t['ok'], t['changed'], t['unreachable'], t['failures']
)
print "\n"
except errors.AnsibleError, e:

View File

@ -2,12 +2,12 @@
.\" Title: ansible-playbook
.\" Author: [see the "AUTHOR" section]
.\" Generator: DocBook XSL Stylesheets v1.75.2 <http://docbook.sf.net/>
.\" Date: 07/24/2012
.\" Date: 06/19/2012
.\" Manual: System administration commands
.\" Source: Ansible 0.6
.\" Source: Ansible 0.5
.\" Language: English
.\"
.TH "ANSIBLE\-PLAYBOOK" "1" "07/24/2012" "Ansible 0\&.6" "System administration commands"
.TH "ANSIBLE\-PLAYBOOK" "1" "06/19/2012" "Ansible 0\&.5" "System administration commands"
.\" -----------------------------------------------------------------
.\" * set default formatting
.\" -----------------------------------------------------------------

View File

@ -2,12 +2,12 @@
.\" Title: ansible
.\" Author: [see the "AUTHOR" section]
.\" Generator: DocBook XSL Stylesheets v1.75.2 <http://docbook.sf.net/>
.\" Date: 07/24/2012
.\" Date: 06/19/2012
.\" Manual: System administration commands
.\" Source: Ansible 0.6
.\" Source: Ansible 0.5
.\" Language: English
.\"
.TH "ANSIBLE" "1" "07/24/2012" "Ansible 0\&.6" "System administration commands"
.TH "ANSIBLE" "1" "06/19/2012" "Ansible 0\&.5" "System administration commands"
.\" -----------------------------------------------------------------
.\" * set default formatting
.\" -----------------------------------------------------------------
@ -140,9 +140,7 @@ Connection type to use\&. Possible options are
.RE
.SH "INVENTORY"
.sp
Ansible stores the hosts it can potentially operate on in an inventory file\&. The syntax is one host per line\&. Groups headers are allowed and are included on their own line, enclosed in square brackets that start the line\&.
.sp
Ranges of hosts are also supported\&. For more information and additional options, see the documentation on http://ansible\&.github\&.com/\&.
Ansible stores the hosts it can potentially operate on in an inventory file\&. The syntax is one host per line\&. Groups headers are allowed and are included on their own line, enclosed in square brackets\&.
.SH "FILES"
.sp
/etc/ansible/hosts \(em Default inventory file

View File

@ -31,6 +31,7 @@ A name of a group in the inventory file, a shell-like glob selecting
hosts in inventory file, or any combination of the two separated by
semicolons.
OPTIONS
-------
@ -111,11 +112,7 @@ INVENTORY
Ansible stores the hosts it can potentially operate on in an inventory
file. The syntax is one host per line. Groups headers are allowed and
are included on their own line, enclosed in square brackets that
start the line.
Ranges of hosts are also supported. For more information and
additional options, see the documentation on http://ansible.github.com/.
are included on their own line, enclosed in square brackets.
FILES
-----

View File

@ -7,38 +7,31 @@
# - Groups of hosts are delimited by [header] elements
# - You can enter hostnames or ip addresses
# - A hostname/ip can be a member of multiple groups
#
# Ex 1: Ungrouped hosts, specify before any group headers.
green.example.com
blue.example.com
green.bikeshed.org
blue.bikeshed.org
red.bikeshed.org
bikeshed.org
bastion.secure.bikeshed.org
192.168.100.1
192.168.100.10
# Ex 2: A collection of hosts belonging to the 'webservers' group
[webservers]
alpha.example.org
beta.example.org
www01.bikeshed.org
www02.bikeshed.org
wheel.colors.com
192.168.1.100
192.168.1.110
# If you have multiple hosts following a pattern you can specify
# them like this:
www[001:006].example.com
# Your personal website also runs a webserver:
myserver.com
# Ex 3: A collection of database servers in the 'dbservers' group
[dbservers]
db01.intranet.mydomain.net
db02.intranet.mydomain.net
10.25.1.56
db02.intranet.mydomain.net
10.25.1.57
# Here's another example of host ranges, this time there are no
# leading 0s:
db-[99:101]-node.example.com
# Perhaps you serve a db off your personal server too:
myserver.com

View File

@ -1,16 +0,0 @@
---
- hosts: webservers
vars:
- jquery_directory: /var/www/html/javascript
- person: 'Susie%20Smith'
tasks:
- name: Create directory for jQuery
action: file dest=${jquery_directory} state=directory mode=0755
- name: Grab a bunch of jQuery stuff
action: get_url url=http://code.jquery.com/$item dest=${jquery_directory} mode=0444
with_items:
- jquery.min.js
- mobile/latest/jquery.mobile.min.js
- ui/jquery-ui-git.css
#- name: Pass urlencoded name to CGI
# action: get_url url=http://example.com/name.cgi?name='${person}' dest=/tmp/test

View File

@ -1,21 +0,0 @@
##
# Example Ansible playbook that uses the MySQL module.
#
---
- hosts: all
user: root
vars:
mysql_root_password: 'password'
tasks:
- name: Create database user
action: mysql_user loginpass=$mysql_root_password user=bob passwd=12345 priv=*.*:ALL state=present
- name: Create database
action: mysql_db loginpass=$mysql_root_password db=bobdata state=present
- name: Ensure no user named 'sally' exists
action: mysql_user loginpass=$mysql_root_password user=sally state=absent

View File

@ -1,26 +0,0 @@
---
# it is possible to have top level playbook files import other playbook
# files. For example, a playbook called could include three
# different playbooks, such as webservers, workers, dbservers, etc.
#
# Running the site playbook would run all playbooks, while individual
# playbooks could still be run directly. This is somewhat like
# the tag feature and can be used in conjunction for very fine grained
# control over what you want to target when running ansible.
- name: this is a play at the top level of a file
hosts: all
user: root
tasks:
- name: say hi
tags: foo
action: shell echo "hi..."
# and this is how we include another playbook, be careful and
# don't recurse infinitely or anything. Note you can't use
# any variables here.
- include: intro_example.yml
# and if we wanted, we can continue with more includes here,
# or more plays inline in this file

View File

@ -1,38 +0,0 @@
##
# Example Ansible playbook that uses the PostgreSQL module.
#
# This installs PostgreSQL on an Ubuntu system, creates a database called
# "myapp" and a user called "django" with password "mysupersecretpassword"
# with access to the "myapp" database.
#
---
- hosts: webservers
sudo: True
gather_facts: False
tasks:
- name: ensure apt cache is up to date
action: apt update_cache=yes
- name: ensure packages are installed
action: apt pkg=$item
with_items:
- postgresql
- libpq-dev
- python-psycopg2
- hosts: webservers
sudo: True
sudo_user: postgres
gather_facts: False
vars:
dbname: myapp
dbuser: django
dbpassword: mysupersecreetpassword
tasks:
- name: ensure database is created
action: postgresql_db db=$dbname
- name: ensure user has access to database
action: postgresql_user db=$dbname user=$dbuser password=$dbpassword

View File

@ -12,32 +12,22 @@
this_is_a_regular_var: 'moo'
so_is_this: 'quack'
# prompted variables are a list of variable names and a description
# that will be presented to the user.
#
# alternatively, they can ALSO be passed in from the outside:
# ansible-playbook foo.yml --extra-vars="foo=100 bar=101"
# or through external inventory scripts (see online API docs)
# here's basic mode prompting. Specify a hash of variable names and a prompt for
# each.
#
# vars_prompt:
# release_version: "product release version"
# prompts can also be specified like this, allowing for hiding the prompt as
# entered. In the future, this may also be used to support crypted variables
vars_prompt:
- name: "some_password"
prompt: "Enter password"
private: True
- name: "release_version"
prompt: "Product release version"
private: False
release_version: "product release version"
tasks:
# this is just a simple example to show that vars_prompt works, but
# you might ask for a tag to use with the git module or perhaps
# a package version to use with the yum module.
tasks:
- name: imagine this did something interesting with $release_version
action: shell echo foo >> /tmp/$release_version-$alpha

View File

@ -1,44 +0,0 @@
---
# tags allow us to run all of a playbook or part of it.
#
# assume: ansible-playbook tags.yml --tags foo
#
# try this with:
# --tags foo
# --tags bar
# --tags extra
#
# the value of a 'tags:' element can be a string or list
# of tag names. Variables are not usable in tag names.
- name: example play one
hosts: all
user: root
# any tags applied to the play are shorthand to applying
# the tag to all tasks in it. Here, each task is given
# the tag extra
tags:
- extra
tasks:
# this task will run if you don't specify any tags,
# if you specify 'foo' or if you specify 'extra'
- name: hi
tags: foo
action: shell echo "first task ran"
- name: example play two
hosts: all
user: root
tasks:
- name: hi
tags:
- bar
action: shell echo "second task ran"
- include: tasks/base.yml tags=base

View File

@ -1,10 +1,4 @@
# This is a very simple Jinja2 template representing an imaginary configuration file
# for an imaginary app.
# this is an example of loading a fact from the setup module
system={{ ansible_system }}
# here is a variable that could be set in a playbook or inventory file
http_port={{ http_port }}

View File

@ -1,3 +1,3 @@
---
packager: apt
packager: aptitude
apache: apache

View File

@ -69,7 +69,7 @@ conn = xmlrpclib.Server("http://127.0.0.1/cobbler_api", allow_none=True)
# executed with no parameters, return the list of
# all groups and hosts
if len(sys.argv) == 2 and (sys.argv[1] == '--list'):
if len(sys.argv) == 1:
systems = conn.get_item_names('system')
groups = { 'ungrouped' : [] }
@ -103,10 +103,10 @@ if len(sys.argv) == 2 and (sys.argv[1] == '--list'):
# executed with a hostname as a parameter, return the
# variables for that host
elif len(sys.argv) == 3 and (sys.argv[1] == '--host'):
if len(sys.argv) == 2:
# look up the system record for the given DNS name
result = conn.find_system_by_dns_name(sys.argv[2])
result = conn.find_system_by_dns_name(sys.argv[1])
system = result.get('name', None)
data = {}
if system is None:
@ -125,7 +125,3 @@ elif len(sys.argv) == 3 and (sys.argv[1] == '--host'):
print json.dumps(results)
sys.exit(0)
else:
print "usage: --list ..OR.. --host <hostname>"
sys.exit(1)

View File

@ -1,42 +0,0 @@
# Ansible EC2 external inventory script settings
#
[ec2]
# AWS regions to make calls to. Set this to 'all' to make request to all regions
# in AWS and merge the results together. Alternatively, set this to a comma
# separated list of regions. E.g. 'us-east-1,us-west-1,us-west-2'
regions = all
# When generating inventory, Ansible needs to know how to address a server.
# Each EC2 instance has a lot of variables associated with it. Here is the list:
# http://docs.pythonboto.org/en/latest/ref/ec2.html#module-boto.ec2.instance
# Below are 2 variables that are used as the address of a server:
# - destination_variable
# - vpc_destination_variable
# This is the normal destination variable to use. If you are running Ansible
# from outside EC2, then 'public_dns_name' makes the most sense. If you are
# running Ansible from within EC2, then perhaps you want to use the internal
# address, and should set this to 'private_dns_name'.
destination_variable = public_dns_name
# For server inside a VPC, using DNS names may not make sense. When an instance
# has 'subnet_id' set, this variable is used. If the subnet is public, setting
# this to 'ip_address' will return the public IP address. For instances in a
# private subnet, this should be set to 'private_ip_address', and Ansible must
# be run from with EC2.
vpc_destination_variable = ip_address
# API calls to EC2 are slow. For this reason, we cache the results of an API
# call. Set this to the path you want cache files to be written to. Two files
# will be written to this directory:
# - ansible-ec2.cache
# - ansible-ec2.index
cache_path = /tmp
# The number of seconds a cache file is considered valid. After this many
# seconds, a new API call will be made, and the cache file will be updated.
cache_max_age = 300

View File

@ -1,377 +0,0 @@
#!/usr/bin/python -tt
'''
EC2 external inventory script
=================================
Generates inventory that Ansible can understand by making API request to
AWS EC2 using the Boto library.
NOTE: This script assumes Ansible is being executed where the environment
variables needed for Boto have already been set:
export AWS_ACCESS_KEY_ID='AK123'
export AWS_SECRET_ACCESS_KEY='abc123'
For more details, see: http://docs.pythonboto.org/en/latest/boto_config_tut.html
When run against a specific host, this script returns the following variables:
- ec2_ami_launch_index
- ec2_architecture
- ec2_association
- ec2_attachTime
- ec2_attachment
- ec2_attachmentId
- ec2_client_token
- ec2_deleteOnTermination
- ec2_description
- ec2_deviceIndex
- ec2_dns_name
- ec2_eventsSet
- ec2_group_name
- ec2_hypervisor
- ec2_id
- ec2_image_id
- ec2_instanceState
- ec2_instance_type
- ec2_ipOwnerId
- ec2_ip_address
- ec2_item
- ec2_kernel
- ec2_key_name
- ec2_launch_time
- ec2_monitored
- ec2_monitoring
- ec2_networkInterfaceId
- ec2_ownerId
- ec2_persistent
- ec2_placement
- ec2_platform
- ec2_previous_state
- ec2_private_dns_name
- ec2_private_ip_address
- ec2_publicIp
- ec2_public_dns_name
- ec2_ramdisk
- ec2_reason
- ec2_region
- ec2_requester_id
- ec2_root_device_name
- ec2_root_device_type
- ec2_security_group_ids
- ec2_security_group_names
- ec2_shutdown_state
- ec2_sourceDestCheck
- ec2_spot_instance_request_id
- ec2_state
- ec2_state_code
- ec2_state_reason
- ec2_status
- ec2_subnet_id
- ec2_tenancy
- ec2_virtualization_type
- ec2_vpc_id
These variables are pulled out of a boto.ec2.instance object. There is a lack of
consistency with variable spellings (camelCase and underscores) since this
just loops through all variables the object exposes. It is preferred to use the
ones with underscores when multiple exist.
In addition, if an instance has AWS Tags associated with it, each tag is a new
variable named:
- ec2_tag_[Key] = [Value]
Security groups are comma-separated in 'ec2_security_group_ids' and
'ec2_security_group_names'.
'''
# (c) 2012, Peter Sankauskas
#
# 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 argparse
import re
from time import time
from boto import ec2
import ConfigParser
try:
import json
except ImportError:
import simplejson as json
class Ec2Inventory(object):
def __init__(self):
''' Main execution path '''
# Inventory grouped by instance IDs, tags, security groups, regions,
# and availability zones
self.inventory = {}
# Index of hostname (address) to instance ID
self.index = {}
# Read settings and parse CLI arguments
self.read_settings()
self.parse_cli_args()
# Cache
if self.args.refresh_cache:
self.do_api_calls_update_cache()
elif not self.is_cache_valid():
self.do_api_calls_update_cache()
# Data to print
if self.args.host:
data_to_print = self.get_host_info()
elif self.args.list:
# Display list of instances for inventory
if len(self.inventory) == 0:
data_to_print = self.get_inventory_from_cache()
else:
data_to_print = self.json_format_dict(self.inventory, True)
print data_to_print
def is_cache_valid(self):
''' Determines if the cache files have expired, or if it is still valid '''
if os.path.isfile(self.cache_path_cache):
mod_time = os.path.getmtime(self.cache_path_cache)
current_time = time()
if (mod_time + self.cache_max_age) > current_time:
if os.path.isfile(self.cache_path_index):
return True
return False
def read_settings(self):
''' Reads the settings from the ec2.ini file '''
config = ConfigParser.SafeConfigParser()
config.read(os.path.dirname(os.path.realpath(__file__)) + '/ec2.ini')
# Regions
self.regions = []
configRegions = config.get('ec2', 'regions')
if (configRegions == 'all'):
for regionInfo in ec2.regions():
self.regions.append(regionInfo.name)
else:
self.regions = configRegions.split(",")
# Destination addresses
self.destination_variable = config.get('ec2', 'destination_variable')
self.vpc_destination_variable = config.get('ec2', 'vpc_destination_variable')
# Cache related
cache_path = config.get('ec2', 'cache_path')
self.cache_path_cache = cache_path + "/ansible-ec2.cache"
self.cache_path_index = cache_path + "/ansible-ec2.index"
self.cache_max_age = config.getint('ec2', 'cache_max_age')
def parse_cli_args(self):
''' Command line argument processing '''
parser = argparse.ArgumentParser(description='Produce an Ansible Inventory file based on EC2')
parser.add_argument('--list', action='store_true', default=True,
help='List instances (default: True)')
parser.add_argument('--host', action='store',
help='Get all the variables about a specific instance')
parser.add_argument('--refresh-cache', action='store_true', default=False,
help='Force refresh of cache by making API requests to EC2 (default: False - use cache files)')
self.args = parser.parse_args()
def do_api_calls_update_cache(self):
''' Do API calls to each region, and save data in cache files '''
for region in self.regions:
self.get_instances_by_region(region)
self.write_to_cache(self.inventory, self.cache_path_cache)
self.write_to_cache(self.index, self.cache_path_index)
def get_instances_by_region(self, region):
''' Makes an AWS EC2 API call to the list of instances in a particular
region '''
conn = ec2.connect_to_region(region)
reservations = conn.get_all_instances()
for reservation in reservations:
for instance in reservation.instances:
self.add_instance(instance, region)
def get_instance(self, region, instance_id):
''' Gets details about a specific instance '''
conn = ec2.connect_to_region(region)
reservations = conn.get_all_instances([instance_id])
for reservation in reservations:
for instance in reservation.instances:
return instance
def add_instance(self, instance, region):
''' Adds an instance to the inventory and index, as long as it is
addressable '''
# Only want running instances
if instance.state == 'terminated':
return
# Select the best destination address
if instance.subnet_id:
dest = getattr(instance, self.vpc_destination_variable)
else:
dest = getattr(instance, self.destination_variable)
if dest == None:
# Skip instances we cannot address (e.g. private VPC subnet)
return
# Add to index
self.index[dest] = [region, instance.id]
# Inventory: Group by instance ID (always a group of 1)
self.inventory[instance.id] = [dest]
# Inventory: Group by region
self.push(self.inventory, region, dest)
# Inventory: Group by availability zone
self.push(self.inventory, instance.placement, dest)
# Inventory: Group by security group
for group in instance.groups:
key = self.to_safe("security_group_" + group.name)
self.push(self.inventory, key, dest)
# Inventory: Group by tag keys
for k, v in instance.tags.iteritems():
key = self.to_safe("tag_" + k + "=" + v)
self.push(self.inventory, key, dest)
def get_host_info(self):
''' Get variables about a specific host '''
if len(self.index) == 0:
# Need to load index from cache
self.load_index_from_cache()
(region, instance_id) = self.index[self.args.host]
instance = self.get_instance(region, instance_id)
instance_vars = {}
for key in vars(instance):
value = getattr(instance, key)
key = self.to_safe('ec2_' + key)
# Handle complex types
if type(value) in [int, bool]:
instance_vars[key] = value
elif type(value) in [str, unicode]:
instance_vars[key] = value.strip()
elif type(value) == type(None):
instance_vars[key] = ''
elif key == 'ec2_region':
instance_vars[key] = value.name
elif key == 'ec2_tags':
for k, v in value.iteritems():
key = self.to_safe('ec2_tag_' + k)
instance_vars[key] = v
elif key == 'ec2_groups':
group_ids = []
group_names = []
for group in value:
group_ids.append(group.id)
group_names.append(group.name)
instance_vars["ec2_security_group_ids"] = ','.join(group_ids)
instance_vars["ec2_security_group_names"] = ','.join(group_names)
else:
pass
# TODO Product codes if someone finds them useful
#print key
#print type(value)
#print value
return self.json_format_dict(instance_vars, True)
def push(self, my_dict, key, element):
''' Pushed an element onto an array that may not have been defined in
the dict '''
if key in my_dict:
my_dict[key].append(element);
else:
my_dict[key] = [element]
def get_inventory_from_cache(self):
''' Reads the inventory from the cache file and returns it as a JSON
object '''
cache = open(self.cache_path_cache, 'r')
json_inventory = cache.read()
return json_inventory
def load_index_from_cache(self):
''' Reads the index from the cache file sets self.index '''
cache = open(self.cache_path_index, 'r')
json_index = cache.read()
self.index = json.loads(json_index)
def write_to_cache(self, data, filename):
''' Writes data in JSON format to a file '''
json_data = self.json_format_dict(data, True)
cache = open(filename, 'w')
cache.write(json_data)
cache.close()
def to_safe(self, word):
''' Converts 'bad' characters in a string to underscores so they can be
used as Ansible groups '''
return re.sub("[^A-Za-z0-9\-]", "_", word)
def json_format_dict(self, data, pretty=False):
''' Converts a dict to a JSON object and dumps it as a formatted
string '''
if pretty:
return json.dumps(data, sort_keys=True, indent=2)
else:
return json.dumps(data)
# Run the script
Ec2Inventory()

View File

@ -1,206 +0,0 @@
# (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 ansible.constants as C
from ansible.inventory.host import Host
from ansible.inventory.group import Group
from ansible import errors
from ansible import utils
import os
import yaml
import sys
class InventoryParserYaml(object):
''' Host inventory parser for ansible '''
def __init__(self, filename=C.DEFAULT_HOST_LIST):
sys.stderr.write("WARNING: YAML inventory files are deprecated in 0.6 and will be removed in 0.7, to migrate" +
" download and run https://github.com/ansible/ansible/blob/devel/examples/scripts/yaml_to_ini.py\n")
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):
# FIXME: refactor into subfunctions
all = Group('all')
ungrouped = Group('ungrouped')
all.add_child_group(ungrouped)
self.groups = dict(all=all, ungrouped=ungrouped)
grouped_hosts = []
yaml = utils.parse_yaml(data)
# first add all groups
for item in yaml:
if 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)
grouped_hosts.append(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)
grouped_hosts.append(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)
# add host definitions
for item in yaml:
if type(item) in [ str, unicode ]:
host = self._make_host(item)
if host not in grouped_hosts:
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)
groups = item.get('groups', {})
if type(groups) in [ str, unicode ]:
groups = [ groups ]
if type(groups)==list:
for subitem in groups:
if subitem in self.groups:
group = self.groups[subitem]
else:
group = Group(subitem)
self.groups[group.name] = group
all.add_child_group(group)
group.add_host(host)
grouped_hosts.append(host)
if host not in grouped_hosts:
ungrouped.add_host(host)
# make sure ungrouped.hosts is the complement of grouped_hosts
ungrouped_hosts = [host for host in ungrouped.hosts if host not in grouped_hosts]
if __name__ == "__main__":
if len(sys.argv) != 2:
print "usage: yaml_to_ini.py /path/to/ansible/hosts"
sys.exit(1)
result = ""
original = sys.argv[1]
yamlp = InventoryParserYaml(filename=sys.argv[1])
dirname = os.path.dirname(original)
group_names = [ g.name for g in yamlp.groups.values() ]
for group_name in sorted(group_names):
record = yamlp.groups[group_name]
if group_name == 'all':
continue
hosts = record.hosts
result = result + "[%s]\n" % record.name
for h in hosts:
result = result + "%s\n" % h.name
result = result + "\n"
groupfiledir = os.path.join(dirname, "group_vars")
if not os.path.exists(groupfiledir):
print "* creating: %s" % groupfiledir
os.makedirs(groupfiledir)
groupfile = os.path.join(groupfiledir, group_name)
print "* writing group variables for %s into %s" % (group_name, groupfile)
groupfh = open(groupfile, 'w')
groupfh.write(yaml.dump(record.get_variables()))
groupfh.close()
for (host_name, host_record) in yamlp._hosts.iteritems():
hostfiledir = os.path.join(dirname, "host_vars")
if not os.path.exists(hostfiledir):
print "* creating: %s" % hostfiledir
os.makedirs(hostfiledir)
hostfile = os.path.join(hostfiledir, host_record.name)
print "* writing host variables for %s into %s" % (host_record.name, hostfile)
hostfh = open(hostfile, 'w')
hostfh.write(yaml.dump(host_record.get_variables()))
hostfh.close()
# also need to keep a hash of variables per each host
# and variables per each group
# and write those to disk
newfilepath = os.path.join(dirname, "hosts.new")
fdh = open(newfilepath, 'w')
fdh.write(result)
fdh.close()
print "* COMPLETE: review your new inventory file and replace your original when ready"
print "* new inventory file saved as %s" % newfilepath
print "* edit group specific variables in %s/group_vars/" % dirname
print "* edit host specific variables in %s/host_vars/" % dirname
# now need to write this to disk as (oldname).new
# and inform the user

View File

@ -1,35 +1,20 @@
#!/bin/bash
# usage: source ./hacking/env-setup [-q]
# usage: source ./hacking/env-setup
# modifies environment for running Ansible from checkout
# When run using source as directed, $0 gets set to bash, so we must use $BASH_SOURCE
if [ -n "$BASH_SOURCE" ] ; then
HACKING_DIR=`dirname $BASH_SOURCE`
else
HACKING_DIR="$PWD/hacking"
fi
# The below is an alternative to readlink -fn which doesn't exist on OS X
# Source: http://stackoverflow.com/a/1678636
FULL_PATH=`python -c "import os, sys; print os.path.realpath('$HACKING_DIR')"`
ANSIBLE_HOME=`dirname $FULL_PATH`
PREFIX_PYTHONPATH="$ANSIBLE_HOME/lib"
PREFIX_PATH="$ANSIBLE_HOME/bin"
PREFIX_MANPATH="$ANSIBLE_HOME/docs/man"
PREFIX_PYTHONPATH="$PWD/lib"
PREFIX_PATH="$PWD/bin"
PREFIX_MANPATH="$PWD/docs/man"
export PYTHONPATH=$PREFIX_PYTHONPATH:$PYTHONPATH
export PATH=$PREFIX_PATH:$PATH
export ANSIBLE_LIBRARY="$ANSIBLE_HOME/library"
export ANSIBLE_LIBRARY="$PWD/library"
export MANPATH=$PREFIX_MANPATH:$MANPATH
# Print out values unless -q is set
echo "PATH=$PATH"
echo "PYTHONPATH=$PYTHONPATH"
echo "ANSIBLE_LIBRARY=$ANSIBLE_LIBRARY"
echo "MANPATH=$MANPATH"
if [ $# -eq 0 -o "$1" != "-q" ] ; then
echo "PATH=$PATH"
echo "PYTHONPATH=$PYTHONPATH"
echo "ANSIBLE_LIBRARY=$ANSIBLE_LIBRARY"
echo "MANPATH=$MANPATH"
echo "Reminder: specify your host file with -i"
echo "Done."
fi
echo "Reminder: specify your host file with -i"
echo "Done."

View File

@ -23,129 +23,69 @@
# modules
#
# example:
# test-module -m ../library/command -a "/bin/sleep 3"
# test-module -m ../library/service -a "name=httpd ensure=restarted"
# test-module -m ../library/service -a "name=httpd ensure=restarted" --debugger /usr/bin/pdb
# test-module ../library/command /bin/sleep 3
# test-module ../library/service name=httpd ensure=restarted
import sys
import base64
import os
import subprocess
import traceback
import optparse
import ansible.utils as utils
import ansible.module_common as module_common
from ansible import utils
try:
import json
except ImportError:
import simplejson as json
def parse():
"""parse command line
modfile = None
:return : (options, args)"""
parser = optparse.OptionParser()
if len(sys.argv) == 1:
print >>sys.stderr, "usage: test-module ./library/command [key=value ...]"
sys.exit(1)
parser.usage = "%prog -[options] (-h for help)"
modfile = sys.argv[1]
if len(sys.argv) > 1:
args = " ".join(sys.argv[2:])
else:
args = ""
parser.add_option('-m', '--module-path', dest='module_path',
help="REQUIRED: full path of module source to execute")
parser.add_option('-a', '--args', dest='module_args', default="",
help="module argument string")
parser.add_option('-D', '--debugger', dest='debugger',
help="path to python debugger (e.g. /usr/bin/pdb)")
options, args = parser.parse_args()
if not options.module_path:
parser.print_help()
sys.exit(1)
else:
return options, args
argspath = os.path.expanduser("~/.ansible_test_module_arguments")
argsfile = open(argspath, 'w')
argsfile.write(args)
argsfile.close()
def write_argsfile(argstring):
""" Write args to a file for old-style module's use. """
argspath = os.path.expanduser("~/.ansible_test_module_arguments")
argsfile = open(argspath, 'w')
argsfile.write(argstring)
argsfile.close()
return argspath
def boilerplate_module(modfile, args):
""" simulate what ansible does with new style modules """
module_fh = open(modfile)
module_data = module_fh.read()
included_boilerplate = module_data.find(module_common.REPLACER) != -1
module_fh.close()
if included_boilerplate:
module_data = module_data.replace(module_common.REPLACER, module_common.MODULE_COMMON)
encoded_args = "\"\"\"%s\"\"\"" % args.replace("\"","\\\"")
module_data = module_data.replace(module_common.REPLACER_ARGS, encoded_args)
modfile2_path = os.path.expanduser("~/.ansible_module_generated")
print "* including generated source, if any, saving to: %s" % modfile2_path
print "* this will offset any line numbers in tracebacks/debuggers!"
modfile2 = open(modfile2_path, 'w')
modfile2.write(module_data)
modfile2.close()
modfile = modfile2_path
return (modfile2_path, included_boilerplate)
else:
print "* module boilerplate substitution not requested in module, line numbers will be unaltered"
return (modfile, included_boilerplate)
def runtest( modfile, argspath):
"""Test run a module, piping it's output for reporting."""
os.system("chmod +x %s" % modfile)
invoke = "%s" % (modfile)
if argspath is not None:
invoke = "%s %s" % (modfile, argspath)
cmd = subprocess.Popen(invoke, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
(out, err) = cmd.communicate()
try:
print "***********************************"
print "RAW OUTPUT"
print out
print err
results = utils.parse_json(out)
except:
print "***********************************"
print "INVALID OUTPUT FORMAT"
print out
traceback.print_exc()
sys.exit(1)
os.system("chmod +x %s" % modfile)
cmd = subprocess.Popen("%s %s" % (modfile, argspath),
shell=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
(out, err) = cmd.communicate()
if err and err != '':
print "***********************************"
print "PARSED OUTPUT"
print utils.jsonify(results,format=True)
print "RECIEVED DATA ON STDERR, THIS WILL CRASH YOUR MODULE"
print err
sys.exit(1)
def rundebug(debugger, modfile, argspath):
"""Run interactively with console debugger."""
try:
print "***********************************"
print "RAW OUTPUT"
print out
results = utils.parse_json(out)
except:
print "***********************************"
print "INVALID OUTPUT FORMAT"
print out
traceback.print_exc()
sys.exit(1)
if argspath is not None:
subprocess.call("%s %s %s" % (debugger, modfile, argspath), shell=True)
else:
subprocess.call("%s %s" % (debugger, modfile), shell=True)
print "***********************************"
print "PARSED OUTPUT"
def main():
print utils.bigjson(results)
sys.exit(0)
options, args = parse()
(modfile, is_new_style) = boilerplate_module(options.module_path, options.module_args)
argspath=None
if not is_new_style:
argspath = write_argsfile(options.module_args)
if options.debugger:
rundebug(options.debugger, modfile, argspath)
else:
runtest(modfile, argspath)
if __name__ == "__main__":
main()

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.6'
__version__ = '0.5'
__author__ = 'Michael DeHaan'

View File

@ -15,24 +15,20 @@
# You should have received a copy of the GNU General Public License
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
#######################################################
import utils
import sys
import getpass
import os
import subprocess
from ansible.color import stringc
cowsay = None
if os.path.exists("/usr/bin/cowsay"):
cowsay = "/usr/bin/cowsay"
elif os.path.exists("/usr/games/cowsay"):
cowsay = "/usr/games/cowsay"
#######################################################
class AggregateStats(object):
''' holds stats about per-host activity during playbook runs '''
def __init__(self):
self.processed = {}
self.failures = {}
self.ok = {}
@ -80,66 +76,17 @@ class AggregateStats(object):
########################################################################
def regular_generic_msg(hostname, result, oneline, caption):
''' output on the result of a module run that is not command '''
if not oneline:
return "%s | %s >> %s\n" % (hostname, caption, utils.jsonify(result,format=True))
else:
return "%s | %s >> %s\n" % (hostname, caption, utils.jsonify(result))
def banner(msg):
if cowsay != None:
cmd = subprocess.Popen("%s -W 60 \"%s\"" % (cowsay, msg),
res = ""
if os.path.exists("/usr/bin/cowsay"):
cmd = subprocess.Popen("/usr/bin/cowsay -W 60 \"%s\"" % msg,
stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True)
(out, err) = cmd.communicate()
return "%s\n" % out
res = "%s\n" % out
else:
return "\n%s ********************* " % msg
def command_generic_msg(hostname, result, oneline, caption):
''' output the result of a command run '''
rc = result.get('rc', '0')
stdout = result.get('stdout','')
stderr = result.get('stderr', '')
msg = result.get('msg', '')
if not oneline:
buf = "%s | %s | rc=%s >>\n" % (hostname, caption, result.get('rc',0))
if stdout:
buf += stdout
if stderr:
buf += stderr
if msg:
buf += msg
return buf + "\n"
else:
if stderr:
return "%s | %s | rc=%s | (stdout) %s (stderr) %s\n" % (hostname, caption, rc, stdout, stderr)
else:
return "%s | %s | rc=%s | (stdout) %s\n" % (hostname, caption, rc, stdout)
def host_report_msg(hostname, module_name, result, oneline):
''' summarize the JSON results for a particular host '''
failed = utils.is_failed(result)
msg = ''
if module_name in [ 'command', 'shell', 'raw' ] and 'ansible_job_id' not in result:
if not failed:
msg = command_generic_msg(hostname, result, oneline, 'success')
else:
msg = command_generic_msg(hostname, result, oneline, 'FAILED')
else:
if not failed:
msg = regular_generic_msg(hostname, result, oneline, 'success')
else:
msg = regular_generic_msg(hostname, result, oneline, 'FAILED')
return msg
###############################################
res = "%s ********************* \n" % msg
return res
class DefaultRunnerCallbacks(object):
''' no-op callbacks for API usage of Runner() if no callbacks are specified '''
@ -147,7 +94,7 @@ class DefaultRunnerCallbacks(object):
def __init__(self):
pass
def on_failed(self, host, res, ignore_errors=False):
def on_failed(self, host, res):
pass
def on_ok(self, host, res):
@ -180,43 +127,33 @@ class CliRunnerCallbacks(DefaultRunnerCallbacks):
''' callbacks for use by /usr/bin/ansible '''
def __init__(self):
# set by /usr/bin/ansible later
self.options = None
self._async_notified = {}
def on_failed(self, host, res, ignore_errors=False):
def on_failed(self, host, res):
self._on_any(host,res)
def on_ok(self, host, res):
self._on_any(host,res)
def on_unreachable(self, host, res):
if type(res) == dict:
res = res.get('msg','')
print "%s | FAILED => %s" % (host, res)
if self.options.tree:
utils.write_tree_file(
self.options.tree, host,
utils.jsonify(dict(failed=True, msg=res),format=True)
)
utils.write_tree_file(self.options.tree, host, utils.bigjson(dict(failed=True, msg=res)))
def on_skipped(self, host):
pass
def on_error(self, 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:
@ -224,18 +161,15 @@ class CliRunnerCallbacks(DefaultRunnerCallbacks):
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.jsonify(res,format=True))
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.jsonify(res,format=True))
print "<job %s> FAILED on %s => %s"%(jid, host, utils.bigjson(res))
def _on_any(self, host, result):
print host_report_msg(host, self.options.module_name, result, self.options.one_line)
print utils.host_report_msg(host, self.options.module_name, result, self.options.one_line)
if self.options.tree:
utils.write_tree_file(self.options.tree, host, utils.jsonify(result,format=True))
utils.write_tree_file(self.options.tree, host, utils.bigjson(result))
########################################################################
@ -243,105 +177,44 @@ class PlaybookRunnerCallbacks(DefaultRunnerCallbacks):
''' callbacks used for Runner() from /usr/bin/ansible-playbook '''
def __init__(self, stats, verbose=False):
self.stats = stats
self._async_notified = {}
self.verbose = verbose
def on_unreachable(self, host, msg):
print "fatal: [%s] => %s" % (host, msg)
item = None
if type(msg) == dict:
item = msg.get('item', None)
if item:
print "fatal: [%s] => (item=%s) => %s" % (host, item, msg)
else:
print "fatal: [%s] => %s" % (host, msg)
def on_failed(self, host, results, ignore_errors=False):
item = results.get('item', None)
if item:
msg = "failed: [%s] => (item=%s) => %s" % (host, item, utils.jsonify(results))
else:
msg = "failed: [%s] => %s" % (host, utils.jsonify(results))
print stringc(msg, 'red')
if ignore_errors:
print stringc("...ignoring", 'yellow')
def on_failed(self, host, results):
print "failed: [%s] => %s\n" % (host, utils.smjson(results))
def on_ok(self, host, host_result):
item = host_result.get('item', None)
# show verbose output for non-setup module results if --verbose is used
msg = ''
if not self.verbose or host_result.get("verbose_override",None) is not None:
if item:
msg = "ok: [%s] => (item=%s)" % (host,item)
else:
if 'ansible_job_id' not in host_result or 'finished' in host_result:
msg = "ok: [%s]" % (host)
print "ok: [%s]\n" % (host)
else:
# verbose ...
if item:
msg = "ok: [%s] => (item=%s) => %s" % (host, item, utils.jsonify(host_result))
else:
if 'ansible_job_id' not in host_result or 'finished' in host_result:
msg = "ok: [%s] => %s" % (host, utils.jsonify(host_result))
if msg != '':
if not 'changed' in host_result or not host_result['changed']:
print stringc(msg, 'green')
else:
print stringc(msg, 'yellow')
print "ok: [%s] => %s" % (host, utils.smjson(host_result))
def on_error(self, host, err):
print >>sys.stderr, "err: [%s] => %s\n" % (host, err)
item = err.get('item', None)
msg = ''
if item:
msg = "err: [%s] => (item=%s) => %s" % (host, item, err)
else:
msg = "err: [%s] => %s" % (host, err)
msg = stringc(msg, 'red')
print >>sys.stderr, msg
def on_skipped(self, host, item=None):
msg = ''
if item:
msg = "skipping: [%s] => (item=%s)" % (host, item)
else:
msg = "skipping: [%s]" % host
print stringc(msg, 'yellow')
def on_skipped(self, host):
print "skipping: [%s]\n" % host
def on_no_hosts(self):
print stringc("no hosts matched or remaining\n", 'red')
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
msg = "<job %s> polling, %ss remaining"%(jid, clock)
print stringc(msg, 'cyan')
print "<job %s> polling, %ss remaining"%(jid, clock)
def on_async_ok(self, host, res, jid):
msg = "<job %s> finished on %s"%(jid, host)
print stringc(msg, 'cyan')
print "<job %s> finished on %s"%(jid, host)
def on_async_failed(self, host, res, jid):
msg = "<job %s> FAILED on %s"%(jid, host)
print stringc(msg, 'red')
print "<job %s> FAILED on %s"%(jid, host)
########################################################################
@ -349,50 +222,34 @@ class PlaybookCallbacks(object):
''' playbook.py callbacks used by /usr/bin/ansible-playbook '''
def __init__(self, verbose=False):
self.verbose = verbose
def on_start(self):
pass
print "\n"
def on_notify(self, host, handler):
pass
def on_task_start(self, name, is_conditional):
print banner(utils.task_start_msg(name, is_conditional))
msg = "TASK: [%s]" % name
if is_conditional:
msg = "NOTIFIED: [%s]" % name
print banner(msg)
def on_vars_prompt(self, varname, private=True, prompt=None):
if prompt:
msg = prompt
else:
msg = 'input for %s: ' % varname
def on_vars_prompt(self, varname, private=True):
msg = 'input for %s: ' % varname
if private:
return getpass.getpass(msg)
return raw_input(msg)
def on_setup(self):
print banner("GATHERING FACTS")
def on_setup_primary(self):
print banner("SETUP PHASE")
def on_import_for_host(self, host, imported_file):
def on_setup_secondary(self):
print banner("VARIABLE IMPORT PHASE")
msg = "%s: importing %s" % (host, imported_file)
print stringc(msg, 'cyan')
def on_import_for_host(self, host, imported_file):
print "%s: importing %s" % (host, imported_file)
def on_not_import_for_host(self, host, missing_file):
msg = "%s: not importing file: %s" % (host, missing_file)
print stringc(msg, 'cyan')
print "%s: not importing file: %s" % (host, missing_file)
def on_play_start(self, pattern):
print banner("PLAY [%s]" % pattern)

View File

@ -1,59 +0,0 @@
# (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 sys
ANSIBLE_COLOR=True
if os.getenv("ANSIBLE_NOCOLOR") is not None:
ANSIBLE_COLOR=False
if not sys.stdout.isatty():
ANSIBLE_COLOR=False
# --- begin "pretty"
#
# pretty - A miniature library that provides a Python print and stdout
# wrapper that makes colored terminal text easier to use (eg. without
# having to mess around with ANSI escape sequences). This code is public
# domain - there is no license except that you must leave this header.
#
# Copyright (C) 2008 Brian Nez <thedude at bri1 dot com>
#
# http://nezzen.net/2008/06/23/colored-text-in-python-using-ansi-escape-sequences/
codeCodes = {
'black': '0;30', 'bright gray': '0;37',
'blue': '0;34', 'white': '1;37',
'green': '0;32', 'bright blue': '1;34',
'cyan': '0;36', 'bright green': '1;32',
'red': '0;31', 'bright cyan': '1;36',
'purple': '0;35', 'bright red': '1;31',
'yellow': '0;33', 'bright purple': '1;35',
'dark gray': '1;30', 'bright yellow': '1;33',
'normal': '0'
}
def stringc(text, color):
"""String in color."""
if ANSIBLE_COLOR:
return "\033["+codeCodes[color]+"m"+text+"\033[0m"
else:
return text
# --- end "pretty"

View File

@ -14,12 +14,17 @@
#
# 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
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_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 = '*'
@ -29,7 +34,7 @@ DEFAULT_TIMEOUT = os.environ.get('ANSIBLE_TIMEOUT',10)
DEFAULT_POLL_INTERVAL = os.environ.get('ANSIBLE_POLL_INTERVAL',15)
DEFAULT_REMOTE_USER = os.environ.get('ANSIBLE_REMOTE_USER','root')
DEFAULT_REMOTE_PASS = None
DEFAULT_PRIVATE_KEY_FILE = os.environ.get('ANSIBLE_PRIVATE_KEY_FILE',None)
DEFAULT_PRIVATE_KEY_FILE = os.environ.get('ANSIBLE_REMOTE_USER',None)
DEFAULT_SUDO_PASS = None
DEFAULT_SUDO_USER = os.environ.get('ANSIBLE_SUDO_USER','root')
DEFAULT_REMOTE_PORT = 22

View File

@ -15,8 +15,11 @@
# You should have received a copy of the GNU General Public License
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
class AnsibleError(Exception):
''' The base Ansible exception from which all others should subclass '''
"""
The base Ansible exception from which all others should subclass.
"""
def __init__(self, msg):
self.msg = msg
@ -24,9 +27,11 @@ class AnsibleError(Exception):
def __str__(self):
return self.msg
class AnsibleFileNotFound(AnsibleError):
pass
class AnsibleConnectionFailed(AnsibleError):
pass

View File

@ -35,22 +35,12 @@ class Inventory(object):
Host inventory for ansible.
"""
__slots__ = [ 'host_list', 'groups', '_restriction', '_is_script',
'parser', '_vars_per_host', '_vars_per_group', '_hosts_cache' ]
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
# caching to avoid repeated calculations, particularly with
# external inventory scripts.
self._vars_per_host = {}
self._vars_per_group = {}
self._hosts_cache = {}
# the inventory object holds a list of groups
self.groups = []
@ -62,8 +52,7 @@ class Inventory(object):
if type(host_list) in [ str, unicode ]:
if host_list.find(",") != -1:
host_list = host_list.split(",")
host_list = [ h for h in host_list if h and h.strip() ]
host_list = host_list.split(",")
if type(host_list) == list:
all = Group('all')
@ -90,7 +79,6 @@ class Inventory(object):
def _match(self, str, pattern_str):
return fnmatch.fnmatch(str, pattern_str)
# TODO: cache this logic so if called a second time the result is not recalculated
def get_hosts(self, pattern="all"):
""" Get all host objects matching the pattern """
hosts = {}
@ -105,8 +93,8 @@ class Inventory(object):
inverted = False
for group in groups:
for host in group.get_hosts():
if self._match(group.name, pat) or pat == 'all' or self._match(host.name, pat):
# must test explicitly for None because [] means no hosts allowed
if group.name == pat or pat == 'all' or self._match(host.name, pat):
#must test explicitly for None because [] means no hosts allowed
if self._restriction==None or host.name in self._restriction:
if inverted:
if host.name in hosts:
@ -119,14 +107,9 @@ class Inventory(object):
return self.groups
def get_host(self, hostname):
if hostname not in self._hosts_cache:
self._hosts_cache[hostname] = self._get_host(hostname)
return self._hosts_cache[hostname]
def _get_host(self, hostname):
for group in self.groups:
for host in group.get_hosts():
if hostname == host.name:
for host in group.get_hosts():
if hostname == host.name:
return host
return None
@ -135,26 +118,17 @@ class Inventory(object):
if group.name == groupname:
return group
return None
def get_group_variables(self, groupname):
if groupname not in self._vars_per_group:
self._vars_per_group[groupname] = self._get_group_variables(groupname)
return self._vars_per_group[groupname]
def _get_group_variables(self, groupname):
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 hostname not in self._vars_per_host:
self._vars_per_host[hostname] = self._get_variables(hostname)
return self._vars_per_host[hostname]
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],
@ -163,13 +137,9 @@ class Inventory(object):
)
(out, err) = cmd.communicate()
results = utils.parse_json(out)
# FIXME: this is a bit redundant with host.py and should share code
results['inventory_hostname'] = hostname
results['inventory_hostname_short'] = hostname.split('.')[0]
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)
@ -191,23 +161,12 @@ class Inventory(object):
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 is_file(self):
""" did inventory come from a file? """
if not isinstance(self.host_list, basestring):
return False
return os.path.exists(self.host_list)
def basedir(self):
""" if inventory came from a file, what's the directory? """
if not self.is_file():
return None
return os.path.dirname(self.host_list)

View File

@ -1,97 +0,0 @@
# (c) 2012, Zettar Inc.
# Written by Chin Fang <fangchin@zettar.com>
#
# This file is part of Ansible
#
# This module 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.
#
# This software 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 this software. If not, see <http://www.gnu.org/licenses/>.
#
'''
This module is for enhancing ansible's inventory parsing capability such
that it can deal with hostnames specified using a simple pattern in the
form of [beg:end], example: [1:5] where if beg is not specified, it
defaults to 0.
If beg is given and is left-zero-padded, e.g. '001', it is taken as a
formatting hint when the range is expanded. e.g. [001:010] is to be
expanded into 001, 002 ...009, 010.
Note that when beg is specified with left zero padding, then the length of
end must be the same as that of beg, else a exception is raised.
'''
from ansible import errors
def detect_range(line = None):
'''
A helper function that checks a given host line to see if it contains
a range pattern descibed in the docstring above.
Returnes True if the given line contains a pattern, else False.
'''
if (not line.startswith("[") and
line.find("[") != -1 and
line.find(":") != -1 and
line.find("]") != -1 and
line.index("[") < line.index(":") < line.index("]")):
return True
else:
return False
def expand_hostname_range(line = None):
'''
A helper function that expands a given line that contains a pattern
specified in top docstring, and returns a list that consists of the
expanded version.
The '[' and ']' characters are used to maintain the pseudo-code
appearance. They are replaced in this function with '|' to ease
string splitting.
References: http://ansible.github.com/patterns.html#hosts-and-groups
'''
all_hosts = []
if line:
# A hostname such as db[1:6]-node is considered to consists
# three parts:
# head: 'db'
# nrange: [1:6]; range() is a built-in. Can't use the name
# tail: '-node'
(head, nrange, tail) = line.replace('[','|').replace(']','|').split('|')
bounds = nrange.split(":")
if len(bounds) != 2:
raise errors.AnsibleError("host range incorrectly specified")
beg = bounds[0]
end = bounds[1]
if not beg:
beg = "0"
if not end:
raise errors.AnsibleError("host range end value missing")
if beg[0] == '0' and len(beg) > 1:
rlen = len(beg) # range length formatting hint
else:
rlen = None
if rlen > 1 and rlen != len(end):
raise errors.AnsibleError("host range format incorrectly specified!")
for _ in range(int(beg), int(end)+1):
if rlen:
rseq = str(_).zfill(rlen) # range sequence
else:
rseq = str(_)
hname = ''.join((head, rseq, tail))
all_hosts.append(hname)
return all_hosts

View File

@ -15,39 +15,38 @@
# You should have received a copy of the GNU General Public License
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
class Group(object):
''' a group of ansible hosts '''
#############################################
__slots__ = [ 'name', 'hosts', 'vars', 'child_groups', 'parent_groups' ]
# 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")
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())
@ -55,16 +54,14 @@ class Group(object):
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(ancestor.get_variables())
vars.update(self.vars)
return vars
def _get_ancestors(self):
results = {}
for g in self.parent_groups:
results[g.name] = g
@ -72,6 +69,8 @@ class Group(object):
return results
def get_ancestors(self):
return self._get_ancestors().values()

View File

@ -15,16 +15,17 @@
# 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):
''' a single ansible host '''
__slots__ = [ 'name', 'vars', 'groups' ]
"""
Group of ansible hosts
"""
def __init__(self, name=None, port=None):
self.name = name
self.vars = {}
self.groups = []
@ -35,15 +36,12 @@ class Host(object):
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
self.vars[key]=value;
def get_groups(self):
groups = {}
for g in self.groups:
groups[g.name] = g
@ -53,13 +51,11 @@ class Host(object):
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
results['inventory_hostname_short'] = self.name.split('.')[0]
groups = self.get_groups()
results['group_names'] = sorted([ g.name for g in groups if g.name != 'all'])
return results

View File

@ -24,8 +24,6 @@ import subprocess
import ansible.constants as C
from ansible.inventory.host import Host
from ansible.inventory.group import Group
from ansible.inventory.expand_hosts import detect_range
from ansible.inventory.expand_hosts import expand_hostname_range
from ansible import errors
from ansible import utils
@ -48,7 +46,6 @@ class InventoryParser(object):
self._parse_group_children()
self._parse_group_variables()
return self.groups
# [webservers]
# alpha
@ -57,7 +54,6 @@ class InventoryParser(object):
# delta asdf=jkl favcolor=red
def _parse_base_groups(self):
# FIXME: refactor
ungrouped = Group(name='ungrouped')
all = Group(name='all')
@ -82,40 +78,21 @@ class InventoryParser(object):
continue
hostname = tokens[0]
port = C.DEFAULT_REMOTE_PORT
# Two cases to check:
# 0. A hostname that contains a range pesudo-code and a port
# 1. A hostname that contains just a port
if (hostname.find("[") != -1 and
hostname.find("]") != -1 and
hostname.find(":") != -1 and
(hostname.rindex("]") < hostname.rindex(":")) or
(hostname.find("]") == -1 and hostname.find(":") != -1)):
tokens2 = hostname.rsplit(":", 1)
hostname = tokens2[0]
port = tokens2[1]
if hostname.find(":") != -1:
tokens2 = hostname.split(":")
hostname = tokens2[0]
port = tokens2[1]
host = None
_all_hosts = []
if hostname in self.hosts:
host = self.hosts[hostname]
_all_hosts.append(host)
else:
if detect_range(hostname):
_hosts = expand_hostname_range(hostname)
for _ in _hosts:
host = Host(name=_, port=port)
self.hosts[_] = host
_all_hosts.append(host)
else:
host = Host(name=hostname, port=port)
self.hosts[hostname] = host
_all_hosts.append(host)
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)
for _ in _all_hosts:
self.groups[active_group_name].add_host(_)
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
@ -157,7 +134,7 @@ class InventoryParser(object):
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)
raise errors.AnsibleError("can't add vars to undefined group: %s" % line)
elif line.startswith("#"):
pass
elif line.startswith("["):
@ -168,7 +145,7 @@ class InventoryParser(object):
if line.find("=") == -1:
raise errors.AnsibleError("variables assigned to group must be in key=value form")
else:
(k,v) = line.split("=",1)
(k,v) = line.split("=")
group.set_variable(k,v)

View File

@ -26,7 +26,9 @@ from ansible import errors
from ansible import utils
class InventoryScript(object):
''' Host inventory parser for ansible using external inventory scripts. '''
"""
Host inventory parser for ansible using external inventory scripts.
"""
def __init__(self, filename=C.DEFAULT_HOST_LIST):
@ -37,7 +39,6 @@ class InventoryScript(object):
self.groups = self._parse()
def _parse(self):
groups = {}
self.raw = utils.parse_json(self.data)
all=Group('all')
@ -54,3 +55,4 @@ class InventoryScript(object):
all.add_child_group(group)
return groups

View File

@ -15,23 +15,21 @@
# You should have received a copy of the GNU General Public License
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
#############################################
import ansible.constants as C
from ansible.inventory.host import Host
from ansible.inventory.group import Group
from ansible import errors
from ansible import utils
import sys
class InventoryParserYaml(object):
''' Host inventory parser for ansible '''
__slots__ = [ '_hosts', 'groups' ]
"""
Host inventory for ansible.
"""
def __init__(self, filename=C.DEFAULT_HOST_LIST):
sys.stderr.write("WARNING: YAML inventory files are deprecated in 0.6 and will be removed in 0.7, to migrate" +
" download and run https://github.com/ansible/ansible/blob/devel/examples/scripts/yaml_to_ini.py\n")
fh = open(filename)
data = fh.read()
fh.close()
@ -39,7 +37,6 @@ class InventoryParserYaml(object):
self._parse(data)
def _make_host(self, hostname):
if hostname in self._hosts:
return self._hosts[hostname]
else:
@ -50,7 +47,6 @@ class InventoryParserYaml(object):
# see file 'test/yaml_hosts' for syntax
def _parse(self, data):
# FIXME: refactor into subfunctions
all = Group('all')
ungrouped = Group('ungrouped')
@ -77,8 +73,8 @@ class InventoryParserYaml(object):
vars = subresult.get('vars',{})
if type(vars) == list:
for subitem in vars:
for (k,v) in subitem.items():
host.set_variable(k,v)
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)
@ -110,33 +106,12 @@ class InventoryParserYaml(object):
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)
groups = item.get('groups', {})
if type(groups) in [ str, unicode ]:
groups = [ groups ]
if type(groups)==list:
for subitem in groups:
if subitem in self.groups:
group = self.groups[subitem]
else:
group = Group(subitem)
self.groups[group.name] = group
all.add_child_group(group)
group.add_host(host)
grouped_hosts.append(host)
host.set_variable(k,v)
if host not in grouped_hosts:
ungrouped.add_host(host)
# make sure ungrouped.hosts is the complement of grouped_hosts
ungrouped_hosts = [host for host in ungrouped.hosts if host not in grouped_hosts]

View File

@ -1,211 +0,0 @@
# (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/>.
REPLACER = "#<<INCLUDE_ANSIBLE_MODULE_COMMON>>"
REPLACER_ARGS = "<<INCLUDE_ANSIBLE_MODULE_ARGS>>"
MODULE_COMMON = """
# == BEGIN DYNAMICALLY INSERTED CODE ==
MODULE_ARGS = <<INCLUDE_ANSIBLE_MODULE_ARGS>>
BOOLEANS_TRUE = ['yes', 'on', '1', 'true', 1]
BOOLEANS_FALSE = ['no', 'off', '0', 'false', 0]
BOOLEANS = BOOLEANS_TRUE + BOOLEANS_FALSE
# ansible modules can be written in any language. To simplify
# development of Python modules, the functions available here
# can be inserted in any module source automatically by including
# #<<INCLUDE_ANSIBLE_MODULE_COMMON>> on a blank line by itself inside
# of an ansible module. The source of this common code lives
# in lib/ansible/module_common.py
try:
import json
except ImportError:
import simplejson as json
import base64
import os
import re
import shlex
import subprocess
import sys
import syslog
import types
try:
from hashlib import md5 as _md5
except ImportError:
from md5 import md5 as _md5
class AnsibleModule(object):
def __init__(self, argument_spec, bypass_checks=False, no_log=False, check_invalid_arguments=True):
'''
common code for quickly building an ansible module in Python
(although you can write modules in anything that can return JSON)
see library/slurp and others for examples
'''
self.argument_spec = argument_spec
(self.params, self.args) = self._load_params()
self._legal_inputs = []
self._handle_aliases()
# this may be disabled where modules are going to daisy chain into others
if check_invalid_arguments:
self._check_invalid_arguments()
self._set_defaults(pre=True)
if not bypass_checks:
self._check_required_arguments()
self._check_argument_types()
self._set_defaults(pre=False)
if not no_log:
self._log_invocation()
def _handle_aliases(self):
for (k,v) in self.argument_spec.iteritems():
self._legal_inputs.append(k)
aliases = v.get('aliases', None)
default = v.get('default', None)
required = v.get('required', False)
if default is not None and required:
# not alias specific but this is a good place to check this
self.fail_json(msg="internal error: required and default are mutally exclusive for %s" % k)
if aliases is None:
continue
if type(aliases) != list:
self.fail_json(msg='internal error: aliases must be a list')
for alias in aliases:
self._legal_inputs.append(alias)
if alias in self.params:
self.params[k] = self.params[alias]
def _check_invalid_arguments(self):
for (k,v) in self.params.iteritems():
if k not in self._legal_inputs:
self.fail_json(msg="unsupported parameter for module: %s" % k)
def _check_required_arguments(self):
''' ensure all required arguments are present '''
missing = []
for (k,v) in self.argument_spec.iteritems():
required = v.get('required', False)
if required and k not in self.params:
missing.append(k)
if len(missing) > 0:
self.fail_json(msg="missing required arguments: %s" % ",".join(missing))
def _check_argument_types(self):
''' ensure all arguments have the requested values, and there are no stray arguments '''
for (k,v) in self.argument_spec.iteritems():
choices = v.get('choices',None)
if choices is None:
continue
if type(choices) == list:
if k in self.params:
if self.params[k] not in choices:
choices_str=",".join(choices)
msg="value of %s must be one of: %s, got: %s" % (k, choices_str, self.params[k])
self.fail_json(msg=msg)
else:
self.fail_json(msg="internal error: do not know how to interpret argument_spec")
def _set_defaults(self, pre=True):
for (k,v) in self.argument_spec.iteritems():
default = v.get('default', None)
if pre == True:
# this prevents setting defaults on required items
if default and k not in self.params:
self.params[k] = default
else:
# make sure things without a default still get set None
if k not in self.params:
self.params[k] = default
def _load_params(self):
''' read the input and return a dictionary and the arguments string '''
args = MODULE_ARGS
items = shlex.split(args)
params = {}
for x in items:
(k, v) = x.split("=",1)
params[k] = v
return (params, args)
def _log_invocation(self):
''' log that ansible ran the module '''
syslog.openlog('ansible-%s' % os.path.basename(__file__))
# Sanitize possible password argument when logging
log_args = re.sub(r'password=.+ (.*)', r"password=NOT_LOGGING_PASSWORD \1", self.args)
log_args = re.sub(r'login_password=.+ (.*)', r"login_password=NOT_LOGGING_PASSWORD \1", log_args)
syslog.syslog(syslog.LOG_NOTICE, 'Invoked with %s' % log_args)
def boolean(self, arg):
''' return a bool for the arg '''
if arg is None or type(arg) == bool:
return arg
if type(arg) in types.StringTypes:
arg = arg.lower()
if arg in BOOLEANS_TRUE:
return True
elif arg in BOOLEANS_FALSE:
return False
else:
self.fail_json(msg='Boolean %s not in either boolean list' % arg)
def jsonify(self, data):
return json.dumps(data)
def exit_json(self, **kwargs):
''' return from the module, without error '''
print self.jsonify(kwargs)
sys.exit(0)
def fail_json(self, **kwargs):
''' return from the module, with an error message '''
assert 'msg' in kwargs, "implementation error -- msg to explain the error is required"
kwargs['failed'] = True
print self.jsonify(kwargs)
sys.exit(1)
def md5(self, filename):
''' Return MD5 hex digest of local file, or None if file is not present. '''
if not os.path.exists(filename):
return None
if os.path.isdir(filename):
self.fail_json(msg="attempted to take md5sum of directory: %s" % filename)
digest = _md5()
blocksize = 64 * 1024
infile = open(filename, 'rb')
block = infile.read(blocksize)
while block:
digest.update(block)
block = infile.read(blocksize)
infile.close()
return digest.hexdigest()
# == END DYNAMICALLY INSERTED CODE ===
"""

View File

@ -15,26 +15,30 @@
# You should have received a copy of the GNU General Public License
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
#############################################
import ansible.inventory
import ansible.runner
import ansible.constants as C
from ansible import utils
from ansible import errors
import os
import collections
from play import Play
SETUP_CACHE = collections.defaultdict(dict)
#############################################
class PlayBook(object):
'''
runs an ansible playbook, given as a datastructure or YAML filename.
A playbook is a deployment, config management, or automation based
set of commands to run in series.
multiple plays/tasks do not execute simultaneously, but tasks in each
pattern do execute in parallel (according to the number of forks
requested) among the hosts they address
'''
runs an ansible playbook, given as a datastructure
or YAML filename. a playbook is a deployment, config
management, or automation based set of commands to
run in series.
multiple plays/tasks do not execute simultaneously,
but tasks in each pattern do execute in parallel
(according to the number of forks requested) among
the hosts they address
'''
# *****************************************************
@ -57,8 +61,7 @@ class PlayBook(object):
stats = None,
sudo = False,
sudo_user = C.DEFAULT_SUDO_USER,
extra_vars = None,
only_tags = None):
extra_vars = None):
"""
playbook: path to a playbook file
@ -77,16 +80,14 @@ class PlayBook(object):
sudo: if not specified per play, requests all plays use sudo mode
"""
self.SETUP_CACHE = SETUP_CACHE
self.SETUP_CACHE = {}
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 = {}
if only_tags is None:
only_tags = [ 'all' ]
self.module_path = module_path
self.forks = forks
self.timeout = timeout
@ -104,45 +105,18 @@ class PlayBook(object):
self.extra_vars = extra_vars
self.global_vars = {}
self.private_key_file = private_key_file
self.only_tags = only_tags
self.inventory = ansible.inventory.Inventory(host_list)
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._load_playbook_from_file(playbook)
self.basedir = os.path.dirname(playbook)
self.playbook = utils.parse_yaml_from_file(playbook)
self.module_path = self.module_path + os.pathsep + os.path.join(self.basedir, "library")
# *****************************************************
def _load_playbook_from_file(self, path):
'''
run top level error checking on playbooks and allow them to include other playbooks.
'''
playbook_data = utils.parse_yaml_from_file(path)
accumulated_plays = []
if type(playbook_data) != list:
raise errors.AnsibleError("parse error: playbooks must be formatted as a YAML list")
for play in playbook_data:
if type(play) != dict:
raise errors.AnsibleError("parse error: each play in a playbook must a YAML dictionary (hash), recieved: %s" % play)
if 'include' in play:
if len(play.keys()) == 1:
included_path = utils.path_dwim(self.basedir, play['include'])
accumulated_plays.extend(self._load_playbook_from_file(included_path))
else:
raise errors.AnsibleError("parse error: top level includes cannot be used with other directives: %s" % play)
else:
accumulated_plays.append(play)
return accumulated_plays
# *****************************************************
def run(self):
''' run all patterns in the playbook '''
@ -150,6 +124,7 @@ class PlayBook(object):
# loop through all patterns and run them
self.callbacks.on_start()
for play_ds in self.playbook:
self.SETUP_CACHE = {}
self._run_play(Play(self,play_ds))
# summarize the results
@ -216,16 +191,18 @@ class PlayBook(object):
# load up an appropriate ansible runner to run the task in parallel
results = self._run_task_internal(task)
# if no hosts are matched, carry on
if results is None:
results = {}
self.stats.compute(results)
# add facts to the global setup cache
for host, result in results['contacted'].iteritems():
facts = result.get('ansible_facts', {})
self.SETUP_CACHE[host].update(facts)
if "ansible_facts" in result:
for k,v in result['ansible_facts'].iteritems():
self.SETUP_CACHE[host][k]=v
self.stats.compute(results)
# if no hosts are matched, carry on
if results is None:
results = {}
# flag which notify handlers need to be run
if len(task.notify) > 0:
@ -254,21 +231,28 @@ class PlayBook(object):
# *****************************************************
def _do_setup_step(self, play):
''' get facts from the remote system '''
def _do_setup_step(self, play, 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
# after the original pass when we have that data.
#
if vars_files is not None:
self.callbacks.on_setup_secondary()
play.update_vars_files(self.inventory.list_hosts(play.hosts))
else:
self.callbacks.on_setup_primary()
host_list = [ h for h in self.inventory.list_hosts(play.hosts)
if not (h in self.stats.failures or h in self.stats.dark) ]
if not play.gather_facts:
return {}
self.callbacks.on_setup()
self.inventory.restrict_to(host_list)
# push any variables down to the system
setup_results = ansible.runner.Runner(
pattern=play.hosts, module_name='setup', module_args={}, inventory=self.inventory,
pattern=play.hosts, module_name='setup', module_args=play.vars, inventory=self.inventory,
forks=self.forks, module_path=self.module_path, timeout=self.timeout, remote_user=play.remote_user,
remote_pass=self.remote_pass, remote_port=play.remote_port, private_key_file=self.private_key_file,
setup_cache=self.SETUP_CACHE, callbacks=self.runner_callbacks, sudo=play.sudo, sudo_user=play.sudo_user,
@ -281,9 +265,11 @@ class PlayBook(object):
# now for each result, load into the setup cache so we can
# let runner template out future commands
setup_ok = setup_results.get('contacted', {})
for (host, result) in setup_ok.iteritems():
facts = result.get('ansible_facts', {})
self.SETUP_CACHE[host] = result.get('ansible_facts', {})
if vars_files is None:
# first pass only or we'll erase good work
for (host, result) in setup_ok.iteritems():
if 'ansible_facts' in result:
self.SETUP_CACHE[host] = result['ansible_facts']
return setup_results
# *****************************************************
@ -291,29 +277,18 @@ class PlayBook(object):
def _run_play(self, play):
''' run a list of tasks for a given pattern, in order '''
if not play.should_run(self.only_tags):
return
self.callbacks.on_play_start(play.name)
# get facts from system
rc = self._do_setup_step(play)
# push any variables down to the system # and get facts/ohai/other data back up
rc = self._do_setup_step(play) # pattern, vars, user, port, sudo, sudo_user, transport, None)
# now with that data, handle contentional variable file imports!
if play.vars_files and len(play.vars_files) > 0:
play.update_vars_files(self.inventory.list_hosts(play.hosts))
rc = self._do_setup_step(play, play.vars_files)
# run all the top level tasks, these get run on every node
for task in play.tasks():
# only run the task if the requested tags match
should_run = False
for x in self.only_tags:
for y in task.tags:
if (x==y):
should_run = True
break
if should_run:
self._run_task(play, task, False)
self._run_task(play, task, False)
# run notify actions
for handler in play.handlers():

View File

@ -26,18 +26,8 @@ import os
class Play(object):
__slots__ = [
'hosts', 'name', 'vars', 'vars_prompt', 'vars_files',
'handlers', 'remote_user', 'remote_port',
'sudo', 'sudo_user', 'transport', 'playbook',
'tags', 'gather_facts', '_ds', '_handlers', '_tasks'
]
# to catch typos and so forth -- these are userland names
# and don't line up 1:1 with how they are stored
VALID_KEYS = [
'hosts', 'name', 'vars', 'vars_prompt', 'vars_files',
'tasks', 'handlers', 'user', 'port', 'include',
'sudo', 'sudo_user', 'connection', 'tags', 'gather_facts'
'hosts', 'name', 'vars', 'vars_prompt', 'vars_files', 'handlers', 'remote_user', 'remote_port',
'sudo', 'sudo_user', 'transport', 'playbook', '_ds', '_handlers', '_tasks'
]
# *************************************************
@ -45,10 +35,6 @@ class Play(object):
def __init__(self, playbook, ds):
''' constructor loads from a play datastructure '''
for x in ds.keys():
if not x in Play.VALID_KEYS:
raise errors.AnsibleError("%s is not a legal parameter in an Ansible Playbook" % x)
# TODO: more error handling
hosts = ds.get('hosts')
@ -56,38 +42,26 @@ class Play(object):
raise errors.AnsibleError('hosts declaration is required')
elif isinstance(hosts, list):
hosts = ';'.join(hosts)
hosts = utils.template(hosts, playbook.extra_vars)
self._ds = ds
self.playbook = playbook
self.hosts = hosts
self.name = ds.get('name', self.hosts)
self.vars = ds.get('vars', {})
self.vars_files = ds.get('vars_files', [])
self.vars_prompt = ds.get('vars_prompt', {})
self.vars = self._get_vars(self.playbook.basedir)
self._tasks = ds.get('tasks', [])
self._handlers = ds.get('handlers', [])
self.remote_user = ds.get('user', self.playbook.remote_user)
self.remote_port = ds.get('port', self.playbook.remote_port)
self.sudo = ds.get('sudo', self.playbook.sudo)
self.sudo_user = ds.get('sudo_user', self.playbook.sudo_user)
self.transport = ds.get('connection', self.playbook.transport)
self.tags = ds.get('tags', None)
self.gather_facts = ds.get('gather_facts', True)
self._update_vars_files_for_host(None)
hosts = utils.template(hosts, playbook.extra_vars, {})
self._ds = ds
self.playbook = playbook
self.hosts = hosts
self.name = ds.get('name', self.hosts)
self.vars = ds.get('vars', {})
self.vars_files = ds.get('vars_files', [])
self.vars_prompt = ds.get('vars_prompt', {})
self.vars = self._get_vars(self.playbook.basedir)
self._tasks = ds.get('tasks', [])
self._handlers = ds.get('handlers', [])
self.remote_user = ds.get('user', self.playbook.remote_user)
self.remote_port = ds.get('port', self.playbook.remote_port)
self.sudo = ds.get('sudo', self.playbook.sudo)
self.sudo_user = ds.get('sudo_user', self.playbook.sudo_user)
self.transport = ds.get('connection', self.playbook.transport)
self._tasks = self._load_tasks(self._ds, 'tasks')
self._handlers = self._load_tasks(self._ds, 'handlers')
if self.tags is None:
self.tags = []
elif type(self.tags) in [ str, unicode ]:
self.tags = [ self.tags ]
elif type(self.tags) != list:
self.tags = []
if self.sudo_user != 'root':
self.sudo = True
@ -102,24 +76,25 @@ class Play(object):
task_vars = self.vars.copy()
if 'include' in x:
tokens = shlex.split(x['include'])
for t in tokens[1:]:
(k,v) = t.split("=", 1)
task_vars[k]=v
include_file = utils.template(tokens[0], task_vars)
include_file = tokens[0]
data = utils.parse_yaml_from_file(utils.path_dwim(self.playbook.basedir, include_file))
elif type(x) == dict:
data = [x]
else:
raise Exception("unexpected task type")
for y in data:
mv = task_vars.copy()
results.append(Task(self,y,module_vars=mv))
for x in results:
if self.tags is not None:
x.tags.extend(self.tags)
items = y.get('with_items',None)
if items is None:
items = [ '' ]
elif isinstance(items, basestring):
items = utils.varLookup(items, task_vars)
for item in items:
mv = task_vars.copy()
mv['item'] = item
results.append(Task(self,y,module_vars=mv))
return results
# *************************************************
@ -155,20 +130,10 @@ class Play(object):
else:
vars.update(self.vars)
if type(self.vars_prompt) == list:
for var in self.vars_prompt:
if not 'name' in var:
raise errors.AnsibleError("'vars_prompt' item is missing 'name:'")
vname = var['name']
prompt = "%s: " % var.get("prompt", vname)
private = var.get("private", True)
vars[vname] = self.playbook.callbacks.on_vars_prompt(vname, private, prompt)
elif type(self.vars_prompt) == dict:
for (vname, prompt) in self.vars_prompt.iteritems():
prompt = "%s: " % prompt
vars[vname] = self.playbook.callbacks.on_vars_prompt(vname, False, prompt)
else:
raise errors.AnsibleError("'vars_prompt' section is malformed, see docs")
if type(self.vars_prompt) != dict:
raise errors.AnsibleError("'vars_prompt' section must contain only key/value pairs")
for vname in self.vars_prompt:
vars[vname] = self.playbook.callbacks.on_vars_prompt(vname)
results = self.playbook.extra_vars.copy()
results.update(vars)
@ -178,61 +143,16 @@ class Play(object):
def update_vars_files(self, hosts):
''' calculate vars_files, which requires that setup runs first so ansible facts can be mixed in '''
# now loop through all the hosts...
for h in hosts:
self._update_vars_files_for_host(h)
# *************************************************
def should_run(self, tags):
''' does the play match any of the tags? '''
tags_counted = 0
for task in self._tasks:
for task_tag in task.tags:
tags_counted = tags_counted + 1
if task_tag in tags:
return True
if tags_counted > 0:
return False
# didn't tag the play, and the play contains no steps
# so assume we just want to gather facts
return True
# *************************************************
def _has_vars_in(self, msg):
return ((msg.find("$") != -1) or (msg.find("{{") != -1))
# *************************************************
def _update_vars_files_for_host(self, host):
if type(self.vars_files) != list:
self.vars_files = [ self.vars_files ]
if (host is not None):
inventory = self.playbook.inventory
hostrec = inventory.get_host(host)
groups = [ g.name for g in hostrec.groups ]
basedir = inventory.basedir()
if basedir is not None:
for x in groups:
path = os.path.join(basedir, "group_vars/%s" % x)
if os.path.exists(path):
data = utils.parse_yaml_from_file(path)
if type(data) != dict:
raise errors.AnsibleError("%s must be stored as a dictionary/hash" % path)
self.playbook.SETUP_CACHE[host].update(data)
path = os.path.join(basedir, "host_vars/%s" % hostrec.name)
if os.path.exists(path):
data = utils.parse_yaml_from_file(path)
if type(data) != dict:
raise errors.AnsibleError("%s must be stored as a dictionary/hash" % path)
self.playbook.SETUP_CACHE[host].update(data)
if not host in self.playbook.SETUP_CACHE:
# no need to process failed hosts or hosts not in this play
return
for filename in self.vars_files:
@ -242,54 +162,29 @@ class Play(object):
found = False
sequence = []
for real_filename in filename:
filename2 = utils.template(real_filename, self.vars)
filename3 = filename2
if host is not None:
filename3 = utils.template(filename2, self.playbook.SETUP_CACHE[host])
filename4 = utils.path_dwim(self.playbook.basedir, filename3)
sequence.append(filename4)
if os.path.exists(filename4):
filename2 = utils.template(real_filename, self.playbook.SETUP_CACHE[host])
filename2 = utils.template(filename2, self.vars)
filename2 = utils.path_dwim(self.playbook.basedir, filename2)
sequence.append(filename2)
if os.path.exists(filename2):
found = True
data = utils.parse_yaml_from_file(filename4)
if type(data) != dict:
raise errors.AnsibleError("%s must be stored as a dictionary/hash" % filename4)
if host is not None:
if self._has_vars_in(filename2) and not self._has_vars_in(filename3):
# this filename has variables in it that were fact specific
# so it needs to be loaded into the per host SETUP_CACHE
self.playbook.SETUP_CACHE[host].update(data)
self.playbook.callbacks.on_import_for_host(host, filename4)
elif not self._has_vars_in(filename4):
# found a non-host specific variable, load into vars and NOT
# the setup cache
self.vars.update(data)
elif host is not None:
self.playbook.callbacks.on_not_import_for_host(host, filename4)
if found:
data = utils.parse_yaml_from_file(filename2)
self.playbook.SETUP_CACHE[host].update(data)
self.playbook.callbacks.on_import_for_host(host, filename2)
break
else:
self.playbook.callbacks.on_not_import_for_host(host, filename2)
if not found:
raise errors.AnsibleError(
"%s: FATAL, no files matched for vars_files import sequence: %s" % (host, sequence)
)
else:
# just one filename supplied, load it!
filename2 = utils.template(filename, self.vars)
filename3 = filename2
if host is not None:
filename3 = utils.template(filename2, self.playbook.SETUP_CACHE[host])
filename4 = utils.path_dwim(self.playbook.basedir, filename3)
if self._has_vars_in(filename4):
return
new_vars = utils.parse_yaml_from_file(filename4)
filename2 = utils.template(filename, self.playbook.SETUP_CACHE[host])
filename2 = utils.template(filename2, self.vars)
fpath = utils.path_dwim(self.playbook.basedir, filename2)
new_vars = utils.parse_yaml_from_file(fpath)
if new_vars:
if type(new_vars) != dict:
raise errors.AnsibleError("%s must be stored as dictonary/hash: %s" % filename4)
if host is not None and self._has_vars_in(filename2) and not self._has_vars_in(filename3):
# running a host specific pass and has host specific variables
# load into setup cache
self.playbook.SETUP_CACHE[host].update(new_vars)
elif host is None:
# running a non-host specific pass and we can update the global vars instead
self.vars.update(new_vars)
self.playbook.SETUP_CACHE[host].update(new_vars)
#else: could warn if vars file contains no vars.

View File

@ -15,6 +15,8 @@
# 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
from ansible import utils
@ -22,91 +24,47 @@ class Task(object):
__slots__ = [
'name', 'action', 'only_if', 'async_seconds', 'async_poll_interval',
'notify', 'module_name', 'module_args', 'module_vars',
'play', 'notified_by', 'tags', 'with_items', 'first_available_file', 'ignore_errors'
]
# to prevent typos and such
VALID_KEYS = [
'name', 'action', 'only_if', 'async', 'poll', 'notify', 'with_items', 'first_available_file',
'include', 'tags', 'ignore_errors'
'notify', 'module_name', 'module_args', 'module_vars', 'play', 'notified_by',
]
def __init__(self, play, ds, module_vars=None):
''' constructor loads from a task or handler datastructure '''
for x in ds.keys():
if not x in Task.VALID_KEYS:
raise errors.AnsibleError("%s is not a legal parameter in an Ansible task or handler" % x)
# TODO: more error handling
# include task specific vars
self.module_vars = module_vars
self.play = play
# load various attributes
self.play = play
self.name = ds.get('name', None)
self.action = ds.get('action', '')
self.tags = [ 'all' ]
# notified by is used by Playbook code to flag which hosts
# need to run a notifier
self.notified_by = []
# if no name is specified, use the action line as the name
if self.name is None:
self.name = self.action
# load various attributes
self.only_if = ds.get('only_if', 'True')
self.async_seconds = int(ds.get('async', 0)) # not async by default
self.async_poll_interval = int(ds.get('poll', 10)) # default poll = 10 seconds
self.notify = ds.get('notify', [])
self.first_available_file = ds.get('first_available_file', None)
self.with_items = ds.get('with_items', None)
self.ignore_errors = ds.get('ignore_errors', False)
# notify can be a string or a list, store as a list
if isinstance(self.notify, basestring):
self.notify = [ self.notify ]
# split the action line into a module name + arguments
tokens = self.action.split(None, 1)
if len(tokens) < 1:
raise errors.AnsibleError("invalid/missing action in task. name: %s" % self.name)
raise errors.AnsibleError("invalid/missing action in task")
self.module_name = tokens[0]
self.module_args = ''
if len(tokens) > 1:
self.module_args = tokens[1]
import_tags = self.module_vars.get('tags',[])
if type(import_tags) in [str,unicode]:
# allow the user to list comma delimited tags
import_tags = import_tags.split(",")
self.name = utils.template(self.name, self.module_vars)
self.action = utils.template(self.action, self.module_vars)
self.action = utils.template(self.name, self.module_vars)
# handle mutually incompatible options
if self.with_items is not None and self.first_available_file is not None:
raise errors.AnsibleError("with_items and first_available_file are mutually incompatible in a single task")
# make first_available_file accessable to Runner code
if self.first_available_file:
self.module_vars['first_available_file'] = self.first_available_file
# process with_items so it can be used by Runner code
if self.with_items is None:
self.with_items = [ ]
self.module_vars['items'] = self.with_items
if 'first_available_file' in ds:
self.module_vars['first_available_file'] = ds.get('first_available_file')
# make ignore_errors accessable to Runner code
self.module_vars['ignore_errors'] = self.ignore_errors
# tags allow certain parts of a playbook to be run without running the whole playbook
apply_tags = ds.get('tags', None)
if apply_tags is not None:
if type(apply_tags) in [ str, unicode ]:
self.tags.append(apply_tags)
elif type(apply_tags) == list:
self.tags.extend(apply_tags)
self.tags.extend(import_tags)

File diff suppressed because it is too large Load Diff

View File

@ -18,6 +18,17 @@
################################################
import warnings
import traceback
import os
import time
import re
import shutil
import subprocess
import pipes
import socket
import random
import local
import paramiko_ssh
import ssh
@ -25,17 +36,17 @@ import ssh
class Connection(object):
''' Handles abstract connections to remote hosts '''
def __init__(self, runner):
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
transport = self.runner.transport
if transport == 'local':
if self.transport == 'local':
conn = local.LocalConnection(self.runner, host)
elif transport == 'paramiko':
elif self.transport == 'paramiko':
conn = paramiko_ssh.ParamikoConnection(self.runner, host, port)
elif transport == 'ssh':
elif self.transport == 'ssh':
conn = ssh.SSHConnection(self.runner, host, port)
if conn is None:
raise Exception("unsupported connection type")

View File

@ -14,11 +14,21 @@
#
# You should have received a copy of the GNU General Public License
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
#
################################################
import warnings
import traceback
import os
import time
import re
import shutil
import subprocess
import pipes
import socket
import random
from ansible import errors
class LocalConnection(object):
@ -33,16 +43,15 @@ class LocalConnection(object):
return self
def exec_command(self, cmd, tmp_path, sudo_user, 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:
if self.runner.sudo_pass:
# NOTE: if someone wants to add sudo w/ password to the local connection type, they are welcome
# to do so. The primary usage of the local connection is for crontab and kickstart usage however
# so this doesn't seem to be a huge priority
raise errors.AnsibleError("sudo with password is presently only supported on the paramiko (SSH) connection type")
cmd = "sudo -u {0} -s {1}".format(sudo_user, cmd)
cmd = "sudo -s %s" % cmd
if self.runner.sudo_pass:
# NOTE: if someone wants to add sudo w/ password to the local connection type, they are welcome
# to do so. The primary usage of the local connection is for crontab and kickstart usage however
# so this doesn't seem to be a huge priority
raise errors.AnsibleError("sudo with password is presently only supported on the paramiko (SSH) connection type")
p = subprocess.Popen(cmd, shell=True, stdin=None,
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
@ -51,7 +60,6 @@ class LocalConnection(object):
def put_file(self, in_path, out_path):
''' transfer a file from local to local '''
if not os.path.exists(in_path):
raise errors.AnsibleFileNotFound("file or module does not exist: %s" % in_path)
try:
@ -69,4 +77,5 @@ class LocalConnection(object):
def close(self):
''' terminate the connection; nothing to do here '''
pass

View File

@ -14,27 +14,27 @@
#
# You should have received a copy of the GNU General Public License
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
#
################################################
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/
HAVE_PARAMIKO=False
from ansible import errors
# prevent paramiko warning noise
# see http://stackoverflow.com/questions/3920502/
with warnings.catch_warnings():
warnings.simplefilter("ignore")
try:
import paramiko
HAVE_PARAMIKO=True
except ImportError:
pass
import paramiko
class ParamikoConnection(object):
''' SSH based connections with Paramiko '''
@ -47,20 +47,23 @@ class ParamikoConnection(object):
if port is None:
self.port = self.runner.remote_port
def connect(self):
''' activates the connection object '''
if not HAVE_PARAMIKO:
raise errors.AnsibleError("paramiko is not installed")
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=user, allow_agent=True, look_for_keys=True,
key_filename=self.runner.private_key_file, password=self.runner.remote_pass,
timeout=self.runner.timeout, port=self.port)
ssh.connect(
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,
port=self.port
)
except Exception, e:
msg = str(e)
if "PID check failed" in msg:
@ -72,12 +75,17 @@ class ParamikoConnection(object):
else:
raise errors.AnsibleConnectionFailed(msg)
self.ssh = ssh
return ssh
def connect(self):
''' connect to the remote host '''
self.ssh = self._get_conn()
return self
def exec_command(self, cmd, tmp_path, sudo_user, sudoable=False):
''' run a command on the remote host '''
def exec_command(self, cmd, tmp_path,sudo_user,sudoable=False):
''' run a command on the remote host '''
bufsize = 4096
chan = self.ssh.get_transport().open_session()
chan.get_pty()
@ -105,18 +113,16 @@ class ParamikoConnection(object):
while not sudo_output.endswith(prompt):
chunk = chan.recv(bufsize)
if not chunk:
if 'unknown user' in sudo_output:
raise errors.AnsibleError(
'user %s does not exist' % sudo_user)
else:
raise errors.AnsibleError('ssh connection ' +
'closed waiting for password prompt')
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)
return (chan.makefile('wb', bufsize), chan.makefile('rb', bufsize), '')
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 '''
@ -126,19 +132,21 @@ class ParamikoConnection(object):
try:
sftp.put(in_path, out_path)
except IOError:
traceback.print_exc()
raise errors.AnsibleError("failed to transfer file to %s" % out_path)
sftp.close()
def fetch_file(self, in_path, out_path):
''' save a remote file to the specified path '''
sftp = self.ssh.open_sftp()
try:
sftp.get(in_path, out_path)
except IOError:
traceback.print_exc()
raise errors.AnsibleError("failed to transfer file from %s" % in_path)
sftp.close()
def close(self):
''' terminate the connection '''
self.ssh.close()

View File

@ -16,13 +16,17 @@
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
#
################################################
import os
import time
import subprocess
import shlex
import pipes
import random
import select
import fcntl
from ansible import errors
class SSHConnection(object):
@ -35,7 +39,6 @@ class SSHConnection(object):
def connect(self):
''' connect to the remote host '''
self.common_args = []
extra_args = os.getenv("ANSIBLE_SSH_ARGS", None)
if extra_args is not None:
@ -53,7 +56,7 @@ class SSHConnection(object):
return self
def exec_command(self, cmd, tmp_path, sudo_user,sudoable=False):
def exec_command(self, cmd, tmp_path,sudo_user,sudoable=False):
''' run a command on the remote host '''
ssh_cmd = ["ssh", "-tt", "-q"] + self.common_args + [self.host]
@ -104,8 +107,6 @@ class SSHConnection(object):
stdout += os.read(p.stdout.fileno(), 1024)
# older versions of ssh generate this error which we ignore
stdout=stdout.replace("tcgetattr: Invalid argument\n", "")
# suppress Ubuntu 10.04/12.04 error on -tt option
stdout=stdout.replace("tcgetattr: Inappropriate ioctl for device\n","")
if p.returncode != 0 and stdout.find('Bad configuration option: ControlPersist') != -1:
raise errors.AnsibleError('using -c ssh on certain older ssh versions may not support ControlPersist, set ANSIBLE_SSH_ARGS="" before running again')
@ -125,6 +126,8 @@ class SSHConnection(object):
def fetch_file(self, in_path, out_path):
''' fetch a file from remote to local '''
if not os.path.exists(in_path):
raise errors.AnsibleFileNotFound("file or module does not exist: %s" % in_path)
sftp_cmd = ["sftp"] + self.common_args + [self.host]
p = subprocess.Popen(sftp_cmd, stdin=subprocess.PIPE,
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
@ -133,6 +136,5 @@ class SSHConnection(object):
raise errors.AnsibleError("failed to transfer file from %s:\n%s\n%s" % (in_path, stdout, stderr))
def close(self):
''' not applicable since we're executing openssh binaries '''
''' terminate the connection '''
pass

View File

@ -15,6 +15,8 @@
# You should have received a copy of the GNU General Public License
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
###############################################################
import sys
import os
import shlex
@ -23,21 +25,14 @@ import codecs
import jinja2
import yaml
import optparse
import operator
from ansible import errors
from ansible import color
from ansible import __version__
import ansible.constants as C
from operator import methodcaller
try:
import json
except ImportError:
import simplejson as json
try:
from hashlib import md5 as _md5
except ImportError:
from md5 import md5 as _md5
from ansible import errors
import ansible.constants as C
###############################################################
# UTILITY FUNCTIONS FOR COMMAND LINE TOOLS
@ -45,27 +40,76 @@ except ImportError:
def err(msg):
''' print an error message to stderr '''
print >> sys.stderr, msg
def exit(msg, rc=1):
''' quit with an error to stdout and a failure code '''
err(msg)
sys.exit(rc)
def jsonify(result, format=False):
''' format JSON output (uncompressed or uncompressed) '''
def bigjson(result):
''' format JSON output (uncompressed) '''
result2 = result.copy()
if format:
return json.dumps(result2, sort_keys=True, indent=4)
return json.dumps(result2, sort_keys=True, indent=4)
def smjson(result):
''' format JSON output (compressed) '''
result2 = result.copy()
return json.dumps(result2, sort_keys=True)
def task_start_msg(name, conditional):
if conditional:
return "NOTIFIED: [%s]" % name
else:
return json.dumps(result2, sort_keys=True)
return "TASK: [%s]" % name
def regular_generic_msg(hostname, result, oneline, caption):
''' output on the result of a module run that is not command '''
if not oneline:
return "%s | %s >> %s\n" % (hostname, caption, bigjson(result))
else:
return "%s | %s >> %s\n" % (hostname, caption, smjson(result))
def regular_success_msg(hostname, result, oneline):
''' output the result of a successful module run '''
return regular_generic_msg(hostname, result, oneline, 'success')
def regular_failure_msg(hostname, result, oneline):
''' output the result of a failed module run '''
return regular_generic_msg(hostname, result, oneline, 'FAILED')
def command_generic_msg(hostname, result, oneline, caption):
''' output the result of a command run '''
rc = result.get('rc', '0')
stdout = result.get('stdout','')
stderr = result.get('stderr', '')
msg = result.get('msg', '')
if not oneline:
buf = "%s | %s | rc=%s >>\n" % (hostname, caption, result.get('rc',0))
if stdout:
buf += stdout
if stderr:
buf += stderr
if msg:
buf += msg
buf += "\n"
return buf
else:
if stderr:
return "%s | %s | rc=%s | (stdout) %s (stderr) %s\n" % (hostname, caption, rc, stdout, stderr)
else:
return "%s | %s | rc=%s | (stdout) %s\n" % (hostname, caption, rc, stdout)
def command_success_msg(hostname, result, oneline):
''' output from a successful command run '''
return command_generic_msg(hostname, result, oneline, 'success')
def command_failure_msg(hostname, result, oneline):
''' output from a failed command run '''
return command_generic_msg(hostname, result, oneline, 'FAILED')
def write_tree_file(tree, hostname, buf):
''' write something into treedir/hostname '''
# TODO: might be nice to append playbook runs per host in a similar way
# in which case, we'd want append mode.
path = os.path.join(tree, hostname)
@ -75,12 +119,33 @@ def write_tree_file(tree, hostname, buf):
def is_failed(result):
''' is a given JSON result a failed result? '''
failed = False
rc = 0
if type(result) == dict:
failed = result.get('failed', 0)
rc = result.get('rc', 0)
if rc != 0:
return True
return failed
return ((result.get('rc', 0) != 0) or (result.get('failed', False) in [ True, 'True', 'true']))
def host_report_msg(hostname, module_name, result, oneline):
''' summarize the JSON results for a particular host '''
buf = ''
failed = is_failed(result)
if module_name in [ 'command', 'shell', 'raw' ] and 'ansible_job_id' not in result:
if not failed:
buf = command_success_msg(hostname, result, oneline)
else:
buf = command_failure_msg(hostname, result, oneline)
else:
if not failed:
buf = regular_success_msg(hostname, result, oneline)
else:
buf = regular_failure_msg(hostname, result, oneline)
return buf
def prepare_writeable_dir(tree):
''' make sure a directory exists and is writeable '''
if tree != '/':
tree = os.path.realpath(os.path.expanduser(tree))
if not os.path.exists(tree):
@ -93,7 +158,6 @@ def prepare_writeable_dir(tree):
def path_dwim(basedir, given):
''' make relative paths work like folks expect '''
if given.startswith("/"):
return given
elif given.startswith("~/"):
@ -102,24 +166,21 @@ def path_dwim(basedir, given):
return os.path.join(basedir, given)
def json_loads(data):
''' parse a JSON string and return a data structure '''
return json.loads(data)
def parse_json(data):
''' this version for module return data only '''
try:
return json.loads(data)
except:
# not JSON, but try "Baby JSON" which allows many of our modules to not
# require JSON and makes writing modules in bash much simpler
results = {}
try:
try :
tokens = shlex.split(data)
except:
print "failed to parse json: "+ data
raise
raise;
for t in tokens:
if t.find("=") == -1:
@ -141,7 +202,6 @@ _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:
@ -160,20 +220,25 @@ def _varLookup(name, vars):
return space
_KEYCRE = re.compile(r"\$(?P<complex>\{){0,1}((?(complex)[\w\.\[\]]+|\w+))(?(complex)\})")
# if { -> complex if complex, allow . and need trailing }
def varLookup(varname, vars):
''' helper function used by varReplace '''
m = _KEYCRE.search(varname)
if not m:
return None
return _varLookup(m.group(2), vars)
def varReplace(raw, vars):
''' Perform variable replacement of $variables in string raw using vars dictionary '''
'''Perform variable replacement of $vars
@param raw: String to perform substitution on.
@param vars: Dictionary of variables to replace. Key is variable name
(without $ prefix). Value is replacement string.
@return: Input raw string with substituted values.
'''
# this code originally from yum
done = [] # Completed chunks to return
done = [] # Completed chunks to return
while raw:
m = _KEYCRE.search(raw)
@ -194,39 +259,38 @@ def varReplace(raw, vars):
return ''.join(done)
def template(text, vars):
''' run a text buffer through the templating engine until it no longer changes '''
prev_text = ''
depth = 0
while prev_text != text:
depth = depth + 1
if (depth > 20):
raise errors.AnsibleError("template recursion depth exceeded")
prev_text = text
text = varReplace(unicode(text), vars)
def _template(text, vars, setup_cache=None):
''' run a text buffer through the templating engine '''
vars = vars.copy()
vars['hostvars'] = setup_cache
text = varReplace(unicode(text), vars)
return text
def template_from_file(basedir, path, vars):
''' run a file through the templating engine '''
def template(text, vars, setup_cache=None):
''' run a text buffer through the templating engine
until it no longer changes '''
prev_text = ''
while prev_text != text:
prev_text = text
text = _template(text, vars, setup_cache)
return text
def template_from_file(basedir, path, vars, setup_cache):
''' run a file through the templating engine '''
environment = jinja2.Environment(loader=jinja2.FileSystemLoader(basedir), trim_blocks=False)
data = codecs.open(path_dwim(basedir, path), encoding="utf8").read()
t = environment.from_string(data)
vars = vars.copy()
vars['hostvars'] = setup_cache
res = t.render(vars)
if data.endswith('\n') and not res.endswith('\n'):
res = res + '\n'
return template(res, vars)
return template(res, vars, setup_cache)
def parse_yaml(data):
''' convert a yaml string to a data structure '''
return yaml.load(data)
def parse_yaml_from_file(path):
''' convert a yaml file to a data structure '''
try:
data = file(path).read()
except IOError:
@ -235,53 +299,25 @@ def parse_yaml_from_file(path):
def parse_kv(args):
''' convert a string of key/value items to a dict '''
options = {}
if args is not None:
# attempting to split a unicode here does bad things
vargs = shlex.split(str(args), posix=True)
vargs = shlex.split(args, posix=True)
for x in vargs:
if x.find("=") != -1:
k, v = x.split("=",1)
k, v = x.split("=", 1)
options[k]=v
return options
def md5(filename):
''' Return MD5 hex digest of local file, or None if file is not present. '''
if not os.path.exists(filename):
return None
digest = _md5()
blocksize = 64 * 1024
infile = open(filename, 'rb')
block = infile.read(blocksize)
while block:
digest.update(block)
block = infile.read(blocksize)
infile.close()
return digest.hexdigest()
def default(value, function):
''' syntactic sugar around lazy evaluation of defaults '''
if value is None:
return function()
return value
####################################################################
# option handling code for /usr/bin/ansible and ansible-playbook
# below this line
class SortedOptParser(optparse.OptionParser):
'''Optparser which sorts the options by opt before outputting --help'''
def format_help(self, formatter=None):
self.option_list.sort(key=operator.methodcaller('get_opt_string'))
self.option_list.sort(key=methodcaller('get_opt_string'))
return optparse.OptionParser.format_help(self, formatter=None)
def base_parser(constants=C, usage="", output_opts=False, runas_opts=False, async_opts=False, connect_opts=False):
''' create an options parser for any ansible script '''
parser = SortedOptParser(usage, version="%prog " + __version__)
parser = SortedOptParser(usage)
parser.add_option('-v','--verbose', default=False, action="store_true",
help='verbose mode')
parser.add_option('-f','--forks', dest='forks', default=constants.DEFAULT_FORKS, type='int',
@ -333,4 +369,3 @@ def base_parser(constants=C, usage="", output_opts=False, runas_opts=False, asyn
return parser

View File

@ -1,6 +1,4 @@
#!/usr/bin/python -tt
# -*- coding: utf-8 -*-
# (c) 2012, Flowroute LLC
# Written by Matthew Williams <matthew@flowroute.com>
# Based on yum module written by Seth Vidal <skvidal at fedoraproject.org>
@ -19,18 +17,36 @@
# along with this software. If not, see <http://www.gnu.org/licenses/>.
#
try:
import json
except ImportError:
import simplejson as json
import os
import sys
import shlex
import subprocess
import syslog
import traceback
# added to stave off future warnings about apt api
import warnings;
warnings.filterwarnings('ignore', "apt API not stable yet", FutureWarning)
# APT related constants
APT_PATH = "/usr/bin/apt-get"
APT = "DEBIAN_FRONTEND=noninteractive DEBIAN_PRIORITY=critical %s" % APT_PATH
APT = "DEBIAN_PRIORITY=critical %s" % APT_PATH
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)
try:
import apt, apt_pkg
except ImportError:
fail_json(msg="could not import apt, please install the python-apt package on this host")
def run_apt(command):
try:
cmd = subprocess.Popen(command, shell=True,
cmd = subprocess.Popen(command, shell=True,
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
out, err = cmd.communicate()
except (OSError, IOError), e:
@ -52,133 +68,115 @@ def package_split(pkgspec):
else:
return parts[0], None
def package_status(m, pkgname, version, cache):
def package_status(pkgname, version, cache):
try:
pkg = cache[pkgname]
except KeyError:
m.fail_json(msg="No package matching '%s' is available" % pkgname)
fail_json(msg="No package matching '%s' is available" % pkgname)
if version:
try :
return pkg.is_installed and pkg.installed.version == version, False
except AttributeError:
except AttributeError:
#assume older version of python-apt is installed
return pkg.isInstalled and pkg.installedVersion == version, False
else:
try :
return pkg.is_installed, pkg.is_upgradable
except AttributeError:
except AttributeError:
#assume older version of python-apt is installed
return pkg.isInstalled, pkg.isUpgradable
def install(m, pkgspec, cache, upgrade=False, default_release=None, install_recommends=True, force=False):
packages = ""
for package in pkgspec:
name, version = package_split(package)
installed, upgradable = package_status(m, name, version, cache)
if not installed or (upgrade and upgradable):
packages += "'%s' " % package
if len(packages) != 0:
if force:
force_yes = '--force-yes'
else:
force_yes = ''
cmd = "%s --option Dpkg::Options::=--force-confold -q -y %s install %s" % (APT, force_yes,packages)
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 --option Dpkg::Options::=--force-confold -q -y install '%s'" % (APT, pkgspec)
if default_release:
cmd += " -t '%s'" % (default_release,)
if not install_recommends:
cmd += " --no-install-recommends"
rc, out, err = run_apt(cmd)
if rc:
m.fail_json(msg="'apt-get install %s' failed: %s" % (packages, err))
else:
m.exit_json(changed=True)
fail_json(msg="'apt-get install %s' failed: %s" % (pkgspec, err))
return True
else:
m.exit_json(changed=False)
return False
def remove(m, pkgspec, cache, purge=False):
packages = ""
for package in pkgspec:
name, version = package_split(package)
installed, upgradable = package_status(m, name, version, cache)
if installed:
packages += "'%s' " % package
if len(packages) == 0:
m.exit_json(changed=False)
def remove(pkgspec, cache, purge=False):
name, version = package_split(pkgspec)
installed, upgradable = package_status(name, version, cache)
if not installed:
return False
else:
purge = '--purge' if purge else ''
cmd = "%s -q -y %s remove %s" % (APT, purge,packages)
cmd = "%s -q -y %s remove '%s'" % (APT, purge, name)
rc, out, err = run_apt(cmd)
if rc:
m.fail_json(msg="'apt-get remove %s' failed: %s" % (packages, err))
m.exit_json(changed=True)
def main():
module = AnsibleModule(
argument_spec = dict(
state = dict(default='installed', choices=['installed', 'latest', 'removed']),
update_cache = dict(default='no', choices=['yes', 'no'], aliases=['update-cache']),
purge = dict(default='no', choices=['yes', 'no']),
package = dict(default=None, aliases=['pkg', 'name']),
default_release = dict(default=None, aliases=['default-release']),
install_recommends = dict(default='yes', aliases=['install-recommends'], choices=['yes', 'no']),
force = dict(default='no', choices=['yes', 'no'])
)
)
try:
import apt, apt_pkg
except:
module.fail_json("Could not import python modules: apt, apt_pkg. Please install python-apt package.")
if not os.path.exists(APT_PATH):
module.fail_json(msg="Cannot find apt-get")
p = module.params
if p['package'] is None and p['update_cache'] != 'yes':
module.fail_json(msg='pkg=name and/or update_cache=yes is required')
install_recommends = module.boolean(p['install_recommends'])
cache = apt.Cache()
if p['default_release']:
apt_pkg.config['APT::Default-Release'] = p['default_release']
# reopen cache w/ modified config
cache.open(progress=None)
if module.boolean(p['update_cache']):
cache.update()
cache.open(progress=None)
if p['package'] == None:
module.exit_json(changed=False)
force_yes = module.boolean(p['force'])
fail_json(msg="'apt-get remove %s' failed: %s" % (name, err))
return True
packages = p['package'].split(',')
latest = p['state'] == 'latest'
for package in packages:
if package.count('=') > 1:
module.fail_json(msg="invalid package spec: %s" % package)
if latest and '=' in package:
module.fail_json(msg='version number inconsistent with state=latest: %s' % package)
if p['state'] == 'latest':
install(module, packages, cache, upgrade=True,
default_release=p['default_release'],
install_recommends=install_recommends,
force=force_yes)
elif p['state'] == 'installed':
install(module, packages, cache, default_release=p['default_release'],
install_recommends=install_recommends,force=force_yes)
elif p['state'] == 'removed':
remove(module, packages, cache, purge = module.boolean(p['purge']))
# ===========================================
# this is magic, see lib/ansible/module_common.py
#<<INCLUDE_ANSIBLE_MODULE_COMMON>>
if not os.path.exists(APT_PATH):
fail_json(msg="Cannot find apt-get")
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')
sys.exit(1)
params = {}
for x in items:
(k, v) = x.split("=", 1)
params[k] = v
state = params.get('state', 'installed')
package = params.get('pkg', params.get('package', params.get('name', None)))
update_cache = params.get('update-cache', 'no')
purge = params.get('purge', 'no')
default_release = params.get('default-release', None)
if state not in ['installed', 'latest', 'removed']:
fail_json(msg='invalid state')
if update_cache not in ['yes', 'no']:
fail_json(msg='invalid value for update_cache (requires yes or no -- default is no')
if purge not in ['yes', 'no']:
fail_json(msg='invalid value for purge (requires yes or no -- default is no)')
if package is None and update_cache != 'yes':
fail_json(msg='pkg=name and/or update-cache=yes is required')
cache = apt.Cache()
if default_release:
apt_pkg.config['APT::Default-Release'] = default_release
# reopen cache w/ modified config
cache.open()
if update_cache == 'yes':
cache.update()
cache.open()
if package == None:
exit_json(changed=False)
if package.count('=') > 1:
fail_json(msg='invalid package spec')
if state == 'latest':
if '=' in package:
fail_json(msg='version number inconsistent with state=latest')
changed = install(package, cache, upgrade=True,
default_release=default_release)
elif state == 'installed':
changed = install(package, cache, default_release=default_release)
elif state == 'removed':
changed = remove(package, cache, purge == 'yes')
exit_json(changed=changed)
main()

View File

@ -1,5 +1,4 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# (c) 2012, Stephen Fromm <sfromm@gmail.com>
#
@ -18,14 +17,38 @@
# 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 os.path
import sys
import shlex
import shutil
import syslog
import tempfile
try:
import hashlib
HAVE_HASHLIB=True
except ImportError:
import md5
HAVE_HASHLIB=False
# Since hashlib is only available in 2.5 and onwards, this module
# uses md5 which is available in 2.4.
# ===========================================
# 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)
def assemble_from_fragments(path):
''' assemble a file from a directory of fragments '''
assembled = []
@ -41,47 +64,60 @@ def write_temp_file(data):
os.close(fd)
return path
# ==============================================================
# main
def file_digest(path):
if HAVE_HASHLIB:
digest = hashlib.md5(file(path).read()).hexdigest()
else:
digest = md5.new(file(path).read()).hexdigest()
return digest
def main():
module = AnsibleModule(
argument_spec = dict(
src = dict(required=True),
dest = dict(required=True),
)
)
changed=False
pathmd5 = None
destmd5 = None
src = os.path.expanduser(module.params['src'])
dest = os.path.expanduser(module.params['dest'])
if not os.path.exists(src):
module.fail_json(msg="Source (%s) does not exist" % src)
if not os.path.isdir(src):
module.fail_json(msg="Source (%s) is not a directory" % src)
path = write_temp_file(assemble_from_fragments(src))
pathmd5 = module.md5(path)
if os.path.exists(dest):
destmd5 = module.md5(dest)
if pathmd5 != destmd5:
shutil.copy(path, dest)
changed = True
# ===========================================
# Mission complete
module.exit_json(src=src, dest=dest, md5sum=destmd5,
changed=changed, msg="OK",
daisychain="file", daisychain_args=module.params)
if len(sys.argv) == 1:
fail_json(msg="the assemble module requires arguments (-a)")
# this is magic, see lib/ansible/module_common.py
#<<INCLUDE_ANSIBLE_MODULE_COMMON>>
argfile = sys.argv[1]
if not os.path.exists(argfile):
fail_json(msg="Argument file not found")
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):
fail_json(msg="the assemble module requires arguments (-a)")
params = {}
for x in items:
(k, v) = x.split("=")
params[k] = v
changed = False
pathmd5 = None
destmd5 = None
src = params.get('src', None)
dest = params.get('dest', None)
if src:
src = os.path.expanduser(src)
if dest:
dest = os.path.expanduser(dest)
if not os.path.exists(src):
fail_json(msg="Source (%s) does not exist" % src)
if not os.path.isdir(src):
fail_json(msg="Source (%s) is not a directory" % src)
path = write_temp_file(assemble_from_fragments(src))
pathmd5 = file_digest(path)
if os.path.exists(dest):
destmd5 = file_digest(dest)
if pathmd5 != destmd5:
shutil.copy(path, dest)
changed = True
exit_json(md5sum=pathmd5, changed=changed)

View File

@ -1,5 +1,4 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# (c) 2012, Michael DeHaan <michael.dehaan@gmail.com>, and others
#
@ -19,50 +18,96 @@
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
#
try:
import json
except ImportError:
import simplejson as json
import shlex
import os
import subprocess
import sys
import datetime
import traceback
import syslog
def main():
# ===========================================
module = AnsibleModule(argument_spec=dict(
jid=dict(required=True),
mode=dict(default='status', choices=['status','cleanup']),
))
# FIXME: better error handling
mode = module.params['mode']
jid = module.params['jid']
argsfile = sys.argv[1]
args = open(argsfile, 'r').read()
items = shlex.split(args)
# setup logging directory
logdir = os.path.expanduser("~/.ansible_async")
log_path = os.path.join(logdir, jid)
syslog.openlog('ansible-%s' % os.path.basename(__file__))
syslog.syslog(syslog.LOG_NOTICE, 'Invoked with %s' % args)
if not os.path.exists(log_path):
module.fail_json(msg="could not find job", ansible_job_id=jid)
params = {}
for x in items:
(k, v) = x.split("=")
params[k] = v
if mode == 'cleanup':
os.unlink(log_path)
module.exit_json(ansible_job_id=jid, erased=log_path)
mode = params.get('mode', 'status')
jid = params.get('jid', None)
# NOT in cleanup mode, assume regular status mode
# no remote kill mode currently exists, but probably should
# consider log_path + ".pid" file and also unlink that above
# ===========================================
data = file(log_path).read()
try:
data = json.loads(data)
except Exception, e:
if data == '':
# file not written yet? That means it is running
module.exit_json(results_file=log_path, ansible_job_id=jid, started=1)
else:
module_fail_json(ansible_job_id=jid, results_file=log_path,
msg="Could not parse job output: %s" % data)
if jid is None:
print json.dumps({
"failed" : True,
"msg" : "jid=INTEGER is required"
})
sys.exit(1)
# setup logging directory
logdir = os.path.expanduser("~/.ansible_async")
log_path = os.path.join(logdir, jid)
if not os.path.exists(log_path):
print json.dumps({
"failed" : 1,
"msg" : "could not find job",
"ansible_job_id" : jid
})
sys.exit(1)
if mode == 'cleanup':
os.unlink(log_path)
print json.dumps({
"ansible_job_id" : jid,
"erased" : log_path
})
sys.exit(0)
# NOT in cleanup mode, assume regular status mode
# no remote kill mode currently exists, but probably should
# consider log_path + ".pid" file and also unlink that above
data = file(log_path).read()
try:
data = json.loads(data)
except Exception, e:
if data == '':
# file not written yet? That means it is running
print json.dumps({
"results_file" : log_path,
"ansible_job_id" : jid,
"started" : 1,
})
else:
print json.dumps({
"failed" : True,
"ansible_job_id" : jid,
"results_file" : log_path,
"msg" : "Could not parse job output: %s" % data,
})
sys.exit(0)
if not data.has_key("started"):
data['finished'] = 1
data['ansible_job_id'] = jid
print json.dumps(data)
sys.exit(0)
if not data.has_key("started"):
data['finished'] = 1
data['ansible_job_id'] = jid
module.exit_json(**data)
# this is magic, see lib/ansible/module_common.py
#<<INCLUDE_ANSIBLE_MODULE_COMMON>>
main()

View File

@ -1,5 +1,4 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# (c) 2012, Michael DeHaan <michael.dehaan@gmail.com>, and others
#

View File

@ -1,10 +1,35 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""Ansible module to add authorized_keys for ssh logins.
"""
Ansible module to add authorized_keys for ssh logins.
(c) 2012, Brad Olson <brado@movedbylight.com>
Results: Makes sure the public key line is present or absent in the user's .ssh/authorized_keys.
Arguments
=========
user = username
key = line to add to authorized_keys for user
state = absent|present (default: present)
Command Line Example
====================
ansible somehost -m authorized_key -a user=charlie key="ssh-dss AAAABUfOL+8BTwaRYr/rycsBF1D8e5pTxEsXHQs4iq+mZdyWqlW++L6pMiam1A8yweP+rKtgjK2httVS6GigVsuWWfOd7/sdWippefq74nppVUELHPKkaIOjJNN1zUHFoL/YMwAAAEBALnAsQN10TNGsRDe5arBsW8cTOjqLyYBcIqgPYTZW8zENErFxt7ij3fW3Jh/sCpnmy8rkS7FyK8ULX0PEy/2yDx8/5rXgMIICbRH/XaBy9Ud5bRBFVkEDu/r+rXP33wFPHjWjwvHAtfci1NRBAudQI/98DbcGQw5HmE89CjgZRo5ktkC5yu/8agEPocVjdHyZr7PaHfxZGUDGKtGRL2QzRYukCmWo1cZbMBHcI5FzImvTHS9/8B3SATjXMPgbfBuEeBwuBK5EjL+CtHY5bWs9kmYjmeo0KfUMH8hY4MAXDoKhQ7DhBPIrcjS5jPtoGxIREZjba67r6/P2XKXaCZH6Fc= charlie@somemail.org 2011-01-17"
Playbook Example
================
---
# include like this:
# - include: tasks/logins.yaml users=charlie,sue
- name: create user charlie
action: user name=charlie shell=/bin/bash createhome=yes groups=www-data
only_if: "'charlie' in '$users'"
- name: add public key for charlie
action: authorized_key user=charlie key="ssh-dss AAAABUfOL+8BTwaRYr/rycsBF1D8e5pTxEsXHQs4iq+mZdyWqlW++L6pMiam1A8yweP+rKtgjK2httVS6GigVsuWWfOd7/sdWippefq74nppVUELHPKkaIOjJNN1zUHFoL/YMwAAAEBALnAsQN10TNGsRDe5arBsW8cTOjqLyYBcIqgPYTZW8zENErFxt7ij3fW3Jh/sCpnmy8rkS7FyK8ULX0PEy/2yDx8/5rXgMIICbRH/XaBy9Ud5bRBFVkEDu/r+rXP33wFPHjWjwvHAtfci1NRBAudQI/98DbcGQw5HmE89CjgZRo5ktkC5yu/8agEPocVjdHyZr7PaHfxZGUDGKtGRL2QzRYukCmWo1cZbMBHcI5FzImvTHS9/8B3SATjXMPgbfBuEeBwuBK5EjL+CtHY5bWs9kmYjmeo0KfUMH8hY4MAXDoKhQ7DhBPIrcjS5jPtoGxIREZjba67r6/P2XKXaCZH6Fc= charlie@somemail.org 2011-01-17"
only_if: "'charlie' in '$users'"
This file is part of Ansible
Ansible is free software: you can redistribute it and/or modify
@ -21,24 +46,57 @@ You should have received a copy of the GNU General Public License
along with Ansible. If not, see <http://www.gnu.org/licenses/>.
"""
# Makes sure the public key line is present or absent in the user's .ssh/authorized_keys.
#
# Arguments
# =========
# user = username
# key = line to add to authorized_keys for user
# state = absent|present (default: present)
#
# see example in examples/playbooks
try:
import json
except ImportError:
import simplejson as json
import sys
import os
import pwd
import os.path
import sys, os, shlex, pwd, syslog
from os.path import expanduser, exists, isfile, join
params = {}
msg=""
def exit_json(rc=0, **kwargs):
if 'name' in kwargs:
add_user_info(kwargs)
print json.dumps(kwargs)
sys.exit(rc)
def fail_json(**kwargs):
kwargs['failed'] = True
exit_json(rc=1, **kwargs)
def get_params():
"""Startup tasks and read params.
:return: parameters as dictionary.
"""
global msg
msg = "reading params"
argfile = sys.argv[1]
try:
f = open(argfile,"r")
args = f.read()
finally:
f.close()
msg = "writing syslog."
syslog.openlog('ansible-%s' % os.path.basename(__file__))
syslog.syslog(syslog.LOG_NOTICE, 'Invoked with %s' % args)
msg = "parsing params"
params = dict( # make a dictionary of...
[ arg.split("=", 1) # assignment pairs
for arg in shlex.split(args) # using shell lexing
if "=" in arg # ignoring tokens without assignment
])
return params
def keyfile(user, write=False):
"""
Calculate name of authorized keys file, optionally creating the
"""Calculate name of authorized keys file, optionally creating the
directories and file, properly setting permissions.
:param str user: name of user in passwd file
@ -46,91 +104,98 @@ def keyfile(user, write=False):
:return: full path string to authorized_keys for user
"""
global msg
msg = "Reading system user entry."
user_entry = pwd.getpwnam(user)
homedir = user_entry.pw_dir
sshdir = os.path.join(homedir, ".ssh")
keysfile = os.path.join(sshdir, "authorized_keys")
if not write:
return keysfile
msg = "Calculating special directories"
homedir = user_entry.pw_dir
sshdir = join(homedir, ".ssh")
keysfile = join(sshdir, "authorized_keys")
if not write: return keysfile
#create directories and files for authorized keys
msg = "Reading user and group info."
uid = user_entry.pw_uid
gid = user_entry.pw_gid
if not os.path.exists(sshdir):
os.mkdir(sshdir, 0700)
msg = "Making ~/.ssh."
if not exists(sshdir): os.mkdir(sshdir, 0700)
os.chown(sshdir, uid, gid)
os.chmod(sshdir, 0700)
if not os.path.exists( keysfile):
msg = "Touching authorized keys file."
if not exists( keysfile):
try:
f = open(keysfile, "w") #touches file so we can set ownership and perms
finally:
f.close()
os.chown(keysfile, uid, gid)
os.chmod(keysfile, 0600)
return keysfile
def readkeys(filename):
if not os.path.isfile(filename):
return []
f = open(filename)
keys = [line.rstrip() for line in f.readlines()]
f.close()
def readkeys( filename):
global msg
msg = "Reading authorized_keys."
if not isfile(filename): return []
try:
f = open(filename)
keys = [line.rstrip() for line in f.readlines()]
finally:
f.close()
return keys
def writekeys( filename, keys):
global msg
msg = "Writing authorized_keys."
try:
f = open(filename,"w")
f.writelines( (key + "\n" for key in keys) )
finally:
f.close()
f = open(filename,"w")
f.writelines( (key + "\n" for key in keys) )
f.close()
def enforce_state( params):
"""Add or remove key.
def enforce_state(module, params):
"""
Add or remove key.
:return: True=changed, False=unchanged
"""
global msg
user = params["user"]
key = params["key"]
#== scrub params
msg = "Invalid or missing param: user."
user = params["user"]
msg = "Invalid or missing param: key."
key = params["key"]
state = params.get("state", "present")
# check current state -- just get the filename, don't create file
params["keyfile"] = keyfile(user, write=False)
keys = readkeys(params["keyfile"])
#== check current state
params["keyfile"] = keyfile(user, write=False) #just get the filename, don't create file
keys = readkeys( params["keyfile"])
present = key in keys
# handle idempotent state=present
#== handle idempotent state=present
if state=="present":
if present:
module.exit_json(changed=False)
if present: return False #nothing to do
keys.append(key)
writekeys(keyfile(user,write=True), keys)
elif state=="absent":
if not present:
module.exit_json(changed=False)
if not present: return False #nothing to do
keys.remove(key)
writekeys(keyfile(user,write=True), keys)
else:
msg = "Invalid param: state."
raise StandardError(msg)
return True
params['changed'] = True
return params
#===== MAIN SCRIPT ===================================================
def main():
try:
params = get_params()
changed = enforce_state( params)
msg = ""
except:
msg = "Error %s" % msg
module = AnsibleModule(
argument_spec = dict(
user = dict(required=True),
key = dict(required=True),
state = dict(default='present', choices=['absent','present'])
)
)
params = module.params
results = enforce_state(module, module.params)
module.exit_json(**results)
# this is magic, see lib/ansible/module_common.py
#<<INCLUDE_ANSIBLE_MODULE_COMMON>>
main()
# Don't do sys.exit() within try...except
if msg:
fail_json(msg=msg)
else:
exit_json( user=params["user"], changed=changed)

View File

@ -1,5 +1,4 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# (c) 2012, Michael DeHaan <michael.dehaan@gmail.com>, and others
#
@ -17,6 +16,12 @@
#
# 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 subprocess
import sys
@ -24,102 +29,79 @@ import datetime
import traceback
import shlex
import os
import syslog
def main():
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)
# the command module is the one ansible module that does not take key=value args
# hence don't copy this one if you are looking to build others!
module = CommandModule(argument_spec=dict())
shell = False
shell = module.params['shell']
chdir = module.params['chdir']
args = module.params['args']
if args.find("#USE_SHELL") != -1:
args = args.replace("#USE_SHELL", "")
shell = True
if chdir:
os.chdir(chdir)
check_args = shlex.split(args)
for x in check_args:
if x.startswith("creates="):
# do not run the command if the line contains creates=filename
# and the filename already exists. This allows idempotence
# of command executions.
(k,v) = x.split("=",1)
if os.path.exists(v):
print json.dumps({
"cmd" : args,
"stdout" : "skipped, since %s exists" % v,
"skipped" : True,
"changed" : False,
"stderr" : "",
"rc" : 0,
})
sys.exit(0)
args = args.replace(x,'')
if not shell:
args = shlex.split(args)
startd = datetime.datetime.now()
if not shell:
args = shlex.split(args)
try:
cmd = subprocess.Popen(args, shell=shell, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
out, err = cmd.communicate()
except (OSError, IOError), e:
module.fail_json(cmd=args, msg=str(e))
except:
module.fail_json(msg=traceback.format_exc())
startd = datetime.datetime.now()
endd = datetime.datetime.now()
delta = endd - startd
try:
cmd = subprocess.Popen(args, shell=shell,
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
out, err = cmd.communicate()
except (OSError, IOError), e:
print json.dumps({
"cmd" : args,
"failed" : 1,
"msg" : str(e),
})
sys.exit(1)
except:
print json.dumps({
"failed" : 1,
"msg" : traceback.format_exc()
})
sys.exit(1)
if out is None:
out = ''
if err is None:
err = ''
endd = datetime.datetime.now()
delta = endd - startd
module.exit_json(
cmd = args,
stdout = out.strip(),
stderr = err.strip(),
rc = cmd.returncode,
start = str(startd),
end = str(endd),
delta = str(delta),
changed = True
)
if out is None:
out = ''
if err is None:
err = ''
# include magic from lib/ansible/module_common.py
#<<INCLUDE_ANSIBLE_MODULE_COMMON>>
result = {
"cmd" : args,
"stdout" : out.strip(),
"stderr" : err.strip(),
"rc" : cmd.returncode,
"start" : str(startd),
"end" : str(endd),
"delta" : str(delta),
"changed" : True
}
# only the command module should ever need to do this
# everything else should be simple key=value
class CommandModule(AnsibleModule):
def _handle_aliases(self):
pass
def _check_invalid_arguments(self):
pass
def _load_params(self):
''' read the input and return a dictionary and the arguments string '''
args = MODULE_ARGS
items = shlex.split(args)
params = {}
params['chdir'] = None
params['shell'] = False
if args.find("#USE_SHELL") != -1:
args = args.replace("#USE_SHELL", "")
params['shell'] = True
check_args = shlex.split(args)
for x in check_args:
if x.startswith("creates="):
# do not run the command if the line contains creates=filename
# and the filename already exists. This allows idempotence
# of command executions.
(k,v) = x.split("=",1)
if os.path.exists(v):
self.exit_json(
cmd=args,
stdout="skipped, since %s exists" % v,
skipped=True,
changed=False,
stderr=False,
rc=0
)
args = args.replace(x,'')
elif x.startswith("chdir="):
(k,v) = x.split("=", 1)
if not (os.path.exists(v) and os.path.isdir(v)):
self.fail_json(msg="cannot change to directory '%s': path does not exist" % v)
elif v[0] != '/':
self.fail_json(msg="the path for 'chdir' argument must be fully qualified")
params['chdir'] = v
args = args.replace(x, '')
params['args'] = args
return (params, args)
main()
print json.dumps(result)

View File

@ -1,5 +1,4 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# (c) 2012, Michael DeHaan <michael.dehaan@gmail.com>
#
@ -17,58 +16,84 @@
#
# You should have received a copy of the GNU General Public License
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
#
import sys
import os
import shutil
import shlex
import syslog
def main():
# ===========================================
# convert arguments of form a=b c=d
# to a dictionary
# FIXME: make more idiomatic
module = AnsibleModule(
# not checking because of daisy chain to file module
check_invalid_arguments = False,
argument_spec = dict(
src=dict(required=True),
dest=dict(required=True)
)
)
def dump_kv(vars):
return " ".join("%s='%s'" % (k,v) for (k,v) in vars.items())
src = os.path.expanduser(module.params['src'])
dest = os.path.expanduser(module.params['dest'])
def exit_kv(rc=0, **kwargs):
if 'path' in kwargs:
add_path_info(kwargs)
print dump_kv(kwargs)
sys.exit(rc)
if not os.path.exists(src):
module.fail_json(msg="Source %s failed to transfer" % (src))
if not os.access(src, os.R_OK):
module.fail_json(msg="Source %s not readable" % (src))
def md5_sum(f):
md5sum = os.popen("/usr/bin/md5sum %(file)s 2>/dev/null || /sbin/md5 -q %(file)s 2>/dev/null || /usr/bin/digest -a md5 -v %(file)s 2>/dev/null" % {"file": f}).read().split()[0]
return md5sum
md5sum_src = module.md5(src)
md5sum_dest = None
if len(sys.argv) == 1:
exit_kv(rc=1, failed=1, msg="incorrect number of arguments given")
if os.path.exists(dest):
if not os.access(dest, os.W_OK):
module.fail_json(msg="Destination %s not writable" % (dest))
if not os.access(dest, os.R_OK):
module.fail_json(msg="Destination %s not readable" % (dest))
# Allow dest to be directory without compromising md5 check
if (os.path.isdir(dest)):
module.fail_json(msg="Destination %s cannot be a directory" % (dest))
md5sum_dest = module.md5(dest)
else:
if not os.access(os.path.dirname(dest), os.W_OK):
module.fail_json(msg="Destination %s not writable" % (os.path.dirname(dest)))
argfile = sys.argv[1]
if not os.path.exists(argfile):
exit_kv(rc=1, failed=1, msg="file %s does not exist" % (argfile))
if md5sum_src != md5sum_dest:
try:
shutil.copyfile(src, dest)
except shutil.Error:
module.fail_json(msg="failed to copy: %s and %s are the same" % (src, dest))
except IOError:
module.fail_json(msg="failed to copy: %s to %s" % (src, dest))
changed = True
else:
changed = False
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)
module.exit_json(dest=dest, src=src, md5sum=md5sum_src, changed=changed)
# this is magic, see lib/ansible/module_common.py
#<<INCLUDE_ANSIBLE_MODULE_COMMON>>
main()
params = {}
for x in items:
(k, v) = x.split("=")
params[k] = v
src = params['src']
dest = params['dest']
if src:
src = os.path.expanduser(src)
if dest:
dest = os.path.expanduser(dest)
md5sum_src = None
# raise an error if there is no src file
if not os.path.exists(src):
exit_kv(rc=1, failed=1, msg="Source %s failed to transfer" % (src))
if not os.access(src, os.R_OK):
exit_kv(rc=1, failed=1, msg="Source %s not readable" % (src))
md5sum_src = md5_sum(src)
md5sum_dest = None
# check if there is no dest file
if os.path.exists(dest):
# raise an error if copy has no permission on dest
if not os.access(dest, os.W_OK):
exit_kv(rc=1, failed=1, msg="Destination %s not writable" % (dest))
if not os.access(dest, os.R_OK):
exit_kv(rc=1, failed=1, msg="Destination %s not readable" % (dest))
md5sum_dest = md5_sum(dest)
else:
if not os.access(os.path.dirname(dest), os.W_OK):
exit_kv(rc=1, failed=1, msg="Destination %s not writable" % (dest))
if md5sum_src != md5sum_dest:
os.system("cp %s %s" % (src, dest))
changed = True
else:
changed = False
# mission accomplished
#print "md5sum=%s changed=%s" % (md5sum_src, changed)
exit_kv(dest=dest, src=src, changed="md5sum=%s changed=%s" % (md5sum_src, changed))

View File

@ -1,5 +1,4 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
#!/bin/bash
# (c) 2012, Michael DeHaan <michael.dehaan@gmail.com>
#
@ -23,28 +22,5 @@
# facter
# ruby-json
import subprocess
def get_facter_data():
p = subprocess.Popen(["/usr/bin/env", "facter", "--json"], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
(out, err) = p.communicate()
rc = p.returncode
return rc, out, err
def main():
module = AnsibleModule(
argument_spec = dict()
)
rc, out, err = get_facter_data()
if rc != 0:
module.fail_json(msg=err)
else:
module.exit_json(**json.loads(out))
# this is magic, see lib/ansible/module_common.py
#<<INCLUDE_ANSIBLE_MODULE_COMMON>>
main()
/usr/bin/logger -t ansible-facter Invoked as-is
/usr/bin/facter --json 2>/dev/null

34
library/failtest Executable file
View File

@ -0,0 +1,34 @@
#!/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/>.
import sys
try:
import json
except ImportError:
import simplejson as json
print >>sys.stderr, "THIS IS A TEST FAILURE"
print json.dumps({
"failed" : True,
"msg" : "this module always fails"
})

View File

@ -1 +1,24 @@
# this is a virtual module that is entirely implemented server side
#!/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/>.
#
### THIS FILE IS FOR REFERENCE OR FUTURE USE ###
# See lib/ansible/runner.py for implementation of the fetch functionality #

View File

@ -1,5 +1,4 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# (c) 2012, Michael DeHaan <michael.dehaan@gmail.com>
#
@ -18,21 +17,39 @@
# 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 sys
import shlex
import subprocess
import shutil
import stat
import grp
import pwd
import syslog
try:
import selinux
HAVE_SELINUX=True
except ImportError:
HAVE_SELINUX=False
def dump_kv(vars):
return " ".join("%s='%s'" % (k,v) for (k,v) in vars.items())
def exit_kv(rc=0, **kwargs):
if 'path' in kwargs:
add_path_info(kwargs)
print dump_kv(kwargs)
sys.exit(rc)
def fail_kv(**kwargs):
kwargs['failed'] = True
exit_kv(rc=1, **kwargs)
def add_path_info(kwargs):
path = kwargs['path']
if os.path.exists(path):
(user, group) = user_and_group(path)
kwargs['owner'] = user
kwargs['user'] = user
kwargs['group'] = group
st = os.stat(path)
kwargs['mode'] = oct(stat.S_IMODE(st[stat.ST_MODE]))
@ -49,14 +66,6 @@ def add_path_info(kwargs):
kwargs['state'] = 'absent'
return kwargs
def module_exit_json(**kwargs):
add_path_info(kwargs)
module.exit_json(**kwargs)
def module_fail_json(**kwargs):
add_path_info(kwargs)
module.fail_json(**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
@ -106,7 +115,7 @@ def selinux_context(path):
try:
ret = selinux.lgetfilecon(path)
except:
module_fail_json(path=path, msg='failed to retrieve selinux context')
fail_kv(path=path, msg='failed to retrieve selinux context')
if ret[0] == -1:
return context
context = ret[1].split(':')
@ -114,10 +123,59 @@ def selinux_context(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_kv(msg='the module requires arguments -a')
sys.exit(1)
params = {}
for x in items:
(k, v) = x.split("=")
params[k] = v
state = params.get('state','file')
path = params.get('path', params.get('dest', params.get('name', None)))
if path:
path = os.path.expanduser(path)
src = params.get('src', None)
if src:
src = os.path.expanduser(src)
dest = params.get('dest', None)
mode = params.get('mode', None)
owner = params.get('owner', None)
group = params.get('group', None)
# presently unused, we always use -R (FIXME?)
recurse = params.get('recurse', 'false')
# selinux related options
seuser = params.get('seuser', None)
serole = params.get('serole', None)
setype = params.get('setype', None)
selevel = params.get('serange', 's0')
secontext = [seuser, serole, setype]
if selinux_mls_enabled():
secontext.append(selevel)
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_kv(msg='invalid state: %s' % state)
if state == 'link' and (src is None or dest is None):
fail_kv(msg='src and dest are required for "link" state')
elif path is None:
fail_kv(msg='path is required')
changed = False
# ===========================================
# support functions
@ -150,9 +208,9 @@ def set_context_if_different(path, context, changed):
try:
rc = selinux.lsetfilecon(path, ':'.join(new_context))
except OSError:
module_fail_json(path=path, msg='invalid selinux context')
fail_kv(path=path, msg='invalid selinux context')
if rc != 0:
module_fail_json(path=path, msg='set selinux context failed')
fail_kv(path=path, msg='set selinux context failed')
changed = True
return changed
@ -163,7 +221,7 @@ def set_owner_if_different(path, owner, changed):
if owner != user:
rc = os.system("/bin/chown -R %s %s 2>/dev/null" % (owner, path))
if rc != 0:
module_fail_json(path=path, msg='chown failed')
fail_kv(path=path, msg='chown failed')
return True
return changed
@ -175,7 +233,7 @@ def set_group_if_different(path, group, changed):
if old_group != group:
rc = os.system("/bin/chgrp -R %s %s" % (group, path))
if rc != 0:
module_fail_json(path=path, msg='chgrp failed')
fail_kv(path=path, msg='chgrp failed')
return True
return changed
@ -186,7 +244,7 @@ def set_mode_if_different(path, mode, changed):
# FIXME: support English modes
mode = int(mode, 8)
except Exception, e:
module_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])
@ -197,7 +255,7 @@ def set_mode_if_different(path, mode, changed):
try:
os.chmod(path, mode)
except Exception, e:
module_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])
@ -208,147 +266,98 @@ def set_mode_if_different(path, mode, changed):
def rmtree_error(func, path, exc_info):
module_fail_json(path=path, msg='failed to remove directory')
fail_kv(path=path, msg='failed to remove directory')
def main():
# ===========================================
# go...
global module
module = AnsibleModule(
check_invalid_arguments = False,
argument_spec = dict(
state = dict(choices=['file','directory','link','absent'], default='file'),
path = dict(aliases=['dest', 'name'], required=True),
src = dict(),
mode = dict(),
owner = dict(),
group = dict(),
seuser = dict(),
serole = dict(),
selevel = dict(),
setype = dict(),
)
)
prev_state = 'absent'
if os.path.lexists(path):
if os.path.islink(path):
prev_state = 'link'
elif os.path.isfile(path):
prev_state = 'file'
else:
prev_state = 'directory'
params = module.params
state = params['state']
path = os.path.expanduser(params['path'])
src = params.get('src', None)
if src:
src = os.path.expanduser(src)
mode = params.get('mode', None)
owner = params.get('owner', None)
group = params.get('group', None)
# selinux related options
seuser = params.get('seuser', None)
serole = params.get('serole', None)
setype = params.get('setype', None)
selevel = params.get('serange', 's0')
secontext = [seuser, serole, setype]
if selinux_mls_enabled():
secontext.append(selevel)
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 == 'link' and (src is None or path is None):
module_fail_json(msg='src and dest are required for "link" state')
elif path is None:
module_fail_json(msg='path is required')
changed = False
prev_state = 'absent'
if os.path.lexists(path):
if os.path.islink(path):
prev_state = 'link'
elif os.path.isfile(path):
prev_state = 'file'
else:
prev_state = 'directory'
if prev_state != 'absent' and state == 'absent':
try:
if prev_state == 'directory':
if os.path.islink(path):
os.unlink(path)
else:
shutil.rmtree(path, ignore_errors=False, onerror=rmtree_error)
if prev_state != 'absent' and state == 'absent':
try:
if prev_state == 'directory':
if os.path.islink(path):
os.unlink(path)
else:
os.unlink(path)
except Exception, e:
module_fail_json(path=path, msg=str(e))
module_exit_json(path=path, changed=True)
shutil.rmtree(path, ignore_errors=False, onerror=rmtree_error)
else:
os.unlink(path)
except Exception, e:
fail_kv(path=path, msg=str(e))
exit_kv(path=path, changed=True)
sys.exit(0)
if prev_state != 'absent' and prev_state != state:
module_fail_json(path=path, msg='refusing to convert between %s and %s' % (prev_state, state))
if prev_state != 'absent' and 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':
module_exit_json(path=path, changed=False)
if prev_state == 'absent' and state == 'absent':
exit_kv(path=path, changed=False)
if state == 'file':
if state == 'file':
if prev_state == 'absent':
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)
changed = set_owner_if_different(path, owner, changed)
changed = set_group_if_different(path, group, changed)
changed = set_mode_if_different(path, mode, changed)
exit_kv(path=path, changed=changed)
elif state == 'directory':
if prev_state == 'absent':
os.makedirs(path)
changed = True
if prev_state == 'absent':
module_fail_json(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)
changed = set_owner_if_different(path, owner, changed)
changed = set_group_if_different(path, group, changed)
changed = set_mode_if_different(path, mode, changed)
# set modes owners and context as needed
changed = set_context_if_different(path, secontext, changed)
changed = set_owner_if_different(path, owner, changed)
changed = set_group_if_different(path, group, changed)
changed = set_mode_if_different(path, mode, changed)
exit_kv(path=path, changed=changed)
module_exit_json(path=path, changed=changed)
elif state == 'directory':
if prev_state == 'absent':
os.makedirs(path)
changed = True
# set modes owners and context as needed
changed = set_context_if_different(path, secontext, changed)
changed = set_owner_if_different(path, owner, changed)
changed = set_group_if_different(path, group, changed)
changed = set_mode_if_different(path, mode, changed)
module_exit_json(path=path, changed=changed)
elif state == 'link':
elif state == 'link':
if os.path.isabs(src):
abs_src = src
else:
abs_src = os.path.join(os.path.dirname(dest), src)
if not os.path.exists(abs_src):
module_fail_json(path=path, src=src, msg='src file does not exist')
if os.path.isabs(src):
abs_src = src
else:
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, path)
changed = True
elif prev_state == 'link':
old_src = os.readlink(path)
if not os.path.isabs(old_src):
old_src = os.path.join(os.path.dirname(path), old_src)
if old_src != src:
os.unlink(path)
os.symlink(src, path)
else:
module_fail_json(dest=path, src=src, msg='unexpected position reached')
if prev_state == 'absent':
os.symlink(src, dest)
changed = True
elif prev_state == 'link':
old_src = os.readlink(dest)
if not os.path.isabs(old_src):
old_src = os.path.join(os.path.dirname(dest), old_src)
if old_src != src:
os.unlink(dest)
os.symlink(src, dest)
else:
fail_kv(dest=dest, src=src, msg='unexpected position reached')
# set modes owners and context as needed
changed = set_context_if_different(path, secontext, changed)
changed = set_owner_if_different(path, owner, changed)
changed = set_group_if_different(path, group, changed)
changed = set_mode_if_different(path, mode, changed)
# set modes owners and context as needed
changed = set_context_if_different(dest, secontext, changed)
changed = set_owner_if_different(dest, owner, changed)
changed = set_group_if_different(dest, group, changed)
changed = set_mode_if_different(dest, mode, changed)
module.exit_json(dest=path, src=src, changed=changed)
exit_kv(dest=dest, src=src, changed=changed)
module_fail_json(path=path, msg='unexpected position reached')
# this is magic, see lib/ansible/module_common.py
#<<INCLUDE_ANSIBLE_MODULE_COMMON>>
main()
fail_kv(path=path, msg='unexpected position reached')
sys.exit(0)

View File

@ -1,194 +0,0 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# (c) 2012, Jan-Piet Mens <jpmens () 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/>.
#
# see examples/playbooks/get_url.yml
import shutil
import datetime
import tempfile
HAS_URLLIB2=True
try:
import urllib2
except ImportError:
HAS_URLLIB2=False
HAS_URLPARSE=True
try:
import urlparse
import socket
except ImportError:
HAS_URLPARSE=False
# ==============================================================
# url handling
def url_filename(url):
fn = os.path.basename(urlparse.urlsplit(url)[2])
if fn == '':
return 'index.html'
return fn
def url_do_get(module, url, dest):
"""
Get url and return request and info
Credits: http://stackoverflow.com/questions/7006574/how-to-download-file-from-ftp
"""
USERAGENT = 'ansible-httpget'
info = dict(url=url)
r = None
actualdest = None
if os.path.isdir(dest):
urlfilename = url_filename(url)
actualdest = "%s/%s" % (dest, urlfilename)
module.params['path'] = actualdest
else:
actualdest = dest
info['daisychain_args'] = module.params
info['daisychain_args']['state'] = 'file'
info['daisychain_args']['dest'] = actualdest
info['actualdest'] = actualdest
request = urllib2.Request(url)
request.add_header('User-agent', USERAGENT)
if os.path.exists(actualdest):
t = datetime.datetime.utcfromtimestamp(os.path.getmtime(actualdest))
tstamp = t.strftime('%a, %d %b %Y %H:%M:%S +0000')
request.add_header('If-Modified-Since', tstamp)
try:
r = urllib2.urlopen(request)
info.update(r.info())
info.update(dict(msg="OK (%s bytes)" % r.headers.get('Content-Length', 'unknown'), status=200))
except urllib2.HTTPError, e:
# Must not fail_json() here so caller can handle HTTP 304 unmodified
info.update(dict(msg=str(e), status=e.code))
return r, info
except urllib2.URLError, e:
code = getattr(e, 'code', -1)
module.fail_json(msg="Request failed: %s" % str(e), status_code=code)
return r, info
def url_get(module, url, dest):
"""
Download url and store at dest.
If dest is a directory, determine filename from url.
Return (tempfile, info about the request)
"""
req, info = url_do_get(module, url, dest)
# TODO: should really handle 304, but how? src file could exist (and be newer) but empty
if info['status'] == 304:
module.exit_json(url=url, dest=info.get('actualdest', dest), changed=False, msg=info.get('msg', ''))
# create a temporary file and copy content to do md5-based replacement
if info['status'] != 200:
module.fail_json(msg="Request failed", status_code=info['status'], response=info['msg'], url=url)
actualdest = info['actualdest']
fd, tempname = tempfile.mkstemp()
f = os.fdopen(fd, 'wb')
try:
shutil.copyfileobj(req, f)
except Exception, err:
os.remove(tempname)
module.fail_json(msg="failed to create temporary content file: %s" % str(err))
f.close()
req.close()
return tempname, info
# ==============================================================
# main
def main():
# does this really happen on non-ancient python?
if not HAS_URLLIB2:
module.fail_json(msg="urllib2 is not installed")
if not HAS_URLPARSE:
module.fail_json(msg="urlparse is not installed")
module = AnsibleModule(
# not checking because of daisy chain to file module
check_invalid_arguments = False,
argument_spec = dict(
url = dict(required=True),
dest = dict(required=True),
)
)
url = module.params['url']
dest = os.path.expanduser(module.params['dest'])
# download to tmpsrc
tmpsrc, info = url_get(module, url, dest)
md5sum_src = None
md5sum_dest = None
dest = info['actualdest']
# raise an error if there is no tmpsrc file
if not os.path.exists(tmpsrc):
os.remove(tmpsrc)
module.fail_json(msg="Request failed", status_code=info['status'], response=info['msg'])
if not os.access(tmpsrc, os.R_OK):
os.remove(tmpsrc)
module.fail_json( msg="Source %s not readable" % (tmpsrc))
md5sum_src = module.md5(tmpsrc)
# check if there is no dest file
if os.path.exists(dest):
# raise an error if copy has no permission on dest
if not os.access(dest, os.W_OK):
os.remove(tmpsrc)
module.fail_json( msg="Destination %s not writable" % (dest))
if not os.access(dest, os.R_OK):
os.remove(tmpsrc)
module.fail_json( msg="Destination %s not readable" % (dest))
md5sum_dest = module.md5(dest)
else:
if not os.access(os.path.dirname(dest), os.W_OK):
os.remove(tmpsrc)
module.fail_json( msg="Destination %s not writable" % (os.path.dirname(dest)))
if md5sum_src != md5sum_dest:
try:
shutil.copyfile(tmpsrc, dest)
except Exception, err:
os.remove(tmpsrc)
module.fail_json(msg="failed to copy %s to %s: %s" % (tmpsrc, dest, str(err)))
changed = True
else:
changed = False
os.remove(tmpsrc)
# Mission complete
module.exit_json(url=url, dest=dest, src=tmpsrc, md5sum=md5sum_src,
changed=changed, msg=info.get('msg',''),
daisychain="file", daisychain_args=info.get('daisychain_args',''))
# this is magic, see lib/ansible/module_common.py
#<<INCLUDE_ANSIBLE_MODULE_COMMON>>
main()

View File

@ -1,5 +1,4 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# (c) 2012, Michael DeHaan <michael.dehaan@gmail.com>
#
@ -23,175 +22,190 @@
# tag. Latest is not supported, you should not be doing
# 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
def get_version(dest):
''' samples the version of the git repo '''
os.chdir(dest)
cmd = "git show --abbrev-commit"
sha = os.popen(cmd).read().split("\n")
sha = sha[0].split()[1]
return sha
# ===========================================
# Basic support methods
def clone(repo, dest):
''' makes a new git repo if it does not already exist '''
try:
os.makedirs(os.path.dirname(dest))
except:
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
return (rc, out, err)
def exit_json(rc=0, **kwargs):
print json.dumps(kwargs)
sys.exit(rc)
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 fail_json(**kwargs):
kwargs['failed'] = True
exit_json(rc=1, **kwargs)
def switchLocalBranch( branch ):
cmd = "git checkout %s" % branch
cmd = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
return cmd.communicate()
# ===========================================
# convert arguments of form a=b c=d
# to a dictionary
# FIXME: make more idiomatic
def get_branches(module, dest):
os.chdir(dest)
branches = []
cmd = "git branch -a"
cmd = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
out, err = cmd.communicate()
if cmd.returncode != 0:
module.fail_json(msg="Could not determine branch data - received %s" % out)
for line in out.split('\n'):
branches.append(line.strip())
return branches
if len(sys.argv) == 1:
fail_json(msg="the command module requires arguments (-a)")
def is_remote_branch(module, dest, remote, branch):
branches = get_branches(module, dest)
rbranch = 'remotes/%s/%s' % (remote, branch)
if rbranch in branches:
return True
else:
return False
argfile = sys.argv[1]
if not os.path.exists(argfile):
fail_json(msg="Argument file not found")
def is_local_branch(module, dest, branch):
branches = get_branches(module, dest)
lbranch = '%s' % branch
if lbranch in branches:
return True
elif '* %s' % branch in branches:
return True
else:
return False
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)
def is_current_branch(module, dest, branch):
branches = get_branches(module, dest)
for b in branches:
if b.startswith('* '):
cur_branch = b
if branch == cur_branch or '* %s' % branch == cur_branch:
return True
else:
return True
if not len(items):
fail_json(msg="the command module requires arguments (-a)")
def pull(module, repo, dest, version):
''' updates repo from remote sources '''
os.chdir(dest)
branches = get_branches(module, dest)
cur_branch = ''
for b in branches:
if b.startswith('* '):
cur_branch = b
if is_local_branch(module, dest, version) and not is_current_branch(module, dest, version):
(out, err) = switch_version(module, dest, remote, version)
params = {}
for x in items:
(k, v) = x.split("=")
params[k] = v
cmd = "git pull -u origin"
cmd = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
out, err = cmd.communicate()
rc = cmd.returncode
return (rc, out, err)
def switch_version(module, dest, remote, version):
''' once pulled, switch to a particular SHA, tag, or branch '''
os.chdir(dest)
cmd = ''
if version != 'HEAD':
if not is_local_branch(module, dest, version) and is_remote_branch(module, dest, remote, version):
cmd = "git checkout --track -b %s %s/%s" % (version, remote, version)
else:
cmd = "git checkout --force %s" % version
else:
# is there a better way to do this?
cmd = "git rebase origin"
cmd = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
(out, err) = cmd.communicate()
rc = cmd.returncode
return (rc, out, err)
dest = params['dest']
repo = params['repo']
branch = params.get('branch', 'master')
version = params.get('version', 'HEAD')
# ===========================================
def main():
module = AnsibleModule(
argument_spec = dict(
dest=dict(required=True),
repo=dict(required=True, aliases=['name']),
version=dict(default='HEAD'),
remote=dict(default='origin')
)
)
def get_version(dest):
''' samples the version of the git repo '''
os.chdir(dest)
cmd = "git show --abbrev-commit"
sha = os.popen(cmd).read().split("\n")
sha = sha[0].split()[1]
return sha
dest = module.params['dest']
repo = module.params['repo']
version = module.params['version']
remote = module.params['remote']
def clone(repo, dest, branch):
''' makes a new git repo if it does not already exist '''
try:
os.makedirs(dest)
except:
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)
gitconfig = os.path.join(dest, '.git', 'config')
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()
out, err, status = (None, None, None)
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)
# if there is no git configuration, do a clone operation
# else pull and switch the version
before = None
if not os.path.exists(gitconfig):
(rc, out, err) = clone(repo, dest)
if rc != 0:
module.fail_json(msg=err)
else:
# else do a pull
before = get_version(dest)
(rc, out, err) = reset(dest)
if rc != 0:
module.fail_json(msg=err)
(rc, out, err) = pull(module, repo, dest, version)
def switchLocalBranch( branch ):
cmd = "git checkout %s" % branch
cmd = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
return cmd.communicate()
# handle errors from clone or pull
if out.find('error') != -1 or err.find('ERROR') != -1:
module.fail_json(msg=err)
def pull(repo, dest, branch):
''' updates repo from remote sources '''
os.chdir(dest)
cmd = "git branch -a"
cmd = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
(gbranch_out, gbranch_err) = cmd.communicate()
# switch to version specified regardless of whether
# we cloned or pulled
(rc, out, err) = switch_version(module, dest, remote, version)
if err.find('error') != -1:
module.fail_json(msg=err)
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)
# determine if we changed anything
after = get_version(dest)
changed = False
if branch is None:
if cur_branch != default_branch:
(out, err) = switchLocalBranch( default_branch )
if before != after:
changed = True
cmd = "git pull -u origin"
module.exit_json(changed=changed, before=before, after=after)
elif branch == cur_branch:
cmd = "git pull -u origin"
# include magic from lib/ansible/module_common.py
#<<INCLUDE_ANSIBLE_MODULE_COMMON>>
main()
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()
def switchver(version, dest):
''' once pulled, switch to a particular SHA or tag '''
os.chdir(dest)
if version != 'HEAD':
cmd = "git checkout %s --force" % version
else:
# is there a better way to do this?
cmd = "git rebase origin"
cmd = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
(out, err) = cmd.communicate()
return (out, err)
gitconfig = os.path.join(dest, '.git', 'config')
out, err, status = (None, None, None)
# if there is no git configuration, do a clone operation
# else pull and switch the version
before = None
if not os.path.exists(gitconfig):
(out, err) = clone(repo, dest, branch)
else:
# else do a pull
before = get_version(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:
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:
fail_json(out=out, err=err)
# determine if we changed anything
after = get_version(dest)
changed = False
if before != after:
changed = True
exit_json(changed=changed, before=before, after=after)

View File

@ -1,5 +1,4 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# (c) 2012, Stephen Fromm <sfromm@gmail.com>
#
@ -18,25 +17,51 @@
# 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 grp
import shlex
import subprocess
import sys
import syslog
def get_bin_path(module, arg):
if os.path.exists('/usr/sbin/%s' % arg):
return '/usr/sbin/%s' % arg
elif os.path.exists('/sbin/%s' % arg):
return '/sbin/%s' % arg
GROUPADD = "/usr/sbin/groupadd"
GROUPDEL = "/usr/sbin/groupdel"
GROUPMOD = "/usr/sbin/groupmod"
def exit_json(rc=0, **kwargs):
if 'name' in kwargs:
add_group_info(kwargs)
print json.dumps(kwargs)
sys.exit(rc)
def fail_json(**kwargs):
kwargs['failed'] = True
exit_json(rc=1, **kwargs)
def add_group_info(kwargs):
name = kwargs['name']
if group_exists(name):
kwargs['state'] = 'present'
info = group_info(name)
kwargs['gid'] = info[2]
else:
module.fail_json(msg="Cannot find %s" % arg)
kwargs['state'] = 'absent'
return kwargs
def group_del(module, group):
cmd = [get_bin_path(module, 'groupdel'), group]
p = subprocess.Popen(cmd, shell=False, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
(out, err) = p.communicate()
rc = p.returncode
return (rc, out, err)
def group_del(group):
cmd = [GROUPDEL, group]
rc = subprocess.call(cmd, shell=False, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
if rc == 0:
return True
else:
return False
def group_add(module, group, **kwargs):
cmd = [get_bin_path(module, 'groupadd')]
def group_add(group, **kwargs):
cmd = [GROUPADD]
for key in kwargs:
if key == 'gid' and kwargs[key] is not None:
cmd.append('-g')
@ -44,13 +69,14 @@ def group_add(module, group, **kwargs):
elif key == 'system' and kwargs[key] == 'yes':
cmd.append('-r')
cmd.append(group)
p = subprocess.Popen(cmd, shell=False, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
(out, err) = p.communicate()
rc = p.returncode
return (rc, out, err)
rc = subprocess.call(cmd, shell=False, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
if rc == 0:
return True
else:
return False
def group_mod(module, group, **kwargs):
cmd = [get_bin_path(module, 'groupmod')]
def group_mod(group, **kwargs):
cmd = [GROUPMOD]
info = group_info(group)
for key in kwargs:
if key == 'gid':
@ -58,12 +84,13 @@ def group_mod(module, group, **kwargs):
cmd.append('-g')
cmd.append(kwargs[key])
if len(cmd) == 1:
return (None, '', '')
return False
cmd.append(group)
p = subprocess.Popen(cmd, shell=False, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
(out, err) = p.communicate()
rc = p.returncode
return (rc, out, err)
rc = subprocess.call(cmd, shell=False, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
if rc == 0:
return True
else:
return False
def group_exists(group):
try:
@ -83,56 +110,64 @@ def group_info(group):
# ===========================================
def main():
module = AnsibleModule(
argument_spec = dict(
state=dict(default='present', choices=['present', 'absent']),
name=dict(required=True),
gid=dict(default=None),
system=dict(default='no', choices=['yes', 'no']),
)
)
state = module.params['state']
name = module.params['name']
gid = module.params['gid']
system = module.params['system']
rc = None
out = ''
err = ''
result = {}
result['name'] = name
result['state'] = state
if state == 'absent':
if group_exists(name):
(rc, out, err) = group_del(module, name)
if rc != 0:
module.fail_json(name=name, msg=err)
elif state == 'present':
if not group_exists(name):
(rc, out, err) = group_add(module, name, gid=gid, system=system)
else:
(rc, out, err) = group_mod(module, name, gid=gid)
if rc is not None and rc != 0:
module.fail_json(name=name, msg=err)
if rc is None:
result['changed'] = False
if not os.path.exists(GROUPADD):
if os.path.exists("/sbin/groupadd"):
GROUPADD = "/sbin/groupadd"
else:
result['changed'] = True
if out:
result['stdout'] = out
if err:
result['stderr'] = err
fail_json(msg="Cannot find groupadd")
if not os.path.exists(GROUPDEL):
if os.path.exists("/sbin/groupdel"):
GROUPDEL = "/sbin/groupdel"
else:
fail_json(msg="Cannot find groupdel")
if not os.path.exists(GROUPMOD):
if os.path.exists("/sbin/groupmod"):
GROUPDEL = "/sbin/groupmod"
else:
fail_json(msg="Cannot find groupmod")
if len(sys.argv) == 2 and os.path.exists(sys.argv[1]):
argfile = sys.argv[1]
args = open(argfile, 'r').read()
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')
sys.exit(1)
params = {}
for x in items:
(k, v) = x.split("=")
params[k] = v
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')
changed = False
rc = 0
if state == 'absent':
if group_exists(name):
info = group_info(name)
result['system'] = system
result['gid'] = info[2]
changed = group_del(name)
exit_json(name=name, changed=changed)
elif state == 'present':
if not group_exists(name):
changed = group_add(name, gid=gid, system=system)
else:
changed = group_mod(name, gid=gid)
module.exit_json(**result)
exit_json(name=name, changed=changed)
# include magic from lib/ansible/module_common.py
#<<INCLUDE_ANSIBLE_MODULE_COMMON>>
main()
fail_json(name=name, msg='Unexpected position reached')

View File

@ -1,265 +0,0 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# (c) 2012, Red Hat, inc
# Written by Seth Vidal
# based on the mount modules from salt and puppet
#
# 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/>.
# mount module - mount fs and define in fstab
# usage:
#
# mount name=mountpoint, src=device_to_be_mounted fstype=fstype
# opts=mount_opts, dump=0 passno=0 state=[present|absent|mounted|unmounted]
#
# absent == remove from fstab and unmounted
# present == add to fstab, do not change mount state
# mounted == add to fstab if not there and make sure it is mounted
# unmounted == do not change fstab state, but unmount
def write_fstab(lines, dest):
fs_w = open(dest, 'w')
for l in lines:
fs_w.write(l)
fs_w.flush()
fs_w.close()
def set_mount(**kwargs):
""" set/change a mount point location in fstab """
# kwargs: name, src, fstype, opts, dump, passno, state, fstab=/etc/fstab
args = dict(
opts = 'defaults',
dump = '0',
passno = '0',
fstab = '/etc/fstab'
)
args.update(kwargs)
new_line = '%(src)s %(name)s %(fstype)s %(opts)s %(dump)s %(passno)s\n'
to_write = []
exists = False
changed = False
for line in open(args['fstab'], 'r').readlines():
if not line.strip():
to_write.append(line)
continue
if line.strip().startswith('#'):
to_write.append(line)
continue
if len(line.split()) != 6:
# not sure what this is or why it is here
# but it is not our fault so leave it be
to_write.append(line)
continue
ld = {}
ld['src'], ld['name'], ld['fstype'], ld['opts'], ld['dump'], ld['passno'] = line.split()
if ld['name'] != args['name']:
to_write.append(line)
continue
# it exists - now see if what we have is different
exists = True
for t in ('src', 'fstype','opts', 'dump', 'passno'):
if ld[t] != args[t]:
changed = True
ld[t] = args[t]
if changed:
to_write.append(new_line % ld)
else:
to_write.append(line)
if not exists:
to_write.append(new_line % args)
changed = True
if changed:
write_fstab(to_write, args['fstab'])
return (args['name'], changed)
def unset_mount(**kwargs):
""" remove a mount point from fstab """
# kwargs: name, src, fstype, opts, dump, passno, state, fstab=/etc/fstab
args = dict(
opts = 'default',
dump = '0',
passno = '0',
fstab = '/etc/fstab'
)
args.update(kwargs)
to_write = []
changed = False
for line in open(args['fstab'], 'r').readlines():
if not line.strip():
to_write.append(line)
continue
if line.strip().startswith('#'):
to_write.append(line)
continue
if len(line.split()) != 6:
# not sure what this is or why it is here
# but it is not our fault so leave it be
to_write.append(line)
continue
ld = {}
ld['src'], ld['name'], ld['fstype'], ld['opts'], ld['dump'], ld['passno'] = line.split()
if ld['name'] != args['name']:
to_write.append(line)
continue
# if we got here we found a match - continue and mark changed
changed = True
if changed:
write_fstab(to_write, args['fstab'])
return (args['name'], changed)
def mount(**kwargs):
""" mount up a path or remount if needed """
name = kwargs['name']
if os.path.ismount(name):
cmd = ['/bin/mount', '-o', 'remount', name]
else:
cmd = ['/bin/mount', name ]
call = subprocess.Popen(cmd, shell=False, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
out, err = call.communicate()
if call.returncode == 0:
return 0, ''
else:
return call.returncode, out+err
def umount(**kwargs):
""" unmount a path """
name = kwargs['name']
cmd = ['/bin/umount', name]
call = subprocess.Popen(cmd, shell=False, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
out, err = call.communicate()
if call.returncode == 0:
return 0, ''
else:
return call.returncode, out+err
def main():
module = AnsibleModule(
argument_spec = dict(
state = dict(required=True, choices=['present', 'absent', 'mounted', 'unmounted']),
name = dict(required=True),
opts = dict(default=None),
passno = dict(default=None),
dump = dict(default=None),
src = dict(required=True),
fstype = dict(required=True),
fstab = dict(default=None)
)
)
changed = False
rc = 0
args = {
'name': module.params['name'],
'src': module.params['src'],
'fstype': module.params['fstype']
}
if module.params['passno'] is not None:
args['passno'] = module.params['passno']
if module.params['opts'] is not None:
args['opts'] = module.params['opts']
if module.params['dump'] is not None:
args['dump'] = module.params['dump']
if module.params['fstab'] is not None:
args['fstab'] = module.params['fstab']
# absent == remove from fstab and unmounted
# unmounted == do not change fstab state, but unmount
# present == add to fstab, do not change mount state
# mounted == add to fstab if not there and make sure it is mounted, if it has changed in fstab then remount it
state = module.params['state']
name = module.params['name']
if state == 'absent':
name, changed = unset_mount(**args)
if changed:
if os.path.ismount(name):
res,msg = umount(**args)
if res:
fail_json(msg="Error unmounting %s: %s" % (name, msg))
if os.path.exists(name):
try:
os.rmdir(name)
except (OSError, IOError), e:
fail_json(msg="Error rmdir %s: %s" % (name, str(e)))
module.exit_json(changed=changed, **args)
if state == 'unmounted':
if os.path.ismount(name):
res,msg = umount(**args)
if res:
fail_json(msg="Error unmounting %s: %s" % (name, msg))
changed = True
module.exit_json(changed=changed, **args)
if state in ['mounted', 'present']:
name, changed = set_mount(**args)
if state == 'mounted':
if not os.path.exists(name):
try:
os.makedirs(name)
except (OSError, IOError), e:
fail_json(msg="Error making dir %s: %s" % (name, str(e)))
res = 0
if os.path.ismount(name):
if changed:
res,msg = mount(**args)
else:
changed = True
res,msg = mount(**args)
if res:
fail_json(msg="Error mounting %s: %s" % (name, msg))
module.exit_json(changed=changed, **args)
module.fail_json(msg='Unexpected position reached')
sys.exit(0)
# this is magic, see lib/ansible/module_common.py
#<<INCLUDE_ANSIBLE_MODULE_COMMON>>
main()

View File

@ -1,123 +0,0 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# (c) 2012, Mark Theunissen <mark.theunissen@gmail.com>
# Sponsored by Four Kitchens http://fourkitchens.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 ConfigParser
try:
import MySQLdb
except ImportError:
mysqldb_found = False
else:
mysqldb_found = True
# ===========================================
# MySQL module specific support methods.
#
def db_exists(cursor, db):
res = cursor.execute("SHOW DATABASES LIKE %s", (db,))
return bool(res)
def db_delete(cursor, db):
query = "DROP DATABASE %s" % db
cursor.execute(query)
return True
def db_create(cursor, db, encoding, collation):
if encoding:
encoding = " CHARACTER SET %s" % encoding
if collation:
collation = " COLLATE %s" % collation
query = "CREATE DATABASE %s%s%s" % (db, encoding, collation)
res = cursor.execute(query)
return True
def load_mycnf():
config = ConfigParser.RawConfigParser()
mycnf = os.path.expanduser('~/.my.cnf')
if not os.path.exists(mycnf):
return False
try:
config.readfp(open(mycnf))
creds = dict(user=config.get('client', 'user'),passwd=config.get('client', 'pass'))
except (ConfigParser.NoOptionError, IOError):
return False
return creds
# ===========================================
# Module execution.
#
def main():
module = AnsibleModule(
argument_spec = dict(
login_user=dict(default=None),
login_password=dict(default=None),
login_host=dict(default="localhost"),
db=dict(required=True, aliases=['name']),
encoding=dict(default=""),
collation=dict(default=""),
state=dict(default="present", choices=["absent", "present"]),
)
)
if not mysqldb_found:
module.fail_json(msg="the python mysqldb module is required")
db = module.params["db"]
encoding = module.params["encoding"]
collation = module.params["collation"]
state = module.params["state"]
# Either the caller passes both a username and password with which to connect to
# mysql, or they pass neither and allow this module to read the credentials from
# ~/.my.cnf.
login_password = module.params["login_password"]
login_user = module.params["login_user"]
if login_user is None and login_password is None:
mycnf_creds = load_mycnf()
if mycnf_creds is False:
login_user = "root"
login_password = ""
else:
login_user = mycnf_creds["user"]
login_password = mycnf_creds["passwd"]
elif login_password is None or login_user is None:
module.fail_json(msg="when supplying login arguments, both login_user and login_password must be provided")
try:
db_connection = MySQLdb.connect(host=module.params["login_host"], user=login_user, passwd=login_password, db="mysql")
cursor = db_connection.cursor()
except Exception as e:
module.fail_json(msg="unable to connect, check login_user and login_password are correct, or alternatively check ~/.my.cnf contains credentials")
changed = False
if db_exists(cursor, db):
if state == "absent":
changed = db_delete(cursor, db)
else:
if state == "present":
changed = db_create(cursor, db, encoding, collation)
module.exit_json(changed=changed, db=db)
# this is magic, see lib/ansible/module_common.py
#<<INCLUDE_ANSIBLE_MODULE_COMMON>>
main()

View File

@ -1,227 +0,0 @@
#!/usr/bin/python
# (c) 2012, Mark Theunissen <mark.theunissen@gmail.com>
# Sponsored by Four Kitchens http://fourkitchens.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 ConfigParser
try:
import MySQLdb
except ImportError:
mysqldb_found = False
else:
mysqldb_found = True
# ===========================================
# MySQL module specific support methods.
#
def user_exists(cursor, user, host):
cursor.execute("SELECT count(*) FROM user WHERE user = %s AND host = %s", (user,host))
count = cursor.fetchone()
return count[0] > 0
def user_add(cursor, user, host, password, new_priv):
cursor.execute("CREATE USER %s@%s IDENTIFIED BY %s", (user,host,password))
if new_priv is not None:
for db_table, priv in new_priv.iteritems():
privileges_grant(cursor, user,host,db_table,priv)
return True
def user_mod(cursor, user, host, password, new_priv):
changed = False
# Handle passwords.
if password is not None:
cursor.execute("SELECT password FROM user WHERE user = %s AND host = %s", (user,host))
current_pass_hash = cursor.fetchone()
cursor.execute("SELECT PASSWORD(%s)", (password,))
new_pass_hash = cursor.fetchone()
if current_pass_hash[0] != new_pass_hash[0]:
cursor.execute("SET PASSWORD FOR %s@%s = PASSWORD(%s)", (user,host,password))
changed = True
# Handle privileges.
if new_priv is not None:
curr_priv = privileges_get(cursor, user,host)
# If the user has privileges on a db.table that doesn't appear at all in
# the new specification, then revoke all privileges on it.
for db_table, priv in curr_priv.iteritems():
if db_table not in new_priv:
privileges_revoke(cursor, user,host,db_table)
changed = True
# If the user doesn't currently have any privileges on a db.table, then
# we can perform a straight grant operation.
for db_table, priv in new_priv.iteritems():
if db_table not in curr_priv:
privileges_grant(cursor, user,host,db_table,priv)
changed = True
# If the db.table specification exists in both the user's current privileges
# and in the new privileges, then we need to see if there's a difference.
db_table_intersect = set(new_priv.keys()) & set(curr_priv.keys())
for db_table in db_table_intersect:
priv_diff = set(new_priv[db_table]) ^ set(curr_priv[db_table])
if (len(priv_diff) > 0):
privileges_revoke(cursor, user,host,db_table)
privileges_grant(cursor, user,host,db_table,new_priv[db_table])
changed = True
return changed
def user_delete(cursor, user, host):
cursor.execute("DROP USER %s@%s", (user,host))
return True
def privileges_get(cursor, user,host):
""" MySQL doesn't have a better method of getting privileges aside from the
SHOW GRANTS query syntax, which requires us to then parse the returned string.
Here's an example of the string that is returned from MySQL:
GRANT USAGE ON *.* TO 'user'@'localhost' IDENTIFIED BY 'pass';
This function makes the query and returns a dictionary containing the results.
The dictionary format is the same as that returned by privileges_unpack() below.
"""
output = {}
cursor.execute("SHOW GRANTS FOR %s@%s", (user,host))
grants = cursor.fetchall()
for grant in grants:
res = re.match("GRANT\ (.+)\ ON\ (.+)\ TO", grant[0])
if res is None:
module.fail_json(msg="unable to parse the MySQL grant string")
privileges = res.group(1).split(", ")
privileges = ['ALL' if x=='ALL PRIVILEGES' else x for x in privileges]
db = res.group(2).replace('`', '')
output[db] = privileges
return output
def privileges_unpack(priv):
""" Take a privileges string, typically passed as a parameter, and unserialize
it into a dictionary, the same format as privileges_get() above. We have this
custom format to avoid using YAML/JSON strings inside YAML playbooks. Example
of a privileges string:
mydb.*:INSERT,UPDATE/anotherdb.*:SELECT/yetanother.*:ALL
The privilege USAGE stands for no privileges, so we add that in on *.* if it's
not specified in the string, as MySQL will always provide this by default.
"""
output = {}
for item in priv.split('/'):
pieces = item.split(':')
output[pieces[0]] = pieces[1].upper().split(',')
if '*.*' not in output:
output['*.*'] = ['USAGE']
return output
def privileges_revoke(cursor, user,host,db_table):
query = "REVOKE ALL PRIVILEGES ON %s FROM '%s'@'%s'" % (db_table,user,host)
cursor.execute(query)
def privileges_grant(cursor, user,host,db_table,priv):
priv_string = ",".join(priv)
query = "GRANT %s ON %s TO '%s'@'%s'" % (priv_string,db_table,user,host)
cursor.execute(query)
def load_mycnf():
config = ConfigParser.RawConfigParser()
mycnf = os.path.expanduser('~/.my.cnf')
if not os.path.exists(mycnf):
return False
try:
config.readfp(open(mycnf))
creds = dict(user=config.get('client', 'user'),password=config.get('client', 'pass'))
except (ConfigParser.NoOptionError, IOError):
return False
return creds
# ===========================================
# Module execution.
#
def main():
module = AnsibleModule(
argument_spec = dict(
login_user=dict(default=None),
login_password=dict(default=None),
login_host=dict(default="localhost"),
user=dict(required=True, aliases=['name']),
password=dict(default=None),
host=dict(default="localhost"),
state=dict(default="present", choices=["absent", "present"]),
priv=dict(default=None),
)
)
user = module.params["user"]
password = module.params["password"]
host = module.params["host"]
state = module.params["state"]
priv = module.params["priv"]
if not mysqldb_found:
module.fail_json(msg="the python mysqldb module is required")
if priv is not None:
try:
priv = privileges_unpack(priv)
except:
module.fail_json(msg="invalid privileges string")
# Either the caller passes both a username and password with which to connect to
# mysql, or they pass neither and allow this module to read the credentials from
# ~/.my.cnf.
login_password = module.params["login_password"]
login_user = module.params["login_user"]
if login_user is None and login_password is None:
mycnf_creds = load_mycnf()
if mycnf_creds is False:
login_user = "root"
login_password = ""
else:
login_user = mycnf_creds["user"]
login_password = mycnf_creds["password"]
elif login_password is None or login_user is None:
module.fail_json(msg="when supplying login arguments, both login_user and login_password must be provided")
try:
db_connection = MySQLdb.connect(host=module.params["login_host"], user=login_user, passwd=login_password, db="mysql")
cursor = db_connection.cursor()
except Exception as e:
module.fail_json(msg="unable to connect to database, check login_user and login_password are correct or ~/.my.cnf has the credentials")
if state == "present":
if user_exists(cursor, user, host):
changed = user_mod(cursor, user, host, password, priv)
else:
if password is None:
module.fail_json(msg="password parameter required when adding a user")
changed = user_add(cursor, user, host, password, priv)
elif state == "absent":
if user_exists(cursor, user, host):
changed = user_delete(cursor, user, host)
else:
changed = False
module.exit_json(changed=changed, user=user)
# this is magic, see lib/ansible/module_common.py
#<<INCLUDE_ANSIBLE_MODULE_COMMON>>
main()

View File

@ -1,5 +1,4 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
#!/bin/bash
# (c) 2012, Michael DeHaan <michael.dehaan@gmail.com>
#
@ -19,25 +18,5 @@
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
#
def get_ohai_data():
p = subprocess.Popen(["/usr/bin/env", "ohai"], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
(out, err) = p.communicate()
rc = p.returncode
return rc, out, err
def main():
module = AnsibleModule(
argument_spec = dict()
)
rc, out, err = get_ohai_data()
if rc != 0:
module.fail_json(msg=err)
else:
module.exit_json(**json.loads(out))
# this is magic, see lib/ansible/module_common.py
#<<INCLUDE_ANSIBLE_MODULE_COMMON>>
main()
/usr/bin/logger -t ansible-ohai Invoked as-is
/usr/bin/ohai

View File

@ -1,5 +1,4 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# (c) 2012, Michael DeHaan <michael.dehaan@gmail.com>
#
@ -18,13 +17,15 @@
# You should have received a copy of the GNU General Public License
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
def main():
module = AnsibleModule(
argument_spec = dict()
)
module.exit_json(ping='pong')
try:
import json
except ImportError:
import simplejson as json
# this is magic, see lib/ansible/module_common.py
#<<INCLUDE_ANSIBLE_MODULE_COMMON>>
main()
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" })

View File

@ -1,103 +0,0 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# 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/>.
try:
import psycopg2
except ImportError:
postgresqldb_found = False
else:
postgresqldb_found = True
# ===========================================
# PostgreSQL module specific support methods.
#
def db_exists(cursor, db):
query = "SELECT * FROM pg_database WHERE datname=%(db)s"
cursor.execute(query, {'db': db})
return cursor.rowcount == 1
def db_delete(cursor, db):
query = "DROP DATABASE %s" % db
cursor.execute(query)
return True
def db_create(cursor, db, owner, template, encoding):
if owner:
owner = " OWNER %s" % owner
if template:
template = " TEMPLATE %s" % template
if encoding:
encoding = " ENCODING '%s'" % encoding
query = "CREATE DATABASE %s%s%s%s" % (db, owner, template, encoding)
cursor.execute(query)
return True
# ===========================================
# Module execution.
#
def main():
module = AnsibleModule(
argument_spec=dict(
login_user=dict(default="postgres"),
login_password=dict(default=""),
login_host=dict(default=""),
db=dict(required=True, aliases=['name']),
owner=dict(default=""),
template=dict(default=""),
encoding=dict(default=""),
state=dict(default="present", choices=["absent", "present"]),
)
)
if not postgresqldb_found:
module.fail_json(msg="the python psycopg2 module is required")
db = module.params["db"]
owner = module.params["owner"]
template = module.params["template"]
encoding = module.params["encoding"]
state = module.params["state"]
changed = False
try:
db_connection = psycopg2.connect(host=module.params["login_host"],
user=module.params["login_user"],
password=module.params["login_password"],
database="template1")
# Enable autocommit so we can create databases
db_connection.autocommit = True
cursor = db_connection.cursor()
except Exception as e:
module.fail_json(msg="unable to connect to database: %s" % e)
try:
if db_exists(cursor, db):
if state == "absent":
changed = db_delete(cursor, db)
else:
if state == "present":
changed = db_create(cursor, db, owner, template, encoding)
except Exception as e:
module.fail_json(msg="Database query failed: %s" % e)
module.exit_json(changed=changed, db=db)
# this is magic, see lib/ansible/module_common.py
#<<INCLUDE_ANSIBLE_MODULE_COMMON>>
main()

View File

@ -1,153 +0,0 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# 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/>.
try:
import psycopg2
except ImportError:
postgresqldb_found = False
else:
postgresqldb_found = True
# ===========================================
# PostgreSQL module specific support methods.
#
def user_exists(cursor, user):
query = "SELECT rolname FROM pg_roles WHERE rolname=%(user)s"
cursor.execute(query, {'user': user})
return cursor.rowcount > 0
def user_add(cursor, user, password, db):
"""Create a new user with write access to the database"""
query = "CREATE USER %(user)s with PASSWORD '%(password)s'"
cursor.execute(query % {"user": user, "password": password})
grant_privileges(cursor, user, db)
return True
def has_privileges(cursor, user, db):
"""Check if the user has create privileges on the database"""
query = "SELECT has_database_privilege(%(user)s, %(db)s, 'CREATE')"
cursor.execute(query, {'user': user, 'db': db})
return cursor.fetchone()[0]
def grant_privileges(cursor, user, db):
"""Grant all privileges on the database"""
query = "GRANT ALL PRIVILEGES ON DATABASE %(db)s TO %(user)s"
cursor.execute(query % {'user': user, 'db': db})
def revoke_privileges(cursor, user, db):
"""Revoke all privileges on the database"""
query = "REVOKE ALL PRIVILEGES ON DATABASE %(db)s FROM %(user)s"
cursor.execute(query % {'user': user, 'db': db})
def user_mod(cursor, user, password, db):
"""Update password and permissions"""
changed = False
# Handle passwords.
if password is not None:
select = "SELECT rolpassword FROM pg_authid where rolname=%(user)s"
cursor.execute(select, {"user": user})
current_pass_hash = cursor.fetchone()[0]
# Not sure how to hash the new password, so we just initiate the
# change and check if the hash changed
alter = "ALTER USER %(user)s WITH PASSWORD '%(password)s'"
cursor.execute(alter % {"user": user, "password": password})
cursor.execute(select, {"user": user})
new_pass_hash = cursor.fetchone()[0]
if current_pass_hash != new_pass_hash:
changed = True
# Handle privileges.
# For now, we just check if the user has access to the database
if not has_privileges(cursor, user, db):
grant_privileges(cursor, user, db)
changed = True
return changed
def user_delete(cursor, user, db):
"""Delete a user, first revoking privileges"""
revoke_privileges(cursor, user, db)
cursor.execute("DROP USER %(user)s" % {'user': user})
return True
# ===========================================
# Module execution.
#
def main():
module = AnsibleModule(
argument_spec=dict(
login_user=dict(default="postgres"),
login_password=dict(default=""),
login_host=dict(default=""),
user=dict(required=True, aliases=['name']),
password=dict(default=None),
state=dict(default="present", choices=["absent", "present"]),
db=dict(required=True),
)
)
user = module.params["user"]
password = module.params["password"]
state = module.params["state"]
db = module.params["db"]
if not postgresqldb_found:
module.fail_json(msg="the python psycopg2 module is required")
try:
db_connection = psycopg2.connect(host=module.params["login_host"],
user=module.params["login_user"],
password=module.params["login_password"],
database=db)
cursor = db_connection.cursor()
except Exception as e:
module.fail_json(msg="unable to connect to database: %s" % e)
if state == "present":
if user_exists(cursor, user):
changed = user_mod(cursor, user, password, db)
else:
if password is None:
msg = "password parameter required when adding a user"
module.fail_json(msg=msg)
changed = user_add(cursor, user, password, db)
elif state == "absent":
if user_exists(cursor, user):
changed = user_delete(cursor, user, db)
else:
changed = False
# Commit the database changes
db_connection.commit()
module.exit_json(changed=changed, user=user)
# this is magic, see lib/ansible/module_common.py
#<<INCLUDE_ANSIBLE_MODULE_COMMON>>
main()

View File

@ -1 +1,23 @@
# this is a virtual module that is entirely implemented server side
#!/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

@ -1,5 +1,4 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# (c) 2012, Michael DeHaan <michael.dehaan@gmail.com>
#
@ -18,15 +17,33 @@
# 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 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
INITCTL = None
def _find_binaries(m):
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 SERVICE
global CHKCONFIG
global SERVICE
global INITCTL
paths = ['/sbin', '/usr/sbin', '/bin', '/usr/bin']
binaries = [ 'service', 'chkconfig', 'update-rc.d', 'initctl']
@ -46,11 +63,11 @@ def _find_binaries(m):
elif location.get('update-rc.d', None):
CHKCONFIG = location['update-rc.d']
else:
m.fail_json(msg='unable to find chkconfig or update-rc.d binary')
fail_json(dict(failed=True, msg='unable to find chkconfig or update-rc.d binary'))
if location.get('service', None):
SERVICE = location['service']
else:
m.fail_json(msg='unable to find service binary')
fail_json(dict(failed=True, msg='unable to find service binary'))
if location.get('initctl', None):
INITCTL = location['initctl']
else:
@ -58,6 +75,7 @@ def _find_binaries(m):
def _get_service_status(name):
rc, status_stdout, status_stderr = _run("%s %s status" % (SERVICE, name))
status = status_stdout + status_stderr
# set the running state to None because we don't know it yet
running = None
@ -81,19 +99,15 @@ def _get_service_status(name):
# if the job status is still not known check it by status output keywords
if running == None:
# first tranform the status output that could irritate keyword matching
cleanout = status_stdout.lower().replace(name.lower(), '')
if "stop" in cleanout:
cleaned_status_stdout = status_stdout.lower().replace(name.lower(),'')
if cleaned_status_stdout.find("stop") != -1:
running = False
elif "run" in cleanout and "not" in cleanout:
elif cleaned_status_stdout.find("run") != -1 and cleaned_status_stdout.find("not") != -1:
running = False
elif "run" in cleanout and "not" not in cleanout:
elif cleaned_status_stdout.find("run") != -1 and cleaned_status_stdout.find("not") == -1:
running = True
elif "start" in cleanout and "not" not in cleanout:
elif cleaned_status_stdout.find("start") != -1 and cleaned_status_stdout.find("not") == -1:
running = True
elif 'could not access pid file' in cleanout:
running = False
elif 'is dead and pid file exists' in cleanout:
running = False
# if the job status is still not known check it by special conditions
if running == None:
@ -121,67 +135,94 @@ def _do_enable(name, enable):
valid_argument['on'] = "enable"
valid_argument['off'] = "disable"
if enable is not None:
if enable:
rc, stdout, stderr = _run("%s %s %s" % (CHKCONFIG, name, valid_argument['on']))
else:
rc, stdout, stderr = _run("%s %s %s" % (CHKCONFIG, name, valid_argument['off']))
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
def main():
module = AnsibleModule(
argument_spec = dict(
name = dict(required=True),
state = dict(choices=['running', 'started', 'stopped', 'restarted', 'reloaded']),
enabled = dict(choices=BOOLEANS)
)
)
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)
name = module.params['name']
state = module.params['state']
enable = module.boolean(module.params.get('enabled', None))
if not len(items):
fail_json(dict(failed=True, msg='this module requires arguments (-a)'))
# ===========================================
# find binaries locations on minion
_find_binaries(module)
params = {}
for arg in items:
if "=" not in arg:
fail_json(dict(failed=True, msg='expected key=value format arguments'))
(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 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'))
# ===========================================
# get service status
running = _get_service_status(name)
# ===========================================
# Some common variables
changed = False
# ===========================================
# find binaries locations on minion
_find_binaries()
# ===========================================
# get service status
running = _get_service_status(name)
if state or enable:
rc = 0
err = ''
out = ''
if module.params['enabled']:
err = ''
changed = False
if enable:
rc_enable, out_enable, err_enable = _do_enable(name, enable)
rc += rc_enable
out += out_enable
err += err_enable
if state and running == None:
module.fail_json(msg="failed determining the current service state => state stays unchanged", changed=False)
print json.dumps({
"failed" : True,
"msg" : "failed determining the current service state => state stays unchanged",
})
print >> sys.stderr, out + err
elif state:
# a state change command has been requested
# ===========================================
# determine if we are going to change anything
if not running and state in ["started", "running"]:
if not running and state in ("started", "running"):
changed = True
elif running and state in ["stopped","reloaded"]:
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']:
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))
@ -199,19 +240,40 @@ def main():
rc = rc + rc_state
if rc != 0:
module.fail_json(msg=err)
print json.dumps({
"failed" : 1,
"rc" : rc,
})
print >> sys.stderr, out + err
sys.exit(1)
# ===============================================
# success
result = {"changed": changed}
if module.params['enabled']:
result['enabled'] = module.params['enabled']
if state:
result['state'] = state
rc, stdout, stderr = _run("%s %s status" % (SERVICE, name))
module.exit_json(**result);
if list_items and list_items in [ 'status' ]:
result['status'] = stdout
print json.dumps(result)
elif list_items is not None:
# this is magic, see lib/ansible/module_common.py
#<<INCLUDE_ANSIBLE_MODULE_COMMON>>
# solo list=status mode, don't change anything, just return
# suitable for /usr/bin/ansible usage or API, playbooks
# not so much
main()
print json.dumps({
"status" : status
})
else:
print json.dumps(dict(failed=True, msg="expected state or list parameters"))
sys.exit(0)

View File

@ -1,5 +1,4 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# (c) 2012, Michael DeHaan <michael.dehaan@gmail.com>
#
@ -18,13 +17,21 @@
# You should have received a copy of the GNU General Public License
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
DEFAULT_ANSIBLE_SETUP = "/etc/ansible/setup"
import array
import fcntl
import glob
import sys
import os
import platform
import re
import shlex
import socket
import struct
import subprocess
import traceback
import syslog
try:
import selinux
@ -37,625 +44,375 @@ try:
except ImportError:
import simplejson as json
class Facts(object):
"""
This class should only attempt to populate those facts that
are mostly generic to all systems. This includes platform facts,
service facts (eg. ssh keys or selinux), and distribution facts.
Anything that requires extensive code or may have more than one
possible implementation to establish facts for a given topic should
subclass Facts.
"""
_I386RE = re.compile(r'i[3456]86')
# For the most part, we assume that platform.dist() will tell the truth.
# This is the fallback to handle unknowns or exceptions
OSDIST_DICT = { '/etc/redhat-release': 'RedHat',
'/etc/vmware-release': 'VMwareESX' }
SELINUX_MODE_DICT = { 1: 'enforcing', 0: 'permissive', -1: 'disabled' }
def __init__(self):
self.facts = {}
self.get_platform_facts()
self.get_distribution_facts()
self.get_public_ssh_host_keys()
self.get_selinux_facts()
def populate(self):
return self.facts
# Platform
# patform.system() can be Linux, Darwin, Java, or Windows
def get_platform_facts(self):
self.facts['system'] = platform.system()
self.facts['kernel'] = platform.release()
self.facts['machine'] = platform.machine()
self.facts['python_version'] = platform.python_version()
self.facts['fqdn'] = socket.getfqdn()
self.facts['hostname'] = self.facts['fqdn'].split('.')[0]
if self.facts['machine'] == 'x86_64':
self.facts['architecture'] = self.facts['machine']
elif Facts._I386RE.search(self.facts['machine']):
self.facts['architecture'] = 'i386'
else:
self.facts['archtecture'] = self.facts['machine']
if self.facts['system'] == 'Linux':
self.get_distribution_facts()
# platform.dist() is deprecated in 2.6
# in 2.6 and newer, you should use platform.linux_distribution()
def get_distribution_facts(self):
dist = platform.dist()
self.facts['distribution'] = dist[0].capitalize() or 'NA'
self.facts['distribution_version'] = dist[1] or 'NA'
self.facts['distribution_release'] = dist[2] or 'NA'
# Try to handle the exceptions now ...
for (path, name) in Facts.OSDIST_DICT.items():
if os.path.exists(path):
if self.facts['distribution'] == 'Fedora':
pass
elif name == 'RedHat':
data = get_file_content(path)
if 'Red Hat' in data:
self.facts['distribution'] = name
else:
self.facts['distribution'] = data.split()[0]
else:
self.facts['distribution'] = name
def get_public_ssh_host_keys(self):
dsa = get_file_content('/etc/ssh/ssh_host_dsa_key.pub')
rsa = get_file_content('/etc/ssh/ssh_host_rsa_key.pub')
if dsa is None:
dsa = 'NA'
else:
self.facts['ssh_host_key_dsa_public'] = dsa.split()[1]
if rsa is None:
rsa = 'NA'
else:
self.facts['ssh_host_key_rsa_public'] = rsa.split()[1]
def get_selinux_facts(self):
if not HAVE_SELINUX:
self.facts['selinux'] = False
return
self.facts['selinux'] = {}
if not selinux.is_selinux_enabled():
self.facts['selinux']['status'] = 'disabled'
else:
self.facts['selinux']['status'] = 'enabled'
self.facts['selinux']['policyvers'] = selinux.security_policyvers()
(rc, configmode) = selinux.selinux_getenforcemode()
if rc == 0 and Facts.SELINUX_MODE_DICT.has_key(configmode):
self.facts['selinux']['config_mode'] = Facts.SELINUX_MODE_DICT[configmode]
mode = selinux.security_getenforce()
if Facts.SELINUX_MODE_DICT.has_key(mode):
self.facts['selinux']['mode'] = Facts.SELINUX_MODE_DICT[mode]
(rc, policytype) = selinux.selinux_getpolicytype()
if rc == 0:
self.facts['selinux']['type'] = policytype
class Hardware(Facts):
"""
This is a generic Hardware subclass of Facts. This should be further
subclassed to implement per platform. If you subclass this, it
should define:
- memfree_mb
- memtotal_mb
- swapfree_mb
- swaptotal_mb
- processor (a list)
- processor_cores
- processor_count
All subclasses MUST define platform.
"""
platform = 'Generic'
def __new__(cls, *arguments, **keyword):
subclass = cls
for sc in Hardware.__subclasses__():
if sc.platform == platform.system():
subclass = sc
return super(cls, subclass).__new__(subclass, *arguments, **keyword)
def __init__(self):
Facts.__init__(self)
def populate(self):
return self.facts
class LinuxHardware(Hardware):
"""
Linux-specific subclass of Hardware. Defines memory and CPU facts:
- memfree_mb
- memtotal_mb
- swapfree_mb
- swaptotal_mb
- processor (a list)
- processor_cores
- processor_count
In addition, it also defines number of DMI facts.
"""
platform = 'Linux'
MEMORY_FACTS = ['MemTotal', 'SwapTotal', 'MemFree', 'SwapFree']
# DMI bits
DMI_DICT = { 'form_factor': '/sys/devices/virtual/dmi/id/chassis_type',
'product_name': '/sys/devices/virtual/dmi/id/product_name',
'product_serial': '/sys/devices/virtual/dmi/id/product_serial',
'product_uuid': '/sys/devices/virtual/dmi/id/product_uuid',
'product_version': '/sys/devices/virtual/dmi/id/product_version',
'system_vendor': '/sys/devices/virtual/dmi/id/sys_vendor',
'bios_date': '/sys/devices/virtual/dmi/id/bios_date',
'bios_version': '/sys/devices/virtual/dmi/id/bios_version' }
# From smolt and DMI spec
# See http://www.dmtf.org/sites/default/files/standards/documents/DSP0134_2.7.0.pdf
FORM_FACTOR = [ "Unknown", "Other", "Unknown", "Desktop",
"Low Profile Desktop", "Pizza Box", "Mini Tower", "Tower",
"Portable", "Laptop", "Notebook", "Hand Held", "Docking Station",
"All In One", "Sub Notebook", "Space-saving", "Lunch Box",
"Main Server Chassis", "Expansion Chassis", "Sub Chassis",
"Bus Expansion Chassis", "Peripheral Chassis", "RAID Chassis",
"Rack Mount Chassis", "Sealed-case PC", "Multi-system",
"CompactPCI", "AdvancedTCA", "Blade" ]
def __init__(self):
Hardware.__init__(self)
def populate(self):
self.get_cpu_facts()
self.get_memory_facts()
self.get_dmi_facts()
return self.facts
def get_memory_facts(self):
if not os.access("/proc/meminfo", os.R_OK):
return
for line in open("/proc/meminfo").readlines():
data = line.split(":", 1)
key = data[0]
if key in LinuxHardware.MEMORY_FACTS:
val = data[1].strip().split(' ')[0]
self.facts["%s_mb" % key.lower()] = long(val) / 1024
def get_cpu_facts(self):
i = 0
physid = 0
sockets = {}
if not os.access("/proc/cpuinfo", os.R_OK):
return
self.facts['processor'] = []
for line in open("/proc/cpuinfo").readlines():
data = line.split(":", 1)
key = data[0].strip()
if key == 'model name':
if 'processor' not in self.facts:
self.facts['processor'] = []
self.facts['processor'].append(data[1].strip())
i += 1
elif key == 'physical id':
physid = data[1].strip()
if physid not in sockets:
sockets[physid] = 1
elif key == 'cpu cores':
sockets[physid] = int(data[1].strip())
if len(sockets) > 0:
self.facts['processor_count'] = len(sockets)
self.facts['processor_cores'] = reduce(lambda x, y: x + y, sockets.values())
else:
self.facts['processor_count'] = i
self.facts['processor_cores'] = 'NA'
def get_dmi_facts(self):
for (key,path) in LinuxHardware.DMI_DICT.items():
data = get_file_content(path)
if data is not None:
if key == 'form_factor':
try:
self.facts['form_factor'] = LinuxHardware.FORM_FACTOR[int(data)]
except IndexError, e:
self.facts['form_factor'] = 'unknown (%s)' % data
else:
self.facts[key] = data
else:
self.facts[key] = 'NA'
class SunOSHardware(Hardware):
"""
In addition to the generic memory and cpu facts, this also sets
swap_reserved_mb and swap_allocated_mb that is available from *swap -s*.
"""
platform = 'SunOS'
def __init__(self):
Hardware.__init__(self)
def populate(self):
self.get_cpu_facts()
self.get_memory_facts()
return self.facts
def get_cpu_facts(self):
cmd = subprocess.Popen("/usr/sbin/psrinfo -v", shell=True,
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
out, err = cmd.communicate()
self.facts['processor'] = []
for line in out.split('\n'):
if 'processor operates' in line:
if 'processor' not in self.facts:
self.facts['processor'] = []
self.facts['processor'].append(line.strip())
self.facts['processor_cores'] = 'NA'
self.facts['processor_count'] = len(self.facts['processor'])
def get_memory_facts(self):
cmd = subprocess.Popen("/usr/sbin/prtconf", shell=False,
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
out, err = cmd.communicate()
for line in out.split('\n'):
if 'Memory size' in line:
self.facts['memtotal_mb'] = line.split()[2]
cmd = subprocess.Popen("/usr/sbin/swap -s", shell=True,
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
out, err = cmd.communicate()
allocated = long(out.split()[1][:-1])
reserved = long(out.split()[5][:-1])
used = long(out.split()[8][:-1])
free = long(out.split()[10][:-1])
self.facts['swapfree_mb'] = free / 1024
self.facts['swaptotal_mb'] = (free + used) / 1024
self.facts['swap_allocated_mb'] = allocated / 1024
self.facts['swap_reserved_mb'] = reserved / 1024
class FreeBSDHardware(Hardware):
"""
FreeBSD-specific subclass of Hardware. Defines memory and CPU facts:
- memfree_mb
- memtotal_mb
- swapfree_mb
- swaptotal_mb
- processor (a list)
- processor_cores
- processor_count
"""
platform = 'FreeBSD'
DMESG_BOOT = '/var/run/dmesg.boot'
def __init__(self):
Hardware.__init__(self)
def populate(self):
self.get_cpu_facts()
self.get_memory_facts()
return self.facts
def get_cpu_facts(self):
self.facts['processor'] = []
cmd = subprocess.Popen("/sbin/sysctl -n hw.ncpu", shell=True,
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
out, err = cmd.communicate()
self.facts['processor_count'] = out.strip()
for line in open(FreeBSDHardware.DMESG_BOOT).readlines():
if 'CPU:' in line:
cpu = re.sub(r'CPU:\s+', r"", line)
self.facts['processor'].append(cpu.strip())
if 'Logical CPUs per core' in line:
self.facts['processor_cores'] = line.split()[4]
def get_memory_facts(self):
cmd = subprocess.Popen("/sbin/sysctl vm.stats", shell=True,
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
out, err = cmd.communicate()
for line in out.split('\n'):
data = line.split()
if 'vm.stats.vm.v_page_size' in line:
pagesize = long(data[1])
if 'vm.stats.vm.v_page_count' in line:
pagecount = long(data[1])
if 'vm.stats.vm.v_free_count' in line:
freecount = long(data[1])
self.facts['memtotal_mb'] = pagesize * pagecount / 1024 / 1024
self.facts['memfree_mb'] = pagesize * freecount / 1024 / 1024
# Get swapinfo. swapinfo output looks like:
# Device 1M-blocks Used Avail Capacity
# /dev/ada0p3 314368 0 314368 0%
#
cmd = subprocess.Popen("/usr/sbin/swapinfo -m", shell=True,
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
out, err = cmd.communicate()
lines = out.split('\n')
if len(lines[-1]) == 0:
lines.pop()
data = lines[-1].split()
self.facts['swaptotal_mb'] = data[1]
self.facts['swapfree_mb'] = data[3]
class Network(Facts):
"""
This is a generic Network subclass of Facts. This should be further
subclassed to implement per platform. If you subclass this,
you must define:
- interfaces (a list of interface names)
- interface_<name> dictionary of ipv4, ipv6, and mac address information.
All subclasses MUST define platform.
"""
platform = 'Generic'
IPV6_SCOPE = { '0' : 'global',
'10' : 'host',
'20' : 'link',
'40' : 'admin',
'50' : 'site',
'80' : 'organization' }
def __new__(cls, *arguments, **keyword):
subclass = cls
for sc in Network.__subclasses__():
if sc.platform == platform.system():
subclass = sc
return super(cls, subclass).__new__(subclass, *arguments, **keyword)
def __init__(self):
Facts.__init__(self)
def populate(self):
return self.facts
class LinuxNetwork(Network):
"""
This is a Linux-specific subclass of Network. It defines
- interfaces (a list of interface names)
- interface_<name> dictionary of ipv4, ipv6, and mac address information.
"""
platform = 'Linux'
def __init__(self):
Network.__init__(self)
def populate(self):
self.facts['interfaces'] = self.get_interfaces()
self.get_interface_facts()
self.get_ipv4_facts()
self.get_ipv6_facts()
return self.facts
# get list of interfaces
def get_interfaces(self):
names = []
data = get_file_content('/proc/net/dev')
# Format of /proc/net/dev is:
# Inter-| Receive ...
# face |bytes ...
# lo: 595059
for line in data.split('\n'):
if ':' in line:
names.append(line.split(':')[0].strip())
return names
def get_iface_hwaddr(self, iface):
data = get_file_content('/sys/class/net/%s/address' % iface)
if data is None:
return 'unknown'
else:
return data.strip()
def get_interface_facts(self):
for iface in self.facts['interfaces']:
if iface not in self.facts:
self.facts[iface] = {}
self.facts[iface] = { 'macaddress': self.get_iface_hwaddr(iface) }
if os.path.exists('/sys/class/net/%s/mtu' % iface):
mtu = get_file_content('/sys/class/net/%s/mtu' % iface)
self.facts[iface]['mtu'] = mtu.strip()
def get_ipv4_facts(self):
for iface in self.facts['interfaces']:
# This is lame, but there doesn't appear to be a good way
# to get all addresses for both IPv4 and IPv6.
cmd = subprocess.Popen("env LANG=\"\" /sbin/ifconfig %s" % iface, shell=True,
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
out, err = cmd.communicate()
for line in out.split('\n'):
is_ipv4 = False
data = line.split()
if 'inet addr' in line:
if 'ipv4' not in self.facts[iface]:
self.facts[iface]['ipv4'] = {}
is_ipv4 = True
self.facts[iface]['ipv4'] = { 'address': data[1].split(':')[1],
'netmask': data[-1].split(':')[1] }
# Slightly different output in net-tools-1.60-134.20120127git
# Looks like
# inet 192.168.1.2 netmask 255.255.255.0 broadcast 192.168.1.255
elif 'inet ' in line:
is_ipv4 = True
if 'ipv4' not in self.facts[iface]:
self.facts[iface]['ipv4'] = {}
self.facts[iface]['ipv4'] = { 'address': data[1],
'netmask': data[3] }
if is_ipv4:
ip = struct.unpack("!L", socket.inet_aton(self.facts[iface]['ipv4']['address']))[0]
mask = struct.unpack("!L", socket.inet_aton(self.facts[iface]['ipv4']['netmask']))[0]
self.facts[iface]['ipv4']['network'] = socket.inet_ntoa(struct.pack("!L", ip & mask))
def get_ipv6_facts(self):
if not socket.has_ipv6:
return
data = get_file_content('/proc/net/if_inet6')
if data is None:
return
for line in data.split('\n'):
l = line.split()
iface = l[5]
if 'ipv6' not in self.facts[iface]:
self.facts[iface]['ipv6'] = []
scope = l[3]
if Network.IPV6_SCOPE.has_key(l[3]):
scope = Network.IPV6_SCOPE[l[3]]
prefix = int(l[2], 16)
str_addr = ':'.join( [ l[0][i:i+4] for i in range(0, len(l[0]), 4) ] )
# Normalize ipv6 address from format in /proc/net/if_inet6
addr = socket.inet_ntop(socket.AF_INET6,
socket.inet_pton(socket.AF_INET6, str_addr))
self.facts[iface]['ipv6'].append( { 'address': addr,
'prefix': prefix,
'scope': scope } )
class Virtual(Facts):
"""
This is a generic Virtual subclass of Facts. This should be further
subclassed to implement per platform. If you subclass this,
you should define:
- virtualization_type
- virtualization_role
All subclasses MUST define platform.
"""
def __new__(cls, *arguments, **keyword):
subclass = cls
for sc in Virtual.__subclasses__():
if sc.platform == platform.system():
subclass = sc
return super(cls, subclass).__new__(subclass, *arguments, **keyword)
def __init__(self):
Facts.__init__(self)
def populate(self):
return self.facts
class LinuxVirtual(Virtual):
"""
This is a Linux-specific subclass of Virtual. It defines
- virtualization_type
- virtualization_role
"""
platform = 'Linux'
def __init__(self):
Virtual.__init__(self)
def populate(self):
self.get_virtual_facts()
return self.facts
def get_virtual_facts(self):
if os.path.exists("/proc/xen"):
self.facts['virtualization_type'] = 'xen'
self.facts['virtualization_role'] = 'guest'
if os.path.exists("/proc/xen/capabilities"):
self.facts['virtualization_role'] = 'host'
if os.path.exists("/proc/modules"):
modules = []
for line in open("/proc/modules").readlines():
data = line.split(" ", 1)
modules.append(data[0])
if 'kvm' in modules:
self.facts['virtualization_type'] = 'kvm'
self.facts['virtualization_role'] = 'host'
elif 'vboxdrv' in modules:
self.facts['virtualization_type'] = 'virtualbox'
self.facts['virtualization_role'] = 'host'
elif 'vboxguest' in modules:
self.facts['virtualization_type'] = 'virtualbox'
self.facts['virtualization_role'] = 'guest'
data = get_file_content('/proc/cpuinfo')
if 'QEMU' in data:
self.facts['virtualization_type'] = 'kvm'
self.facts['virtualization_role'] = 'guest'
if 'distribution' in self.facts and self.facts['distribution'] == 'VMwareESX':
self.facts['virtualization_type'] = 'VMware'
self.facts['virtualization_role'] = 'host'
# You can spawn a dmidecode process and parse that or infer from devices
for dev_model in glob.glob('/sys/block/?da/device/vendor'):
info = open(dev_model).read()
if 'VMware' in info:
self.facts['virtualization_type'] = 'VMware'
self.facts['virtualization_role'] = 'guest'
elif 'Virtual HD' in info or 'Virtual CD' in info:
self.facts['virtualization_type'] = 'VirtualPC'
self.facts['virtualization_role'] = 'guest'
_I386RE = re.compile(r'i[3456]86')
SIOCGIFCONF = 0x8912
SIOCGIFHWADDR = 0x8927
MEMORY_FACTS = ['MemTotal', 'SwapTotal', 'MemFree', 'SwapFree']
# DMI bits
DMI_DICT = { 'form_factor': '/sys/devices/virtual/dmi/id/chassis_type',
'product_name': '/sys/devices/virtual/dmi/id/product_name',
'product_serial': '/sys/devices/virtual/dmi/id/product_serial',
'product_uuid': '/sys/devices/virtual/dmi/id/product_uuid',
'product_version': '/sys/devices/virtual/dmi/id/product_version',
'system_vendor': '/sys/devices/virtual/dmi/id/sys_vendor',
'bios_date': '/sys/devices/virtual/dmi/id/bios_date',
'bios_version': '/sys/devices/virtual/dmi/id/bios_version' }
# From smolt and DMI spec
FORM_FACTOR = [ "Unknown", "Other", "Unknown", "Desktop",
"Low Profile Desktop", "Pizza Box", "Mini Tower", "Tower",
"Portable", "Laptop", "Notebook", "Hand Held", "Docking Station",
"All In One", "Sub Notebook", "Space-saving", "Lunch Box",
"Main Server Chassis", "Expansion Chassis", "Sub Chassis",
"Bus Expansion Chassis", "Peripheral Chassis", "RAID Chassis",
"Rack Mount Chassis", "Sealed-case PC", "Multi-system",
"CompactPCI", "AdvancedTCA" ]
# For the most part, we assume that platform.dist() will tell the truth.
# This is the fallback to handle unknowns or exceptions
OSDIST_DICT = { '/etc/redhat-release': 'RedHat',
'/etc/vmware-release': 'VMwareESX' }
SELINUX_MODE_DICT = { 1: 'enforcing', 0: 'permissive', -1: 'disabled' }
def get_file_content(path):
data = None
if os.path.exists(path) and os.access(path, os.R_OK):
data = open(path).read().strip()
if len(data) == 0:
data = None
else:
data = None
return data
# platform.dist() is deprecated in 2.6
# in 2.6 and newer, you should use platform.linux_distribution()
def get_distribution_facts(facts):
dist = platform.dist()
facts['distribution'] = dist[0].capitalize() or 'NA'
facts['distribution_version'] = dist[1] or 'NA'
facts['distribution_release'] = dist[2] or 'NA'
# Try to handle the exceptions now ...
for (path, name) in OSDIST_DICT.items():
if os.path.exists(path):
if facts['distribution'] == 'Fedora':
pass
elif name == 'RedHat':
data = get_file_content(path)
if 'Red Hat' in data:
facts['distribution'] = name
else:
facts['distribution'] = data.split()[0]
else:
facts['distribution'] = name
# Platform
# patform.system() can be Linux, Darwin, Java, or Windows
def get_platform_facts(facts):
facts['system'] = platform.system()
facts['kernel'] = platform.release()
facts['machine'] = platform.machine()
facts['python_version'] = platform.python_version()
if facts['machine'] == 'x86_64':
facts['architecture'] = facts['machine']
elif _I386RE.search(facts['machine']):
facts['architecture'] = 'i386'
else:
facts['archtecture'] = facts['machine']
if facts['system'] == 'Linux':
get_distribution_facts(facts)
def get_memory_facts(facts):
if not os.access("/proc/meminfo", os.R_OK):
return facts
for line in open("/proc/meminfo").readlines():
data = line.split(":", 1)
key = data[0]
if key in MEMORY_FACTS:
val = data[1].strip().split(' ')[0]
facts["%s_mb" % key.lower()] = long(val) / 1024
def get_cpu_facts(facts):
i = 0
physid = 0
sockets = {}
if not os.access("/proc/cpuinfo", os.R_OK):
return facts
for line in open("/proc/cpuinfo").readlines():
data = line.split(":", 1)
key = data[0].strip()
if key == 'model name':
if 'processor' not in facts:
facts['processor'] = []
facts['processor'].append(data[1].strip())
i += 1
elif key == 'physical id':
physid = data[1].strip()
if physid not in sockets:
sockets[physid] = 1
elif key == 'cpu cores':
sockets[physid] = int(data[1].strip())
if len(sockets) > 0:
facts['processor_count'] = len(sockets)
facts['processor_cores'] = reduce(lambda x, y: x + y, sockets.values())
else:
facts['processor_count'] = i
facts['processor_cores'] = 'NA'
def get_hardware_facts(facts):
get_memory_facts(facts)
get_cpu_facts(facts)
for (key,path) in DMI_DICT.items():
data = get_file_content(path)
if data is not None:
if key == 'form_factor':
facts['form_factor'] = FORM_FACTOR[int(data)]
else:
facts[key] = data
else:
facts[key] = 'NA'
def get_linux_virtual_facts(facts):
if os.path.exists("/proc/xen"):
facts['virtualization_type'] = 'xen'
facts['virtualization_role'] = 'guest'
if os.path.exists("/proc/xen/capabilities"):
facts['virtualization_role'] = 'host'
if os.path.exists("/proc/modules"):
modules = []
for line in open("/proc/modules").readlines():
data = line.split(" ", 1)
modules.append(data[0])
if 'kvm' in modules:
facts['virtualization_type'] = 'kvm'
facts['virtualization_role'] = 'host'
elif 'vboxdrv' in modules:
facts['virtualization_type'] = 'virtualbox'
facts['virtualization_role'] = 'host'
elif 'vboxguest' in modules:
facts['virtualization_type'] = 'virtualbox'
facts['virtualization_role'] = 'guest'
if 'QEMU' in facts['processor'][0]:
facts['virtualization_type'] = 'kvm'
facts['virtualization_role'] = 'guest'
if facts['distribution'] == 'VMwareESX':
facts['virtualization_type'] = 'VMware'
facts['virtualization_role'] = 'host'
# You can spawn a dmidecode process and parse that or infer from devices
for dev_model in glob.glob('/sys/block/?da/device/vendor'):
info = open(dev_model).read()
if 'VMware' in info:
facts['virtualization_type'] = 'VMware'
facts['virtualization_role'] = 'guest'
elif 'Virtual HD' in info or 'Virtual CD' in info:
facts['virtualization_type'] = 'VirtualPC'
facts['virtualization_role'] = 'guest'
def get_virtual_facts(facts):
facts['virtualization_type'] = 'None'
facts['virtualization_role'] = 'None'
if facts['system'] == 'Linux':
facts = get_linux_virtual_facts(facts)
# get list of interfaces that are up
def get_interfaces(facts):
if facts['system'] == 'SunOS':
return []
length = 4096
offset = 32
step = 32
if platform.architecture()[0] == '64bit':
offset = 16
step = 40
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
names = array.array('B', '\0' * length)
bytelen = struct.unpack('iL', fcntl.ioctl(
s.fileno(), SIOCGIFCONF, struct.pack(
'iL', length, names.buffer_info()[0])
))[0]
return [names.tostring()[i:i+offset].split('\0', 1)[0]
for i in range(0, bytelen, step)]
def get_iface_hwaddr(iface):
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
info = fcntl.ioctl(s.fileno(), SIOCGIFHWADDR,
struct.pack('256s', iface[:15]))
return ''.join(['%02x:' % ord(char) for char in info[18:24]])[:-1]
def get_network_facts(facts):
facts['fqdn'] = socket.getfqdn()
facts['hostname'] = facts['fqdn'].split('.')[0]
facts['interfaces'] = get_interfaces(facts)
for iface in facts['interfaces']:
facts[iface] = { 'macaddress': get_iface_hwaddr(iface) }
# This is lame, but there doesn't appear to be a good way
# to get all addresses for both IPv4 and IPv6.
cmd = subprocess.Popen("/sbin/ifconfig %s" % iface, shell=True,
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
out, err = cmd.communicate()
for line in out.split('\n'):
data = line.split()
if 'inet addr' in line:
if 'ipv4' not in facts[iface]:
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()
if 'ipv6' not in facts[iface]:
facts[iface]['ipv6'] = []
facts[iface]['ipv6'].append( { 'address': ip,
'prefix': prefix,
'scope': scope } )
return facts
def get_public_ssh_host_keys(facts):
dsa = get_file_content('/etc/ssh/ssh_host_dsa_key.pub')
rsa = get_file_content('/etc/ssh/ssh_host_rsa_key.pub')
if dsa is None:
dsa = 'NA'
else:
facts['ssh_host_key_dsa_public'] = dsa.split()[1]
if rsa is None:
rsa = 'NA'
else:
facts['ssh_host_key_rsa_public'] = rsa.split()[1]
def get_selinux_facts(facts):
if not HAVE_SELINUX:
facts['selinux'] = False
return
facts['selinux'] = {}
if not selinux.is_selinux_enabled():
facts['selinux']['status'] = 'disabled'
else:
facts['selinux']['status'] = 'enabled'
facts['selinux']['policyvers'] = selinux.security_policyvers()
(rc, configmode) = selinux.selinux_getenforcemode()
if rc == 0 and SELINUX_MODE_DICT.has_key(configmode):
facts['selinux']['config_mode'] = SELINUX_MODE_DICT[configmode]
mode = selinux.security_getenforce()
if SELINUX_MODE_DICT.has_key(mode):
facts['selinux']['mode'] = SELINUX_MODE_DICT[mode]
(rc, policytype) = selinux.selinux_getpolicytype()
if rc == 0:
facts['selinux']['type'] = policytype
def get_service_facts(facts):
get_public_ssh_host_keys(facts)
get_selinux_facts(facts)
def ansible_facts():
facts = {}
facts.update(Facts().populate())
facts.update(Hardware().populate())
facts.update(Network().populate())
facts.update(Virtual().populate())
get_platform_facts(facts)
get_hardware_facts(facts)
get_virtual_facts(facts)
get_network_facts(facts)
get_service_facts(facts)
return facts
# ===========================================
def md5_sum(f):
md5sum = os.popen("/usr/bin/md5sum %(file)s 2>/dev/null || /sbin/md5 -q %(file)s 2>/dev/null || /usr/bin/digest -a md5 -v %(file)s 2>/dev/null" % {"file": f}).read().split()[0]
return md5sum
def run_setup(module):
# load config & template variables
setup_options = {}
facts = ansible_facts()
for (k, v) in facts.items():
setup_options["ansible_%s" % k] = v
if len(sys.argv) == 1:
sys.exit(1)
# if facter is installed, and we can use --json because
# ruby-json is ALSO installed, include facter data in the JSON
if os.path.exists("/usr/bin/facter"):
cmd = subprocess.Popen("/usr/bin/facter --json", shell=True,
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
out, err = cmd.communicate()
facter = True
try:
facter_ds = json.loads(out)
except:
facter = False
if facter:
for (k,v) in facter_ds.items():
setup_options["facter_%s" % k] = v
argfile = sys.argv[1]
if not os.path.exists(argfile):
sys.exit(1)
# ditto for ohai, but just top level string keys
# because it contains a lot of nested stuff we can't use for
# templating w/o making a nicer key for it (TODO)
setup_options = open(argfile).read().strip()
try:
setup_options = json.loads(setup_options)
except:
list_options = shlex.split(setup_options)
setup_options = {}
for opt in list_options:
(k,v) = opt.split("=")
setup_options[k]=v
if os.path.exists("/usr/bin/ohai"):
cmd = subprocess.Popen("/usr/bin/ohai", shell=True,
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
out, err = cmd.communicate()
ohai = True
try:
ohai_ds = json.loads(out)
except:
ohai = False
if ohai:
for (k,v) in ohai_ds.items():
if type(v) == str or type(v) == unicode:
k2 = "ohai_%s" % k
setup_options[k2] = v
syslog.openlog('ansible-%s' % os.path.basename(__file__))
syslog.syslog(syslog.LOG_NOTICE, 'Invoked with %s' % setup_options)
setup_result = {}
setup_result['ansible_facts'] = setup_options
ansible_file = os.path.expandvars(setup_options.get('metadata', DEFAULT_ANSIBLE_SETUP))
ansible_dir = os.path.dirname(ansible_file)
# hack to keep --verbose from showing all the setup module results
setup_result['verbose_override'] = True
# create the config dir if it doesn't exist
return setup_result
if not os.path.exists(ansible_dir):
os.makedirs(ansible_dir)
def main():
module = AnsibleModule(
argument_spec = dict()
)
data = run_setup(module)
module.exit_json(**data)
changed = False
md5sum = None
if not os.path.exists(ansible_file):
changed = True
else:
md5sum = md5_sum(ansible_file)
# this is magic, see lib/ansible/module_common.py
#<<INCLUDE_ANSIBLE_MODULE_COMMON>>
main()
# Get some basic facts in case facter or ohai are not installed
for (k, v) in ansible_facts().items():
setup_options["ansible_%s" % k] = v
# if facter is installed, and we can use --json because
# ruby-json is ALSO installed, include facter data in the JSON
if os.path.exists("/usr/bin/facter"):
cmd = subprocess.Popen("/usr/bin/facter --json", shell=True,
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
out, err = cmd.communicate()
facter = True
try:
facter_ds = json.loads(out)
except:
facter = False
if facter:
for (k,v) in facter_ds.items():
setup_options["facter_%s" % k] = v
# ditto for ohai, but just top level string keys
# because it contains a lot of nested stuff we can't use for
# templating w/o making a nicer key for it (TODO)
if os.path.exists("/usr/bin/ohai"):
cmd = subprocess.Popen("/usr/bin/ohai", shell=True,
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
out, err = cmd.communicate()
ohai = True
try:
ohai_ds = json.loads(out)
except:
ohai = False
if ohai:
for (k,v) in ohai_ds.items():
if type(v) == str or type(v) == unicode:
k2 = "ohai_%s" % k
setup_options[k2] = v
# write the template/settings file using
# instructions from server
f = open(ansible_file, "w+")
reformat = json.dumps(setup_options, sort_keys=True, indent=4)
f.write(reformat)
f.close()
md5sum2 = md5_sum(ansible_file)
if md5sum != md5sum2:
changed = True
setup_result = {}
setup_result['written'] = ansible_file
setup_result['changed'] = changed
setup_result['md5sum'] = md5sum2
setup_result['ansible_facts'] = setup_options
# hack to keep --verbose from showing all the setup module results
setup_result['verbose_override'] = True
print json.dumps(setup_result)

View File

@ -1,3 +1,6 @@
# There is actually no actual shell module source, when you use 'shell' in ansible,
# it runs the 'command' module with special arguments and it behaves differently.
# See the command source and the comment "#USE_SHELL".
# VIRTUAL
There is actually no actual shell module source, when you use 'shell' in ansible,
it runs the 'command' module with special arguments and it behaves differently.
See the command source and the comment "#USE_SHELL".

View File

@ -1,5 +1,4 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# (c) 2012, Michael DeHaan <michael.dehaan@gmail.com>
#
@ -18,25 +17,60 @@
# You should have received a copy of the GNU General Public License
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
def main():
module = AnsibleModule(
argument_spec = dict(
src = dict(required=True),
)
)
source = module.params['src']
import sys
import os
import shlex
import base64
import syslog
if not os.path.exists(source):
module.fail_json(msg="file not found: %s" % source)
if not os.access(source, os.R_OK):
module.fail_json(msg="file is not readable: %s" % source)
try:
import json
except ImportError:
import simplejson as json
data = base64.b64encode(file(source).read())
# ===========================================
# convert arguments of form a=b c=d
# to a dictionary
module.exit_json(content=data, encoding='base64')
if len(sys.argv) == 1:
sys.exit(1)
argfile = sys.argv[1]
if not os.path.exists(argfile):
sys.exit(1)
# this is magic, see lib/ansible/module_common.py
#<<INCLUDE_ANSIBLE_MODULE_COMMON>>
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)
main()
params = {}
for x in items:
(k, v) = x.split("=")
params[k] = v
source = os.path.expanduser(params['src'])
# ==========================================
# raise an error if there is no template metadata
if not os.path.exists(source):
print json.dumps(dict(
failed = 1,
msg = "file not found: %s" % source
))
sys.exit(1)
if not os.access(source, os.R_OK):
print json.dumps(dict(
failed = 1,
msg = "file is not readable: %s" % source
))
sys.exit(1)
# ==========================================
data = file(source).read()
data = base64.b64encode(data)
print json.dumps(dict(content=data, encoding='base64'))
sys.exit(0)

View File

@ -1 +1,24 @@
# this is a virtual module that is entirely implemented server side
#!/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 template module isn't really a remote transferred
# module. All the magic happens in Runner.py making use of the
# copy module, and if not running from a playbook, also the 'slurp'
# module.

View File

@ -1,5 +1,4 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# (c) 2012, Stephen Fromm <sfromm@gmail.com>
#
@ -18,50 +17,84 @@
# 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 pwd
import grp
import shlex
import subprocess
import sys
import syslog
try:
import spwd
HAVE_SPWD=True
except:
HAVE_SPWD=False
def get_bin_path(module, arg):
if os.path.exists('/usr/sbin/%s' % arg):
return '/usr/sbin/%s' % arg
elif os.path.exists('/sbin/%s' % arg):
return '/sbin/%s' % arg
else:
module.fail_json(msg="Cannot find %s" % arg)
USERADD = "/usr/sbin/useradd"
USERMOD = "/usr/sbin/usermod"
USERDEL = "/usr/sbin/userdel"
def user_del(module, user, **kwargs):
cmd = [get_bin_path(module, 'userdel')]
def exit_json(rc=0, **kwargs):
if 'name' in kwargs:
add_user_info(kwargs)
print json.dumps(kwargs)
sys.exit(rc)
def fail_json(**kwargs):
kwargs['failed'] = True
exit_json(rc=1, **kwargs)
def add_user_info(kwargs):
name = kwargs['name']
if user_exists(name):
kwargs['state'] = 'present'
info = user_info(name)
kwargs['uid'] = info[2]
kwargs['group'] = info[3]
kwargs['comment'] = info[4]
kwargs['home'] = info[5]
kwargs['shell'] = info[6]
kwargs['createhome'] = os.path.exists(info[5])
groups = user_group_membership(name)
if len(groups) > 0:
kwargs['groups'] = groups
else:
kwargs['state'] = 'absent'
return kwargs
def user_del(user, **kwargs):
cmd = [USERDEL]
for key in kwargs:
if key == 'force' and kwargs[key] == 'yes':
if key == 'force' and kwargs[key]:
cmd.append('-f')
elif key == 'remove' and kwargs[key] == 'yes':
elif key == 'remove' and kwargs[key]:
cmd.append('-r')
cmd.append(user)
p = subprocess.Popen(cmd, shell=False, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
(out, err) = p.communicate()
rc = p.returncode
return (rc, out, err)
rc = subprocess.call(cmd, shell=False, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
if rc == 0:
return True
else:
return False
def user_add(module, user, **kwargs):
cmd = [get_bin_path(module, 'useradd')]
def user_add(user, **kwargs):
cmd = [USERADD]
for key in kwargs:
if key == 'uid' and kwargs[key] is not None:
cmd.append('-u')
cmd.append(kwargs[key])
elif key == 'group' and kwargs[key] is not None:
if not group_exists(kwargs[key]):
module.fail_json(msg="Group %s does not exist" % (kwargs[key]))
fail_json(msg="Group %s does not exist" % (kwargs[key]))
cmd.append('-g')
cmd.append(kwargs[key])
elif key == 'groups' and kwargs[key] is not None:
for g in kwargs[key].split(','):
if not group_exists(g):
module.fail_json(msg="Group %s does not exist" % (g))
fail_json(msg="Group %s does not exist" % (g))
cmd.append('-G')
cmd.append(kwargs[key])
elif key == 'comment' and kwargs[key] is not None:
@ -85,17 +118,18 @@ def user_add(module, user, **kwargs):
elif key == 'system' and kwargs[key] == 'yes':
cmd.append('-r')
cmd.append(user)
p = subprocess.Popen(cmd, shell=False, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
(out, err) = p.communicate()
rc = p.returncode
return (rc, out, err)
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(module, user, **kwargs):
cmd = [get_bin_path(module, 'usermod')]
def user_mod(user, **kwargs):
cmd = [USERMOD]
info = user_info(user)
for key in kwargs:
if key == 'uid':
@ -104,8 +138,8 @@ def user_mod(module, user, **kwargs):
cmd.append(kwargs[key])
elif key == 'group' and kwargs[key] is not None:
if not group_exists(kwargs[key]):
module.fail_json(msg="Group %s does not exist" % (kwargs[key]))
ginfo = group_info(kwargs[key])
fail_json(msg="Group %s does not exist" % (kwargs[key]))
ginfo = group_info(group)
if info[3] != ginfo[2]:
cmd.append('-g')
cmd.append(kwargs[key])
@ -114,7 +148,7 @@ def user_mod(module, user, **kwargs):
groups = kwargs[key].split(',')
for g in groups:
if not group_exists(g):
module.fail_json(msg="Group %s does not exist" % (g))
fail_json(msg="Group %s does not exist" % (g))
group_diff = set(sorted(current_groups)).symmetric_difference(set(sorted(groups)))
groups_need_mod = False
@ -149,12 +183,13 @@ def user_mod(module, user, **kwargs):
cmd.append(kwargs[key])
# skip if no changes to be made
if len(cmd) == 1:
return (None, '', '')
return False
cmd.append(user)
p = subprocess.Popen(cmd, shell=False, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
(out, err) = p.communicate()
rc = p.returncode
return (rc, out, err)
rc = subprocess.call(cmd, shell=False, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
if rc == 0:
return True
else:
return False
def group_exists(group):
try:
@ -210,102 +245,93 @@ def user_info(user):
# ===========================================
def main():
module = AnsibleModule(
argument_spec = dict(
state=dict(default='present', choices=['present', 'absent']),
name=dict(required=True),
uid=dict(default=None),
group=dict(default=None),
groups=dict(default=None),
comment=dict(default=None),
home=dict(default=None),
shell=dict(default=None),
password=dict(default=None),
# following options are specific to userdel
force=dict(default='no', choices=['yes', 'no']),
remove=dict(default='no', choices=['yes', 'no']),
# following options are specific to useradd
createhome=dict(default='yes', choices=['yes', 'no']),
system=dict(default='no', choices=['yes', 'no']),
# following options are specific to usermod
append=dict(default='no', choices=['yes', 'no']),
)
)
state = module.params['state']
name = module.params['name']
uid = module.params['uid']
group = module.params['group']
groups = module.params['groups']
comment = module.params['comment']
home = module.params['home']
shell = module.params['shell']
password = module.params['password']
force = module.params['force']
remove = module.params['remove']
createhome = module.params['createhome']
system = module.params['system']
append = module.params['append']
rc = None
out = ''
err = ''
result = {}
result['name'] = name
result['state'] = state
if state == 'absent':
if user_exists(name):
(rc, out, err) = user_del(module, name, force=force, remove=remove)
if rc != 0:
module.fail_json(name=name, msg=err, rc=rc)
result['force'] = force
result['remove'] = remove
elif state == 'present':
if not user_exists(name):
(rc, out, err) = user_add(module,
name, uid=uid, group=group, groups=groups,
comment=comment, home=home, shell=shell,
password=password, createhome=createhome,
system=system)
result['system'] = system
result['createhome'] = createhome
else:
(rc, out, err) = user_mod(module,
name, uid=uid, group=group, groups=groups,
comment=comment, home=home, shell=shell,
password=password, append=append)
result['append'] = append
if rc is not None and rc != 0:
module.fail_json(name=name, msg=err, rc=rc)
if password is not None:
result['password'] = 'NOT_LOGGING_PASSWORD'
if rc is None:
result['changed'] = False
if not os.path.exists(USERADD):
if os.path.exists("/sbin/useradd"):
USERADD = "/sbin/useradd"
else:
result['changed'] = True
if out:
result['stdout'] = out
if err:
result['stderr'] = err
fail_json(msg="Cannot find useradd")
if not os.path.exists(USERMOD):
if os.path.exists("/sbin/usermod"):
USERMOD = "/sbin/usermod"
else:
fail_json(msg="Cannot find usermod")
if not os.path.exists(USERDEL):
if os.path.exists("/sbin/userdel"):
USERDEL = "/sbin/userdel"
else:
fail_json(msg="Cannot find 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')
sys.exit(1)
params = {}
for x in items:
(k, v) = x.split("=")
params[k] = v
state = params.get('state','present')
name = params.get('name', None)
uid = params.get('uid', None)
group = params.get('group', None)
groups = params.get('groups', None)
comment = params.get('comment', None)
home = params.get('home', None)
shell = params.get('shell', None)
password = params.get('password', None)
# ===========================================
# following options are specific to userdel
force = params.get('force', False)
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
append = params.get('append', 'no')
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:
fail_json(msg='name is required')
changed = False
rc = 0
if state == 'absent':
if user_exists(name):
info = user_info(name)
if info == False:
result['msg'] = "failed to look up user name: %s" % name
result['failed'] = True
result['uid'] = info[2]
result['group'] = info[3]
result['comment'] = info[4]
result['home'] = info[5]
result['shell'] = info[6]
groups = user_group_membership(name)
result['uid'] = info[2]
if len(groups) > 0:
result['groups'] = groups
changed = user_del(name, force=force, remove=remove)
exit_json(name=name, changed=changed, force=force, remove=remove)
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,
system=system)
else:
changed = user_mod(name, uid=uid, group=group, groups=groups,
comment=comment, home=home, shell=shell,
password=password, append=append)
module.exit_json(**result)
if password is not None:
exit_json(name=name, changed=changed, password="XXXXXXXX")
else:
exit_json(name=name, changed=changed)
# include magic from lib/ansible/module_common.py
#<<INCLUDE_ANSIBLE_MODULE_COMMON>>
main()
fail_json(name=name, msg='Unexpected position reached')
sys.exit(0)

View File

@ -1,11 +1,9 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
"""
Virt management features
Copyright 2007, 2012 Red Hat, Inc
Michael DeHaan <michael.dehaan@gmail.com>
Michael DeHaan <mdehaan@redhat.com>
Seth Vidal <skvidal@fedoraproject.org>
This software may be freely redistributed under the terms of the GNU
@ -19,18 +17,29 @@ VIRT_FAILED = 1
VIRT_SUCCESS = 0
VIRT_UNAVAILABLE=2
try:
import json
except ImportError:
import simplejson as json
# other modules
import os
import sys
import subprocess
import syslog
try:
import libvirt
except ImportError:
print "failed=True msg='libvirt python module unavailable'"
print json.dumps({
"failed" : True,
"msg" : "libvirt python module unavailable",
"rc": VIRT_UNAVAILABLE,
})
sys.exit(1)
ALL_COMMANDS = []
VM_COMMANDS = ['create','status', 'start', 'stop', 'pause', 'unpause',
'shutdown', 'undefine', 'destroy', 'get_xml', 'autostart']
HOST_COMMANDS = ['freemem', 'list_vms', 'info', 'nodeinfo', 'virttype']
ALL_COMMANDS.extend(VM_COMMANDS)
ALL_COMMANDS.extend(HOST_COMMANDS)
import shlex
VIRT_STATE_NAME_MAP = {
0 : "running",
@ -166,6 +175,7 @@ class Virt(object):
state.append("%s %s" % (vm,state_blurb))
return state
def info(self):
vms = self.list_vms()
info = dict()
@ -217,6 +227,7 @@ class Virt(object):
def virttype(self):
return self.__get_conn().get_type()
def autostart(self, vmid):
self.conn = self.__get_conn()
return self.conn.set_autostart(vmid, True)
@ -226,60 +237,87 @@ class Virt(object):
return self.conn.getFreeMemory()
def shutdown(self, vmid):
""" Make the machine with the given vmid stop running. Whatever that takes. """
"""
Make the machine with the given vmid stop running.
Whatever that takes.
"""
self.__get_conn()
self.conn.shutdown(vmid)
return 0
def pause(self, vmid):
""" Pause the machine with the given vmid. """
"""
Pause the machine with the given vmid.
"""
self.__get_conn()
return self.conn.suspend(vmid)
def unpause(self, vmid):
""" Unpause the machine with the given vmid. """
"""
Unpause the machine with the given vmid.
"""
self.__get_conn()
return self.conn.resume(vmid)
def create(self, vmid):
""" Start the machine via the given vmid """
def create(self, vmid):
"""
Start the machine via the given mac address.
"""
self.__get_conn()
return self.conn.create(vmid)
def start(self, vmid):
""" Start the machine via the given id/name """
"""
Start the machine via the given id/name
"""
self.__get_conn()
return self.conn.create(vmid)
def destroy(self, vmid):
""" Pull the virtual power from the virtual domain, giving it virtually no time to virtually shut down. """
"""
Pull the virtual power from the virtual domain, giving it virtually no
time to virtually shut down.
"""
self.__get_conn()
return self.conn.destroy(vmid)
def undefine(self, vmid):
""" Stop a domain, and then wipe it from the face of the earth. (delete disk/config file) """
"""
Stop a domain, and then wipe it from the face of the earth.
by deleting the disk image and its configuration file.
"""
self.__get_conn()
return self.conn.undefine(vmid)
def status(self, vmid):
"""
Return a state suitable for server consumption. Aka, codes.py values, not XM output.
"""
self.__get_conn()
return self.conn.get_status(vmid)
def get_xml(self, vmid):
"""
Receive a Vm id as input
Return an xml describing vm config returned by a libvirt call
"""
conn = libvirt.openReadOnly(None)
if conn == None:
return (-1,'Failed to open connection to the hypervisor')
@ -289,6 +327,7 @@ class Virt(object):
return (-1,'Failed to find the main domain')
return domV.XMLDesc(0)
def get_maxVcpus(self, vmid):
"""
Gets the max number of VCPUs on a guest
@ -305,19 +344,59 @@ class Virt(object):
self.__get_conn()
return self.conn.get_MaxMemory(vmid)
def core(module):
def main():
rc = VIRT_SUCCESS
vm_commands = ['create','status', 'start', 'stop', 'pause', 'unpause',
'shutdown', 'undefine', 'destroy', 'get_xml', 'autostart']
host_commands = ['freemem', 'list_vms', 'info', 'nodeinfo', 'virttype']
msg = """
virtmodule arguments:
state=[running|shutdown] guest=guestname
command=some_virt_command [guest=guestname]
"""
if len(sys.argv) == 1:
return VIRT_FAILED, msg
state = module.params.get('state', None)
guest = module.params.get('name', None)
command = module.params.get('command', None)
argfile = sys.argv[1]
if not os.path.exists(argfile):
msg = "Argument file not found"
return VIRT_FAILED, msg
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
# guest=name state=[running|shutdown|destroyed|undefined]
# command=[some command] [guest=name]
params = {}
if '=' not in args:
msg = "No proper arguments provided to virt module: %s" % args
return VIRT_FAILED, msg
for x in items:
(k, v) = x.split("=")
params[k] = v
state = params.get('state', None)
guest = params.get('guest', None)
command = params.get('command', None)
options = params.get('options', [])
v = Virt()
res = {}
if state:
if not guest:
module.fail_json(msg = "state change requires a guest specified")
msg = "state change requires a guest specified"
return VIRT_FAILED, msg
res['changed'] = False
if state == 'running':
@ -328,51 +407,46 @@ def core(module):
if v.status(guest) is not 'shutdown':
res['changed'] = True
res['msg'] = v.shutdown(guest)
else:
module.fail_json(msg="unexpected state")
return VIRT_SUCCESS, res
if command:
if command in VM_COMMANDS:
if command in vm_commands:
if not guest:
module.fail_json(msg = "%s requires 1 argument: guest" % command)
msg = "%s requires 1 argument: guest" % command
return VIRT_FAILED, msg
res = getattr(v, command)(guest)
if type(res) != dict:
res = { command: res }
return VIRT_SUCCESS, res
return rc, res
elif hasattr(v, command):
res = getattr(v, command)()
if type(res) != dict:
res = { command: res }
return VIRT_SUCCESS, res
return rc, res
else:
module.fail_json(msg="Command %s not recognized" % basecmd)
module.fail_json(msg="expected state or command parameter to be specified")
msg = "Command %s not recognized" % basecmd
rc = VIRT_FAILED
return rc, msg
def main():
module = AnsibleModule(argument_spec=dict(
name = dict(aliases=['guest']),
state = dict(choices=['running', 'shutdown']),
command = dict(choices=ALL_COMMANDS),
))
rc = VIRT_SUCCESS
if __name__ == "__main__":
try:
rc, result = core(module)
rc, result = main()
except Exception, e:
module.fail_json(msg=str(e))
rc = 1
result = str(e)
if rc != 0: # something went wrong emit the msg
module.fail_json(rc=rc, msg=result)
print json.dumps({
"failed" : rc,
"msg" : result,
"rc": rc,
})
sys.exit(rc)
else:
module.exit_json(**result)
# this is magic, see lib/ansible/module_common.py
#<<INCLUDE_ANSIBLE_MODULE_COMMON>>
main()
print json.dumps(result)

View File

@ -1,6 +1,4 @@
#!/usr/bin/python -tt
# -*- coding: utf-8 -*-
# (c) 2012, Red Hat, Inc
# Written by Seth Vidal <skvidal at fedoraproject.org>
#
@ -21,110 +19,95 @@
#
import traceback
import os
import sys
import yum
import subprocess
import datetime
import shlex
import re
import traceback
import syslog
def_qf = "%{name}-%{version}-%{release}.%{arch}"
repoquery='/usr/bin/repoquery'
yumbin='/usr/bin/yum'
def is_installed(repoq, pkgspec, qf=def_qf):
cmd = repoq + "--disablerepo=\* --pkgnarrow=installed --qf '%s' %s " % (qf, pkgspec)
rc,out,err = run(cmd)
if rc == 0:
return [ p for p in out.split('\n') if p.strip() ]
try:
import json
except ImportError:
import simplejson as json
return []
def is_available(repoq, pkgspec, qf=def_qf):
cmd = repoq + "--qf '%s' %s " % (qf, pkgspec)
rc,out,err = run(cmd)
if rc == 0:
return [ p for p in out.split('\n') if p.strip() ]
return []
def is_update(repoq, pkgspec, qf=def_qf):
cmd = repoq + "--pkgnarrow=updates --qf '%s' %s " % (qf, pkgspec)
rc,out,err = run(cmd)
if rc == 0:
return set([ p for p in out.split('\n') if p.strip() ])
return []
def what_provides(repoq, req_spec, qf=def_qf):
cmd = repoq + "--qf '%s' --whatprovides %s" % (qf, req_spec)
rc,out,err = run(cmd)
ret = []
if rc == 0:
ret = set([ p for p in out.split('\n') if p.strip() ])
def yum_base(conf_file=None, cachedir=False):
my = yum.YumBase()
my.preconf.debuglevel=0
if conf_file and os.path.exists(conf_file):
my.preconf.fn = conf_file
if cachedir:
if hasattr(my, 'setCacheDir'):
my.setCacheDir()
else:
cachedir = yum.misc.getCacheDir()
my.repos.setCacheDir(cachedir)
my.conf.cache = 0
return ret
return my
def local_nvra(path):
"""return nvra of a local rpm passed in"""
cmd = "/bin/rpm -qp --qf='%%{name}-%%{version}-%%{release}.%%{arch}\n' %s'" % path
rc, out, err = run(cmd)
if rc != 0:
return None
nvra = out.split('\n')[0]
return nvra
def pkg_to_dict(pkgstr):
if pkgstr.strip():
n,e,v,r,a,repo = pkgstr.split('|')
else:
return {'error_parsing': pkgstr}
def pkg_to_dict(po):
d = {
'name':n,
'arch':a,
'epoch':e,
'release':r,
'version':v,
'repo':repo,
'nevra': '%s:%s-%s-%s.%s' % (e,n,v,r,a)
'name':po.name,
'arch':po.arch,
'epoch':po.epoch,
'release':po.release,
'version':po.version,
}
if repo == 'installed':
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
def repolist(repoq, qf="%{repoid}"):
cmd = repoq + "--qf '%s' -a" % (qf)
rc,out,err = run(cmd)
ret = []
if rc == 0:
ret = set([ p for p in out.split('\n') if p.strip() ])
return ret
def list_stuff(conf_file, stuff):
qf = "%{name}|%{epoch}|%{version}|%{release}|%{arch}|%{repoid}"
repoq = '%s --plugins --quiet -q ' % repoquery
if conf_file and os.path.exists(conf_file):
repoq = '%s -c %s --plugins --quiet -q ' % (repoquery,conf_file)
def list_stuff(my, stuff):
# FIXME - there are potential tracebacks that could occur here
# need some more catching for them so we can see what happened
if stuff == 'installed':
return [ pkg_to_dict(p) for p in is_installed(repoq, '-a', qf=qf) if p.strip() ]
return [ pkg_to_dict(po) for po in my.rpmdb ]
elif stuff == 'updates':
return [ pkg_to_dict(p) for p in is_update(repoq, '-a', qf=qf) if p.strip() ]
return [ pkg_to_dict(po) for
po in my.doPackageLists(pkgnarrow='updates').updates ]
elif stuff == 'available':
return [ pkg_to_dict(p) for p in is_available(repoq, '-a', qf=qf) if p.strip() ]
return [ pkg_to_dict(po) for po in my.pkgSack ]
elif stuff == 'repos':
return [ dict(repoid=name, state='enabled') for name in repolist(repoq) if name.strip() ]
r = []
for repo in my.repos.repos.values():
t = {}
s = 'disabled'
if repo.enabled:
s = 'enabled'
t[repo.id] = s
r.append(t)
return r
else:
return [ pkg_to_dict(p) for p in is_installed(repoq, stuff, qf=qf) + is_available(repoq, stuff, qf=qf) if p.strip() ]
e,m,u = my.rpmdb.matchPackageNames([stuff])
p = e + m
e,m,u = my.pkgSack.matchPackageNames([stuff])
p = p + e + m
return [ pkg_to_dict(po) for po in p ]
def run(command):
def run_yum(command):
try:
cmd = subprocess.Popen(command, shell=True,
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
@ -147,170 +130,119 @@ def run(command):
return rc, out, err
def install(module, items, repoq, yum_basecmd):
def ensure(my, state, pkgspec):
yumconf = my.conf.config_file_path
res = {}
res['results'] = []
res['msg'] = ''
res['rc'] = 0
res['changed'] = False
for spec in items:
pkg = None
# check if pkgspec is installed (if possible for idempotence)
# localpkg
if spec.endswith('.rpm'):
# get the pkg name-v-r.arch
nvra = local_nvra(spec)
# look for them in the rpmdb
if is_installed(repoq, nvra):
# if they are there, skip it
continue
pkg = spec
#groups :(
elif spec.startswith('@'):
# complete wild ass guess b/c it's a group
pkg = spec
# range requires or file-requires or pkgname :(
if state == 'installed':
# check if pkgspec is installed
if re.search('[></=]',pkgspec):
try:
pkgs = my.returnInstalledPackagesByDep(pkgspec)
except yum.Errors.YumBaseError:
pkgs = None
else:
# look up what pkgs provide this
pkglist = what_provides(repoq, spec)
if not pkglist:
res['msg'] += "No Package matching '%s' found available, installed or updated" % spec
res['failed'] = True
module.exit_json(**res)
# if any of them are installed
# then nothing to do
found = False
for this in pkglist:
if is_installed(repoq, this):
found = True
res['results'].append('%s providing %s is already installed' % (this, spec))
if found:
continue
# if not - then pass in the spec as what to install
# we could get here if nothing provides it but that's not
# the error we're catching here
pkg = spec
e,m,u = my.rpmdb.matchPackageNames([pkgspec])
pkgs = e +m
cmd = "%s install '%s'" % (yum_basecmd, pkg)
rc, out, err = run(cmd)
if pkgs:
return { 'changed':False }
# if not see if it is available
if re.search('[></=]',pkgspec):
try:
pkgs = my.returnPackagesByDep(pkgspec)
except yum.Errors.YumBaseError:
pkgs = None
else:
e,m,u = my.pkgSack.matchPackageNames([pkgspec])
pkgs = e +m
if not pkgs:
msg = "No Package matching %s available" % pkgspec
return { 'changed':False, 'failed': True, 'msg':msg }
my.close()
del my
cmd = "yum -c %s -d1 -y install '%s'" % (yumconf, pkgspec)
rc, out, err = run_yum(cmd)
# FIXME - if we did an install - go and check the rpmdb to see if it actually installed
# look for the pkg in rpmdb
# look for the pkg via obsoletes
if rc:
res['changed'] = False
res['rc'] = rc
res['results'].append(out)
res['msg'] += err
changed = False
failed = True
else:
res['changed'] = True
res['rc'] = 0
res['results'].append(out)
res['msg'] += err
module.exit_json(**res)
changed = True
failed = False
res['changed'] = changed
def remove(module, items, repoq, yum_basecmd):
res = {}
res['results'] = []
res['msg'] = ''
res['changed'] = False
res['rc'] = 0
for spec in items:
pkg = None
# group remove - hope you like things dying!
if spec.startswith('@'):
pkg = spec
# req or pkgname remove
if failed:
res['failed'] = failed
res['msg'] = err
res['results'] = out
return res
if state == 'removed':
# check if pkgspec is installed
# if not return {changed: False, failed=False }
# if so try to remove it:
# if successfull {changed:True, failed=False, results=stdout, errors=stderr }
# if fail {'changed':False, failed='True', results=stdout, errors=stderr }
yumconf = my.conf.config_file_path
if re.search('[></=]',pkgspec):
try:
pkgs = my.returnInstalledPackagesByDep(pkgspec)
except yum.Errors.YumBaseError:
pkgs = None
else:
pkglist = what_provides(repoq, spec)
if not pkglist:
res['msg'] += "No Package matching '%s' found available, installed or updated" % spec
res['failed']=True
module.exit_json(**res)
found = False
for this in pkglist:
if is_installed(repoq, this):
found = True
if not found:
res['results'].append('%s is not installed' % spec)
continue
pkg = spec
e,m,u = my.rpmdb.matchPackageNames([pkgspec])
pkgs = e +m
cmd = "%s remove '%s'" % (yum_basecmd, pkg)
rc, out, err = run(cmd)
if not pkgs:
return { 'changed':False }
my.close()
del my
cmd = "yum -c %s -d1 -y remove '%s'" % (yumconf, pkgspec)
rc, out, err = run_yum(cmd)
# FIXME if we ran the remove - check to make sure it actually removed :(
# look for the pkg in the rpmdb - this is notoriously hard for groups :(
if rc != 0:
res['changed'] = False
res['failed'] = True
res['rc'] = rc
res['results'].append(out)
res['msg'] += err
# look for the pkg in the rpmdb
if rc:
changed = False
failed = True
else:
res['changed'] = True
res['rc'] = 0
res['results'].append(out)
res['msg'] += err
module.exit_json(**res)
changed = True
failed = False
def latest(module, items, repoq, yum_basecmd):
res = {}
res['results'] = []
res['msg'] = ''
res['changed'] = False
res['rc'] = 0
for spec in items:
pkg = None
res['changed'] = changed
# groups, again
if spec.startswith('@'):
pkg = spec
# dep/pkgname - find it
else:
pkglist = what_provides(repoq, spec)
if not pkglist:
res['msg'] += "No Package matching '%s' found available, installed or updated" % spec
res['failed']=True
module.exit_json(**res)
found = False
nothing_to_do = False
can_be_installed = True
for this in pkglist:
if is_installed(repoq, this):
if is_update(repoq, this):
found = True
else:
nothing_to_do = True
if nothing_to_do:
res['results'].append("All packages providing %s are up to date" % spec)
continue
if failed:
res['failed'] = failed
res['msg'] = err
res['results'] = out
return res
if not found:
basecmd = 'install'
else:
basecmd = 'update'
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:
msg = "No Package matching '%s' found available, installed or updated" % pkgspec
return { 'changed':False, 'failed':True, 'msg': msg }
return { 'changed':False,}
pkg = spec
cmd = "%s %s '%s'" % (yum_basecmd, basecmd, pkg)
rc, out, err = run(cmd)
# we have something in updates or available
if not updates:
cmd = "yum -c %s -d1 -y install '%s'" % (yumconf, pkgspec)
else:
cmd = "yum -c %s -d1 -y update '%s'" % (yumconf, pkgspec)
rc, out, err = run_yum(cmd)
# FIXME if it is - update it and check to see if it applied
# check to see if there is no longer an update available for the pkgspec
@ -322,48 +254,24 @@ def latest(module, items, repoq, yum_basecmd):
failed = False
if rc:
res['changed'] = False
res['failed'] = True
res['rc'] = rc
res['results'].append(out)
res['msg'] += err
else:
res['changed'] = True
res['rc'] = 0
res['results'].append(out)
res['msg'] += err
module.exit_json(**res)
res['changed'] = changed
if failed:
res['failed'] = failed
res['msg'] = err
res['results'] = out
return res
return {'changed': False,
'failed': True,
'results':'',
'errors': 'Ensure state %r unknown' % state }
def ensure(module, state, pkgspec, conf_file):
res = {}
stdout = ""
stderr = ""
# take multiple args comma separated
items = [pkgspec]
if pkgspec.find(',') != -1:
items = pkgspec.split(',')
yum_basecmd = '%s -d 1 -y ' % yumbin
repoq = '%s --plugins --quiet -q ' % repoquery
if conf_file and os.path.exists(conf_file):
yum_basecmd = '%s -c %s -d 1 -y' % (yumbin, conf_file)
repoq = '%s -c %s --plugins --quiet -q ' % (repoquery,conf_file)
if state in ['installed', 'present']:
install(module, items, repoq, yum_basecmd)
elif state in ['removed', 'absent']:
remove(module, items, repoq, yum_basecmd)
elif state == 'latest':
latest(module, items, repoq, yum_basecmd)
# should be caught by AnsibleModule argument_spec
return dict(changed=False, failed=True, results='', errors='unexpected state')
def update(args):
#generic update routine
pass
def remove_only(pkgspec):
# remove this pkg and only this pkg - fail if it will require more to remove
@ -381,41 +289,73 @@ def main():
# list=repos
# list=pkgspec
module = AnsibleModule(
argument_spec = dict(
pkg=dict(aliases=['name']),
# removed==absent, installed==present, these are accepted as aliases
state=dict(default='installed', choices=['absent','present','installed','removed','latest']),
list=dict(choices=['installed','updates','available','repos','pkgspec']),
conf_file=dict(default=None),
)
)
if len(sys.argv) == 1:
msg = "the yum module requires arguments (-a)"
return 1, msg
params = module.params
argfile = sys.argv[1]
if not os.path.exists(argfile):
msg = "Argument file not found"
return 1, msg
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 params['list'] and params['pkg']:
module.fail_json(msg="expected 'list=' or 'name=', but not both")
if not len(items):
msg = "the yum module requires arguments (-a)"
return 1, msg
# if nothing else changes - it fails
results = { 'changed':False,
'failed':True,
'results':'',
'errors':'',
'msg':args }
params = {}
for x in items:
try:
(k, v) = x.split("=", 1)
except ValueError:
msg = "invalid arguments: %s" % args
return 1, msg
params[k] = v
if not os.path.exists(repoquery):
module.fail_json(msg="%s is required to run this module. Please install the yum-utils package." % repoquery)
if params['list']:
results = dict(results=list_stuff(params['conf_file'], params['list']))
module.exit_json(**results)
if 'conf_file' not in params:
params['conf_file'] = None
if 'list' in params:
try:
my = yum_base(conf_file=params['conf_file'], cachedir=True)
results = dict(results=list_stuff(my, params['list']))
except Exception, e:
return 1, str(e)
else:
pkg = params['pkg']
pkg = params.get('pkg', params.get('package', params.get('name', None)))
if 'pkg' is None:
module.fail_json(msg="expected 'list=' or 'name='")
results['msg'] = "No pkg specified"
else:
state = params['state']
res = ensure(module, state, pkg, params['conf_file'])
module.fail_json(msg="we should never get here unless this all failed", **res)
try:
my = yum_base(conf_file=params['conf_file'], cachedir=True)
state = params.get('state', 'installed')
results = ensure(my, state, pkg)
except Exception, e:
return 1, str(e)
# this is magic, see lib/ansible/module_common.py
#<<INCLUDE_ANSIBLE_MODULE_COMMON>>
print json.dumps(results)
return 0, None
main()
if __name__ == "__main__":
rc, msg = main()
if rc != 0: # something went wrong emit the msg
print json.dumps({
"failed" : rc,
"msg" : msg
})
sys.exit(rc)

View File

@ -1,13 +0,0 @@
Ansible Debian Package
======================
To create an Ansible DEB package:
sudo apt-get install cdbs debhelper dpkg-dev git-core reprepro
git clone git://github.com/ansible/ansible.git
cd ansible
make deb
The debian package file will be placed in the `../` directory. This can then be added to an APT repository or installed with `dpkg -l`.
Note that `dpkg -i` does not resolve dependencies

View File

@ -0,0 +1,11 @@
I have added a debian folder for use in building a .deb file for ansible. From the ansible directory you can run the following command to construct a debian package of ansible.
~/ansible$ dpkg-buildpackage -us -uc -rfakeroot
The debian package files will be placed in the ../ directory and can be installed with the following command:
~/$ sudo dpkg -i .deb
Dpkg -i doesn't resolve dependencies, so if the previous command fails because of dependencies, you will need to run the following to install the dependencies (if needed) and then re-run the dpkg -i command to install the package:
$ sudo apt-get -f install
--Henry Graham

View File

@ -1,28 +1,22 @@
ansible (0.6) unstable; urgency=low
* 0.6 not released yet
-- Michael DeHaan <michael.dehaan@gmail.com> Wed, 04 Jul 2012 13:40:01 -0400
ansible (0.5) unstable; urgency=low
ansible (0.5) debian; urgency=low
* 0.5 update
-- Michael DeHaan <michael.dehaan@gmail.com> Wed, 04 Jul 2012 13:40:00 -0400
-- Michael DeHaan <michael.dehaan@gmail.com> Wed, 04 Jul 2012 13:40:00 -0400
ansible (0.4) unstable; urgency=low
ansible (0.4) debian; urgency=low
* 0.4 update
-- Michael DeHaan <michael.dehaan@gmail.com> Wed, 23 May 2012 19:40:00 -0400
-- Michael DeHaan <michael.dehaan@gmail.com> Wed, 23 May 2012 19:40:00 -0400
ansible (0.3) unstable; urgency=low
ansible (0.3) debian; urgency=low
* 0.3 update
-- Michael DeHaan <michael.dehaan@gmail.com> Mon, 23 Apr 2012 11:08:00 -0400
-- Michael DeHaan <michael.dehaan@gmail.com> Mon, 23 Apr 2012 11:08:00 -0400
ansible (0.0.2) unstable; urgency=low
ansible (0.0.2) debian; urgency=low
* Initial Release

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.6
Version: 0.5
Group: Development/Libraries
License: GPLv3
@ -58,11 +58,8 @@ rm -rf $RPM_BUILD_ROOT
%changelog
* Mon Aug 6 2012 Michael DeHaan <michael.dehaan@gmail.com> - 0.6-0
- Release of 0.6
* Wed Jul 4 2012 Michael DeHaan <michael.dehaan@gmail.com> - 0.5-0
- Release of 0.5
- Not released yet
* Wed May 23 2012 Michael DeHaan <michael.dehaan@gmail.com> - 0.4-0
- Release of 0.4

View File

@ -14,6 +14,7 @@ class TestInventory(unittest.TestCase):
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)
@ -34,6 +35,9 @@ class TestInventory(unittest.TestCase):
def script_inventory(self):
return Inventory(self.inventory_script)
def yaml_inventory(self):
return Inventory(self.inventory_yaml)
def complex_inventory(self):
return Inventory(self.complex_inventory_file)
@ -44,24 +48,14 @@ class TestInventory(unittest.TestCase):
inventory = self.simple_inventory()
hosts = inventory.list_hosts()
expected_hosts=['jupiter', 'saturn', 'zeus', 'hera',
'cerberus001','cerberus002','cerberus003',
'cottus99', 'cottus100',
'poseidon', 'thor', 'odin', 'loki',
'thrudgelmir0', 'thrudgelmir1', 'thrudgelmir2',
'thrudgelmir3', 'thrudgelmir4', 'thrudgelmir5']
expected_hosts=['jupiter', 'saturn', 'zeus', 'hera', 'poseidon', 'thor', 'odin', 'loki']
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',
'cerberus001','cerberus002','cerberus003',
'cottus99', 'cottus100',
'poseidon', 'thor', 'odin', 'loki',
'thrudgelmir0', 'thrudgelmir1', 'thrudgelmir2',
'thrudgelmir3', 'thrudgelmir4', 'thrudgelmir5']
expected_hosts=['jupiter', 'saturn', 'zeus', 'hera', 'poseidon', 'thor', 'odin', 'loki']
assert sorted(hosts) == sorted(expected_hosts)
def test_simple_norse(self):
@ -75,29 +69,21 @@ class TestInventory(unittest.TestCase):
inventory = self.simple_inventory()
hosts = inventory.list_hosts("ungrouped")
expected_hosts=['jupiter', 'saturn',
'thrudgelmir0', 'thrudgelmir1', 'thrudgelmir2',
'thrudgelmir3', 'thrudgelmir4', 'thrudgelmir5']
expected_hosts=['jupiter', 'saturn']
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',
'cerberus001','cerberus002','cerberus003',
'cottus99','cottus100',
'thor', 'odin', 'loki']
expected_hosts=['zeus', 'hera', 'poseidon', 'thor', 'odin', 'loki']
assert sorted(hosts) == sorted(expected_hosts)
def test_simple_restrict(self):
inventory = self.simple_inventory()
restricted_hosts = ['hera', 'poseidon', 'thor']
expected_hosts=['zeus', 'hera', 'poseidon',
'cerberus001','cerberus002','cerberus003',
'cottus99', 'cottus100',
'thor', 'odin', 'loki']
expected_hosts=['zeus', 'hera', 'poseidon', 'thor', 'odin', 'loki']
inventory.restrict_to(restricted_hosts)
hosts = inventory.list_hosts("norse:greek")
@ -117,15 +103,11 @@ class TestInventory(unittest.TestCase):
inventory = self.simple_inventory()
hosts = inventory.list_hosts("all:!greek")
expected_hosts=['jupiter', 'saturn', 'thor', 'odin', 'loki',
'thrudgelmir0', 'thrudgelmir1', 'thrudgelmir2',
'thrudgelmir3', 'thrudgelmir4', 'thrudgelmir5']
expected_hosts=['jupiter', 'saturn', 'thor', 'odin', 'loki']
assert sorted(hosts) == sorted(expected_hosts)
hosts = inventory.list_hosts("all:!norse:!greek")
expected_hosts=['jupiter', 'saturn',
'thrudgelmir0', 'thrudgelmir1', 'thrudgelmir2',
'thrudgelmir3', 'thrudgelmir4', 'thrudgelmir5']
expected_hosts=['jupiter', 'saturn']
assert sorted(hosts) == sorted(expected_hosts)
def test_simple_vars(self):
@ -134,18 +116,16 @@ class TestInventory(unittest.TestCase):
print vars
assert vars == {'group_names': ['norse'],
'inventory_hostname': 'thor',
'inventory_hostname_short': 'thor'}
'inventory_hostname': 'thor'}
def test_simple_port(self):
inventory = self.simple_inventory()
vars = inventory.get_variables('hera')
print vars
expected = { 'ansible_ssh_port': 3000,
'group_names': ['greek'],
'inventory_hostname': 'hera',
'inventory_hostname_short': 'hera' }
expected = {'ansible_ssh_port': 3000,
'group_names': ['greek'],
'inventory_hostname': 'hera'}
print expected
assert vars == expected
@ -160,7 +140,7 @@ class TestInventory(unittest.TestCase):
expected = dict(
a='1', b='2', c='3', d='100002', rga='1', rgb='2', rgc='3',
inventory_hostname='rtp_a', inventory_hostname_short='rtp_a',
inventory_hostname='rtp_a',
group_names=[ 'eastcoast', 'nc', 'redundantgroup', 'redundantgroup2', 'redundantgroup3', 'rtp', 'us' ]
)
print vars
@ -234,6 +214,112 @@ class TestInventory(unittest.TestCase):
assert vars == {'hammer':True,
'group_names': ['norse'],
'inventory_hostname': 'thor',
'inventory_hostname_short': 'thor'}
'inventory_hostname': 'thor'}
### Tests for yaml inventory file
def test_yaml(self):
inventory = self.yaml_inventory()
hosts = inventory.list_hosts()
print hosts
expected_hosts=['jupiter', 'saturn', 'mars', 'zeus', 'hera', 'poseidon', 'thor', 'odin', 'loki']
self.compare(hosts, expected_hosts)
def test_yaml_all(self):
inventory = self.yaml_inventory()
hosts = inventory.list_hosts('all')
expected_hosts=['jupiter', 'saturn', 'mars', 'zeus', 'hera', 'poseidon', 'thor', 'odin', 'loki']
self.compare(hosts, expected_hosts)
def test_yaml_norse(self):
inventory = self.yaml_inventory()
hosts = inventory.list_hosts("norse")
expected_hosts=['thor', 'odin', 'loki']
self.compare(hosts, expected_hosts)
def test_yaml_ungrouped(self):
inventory = self.yaml_inventory()
hosts = inventory.list_hosts("ungrouped")
expected_hosts=['jupiter', 'mars']
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']
self.compare(hosts, expected_hosts)
def test_yaml_restrict(self):
inventory = self.yaml_inventory()
restricted_hosts = ['hera', 'poseidon', 'thor']
expected_hosts=['zeus', 'hera', 'poseidon', 'thor', 'odin', 'loki']
inventory.restrict_to(restricted_hosts)
hosts = inventory.list_hosts("norse:greek")
self.compare(hosts, restricted_hosts)
inventory.lift_restriction()
hosts = inventory.list_hosts("norse:greek")
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'}
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()
vars = inventory.get_variables('thor')
vars["hammer"] = False
vars = inventory.get_variables('thor')
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')
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')
print vars
assert vars == {'ansible_ssh_port': 3000,
'inventory_hostname': 'hera',
'ntp_server': 'olympus.example.com',
'group_names': ['greek']}
def test_yaml_multiple_groups(self):
inventory = self.yaml_inventory()
vars = inventory.get_variables('odin')
assert 'group_names' in vars
assert sorted(vars['group_names']) == [ 'norse', 'ruler' ]

View File

@ -30,10 +30,13 @@ class TestCallbacks(object):
def on_start(self):
EVENTS.append('start')
def on_setup(self):
def on_setup_primary(self):
EVENTS.append([ 'primary_setup' ])
def on_skipped(self, host, item=None):
def on_setup_secondary(self):
EVENTS.append([ 'secondary_setup' ])
def on_skipped(self, host):
EVENTS.append([ 'skipped', [ host ]])
def on_import_for_host(self, host, filename):
@ -54,8 +57,8 @@ class TestCallbacks(object):
def on_unreachable(self, host, msg):
EVENTS.append([ 'unreachable', [ host, msg ]])
def on_failed(self, host, results, ignore_errors):
EVENTS.append([ 'failed', [ host, results, ignore_errors ]])
def on_failed(self, host, results):
EVENTS.append([ 'failed', [ host, results ]])
def on_ok(self, host, result):
# delete certain info from host_result to make test comparisons easier
@ -83,9 +86,12 @@ class TestCallbacks(object):
def on_unreachable(self, host, msg):
EVENTS.append([ 'failed/dark', [ host, msg ]])
def on_setup(self):
def on_setup_primary(self):
pass
def on_setup_secondary(self):
pass
def on_no_hosts(self):
pass
@ -138,6 +144,7 @@ class TestPlaybook(unittest.TestCase):
runner_callbacks = self.test_callbacks
)
result = self.playbook.run()
# print utils.bigjson(dict(events=EVENTS))
return result
def test_one(self):
@ -146,20 +153,19 @@ class TestPlaybook(unittest.TestCase):
# if different, this will output to screen
print "**ACTUAL**"
print utils.jsonify(actual, format=True)
print utils.bigjson(actual)
expected = {
"127.0.0.2": {
"changed": 9,
"failures": 0,
"ok": 11,
"ok": 12,
"skipped": 1,
"unreachable": 0
}
}
print "**EXPECTED**"
print utils.jsonify(expected, format=True)
assert utils.jsonify(expected, format=True) == utils.jsonify(actual,format=True)
print utils.bigjson(expected)
assert utils.bigjson(expected) == utils.bigjson(actual)
# make sure the template module took options from the vars section
data = file('/tmp/ansible_test_data_template.out').read()

View File

@ -9,7 +9,6 @@ import ansible.runner
import os
import shutil
import time
import tempfile
try:
import json
except:
@ -120,6 +119,28 @@ class TestRunner(unittest.TestCase):
])
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" ])
@ -129,7 +150,7 @@ class TestRunner(unittest.TestCase):
assert result['stdout'] == 'hi'
assert result['stderr'] == ''
result = self._run('command', [ "false" ])
result = self._run('command', [ "/bin/false" ])
assert result['rc'] == 1
assert 'failed' not in result
@ -156,6 +177,21 @@ class TestRunner(unittest.TestCase):
assert len(result['stdout']) > 100000
assert result['stderr'] == ''
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_async(self):
# test async launch and job status
# of any particular module
@ -189,33 +225,20 @@ class TestRunner(unittest.TestCase):
assert 'failed' not in result
def test_git(self):
if not get_binary("yum"):
raise SkipTest
repo = 'http://github.com/ansible/ansible.git'
dest = tempfile.mkdtemp()
result = self._run('git', ['repo=%s' % repo, 'dest=%s' % dest])
assert 'failed' not in result
result = self._run('git', [
'repo=%s' % repo,
'dest=%s' % dest,
'version=master'
])
assert 'false' not in result
try:
shutil.rmtree(dest)
except OSError, e:
print "Failed to remove temp dir %s" % dest
# TODO: tests for the git module
pass
def test_service(self):
# TODO: tests for the service module
pass
def test_assemble(self):
input = self._get_test_file('assemble.d')
metadata = self._get_test_file('metadata.json')
output = self._get_stage_file('sample.out')
result = self._run('assemble', [
"src=%s" % input,
"dest=%s" % output,
"metadata=%s" % metadata
])
assert os.path.exists(output)
out = file(output).read()
@ -228,6 +251,7 @@ class TestRunner(unittest.TestCase):
result = self._run('assemble', [
"src=%s" % input,
"dest=%s" % output,
"metadata=%s" % metadata
])
assert result['changed'] == False

View File

@ -222,7 +222,7 @@ class TestUtils(unittest.TestCase):
'who': 'world',
}
res = ansible.utils.template_from_file("test", "template-basic", vars)
res = ansible.utils.template_from_file("test", "template-basic", vars, {})
assert res == 'hello world'
@ -231,7 +231,7 @@ class TestUtils(unittest.TestCase):
'who': 'world',
}
res = ansible.utils.template_from_file("test", "template-whitespace", vars)
res = ansible.utils.template_from_file("test", "template-whitespace", vars, {})
assert res == 'hello world\n'
@ -240,7 +240,7 @@ class TestUtils(unittest.TestCase):
'who': u'wórld',
}
res = ansible.utils.template_from_file("test", "template-basic", vars)
res = ansible.utils.template_from_file("test", "template-basic", vars, {})
assert res == u'hello wórld'

3
test/metadata.json Normal file
View File

@ -0,0 +1,3 @@
{
"answer" : "Where will we find a duck and a hose at this hour?"
}

View File

@ -18,10 +18,10 @@
tasks:
- name: test basic success command
action: command true
action: command /bin/true
- name: test basic success command 2
action: command true
action: command /bin/true
- name: test basic shell, plus two ways to dereference a variable
action: shell echo $HOME $port {{ port }}

View File

@ -1,13 +1,10 @@
jupiter
saturn
thrudgelmir[:5]
[greek]
zeus
hera:3000
poseidon
cerberus[001:003]
cottus[99:100]
[norse]
thor

39
test/yaml_hosts Normal file
View File

@ -0,0 +1,39 @@
---
- jupiter
- host: saturn
vars:
moon: titan
moon2: enceladus
- host: mars
- host: zeus
vars:
- ansible_ssh_port: 3001
- group: greek
hosts:
- zeus
- hera
- poseidon
vars:
- ansible_ssh_port: 3000
- ntp_server: olympus.example.com
- group: norse
hosts:
- host: thor
vars:
- hammer: True
- odin
- loki
- group: ruler
hosts:
- zeus
- odin
- group: multiple
hosts:
- saturn