Compare commits

..

423 Commits
0.01 ... 0.3

Author SHA1 Message Date
4d62510997 Version bump for 0.3 release 2012-04-23 21:11:43 -04:00
a8707e48e8 Fix merge issue 2012-04-23 21:06:47 -04:00
c00699d0ef Merge branch 'integration'
Conflicts:
	lib/ansible/playbook.py
	lib/ansible/runner.py
	library/apt
2012-04-23 21:05:06 -04:00
321ed53e3a Fetch module doesn't set invocation parameter as it invokes nothing, so don't let that be an error. 2012-04-23 21:02:39 -04:00
39f912899f Merge pull request #212 from sideswipewvu/debmake
Debmake
2012-04-23 17:10:13 -07:00
21269a845f debian packaging 2012-04-23 19:17:01 -04:00
ec56b30248 Merge pull request #198 from dne/apt-default-release
Implemented optional "default-release" argument for apt module
2012-04-22 16:42:01 -07:00
d5f3760acc Add optional "default-release" argument for apt module
The value is passed to apt-get's "-t" option. Useful for installing backports, e.g.:

  ansible webservers -m apt -a "pkg=nginx state=latest default-release=squeeze-backports"
2012-04-23 00:17:07 +02:00
1c5bcb606e Merge pull request #197 from sfermigier/master
Add missing deps in setup.py
2012-04-22 07:42:17 -07:00
f7469f8704 Add requirements in setup.py. 2012-04-22 16:33:19 +02:00
9dc1b6d79d Merge pull request #192 from sfromm/selinux
Update secontext behavior in file module
2012-04-22 05:09:38 -07:00
bcfa6a7865 Add another example to file_secontext.yml
Demonstrate what happens when there is no default context in the policy.
2012-04-22 00:14:40 -07:00
0f044e64f8 Add example playbook of file module's selinux capabilities 2012-04-21 23:30:08 -07:00
be58995278 Add context=default option to file module
This adjusts behavior of file module such that removal of se* option
does not revert the file's selinux context to the default.  In order to
go back to the default context according to the policy, you can use the
context=default option.
2012-04-21 23:27:34 -07:00
e3967af656 Merge pull request #191 from dne/apt-install-version
Apt module support for installing/removing specific version of package
2012-04-21 18:38:11 -07:00
ec4d5fa287 Add apt module support for installing/removing specific version of package
This uses standard APT syntax, e.g.:

    ansible webservers -m apt -a "pkg=nginx=1.1.19-1 state=installed"
2012-04-22 02:48:58 +02:00
533c2c6126 Make it possible to use facts from hosts in templates for other hosts.
It works like this:

{{ hostvars['127.0.0.1']['ansible_eth0']['ipv4']['address'] }}
2012-04-21 12:45:37 -04:00
767282df2a Small style fixes for indentation and spacing. 2012-04-21 12:06:54 -04:00
e36a52f06c Ignore pyflakes E261 style output (multiple spaces before inline comment) 2012-04-21 12:05:15 -04:00
edb8988e8e Unused imports 2012-04-21 12:04:21 -04:00
bed5da6086 Remove unused assignment 2012-04-21 12:03:29 -04:00
ddc0342920 Unused import 2012-04-21 12:01:37 -04:00
1e7b60b9a5 Unused import 2012-04-21 12:01:26 -04:00
9d0f2a6e9b Unused import 2012-04-21 12:01:15 -04:00
f72114c657 Move setup.py stuff back to top level as "make sdist" was not working, plus, it makes sources cleaner. 2012-04-21 11:56:06 -04:00
a8d748220b Don't prompt for password info if the pattern wouldn't have matched any hosts. Also convert
Runner() usage to the new inventory model.
2012-04-21 11:46:32 -04:00
3081bb93f1 Use /var/tmp for root by default to avoid /tmp being mounted noexec, and segregate tmp files for other users
into their home directories.
2012-04-21 11:38:39 -04:00
3d72260887 Make it such that modules with no arguments work fine in playbooks (like ping, which is
non-sensical, but also if the user wrote a module that took none)
2012-04-21 11:26:48 -04:00
c0c691089d Fix bug in src. Should not code this early :) 2012-04-20 08:09:43 -04:00
7794493952 Don't try to expand path for None values 2012-04-20 08:02:12 -04:00
445e48b991 Call os.path.expanduser in modules so things work as expected even when using ./hacking/test-module script 2012-04-20 07:57:39 -04:00
c6b8e1621d A better fix for slurp, expand path in the module. 2012-04-20 07:54:38 -04:00
13ba31231e Fixup slurp module usage when not running as root, fix error handling path in slurp module. 2012-04-19 11:38:44 -04:00
70734f5968 Update bin/ansible to fix usage of inventory API + no more verbose option 2012-04-19 09:21:21 -04:00
8a433ecb96 Merge branch 'align-vars-syntax' of https://github.com/jhoekx/ansible into jhoekx-align-vars-syntax 2012-04-19 09:15:48 -04:00
d4a5c4aee1 Merge pull request #181 from leucos/integration
Adds PKGBUILD file for Arch linux
2012-04-19 15:57:22 -07:00
4f0ed20295 Merge pull request #182 from tbielawa/moveManifest
Move manifest
2012-04-19 15:56:46 -07:00
3a0c5c0b7b Merge pull request #183 from dne/apt-fixes
Apt module spelling fixes
2012-04-19 15:23:22 -07:00
626e8f3543 Fix two misspellings of the apt module's "fail_json" function 2012-04-19 23:40:44 +02:00
516df5f64f Move packaging related things out of the root directory:
Distutils MANIFEST.in, setup.py -> packaging/distutils/ directory.
2012-04-19 13:50:09 -04:00
3fae2ea8a4 Fixes Arch PKGBUILD dependencies
Adds missing depends (python2-yaml helps...)
Adds missing build deps (fakeroot)
2012-04-19 19:26:41 +02:00
a672a5549a Adds ArchLinux build file
Adds PKGBUILD file required to build Arch Linux packages
2012-04-19 15:30:26 +02:00
86878be288 Backport typo fix from integration branch for apt module error path. 2012-04-19 09:14:43 -04:00
4578b17213 Add missing file (slurp) used by templates in non-playbook mode. 2012-04-19 09:03:40 -04:00
cdb8213dcc Supported 'listed' vars in playbooks. 2012-04-19 09:40:17 +02:00
903e4f6eae Support dicts in inventory vars. 2012-04-19 09:40:17 +02:00
51dfb6f88c Add PyYAML requirement 2012-04-19 00:14:37 -04:00
b70ed206c6 Add debian build notes... makefile target pending, some changes to instructions possibly
needed due to moving into packaging/debian?
2012-04-18 23:26:08 -04:00
645e7afaa6 Make note that the integration branch is the development branch (will rename later) 2012-04-18 23:21:55 -04:00
5bf32f9c90 Add gentoo packaging pointer 2012-04-18 22:51:54 -04:00
8539f4926a Move debian under packaging subdirectory 2012-04-18 22:50:09 -04:00
9cd492befe make all templating happen locally, so no jinja2 deps are ever required 2012-04-18 22:43:17 -04:00
30d06dbcea Don't force down ansible facts back to setup, the setup module won't like parsing them on input and that
data is already there.
2012-04-18 22:23:33 -04:00
5fa3d9b148 Teach playbooks to template locally to eliminate the need for Jinja2 on remote nodes.
You still need jinja2 if using /usr/bin/ansible vs /usr/bin/ansible-playbook though
this could change later by fetching the ansible file with a 'slurp' module.
2012-04-18 22:19:25 -04:00
9c64ceb0f8 manpages and control file packaging changes 2012-04-18 21:29:08 -04:00
b501bbfb69 format change to control file 2012-04-18 21:29:08 -04:00
49ca5ba9f2 modified debian files 2012-04-18 21:29:08 -04:00
4f6be6e35b Initial debian packaging 2012-04-18 21:29:08 -04:00
f4132a0162 Merge pull request #160 from sfromm/facts
Facts
2012-04-18 18:28:48 -07:00
da0209dbc4 The fetch module really should preserve the whole directory structure being fetched to allow subsequent calls,
particularly in playbook, to recreate the host tree structure.  Making it thus.
2012-04-18 21:12:48 -04:00
2659c38720 Merge pull request #173 from tbielawa/manifest
Manifest
2012-04-18 13:55:03 -07:00
2beb6592c7 Update spec file location in MANIFEST.in 2012-04-18 16:41:11 -04:00
d79e59787a Merge pull request #166 from tbielawa/integration
Integration
2012-04-18 08:17:23 -07:00
66f294d5c1 Finish up moving spec file 2012-04-18 11:00:59 -04:00
f408060275 Merge pull request #163 from jhoekx/template-copy-args
Template the source file of the copy module.
2012-04-18 07:55:42 -07:00
0a70106b0f Move ansible.spec into subdir. Closes #162 2012-04-18 10:48:37 -04:00
22ff8282a8 Template template module source. 2012-04-18 14:26:33 +02:00
b678cf783c Template the source file of the copy module. 2012-04-18 11:40:15 +02:00
8da67ea4f5 Merge pull request #161 from sfromm/selinux
Rename serange to selevel to be consistent with selinux docs
2012-04-17 17:27:13 -07:00
28895f6615 Rename serange to selevel to be consistent with selinux docs 2012-04-17 17:12:09 -07:00
0c38bb2fd0 Add native facts to library/setup
This collects various facts from the host so that it isn't necessary to
have facter or ohai installed.  It gets various platform/distribution
facts, information about the type of hardware, whether a virtual
environment and what type, assorted interface facts, and ssh host public
keys.  Most facts are flat.  The two exceptions are 'processor' and all
interface facts.  Interface facts are presented as:
    ansible_lo : {
        "macaddress": "00:00:00:00:00:00",
        "ipv4": { "address": "127.0.0.1", "netmask": "255.0.0.0" },
        "ipv6": [
                    { "address": "::1", "prefix": "128", "scope": "host" }
                ]
    }
2012-04-17 16:59:23 -07:00
de70277173 No need to save setup files to disk, this is what SETUP_CACHE effectively does. 2012-04-17 19:39:11 -04:00
d7ad85340b Merge pull request #157 from tbielawa/packaging
Packaging
2012-04-17 08:41:35 -07:00
621dc83ac2 Fix man page option description indentation. 2012-04-17 11:39:51 -04:00
401ce5eb0d Merge pull request #156 from tbielawa/packaging
Packaging fixes
2012-04-17 08:08:54 -07:00
ff1b200644 Merge pull request #155 from jhoekx/inventory-error-fixes
Inventory: AnsibleError is not global...
2012-04-17 08:08:00 -07:00
e9e8417735 Update hacking with MANPATH. Correct hacking README details. 2012-04-17 10:53:32 -04:00
3f9a41b22d Docs build using version in the VERSION file.
Makefile upgraded with comments and some simplifications.

Remove useless 'PHONEY' and 'VPATH' directives from Makefile
2012-04-17 10:45:17 -04:00
d43cf592eb Renaming asciidoc sources to .in.
Change manpages to build from *.in and add comments about how the make
targets work.
2012-04-17 10:31:40 -04:00
b4ca288a7b Set %config(noreplace) on /etc/ansible instead of /etc/ansible/* so
the directory is removed on uninstall and its contents are considered
config files.
2012-04-17 10:31:40 -04:00
2d26d1fd71 Update %description and auto-add version to manpages
Conflicts:

	docs/man/man1/ansible-playbook.1
	docs/man/man1/ansible.1

Update %description
2012-04-17 10:31:34 -04:00
9316a504c5 Fixup specfile 2012-04-17 10:30:44 -04:00
c9ab5ebc6b Fix FSF address in virt header. 2012-04-17 10:30:44 -04:00
38128c4720 Remove asciidoc builddep 2012-04-17 10:30:44 -04:00
aa555b8b16 Inventory: AnsibleError is not global... 2012-04-17 15:29:59 +02:00
ff99a22e6c Merge pull request #154 from jhoekx/test-otheros
Fix tests on other distributions
2012-04-17 06:15:55 -07:00
6f09b41eb5 Don't test modules that aren't present.
Also account for path difference in sleep in different distributions.
2012-04-17 13:30:30 +02:00
9e0b7ee629 template: expand path if metadata is in user home. 2012-04-17 11:16:53 +02:00
da6cb1ca6e Less scrolling over constructor params now that documentation follows 2012-04-16 23:51:03 -04:00
3f26a1c7f6 verbose option is not being used, so remove it. debug variable still exists. 2012-04-16 23:47:41 -04:00
78b5cd64d0 Add pydoc for constructors. Some arguments can be trimmed as we plan to remove the need for them (like setup_cache and
maybe module_vars) with various pending refactoring.
2012-04-16 23:45:15 -04:00
377488aa2a remove --extra-vars since the new YAML inventory stuff wasn't quite using it as designed and it is getting hard to follow
with that and SETUP_CACHE etc
2012-04-16 23:06:27 -04:00
9ce27be878 Remove extra_vars tests 2012-04-16 23:03:04 -04:00
b24ec71ca3 Update docs to reflect --port going away and --sudo and --remote-user being added to ansible-playbook 2012-04-16 22:22:37 -04:00
35fdf6636b Allow --user for playbooks, no need for port setting as can specify in inventory file now. 2012-04-16 22:15:55 -04:00
8a027415d0 Port shouldn't be in this example 2012-04-16 22:04:23 -04:00
08468dcb0c Fixes to make ports DWIM. 2012-04-16 21:52:15 -04:00
957867e088 Merge branch 'yaml-inventory' of https://github.com/jhoekx/ansible into jhoekx-yaml-inventory
Conflicts:
	lib/ansible/runner.py
2012-04-16 21:14:44 -04:00
a5cb16c9d7 Use correct user to determine host files path, do not reuse .ansible
which is already taken for ansible management ops.
2012-04-16 21:12:54 -04:00
6307267cf3 As part of the support for access to external nodes information, save fact data into /var/lib/ansible/setup_data
OR a per-user directory when running from playbooks.  Technically this info is also available via the SETUP_CACHE
but that is a bit more complex of a construct and it would be better to not cross the streams.
2012-04-16 21:12:54 -04:00
8c3206c99f Return a copy of the host variables. 2012-04-16 10:59:34 +02:00
961ccdb2f4 List hosts in no group in the ungrouped group. 2012-04-16 10:55:08 +02:00
3a24aa9a70 Add YAML inventory format.
See test/yaml_hosts for an example.

Hosts can be part of multiple groups.

Groups can also have variables, inherited by the hosts.
There is no variable scope, last variable seen wins.
2012-04-16 10:38:35 +02:00
54f4526160 Export SSH port number as host variable. 2012-04-16 09:16:29 +02:00
f04041b37d Ignore port numbers in simple inventory format 2012-04-16 09:16:29 +02:00
746f1b92ae Reimplement the class method on Runner. 2012-04-16 09:16:29 +02:00
195e6d617b Add tests for Inventory class. 2012-04-16 09:16:29 +02:00
c5cae87eca Refactor inventory code out of Runner.
This introduces the Inventory class.

Playbook uses the internals of Runner to limit the number of hosts to poll
asynchronously. To accomodate this, Inventory can be restricted to specific
hosts.
2012-04-16 09:16:28 +02:00
80a7164057 Merge pull request #145 from rlewczuk/master
merge of: removing executable bits from py files, adding Eclipse/PyDev to .gitignore
2012-04-15 05:08:30 -07:00
d8f9d7c6c9 Use correct user to determine host files path, do not reuse .ansible
which is already taken for ansible management ops.
2012-04-14 16:08:04 -04:00
a87c77958c clean exec bits from lib/ansible/*.py, ignore Eclipse/PyDev files 2012-04-14 21:35:51 +02:00
3454fa9950 As part of the support for access to external nodes information, save fact data into /var/lib/ansible/setup_data
OR a per-user directory when running from playbooks.  Technically this info is also available via the SETUP_CACHE
but that is a bit more complex of a construct and it would be better to not cross the streams.
2012-04-14 11:58:08 -04:00
31d4ee32d1 Looping! With items! See examples/playbook/loop_with_items.yml for details 2012-04-14 09:55:24 -04:00
579bd1d417 manpage updates 2012-04-13 19:40:40 -04:00
347637339c Merge remote branch 'public/integration' into integration 2012-04-13 19:33:57 -04:00
c3cad50075 Update manpages, fix missing variable assignment 2012-04-13 19:33:19 -04:00
f2465e0571 Add support for specifying sudo passwords to both ansible & playbook. Nopasswd sudo is no longer required. 2012-04-13 19:06:11 -04:00
aaecf70586 Merge pull request #143 from jhoekx/fix-ansible-async
/bin/ansible async fixes
2012-04-13 07:18:23 -07:00
d366885367 Stop async /bin/ansible if completed on all hosts. 2012-04-13 15:32:41 +02:00
a975852f66 Module_arguments can be unicode. 2012-04-13 15:32:33 +02:00
b475294564 Merge pull request #142 from makuk66/integration
comment typos
2012-04-13 05:44:38 -07:00
f936616f91 comment typos 2012-04-13 11:19:36 +01:00
788d2bf8d1 Allow playbook to take --connection=local for easier reuse of playbook in kickstart w/o specifying connection: in
the playbook itself.  Both work.
2012-04-12 22:38:19 -04:00
6b50078881 Add defaults to command help to avoid a certain class of user questions. 2012-04-12 21:30:49 -04:00
6129574290 Include default value for connection in help. 2012-04-12 21:20:37 -04:00
09a0b9bea8 If the module return is not parsed, always show stderr information even if -D (debug)
is not specified.  This will help for users that don't have prerequisites installed
(like python-simplejson on old RHEL) and do not know to run -D.
2012-04-12 21:18:09 -04:00
a5df306aa3 Merge pull request #132 from sfromm/localconnection
Make use of LocalConnection explicit
2012-04-12 17:40:03 -07:00
ecef667134 Merge pull request #136 from sfromm/selinux
Add selinux support to file module - issue #46
2012-04-12 17:39:22 -07:00
0675f2511b Merge branch 'master' into localconnection
Merge the SortedOptParser bits and debug attribute commits into
localconnection.

Conflicts:
	bin/ansible
	lib/ansible/playbook.py
	lib/ansible/runner.py
	lib/ansible/utils.py
2012-04-12 11:18:35 -07:00
1e5d34ba35 Add selinux support to file module
This adds the options: seuser, serole, setype, and serange to the file
module.  If the python selinux module doesn't exist, this will set
HAVE_SELINUX to False and punt in the related modules.

This takes the options the user provides and applies those to the
default selinux context as provided from matchpathcon().  If there is no
default context, this uses the value from the current context.  This
implies that if you set the setype and later remove it, the file module
will rever the setype to the default if available.
2012-04-12 10:45:31 -07:00
08c593bee1 Warn if no hosts matched 2012-04-11 21:05:46 -04:00
f3489a53cd English error messages if src and dest are left off the copy, template, or fetch modules 2012-04-11 20:57:41 -04:00
ff5d329374 Yum package state defaults to installed 2012-04-11 20:47:38 -04:00
8152e44efd Also allow the apt module to use package or name as an alias for 'pkg' 2012-04-11 20:45:14 -04:00
b418632a8d Allow yum module to use package or name as an alias for 'pkg' 2012-04-11 20:44:15 -04:00
529a194950 Upgrade apt message if no python-apt to intercept potential user questions. 2012-04-11 20:31:24 -04:00
b9e3b053f9 Simplify playbook tests so things are not timing dependent in the poll section, which is leading to tests
not being consistent between runs, even though there wasn't an error.  Now we'll just check the final change
counts, which should be just as solid and lead to less churn in the events file.
2012-04-11 20:27:17 -04:00
a0480a1bc5 Block some paramiko warnings that are not relevant. 2012-04-11 20:20:55 -04:00
e103bdda93 Rename test class to match what it is testing 2012-04-11 20:16:28 -04:00
95e045d153 Remove remote logging as we're going to move this logging to the modules for performance reasons. 2012-04-11 20:15:17 -04:00
245aa9bf8e Some tweaks to the fetch module. 'err' return was for stderr, so that should be empty string.
Some minor code shortening.  Added a test to TestRunner.
2012-04-11 20:12:01 -04:00
62224271e9 Merge pull request #131 from mgwilliams/feature-fetch-module
Fetch Module
2012-04-11 16:55:51 -07:00
40fc9a3249 Update playbook to be transport aware
This adds transport variable to playbook.py.  It can be set with
'connection' in the playbook file.
2012-04-11 09:39:04 -07:00
b506567c37 Update ansible.1 manpage for option -c, --connection 2012-04-11 09:32:29 -07:00
31d3f52b28 fetch to host specific directory 2012-04-11 09:14:36 -07:00
611e3fec4c fetch 'module' -- working with paramiko and local connections 2012-04-10 20:23:25 -07:00
51e4faf7aa Update test file -- we probably should not include the poll as the number of polls is changing between test
runs (that's ok, it's not intended to be realtime accurate)
2012-04-10 21:17:02 -04:00
4c75b7f3ad Merge commit '8ae71cc' into integration
Conflicts:
	bin/ansible
	bin/ansible-playbook
	lib/ansible/utils.py
2012-04-10 21:13:01 -04:00
1d75a29ec9 Allow variables coming in from the playbook and the API to be expressed as dictionaries throughout their full life cycle
such that nested data can be made available in templates and playbooks.
2012-04-10 20:58:40 -04:00
fdee1d3459 Add support to bin/ansible for --connection option 2012-04-10 16:28:43 -07:00
b5061bb62e Verify that effective uid == remote_user when transport is local
Raise exception if effective uid of process is not the same as
remote_user.
2012-04-10 16:27:19 -07:00
5d74fedeb9 Update Runner to default to C.DEFAULT_TRANSPORT
This also uses self.transport when instantiating Connection object.
2012-04-10 16:17:39 -07:00
1391481523 Add support for -c, --connection argument to specify connection type
Adds support to specify connection type to use.  The option -c,
--connection is added when connect_opts is set to True.  connect_opts is
added to make_parser() and base_parser_options().
2012-04-10 16:13:18 -07:00
896f8de446 Add DEFAULT_TRANSPORT and DEFAULT_TRANSPORT_OPTS constants
DEFAULT_TRANSPORT is set to paramiko.  DEFAULT_TRANSPORT_OPTS is a list
of possible transport options; it is set to local and paramiko.
2012-04-10 16:09:20 -07:00
9213cf896e Change to transport is local *and* is localhost
Connection.connect() now requires that, in order to use LocalConnection,
you specify transport is local and that the hostname is localhost.
2012-04-10 13:20:03 -07:00
8ae71cc7b1 go back to using a normal optparser to add options instead of the dict
interface.

add very small subclass of OptionParser to sort the options so mdehaan is happy
2012-04-10 13:51:58 -04:00
5a4d4bc051 Added usage info to bin/ansible-playbook, now shows options on bare command line. NOTE: bin/ansible outputs usage to stdout, bin/ansible-playbook to stderr. Should they be consistent? 2012-04-10 15:17:25 +00:00
74b26da9ac began playbook tweak 2012-04-10 14:45:44 +00:00
f840c0d167 Wired in Michael's usage string optparse style. 2012-04-09 23:12:05 +00:00
6749903e57 Allow explicit request of the local connection. 2012-04-07 14:51:00 -04:00
eed4a2b717 Update tests to use 127.0.0.2 to bypass the smart "use local connection" code, and also to reflect
that commands now trigger change events.
2012-04-06 12:11:25 -04:00
da9d4eb29a Merge pull request #123 from sfromm/localconnection
Add LocalConnection class to connection.py
2012-04-07 11:41:13 -07:00
70a3fab79f Add LocalConnection class to connection.py
This creates a LocalConnection class for the case when operating on the
localhost.  If the host, argument to Connection.connect(), matches
127.0.0.1, localhost, or the name of the host as returned by
os.uname()[1], Connection.connect() will opt to use LocalConnection
instead of ParamikoConnection.  LocalConnection implements connect(),
exec_command(), put_file(), and close().
2012-04-06 16:38:27 -07:00
38c7f1db4d Added 'creates=filename' to the shell/command module, which can skip command execution if a file
already exists
2012-04-06 11:16:53 -04:00
7b9856bc0e Modification on top of skvidal's common options patch to keep options to command line tools sorted. 2012-04-06 10:59:15 -04:00
8e1f24b822 Amend option descriptions. 2012-04-06 10:18:52 -04:00
3c64292de5 Merge pull request #106 from skvidal/master
3 fixes/changes
2012-04-05 14:17:59 -07:00
7e50d170a8 move the bulk of the opt parsing out of ansible/ansible-playbook and into utils
for other scripts to use.
2012-04-05 17:06:23 -04:00
254f98bdf5 handle incorrect commands w/o '=' slightly better 2012-04-05 17:03:36 -04:00
2df6399176 make dist be ?dist so if it is not there you don't get any noise
as per fedora usage guidelines
https://fedoraproject.org/wiki/Packaging:DistTag
2012-04-05 13:28:56 -04:00
594b16a78b Merge pull request #114 from jhoekx/module-vars
Introduce task local variables.
2012-04-04 17:21:46 -07:00
c1461bc185 Merge pull request #120 from skvidal/master
virt module + idempotents (state=[shutdown|running])
2012-04-04 17:11:04 -07:00
66f9a697ed edit comment to reflect real args 2012-04-04 17:25:02 -04:00
8a61794234 add idempotent and ='s separated parameters to virt module to make people happy 2012-04-04 17:17:29 -04:00
9a84e30053 Merge pull request #115 from jeckersb/dist-in-release
Update spec to include %{dist} in Release
2012-04-04 12:32:49 -07:00
415f4baa15 add virt module for libvirt interaction/support 2012-04-04 14:30:24 -04:00
4a2259bab1 Update spec to include %{dist} in Release 2012-04-04 10:38:03 -04:00
ab86726a15 Introduce per task variables and push them to templates. 2012-04-04 13:39:03 +02:00
edd5baad8b Refactor _transfer_argsfile to generic string transfer function. 2012-04-04 13:36:21 +02:00
09e690fd7c Indentation error causing forks to not be parallel. Ahem :) 2012-04-03 20:20:55 -04:00
32f574b24d Merge pull request #113 from mgwilliams/bugfixes-apt-module
apt module bugfix
2012-04-03 17:08:37 -07:00
778fe8755f Merge pull request #110 from jhoekx/remote-vars
Always add vars to a play.
2012-04-03 17:07:00 -07:00
eae1fdd734 Merge pull request #109 from jeckersb/version-0.0.2
Bump release to 0.0.2
2012-04-03 17:06:09 -07:00
3c314143c4 Merge pull request #108 from jeckersb/pep8-fix
pep8 fix
2012-04-03 17:05:48 -07:00
c819f1715e apt module bugfix 2012-04-03 12:52:08 -07:00
e38ae18627 Always add vars to a play.
A play without vars section would fail to use variables given in an include. They would be added to the dict returned by play.get, but the dict would not be added to the play.
2012-04-03 17:35:05 +02:00
82b781925c pep8 fix 2012-04-03 09:58:00 -04:00
5aad4bacc7 Bump release to 0.0.2 2012-04-03 09:51:05 -04:00
02d5e831ce have apt module raise an error if apt cannot be imported 2012-04-02 20:54:31 -04:00
2cb89c12e3 Add documentation for vars_prompt 2012-04-02 20:37:59 -04:00
88dab0db2d Add prompts example 2012-04-02 20:35:51 -04:00
f92760de6c If missing state or name or list parameter on yum module, raise error explaining what is up. 2012-04-02 20:12:35 -04:00
2a2b5ed73c Polling timing change results in need to change events file for tests. 2012-04-02 20:10:06 -04:00
4792021f47 Fix syntax error in user patch 2012-04-02 20:08:40 -04:00
e5d5b072db Merge remote branch 'public/integration' 2012-04-02 20:02:46 -04:00
6472105496 Merge pull request #105 from skvidal/master
log a bit more and var_prompts
2012-04-02 17:00:36 -07:00
c2e2a47899 Merge pull request #100 from mgwilliams/file-module-bugs
file module: mode formatting & directory ownership
2012-04-02 16:59:55 -07:00
105a43a316 Merge pull request #104 from mgwilliams/feature-apt
improvements to apt module
2012-04-02 16:59:28 -07:00
17a3ce1278 Merge pull request #102 from mgwilliams/file-symlinks
support for 'link' state in file module
2012-04-02 16:58:58 -07:00
59a1ff31db Merge pull request #103 from jeckersb/no-shebang-on-callbacks.py
Remove shebang from callbacks.py
2012-04-02 16:58:41 -07:00
2f1f0d44b4 Merge pull request #106 from mgwilliams/honor-hostfile-returncode
raise error if executable host file execution fails
2012-04-02 16:58:20 -07:00
1c5af046c7 Merge pull request #105 from mgwilliams/bug-extra-vars
bugfix for extra-vars
2012-04-02 16:57:44 -07:00
461a4e78c2 raise error if executable host file execution fails 2012-04-02 15:00:28 -07:00
c742b8eb0b bugfix for extra-vars 2012-04-02 14:57:37 -07:00
bcef25f7eb add vars_prompt to playbooks
- this allows some vars to be prompted for at the start of the playbook
  setup
- defaults to no output since this would mostly be used for passwords
2012-04-02 17:46:02 -04:00
9fdd7a837e improvements to apt module: state=latest, update-cache=yes|no, purge=yes|no 2012-04-02 14:42:54 -07:00
2dad8cc27c Remove shebang from callbacks.py
It is not +x, and has no __main__.  It draws ire of rpmlint.
2012-04-02 17:19:05 -04:00
04aecdcf34 before we run the actual module/command - emit what that would be to the logs 2012-04-02 13:29:12 -04:00
ae38ee0b3f protecting against replacing file or directory with link 2012-04-02 10:21:14 -07:00
25eedcdb43 Merge pull request #104 from skvidal/master
yum module fix - catch more errors
2012-04-02 09:52:37 -07:00
08163c6251 catch all exceptions and emit sensible errors - if we have a config file error this lets us know about it 2012-04-02 12:33:46 -04:00
c5a02048d2 file module: mode formatting & directory ownership 2012-04-01 16:44:08 -07:00
24f61f1583 "link" state in file module 2012-04-01 16:10:23 -07:00
5764ccdb0e Eliminate possible race condition in async_wrapper where ansible deletes the argsfile while the module
is still kicking off.  Should not happen except in modules that are somewhat slow to load and probably
can be fixed better than the included sleep, i.e. some IPC communication that the process has
launched and is ok to exit.  This works pretty well for now though.
2012-04-01 02:35:19 -04:00
2511992659 Surface module debug (-D) to /usr/bin/ansible also 2012-03-31 11:45:29 -04:00
d7e5b390d5 Patch process info. 2012-03-31 11:38:06 -04:00
96a22b582b Disable ohai test as it's hanging for whatever reason, but doesn't do this in real life.
Run tests in verbose mode.
2012-03-31 11:18:26 -04:00
4a8dc50249 Merge pull request #97 from jhoekx/stderr-logging
Stderr logging
2012-03-31 08:01:48 -07:00
b4a07fea3b Merge pull request #98 from jhoekx/no-touch-file
Make copy module idempotent.
2012-03-31 05:21:20 -07:00
38a33c54a6 Make copy module idempotent.
Check md5sum before overwriting a file. Unconditionally copying changes the timestamp.
2012-03-31 13:59:41 +02:00
ba7d0b8cec Merge pull request #96 from jhoekx/playbook-defaults
Correct default user and port in playbook.
2012-03-31 04:53:53 -07:00
a6b8973938 Change debug flag to D instead of d.
d would be useful for --dry-run.
2012-03-31 12:48:19 +02:00
d9a16a3d67 Add --debug option to ansible-playbook.
This prints remote module stderr on local stderr.
2012-03-31 09:32:13 +02:00
74ae4b29ad Add a debug attribute to Runner/Playbook.
This prints the stderr of the executed modules on local stderr.
Most methods on Runner now return a fourth "err" parameter.
2012-03-31 09:32:13 +02:00
fb1a313974 Correct default user and port in playbook.
They are set in the constructor.
2012-03-31 09:11:07 +02:00
6275e57718 pyflakes: Unused import / unused var 2012-03-30 23:04:02 -04:00
d86dad76ba Be more flexible about where the service binary lives for better cross platform support. 2012-03-30 22:57:26 -04:00
9569be8bdb Need for quoting/unquoting problems go away once module_args are all treated as strings throughout. 2012-03-30 22:52:38 -04:00
3ded27fe35 Treat module args as strings everywhere to avoid unneccessary shlex and requoting 2012-03-30 22:47:58 -04:00
f11de2f5c9 --extra-vars option for ansible-playbook
Conflicts:

	lib/ansible/playbook.py

Removed unneccessary shlex and replaced with basic split, some repurcussions in runner
that can be eliminated once we consistently pass args as a string (soon).
2012-03-30 22:29:06 -04:00
6db87a5018 Minor style fix, update test to match quote fix patch 2012-03-30 21:57:34 -04:00
ec6f488d1f shell quoting fixes
(edited author's original commit comment -- MPD)
2012-03-30 21:57:33 -04:00
99d5796605 support for quotes strings in command module (via posix=False), related fixes to keep other things from breaking 2012-03-30 21:57:33 -04:00
f82cc59d43 Merge pull request #92 from jhoekx/fix-template-exception
Fix except statement in template module.
2012-03-30 04:50:14 -07:00
12d01a3444 Fix except statement in template module.
Python 2.4 (RHEL 5) does not know 'as'.
2012-03-30 11:46:30 +02:00
2372a3b734 Sudo support operational in both playbooks and main program. Implementation could use some cleanup. 2012-03-29 22:59:29 -04:00
81e3496037 Added preliminary support for --sudo to ansible, playbook support and further testing pending. 2012-03-29 01:37:06 -04:00
011a0ecbdf Merge pull request #90 from tbielawa/except
Fix exception catching for py2.4
2012-03-29 11:20:22 -07:00
5261d14aef Fix exception catching for py2.4 2012-03-29 14:19:09 -04:00
72cc99722d sudo tweaks 2012-03-28 23:30:31 -04:00
7ae75eb14b Default sudo is false 2012-03-28 22:55:59 -04:00
c2a6e2f97b Work in progress on sudo, hitting some issues with paramiko saying the connection is closed. 2012-03-28 22:51:16 -04:00
7133734d87 Remove debug statements (WIP on sudo) 2012-03-28 20:58:34 -04:00
4971101f27 Handler doesn't take this param. 2012-03-28 20:33:44 -04:00
706ba9fd9a Add paramiko/pycrypto atfork workaround (can back out later if not useful), tweak error
message text per skvidal's suggestion
2012-03-28 20:32:04 -04:00
b30ddc4520 Expose remote_port option in playbook 2012-03-28 19:31:17 -04:00
a05b75dbbb Merge pull request #101 from sfromm/user-module
Supplementary group membership now in users module
2012-03-28 16:24:26 -07:00
13df76d3d5 Merge pull request #100 from sfromm/group-module
Remove support for membership from group module
2012-03-28 16:24:15 -07:00
debd6fee68 Merge pull request #99 from chjohnst/master
Remote port and error check for old paramiko
2012-03-28 16:24:02 -07:00
509c330f53 add support for running and started - both will do the same thing 2012-03-28 16:06:26 -07:00
68818ad1fe add support for remote ports in playbooks 2012-03-28 15:02:50 -07:00
c1e223c424 Remove errant option on password modification action 2012-03-28 14:50:26 -07:00
e851f7542f Update group_commands playbook example
Remove the group membership example.  There's not much to twiddle with
the group module.  Give examples for what is possible.
2012-03-28 14:32:56 -07:00
a379d58cdb Drop support of group membership in group module
Use the user module to define group membership.
2012-03-28 14:29:23 -07:00
4ed971574f Update user_commands playbook with supplementary group examples 2012-03-28 14:24:13 -07:00
9041ad4d83 just some minor clean up single vs double in getopt parsing 2012-03-28 14:17:20 -07:00
416eb36b9a Update user module to support supplementary group membership
This adds two options to the user module: groups and append.  groups is
a comma-delimited list of supplementary groups a user should belong to.
If a user is currently a member of a group not listed in groups, the
user will be removed from it.  To change this behavior, use append=yes.
This will append the user to the list of supplementary groups and *not*
remove the user from unlisted groups.
2012-03-28 14:12:35 -07:00
15e781eb51 add exception check for paramiko if older then 1.7 2012-03-28 14:09:11 -07:00
f06ec76fdb add support for using an alternate remote port 2012-03-28 14:05:31 -07:00
9777653741 Merge remote branch 'origin/master' 2012-03-26 23:05:05 -04:00
5c95f6818e Merge pull request #96 from skvidal/master
fix up yum module for rhel5.
2012-03-27 11:24:30 -07:00
fc02992bcd make sure latest outputs an error if the thing you've listed as 'latest'
is not there at all or even updateable
2012-03-27 14:15:48 -04:00
2fe193034c fix up latest so it behaves like latest should
if installed and updated available: apply update
if not installed and available: install
2012-03-27 13:58:49 -04:00
f9043bf156 make yum module work with older yum for rhel5 :( 2012-03-27 12:06:21 -04:00
b373c6c223 Add playbook to demonstrate use of group module 2012-03-26 23:04:30 -04:00
db677954ac Add group module to manage groups and group membership
This relies on groupadd, groupmod, groupdel, and gpasswd utilities on
the system.  You can optionally modify the gid for the group.  You can
also add/remove a user to/from a group with the option member.  Member
state is defined with the option memberstate.
2012-03-26 23:04:19 -04:00
8592b3b40d Update user module to support group name
This changes the gid option to group.  One may provide a primary group
as either a gid or a name.  The module will then check to verify that
the group already exists.  If the group does not already exist, the
module will fail.
2012-03-26 23:03:09 -04:00
37f599efc3 make sure latest outputs an error if the thing you've listed as 'latest'
is not there at all or even updateable
2012-03-26 23:02:09 -04:00
1d04ec89b7 fix up latest so it behaves like latest should
if installed and updated available: apply update
if not installed and available: install
2012-03-26 23:02:09 -04:00
a27be2651c make yum module work with older yum for rhel5 :( 2012-03-26 23:02:09 -04:00
85a0709d8f Merge pull request #78 from mgwilliams/master
Catch jinja errors in template module
2012-03-27 17:00:49 -07:00
3046f743d9 removed hack 2012-03-27 11:12:33 -07:00
bf0f3eac1f catch jinja template errors in template module 2012-03-27 11:06:31 -07:00
9d9e3d8c56 resetting template.. previous commit not needed 2012-03-27 09:10:51 -07:00
d9a464ee9d added format_advanced jinja filter to template module 2012-03-27 08:51:37 -07:00
3f9f8de031 Update packaging WRT AUTHORS.md 2012-03-26 22:30:48 -04:00
61446fd6c8 Github maintains a nice contributors list, so remove this here as it's out of date. 2012-03-26 22:29:35 -04:00
01e51b120a Recieved quite a few requests to run python via /usr/bin/env, so doing that. 2012-03-26 22:07:04 -04:00
b1471bf857 Add more comments to the callbacks file 2012-03-26 22:05:11 -04:00
95670ce6b5 remove unused function 2012-03-26 21:58:28 -04:00
f9da7cb180 remove functions that are no longer used 2012-03-26 21:56:18 -04:00
35d77d0433 Make it an error to try to hit a change handler that doesn't exist 2012-03-26 21:31:48 -04:00
f693759252 Style fixes from pep8 makefile target 2012-03-26 21:25:43 -04:00
f074f1c4c4 Fixes from pyflakes 2012-03-26 21:23:28 -04:00
fc3b1cf973 Merge remote branch 'public/master' 2012-03-26 21:19:17 -04:00
35c8750bbb Fix tree view to work with callbacks 2012-03-26 21:18:48 -04:00
45a455a805 Make /usr/bin/ansible output realtime and also delete some code! 2012-03-26 21:17:11 -04:00
267faf7c90 Merge pull request #75 from mgwilliams/master
apt module
2012-03-26 16:36:55 -07:00
eb67a91c57 Merge remote branch 'public/master' 2012-03-26 19:36:03 -04:00
aaafd12b59 rm FIXMEs 2012-03-26 19:35:53 -04:00
e2deb94e73 trimmed superfluous code from apt module 2012-03-26 13:53:22 -07:00
e85355f054 cleaned up apt module style 2012-03-26 13:48:02 -07:00
90ba14d60e preliminary apt module 2012-03-26 12:49:13 -07:00
3b950086c7 Merge pull request #73 from jhoekx/include-arguments
Playbook: create one task per include instead of per argument.
2012-03-26 11:31:52 -07:00
a370261dce Playbook: create one task per include instead of per argument. 2012-03-26 10:30:48 +02:00
77a6315939 Merge remote branch 'public/master' 2012-03-25 19:06:41 -04:00
6dda6f12dc Applying callback model to runner, and using that in playbooks, so output can be more immediate in playbooks.
(Runner still does not use callbacks for default output)
2012-03-25 19:05:27 -04:00
50ce7e0ee1 Merge pull request #70 from tbielawa/manupdate
Show when options require arguments in the man pages.
2012-03-24 13:41:29 -07:00
3052d85a6a use capital O. 2012-03-24 16:32:16 -04:00
ab55fa4266 Revert "Only override the pattern if it is ommitted"
This reverts commit bb0bf9eef4.
2012-03-24 16:21:44 -04:00
bb0bf9eef4 Only override the pattern if it is ommitted 2012-03-24 16:20:47 -04:00
9df612f007 Add a "-o" override option so hosts not in a playbook can still be managed by a playbook. 2012-03-24 16:19:38 -04:00
446ba1795a Show when options require arguments in the man pages. 2012-03-23 22:29:51 -04:00
b213437bfa Added 'shell' to modules with a note that it doesn't really exist :) 2012-03-23 21:39:39 -04:00
292ac4aad2 Make it such that the 'name' element of each playbook line is optional. 2012-03-23 21:06:54 -04:00
44d4dede92 Split conditional imports in playbook into subfunction, fix small bug in event reporting on playbook
actions.
2012-03-23 21:03:25 -04:00
b43019f3a1 In playbooks, a return code (rc) of non-zero should fail the playbook. 2012-03-23 20:53:57 -04:00
6ab615c724 Code cleanup for playbooks, also added 'on_skipped' callback 2012-03-23 20:51:26 -04:00
24964b9660 Merge pull request #95 from skvidal/master
buglet - make sure sshpass is included in runner for setup in playbooks
2012-03-23 13:45:12 -07:00
53446d6d45 make sure the setup step in a playbook has the sshpass included 2012-03-23 16:41:44 -04:00
86590ea614 Merge pull request #94 from skvidal/master
minor typo of AnsibleException/AnsibleError
2012-03-23 09:06:17 -07:00
89d4bc542c fix up minor type - AnsibleException should be AnsibleError 2012-03-23 11:59:08 -04:00
1ea1994eca update authors file 2012-03-22 00:34:21 -04:00
3814a46bbf Move playbook for user commands into examples dir 2012-03-22 00:33:10 -04:00
6aa10691c1 Merge pull request #93 from sfromm/user-module
User module
2012-03-22 19:24:53 -07:00
999bead9ed Add playbook to test user module 2012-03-22 11:36:11 -07:00
f4835477b0 Add user module to create, modify, and delete user accounts
This relies on useradd, usermod, and userdel utilities on the system.
The argument name is required; if state is not provided, present is
assumed.  Other options supported for creating or modifying an existing
account:  uid, gid, comment, home, shell, and password.  If managing the
password, it must already be encrypted.  When creating an account, you
can also provide the argument createhome to control whether the home
directory is created.  Arguments supported for deleting an account are:
force (remove account even if user is logged in) and remove (remove home
directory).
2012-03-22 11:21:41 -07:00
a8c921cbcc Allow groups to be passed to runner API to make parse_hosts optional, misc fixes as a result of refactoring in Runner.
Cleanup in Playbooks module is next.
2012-03-22 00:30:05 -04:00
94605b811b Fixes from refactoring 2012-03-21 23:47:58 -04:00
6a7aac38c5 misc cleanup in the runner module, splitting some things out into utils, breaking up functions into smaller functions. 2012-03-21 23:39:09 -04:00
fef73393f0 rename example files 2012-03-21 22:21:42 -04:00
7ed734dfb2 move callbacks to seperate file, anticipating callbacks for Runner() as well 2012-03-21 22:18:57 -04:00
7883f414ce unused module 2012-03-21 21:03:32 -04:00
d75e69853a No need to comment this part of the example out. 2012-03-21 20:22:44 -04:00
e6406fa5a7 Allow variable expressions to be stored as variables themselves, do some things to allow setup strings
to more easily contain spaces without being mangled, which is neccessary because of the above.
2012-03-21 20:00:48 -04:00
e89131a3a2 playbooks -- Ratchet up logging prior to gating things with verbosity controls and upgrading output format 2012-03-20 22:37:51 -04:00
a5f4ca50b8 Ratchet up logging a few notches prior to controlling w/ verbosity settings 2012-03-20 22:29:21 -04:00
b5c62ec068 Enhance logging, way to gate verbosity levels pending. 2012-03-20 21:44:01 -04:00
05e27a419c HTML doesn't work like that on that page :) 2012-03-20 21:07:33 -04:00
d2141feada Add subscribe link to markdown index 2012-03-20 21:06:49 -04:00
54b98effdf Add demonstration of only_if capability 2012-03-20 19:55:24 -04:00
dfbe591cc0 Add "only_if" capability, which allows task steps to be skipped if they do not match a conditional. 2012-03-20 19:55:04 -04:00
149cc57b0f remove unneeded code 2012-03-19 23:54:47 -04:00
c05e7fd83e Fix typing issue 2012-03-19 22:46:54 -04:00
64471db94a Remove test from bottom of example that doesn't really make sense in the context of the example's use case. 2012-03-19 22:44:11 -04:00
4de7bbb169 Allow conditional imports, see examples/playbook3.yml comments for a full explanation. Extensive
refactoring of playbooks now warranted, which we'll do before we move on.   This variable assignment
system makes nearly all possible magic possible, for we can use these variables however we like,
even as module names!
2012-03-19 22:42:31 -04:00
5ed2b894d9 Add an additional way to dereference a variable in a playbook, $foo
(Using varReplace function originally from yum, thanks Seth)
2012-03-19 19:32:38 -04:00
af9596307d Move templating into a utils function. Reuse is our friend. 2012-03-19 19:23:14 -04:00
c1fe0dd719 Quote long variables in playbooks when feeding them to the setup command 2012-03-18 19:50:22 -04:00
5371a9e497 Regression tests for playbooks, logging the events they call via callbacks. 2012-03-18 19:25:56 -04:00
b15c8e9cb1 Playbook test upgrades, made some notes about callbacks we need to add. Still need to compare events
recieved vs expected so we can be notified if this test breaks, and also to add some more detailed
playbooks that use includes & async, etc.
2012-03-18 18:50:25 -04:00
45abe3c16b Add unit tests for playbooks, and fix an error caught by one 2012-03-18 18:29:11 -04:00
9c5ec886a6 Import cleanup, plus have /bin/ansible remind you if no hosts were matched by a pattern (for instance, assume
there was a typo in the pattern)
2012-03-18 17:53:58 -04:00
fde7de80df Remove "import *" from bin/ansible so we can use pyflakes, and remove unneeded imports in the process 2012-03-18 17:41:44 -04:00
faa59e5ef1 Add the failtest module, primarily for easy development testing purposes. RPMs/etc need not ship this. 2012-03-18 17:35:49 -04:00
a4b8cdf8b3 Remove "import *"'s from bin/ansible-playbook code so pyflakes will be usable. 2012-03-18 17:32:36 -04:00
9e07b24239 Fix indentation 2012-03-18 17:25:22 -04:00
6777268f4c Fixup the code that allows facter/ohai/other data to bubble up and be used in future action lines. It's a niche
case compared to usage in templates but it should work and will be wanted later for conditional support.
2012-03-18 17:24:09 -04:00
c861e0de55 Fix "import *" and resultant new things detectable from "make pyflakes" 2012-03-18 17:16:12 -04:00
33aa50eae7 Inside of runner, do not 'import *' from utils, so we can more easily tell where functions come from 2012-03-18 17:04:07 -04:00
9837a616eb Preserve changed data on file and template ops when the mode operations don't result in a change but the files
were still replaced.
2012-03-18 16:58:00 -04:00
82bd75c1e0 Update env-setup script to make it work better 2012-03-18 12:28:22 -04:00
f1c8fc63cb host file must be marked config(noreplace), update rpm build to use checked in manpage src 2012-03-31 11:08:42 -04:00
5905974737 Fix variable referenced before assignment when not using external variables script 2012-03-31 11:03:29 -04:00
3495d402c7 Default for class variable 2012-03-31 11:02:10 -04:00
b8eeb0f906 Add note about localhost/auth 2012-03-30 19:58:28 -04:00
3d2a7ebd68 Added cobbler external inventory script, which is a demo of how external inventory
works in Ansible in general.
2012-03-30 19:56:22 -04:00
9f6d98844e If the inventory file is EXECUTABLE, treat it as a file returning JSON, if called directly, return the host
and groups list.  If called with an argument of a host name, return the key=value data for that particular
host.
2012-03-30 19:06:14 -04:00
f200a467e7 Update example to use correct state name 2012-03-15 23:11:09 -04:00
f39944b89b Update example to use correct service state; minor pep8 indent/spacing fixes 2012-03-15 23:10:03 -04:00
24923c2e17 Purge unused imports per pyflakes 2012-03-15 23:06:59 -04:00
60bf82d266 Remove comment about FIXME that is already resolved 2012-03-15 23:05:34 -04:00
311b93995c Add some comments 2012-03-15 22:37:37 -04:00
6874d853c9 Fix changed=True/False detection when specifying mode= 2012-03-15 22:35:59 -04:00
63818000b4 auto call the file module after copy/template module usage in runner
note some changed=True/False detection bits in file module still need tweaking
2012-03-15 22:32:14 -04:00
4ad885111b Disable debug 2012-03-15 22:04:32 -04:00
5004d21f10 Return file info about the file regardless of changes made 2012-03-15 22:04:04 -04:00
be55145a1e Initial crack at the file module 2012-03-15 21:53:14 -04:00
8914a1a0d7 license file should be in source tree 2012-03-15 20:24:22 -04:00
9049b0e7ca Because folks on RHEL 6 (not CentOS 6) have some issues with asciidoc, do not build the asciidoc from
the rpm or make install targets.  Also modify the make install (only) to copy over files into the library
directory since the setup.py does not do this (due to a distutils bug, more or less).
2012-03-15 20:21:10 -04:00
1a0672fd1e Fix exception type 2012-03-14 12:51:26 -04:00
c909b66864 use iteritems vs items(), probably doesn't matter that much 2012-03-14 21:56:49 -04:00
a735dd2b17 Added the 'test-module' script, useful for testing modules without running them in Ansible. 2012-03-14 21:49:27 -04:00
4bde4926c3 Modules don't have to return JSON, key=value pairs is ok. 2012-03-14 21:16:15 -04:00
40fd778e2c 'shell' is a magic module that executes the command module with shell=True 2012-03-14 20:40:06 -04:00
73d20b81c6 Add basic test of yum module (VERY basic) just to make sure syntax remains ok. Need to do something
trivial for service and git module also.
2012-03-14 20:02:29 -04:00
e8751baaf3 Remove remote log function as it's only used in one place now 2012-03-14 20:00:09 -04:00
696b67f935 Fix async to use the new argfiles method (wrapping brain around rock, really must write module development guide) 2012-03-14 19:57:56 -04:00
aeea46678d Fix the copy module to use the argsfile method 2012-03-14 19:09:44 -04:00
917f929e86 Fix template module 2012-03-14 19:08:10 -04:00
6b152c94b9 Correct the setup module 2012-03-14 19:06:35 -04:00
3ea9174ed7 Split argsfile handling into subfunction, attempt to apply argsfile logic to setup 2012-03-14 19:05:19 -04:00
1f53c89b14 convert so they handle argsfiles rather than arguments 2012-03-14 18:47:13 -04:00
a9a9e3af65 modify yum to be used with argsfile and fix a number of items with
how it handles "advanced" pkgspecs for the state= cases
2012-03-14 18:46:53 -04:00
a9948f97c6 convert runner to use an args file rather than just arguments passed
on the command line
2012-03-14 18:46:33 -04:00
f681425453 Rename hosts file back since included by MANIFEST.in 2012-03-13 22:00:03 -04:00
ee69bd18cc Add -T to playbook manpage 2012-03-13 21:54:42 -04:00
db61e8f346 Fix -t tree mode so it writes out the value for each host, not everything, once again 2012-03-13 21:38:06 -04:00
e6f175e929 Allow the SSH timeout to be passed to playbooks just like it already could with /usr/bin/ansible 2012-03-13 21:35:34 -04:00
e3b4981feb Various additions to make sure timed out and failed hosts are taken out of the playbook even if failed
during setup, async tasks, or poll operations.  This introduced some redundancy so I've made some
notes in the code where things need to be refactored to remove them.
2012-03-13 21:30:34 -04:00
4ae98ed92d Upgrades to error handling, now general try/catch available. 2012-03-13 20:59:05 -04:00
2e1b59a9d2 restructure the examples directory 2012-03-13 19:27:13 -04:00
09a7119e74 Annotate more files, fix missing imports messing with playbooks. Hey Tim, please test your stuff :) 2012-03-13 19:19:54 -04:00
238fffd6ef Annotated playbook example 2012-03-13 18:55:04 -04:00
84c60c2750 Merge pull request #91 from skvidal/feature
modify build/spec file to work on el6 with quasi-broken setup process
2012-03-13 15:37:11 -07:00
8df0c92f3c Merge pull request #28 from tbielawa/DieGracefully
Die gracefully...
2012-03-13 15:35:38 -07:00
3567237dbc modify build/spec file to work on el6 with quasi-broken setup process
remove data_files section from setup.py for some reason or another
2012-03-13 11:08:10 -04:00
dfd2c6dce3 Implement friendlier error handling.
Generic AnsibleError exception + host inventory missing exception.
First shot at catching these in a generic way in bin/ansible*.
2012-03-12 23:11:54 -04:00
ce85222fa6 What we are running is actually a "play" (multiple plays per playbook) so rename the function to make it more clear 2012-03-12 22:05:51 -04:00
adf1492da2 Add external vars example 2012-03-12 22:03:20 -04:00
e582bd5ec5 Add fire and forget example 2012-03-12 21:02:38 -04:00
86e19cd8c8 This adds async poll support to playbooks. See examples. Some more testing due + docs
but this is more or less a mirror of what /bin/ansible does.  It also has a 'fire and
forget' mode if the poll interval is left off or set to 0.
2012-03-12 20:53:10 -04:00
32484f2156 Merge pull request #90 from skvidal/feature
yum module
2012-03-12 16:27:20 -07:00
0b94c78042 yum module
add state=latest and clean up the output cases
added some fixmes on verification
2012-03-12 16:31:13 -04:00
6660a5efbd add polling info to manpage 2012-03-11 21:09:44 -04:00
db7ba87111 Add polling logic in runner such that all actions get kicked off everywhere then polling
happens only on needed hosts, allowing some hosts to fail and drop out of the running.
2012-03-11 20:54:54 -04:00
49a636d8a0 Fixed up async and polling logic. 2012-03-11 19:27:43 -04:00
5be1a612d3 Add async polling logic to runner. Will add to playbook shortly, have to diagnose why paramiko
is not letting async_wrapper daemonize itself when it does work fine when directly executed.
2012-03-11 18:40:35 -04:00
60a13cf540 Ignore commented out lines in the ansible setup files. 2012-03-10 21:19:41 -05:00
e284c8cedc Merge pull request #16 from tbielawa/master
Updates for better RPM packaging (plus tasks)
2012-03-10 16:51:34 -08:00
82befee081 Change Source0 in spec file to point at github downloads 2012-03-10 16:25:46 -05:00
dce6074a81 Fixes for better RPM packaging
Spec file installs default example hosts files
Spec file marks the contents of /etc/ansible as %config(noreplace)

Example hosts file now with instructional comments
2012-03-10 16:19:33 -05:00
3f245498c4 Fixup RPM building: Makefile, Spec File, .gitignore
Fixup distutils: setup.py

Clean old cruft: section 5 man pages
2012-03-10 14:22:11 -05:00
4c9dd972b4 Fixes for output formatting 2012-03-10 13:40:08 -05:00
bb5e4fad48 Abstracted out transport from implementation so it can be pluggable. Also fixes for output format. 2012-03-10 13:35:46 -05:00
8808c814ea Merge pull request #89 from skvidal/feature
touch up to yum module.
2012-03-10 05:40:24 -08:00
a99b491b32 touch up to yum module.
fix a couple of bugs
comment about state='latest'
2012-03-10 02:19:57 -05:00
209760f8f6 Add manpage for ansible-playbook, since it is a command. More detailed docs will remain on the web site. 2012-03-09 22:06:44 -05:00
1699013fbd Rename 'ensure' to 'state' so it lines up with the service module 2012-03-09 18:50:34 -05:00
ae0e4ff0cf Merge pull request #88 from skvidal/feature
runner buglet and yum module
2012-03-09 15:44:54 -08:00
b576e389b1 runner buglet and yum module
runner: fix buglet causing logger output to include a u for the command
yum: add yum module:
   ensure=installed pkg=name/name.arch/name-ver-rel.arch
   ensure=removed pkg=name/name.arch/name-ver-rel.arch
   list=installed
   list=updates
   list=available
   list=pkgspec
   list=repos
2012-03-09 18:33:58 -05:00
c89a0f96bd Merge pull request #87 from skvidal/feature
if we've failed a connection to a host - we cannot set items to the
2012-03-09 04:19:03 -08:00
19b784e480 if we've failed a connection to a host - we cannot set items to the
'conn' object since it is not an object on failure - it's a string.
2012-03-09 00:19:55 -05:00
99 changed files with 7639 additions and 1730 deletions

7
.gitignore vendored
View File

@ -3,3 +3,10 @@ build
# Emacs backup files...
*~
.\#*
# (s)rpm building stuff
MANIFEST
dist
rpm-build
# Eclipse/PyDev stuff...
.project
.pydevproject

View File

@ -1,13 +0,0 @@
Patches and Contributions
=========================
* Michael DeHaan - michael.dehaan AT gmail DOT com
* Jeremy Katz - katzj AT fedoraproject DOT org
* Seth Vidal - skvidal AT fedoraproject DOT org
* Tim Bielawa - tbielawa AT redhat DOT com
* Christopher Johnston - chjohnst AT gmail DOT com
Send in a github pull request to get your name here.
Upstream: github.com/mpdehaan/ansible

675
COPYING Normal file
View File

@ -0,0 +1,675 @@
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU General Public License is a free, copyleft license for
software and other kinds of works.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
the GNU General Public License is intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users. We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors. You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights. Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received. You must make sure that they, too, receive
or can get the source code. And you must show them these terms so they
know their rights.
Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.
For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software. For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.
Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so. This is fundamentally incompatible with the aim of
protecting users' freedom to change the software. The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable. Therefore, we
have designed this version of the GPL to prohibit the practice for those
products. If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.
Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary. To prevent this, the GPL assures that
patents cannot be used to render the program non-free.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Use with the GNU Affero General Public License.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program 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 program 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 program. If not, see <http://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
<program> Copyright (C) <year> <name of author>
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, your program's commands
might be different; for a GUI interface, you would use an "about box".
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU GPL, see
<http://www.gnu.org/licenses/>.
The GNU General Public License does not permit incorporating your program
into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
<http://www.gnu.org/philosophy/why-not-lgpl.html>.

View File

@ -1,3 +1,6 @@
include README.md AUTHORS.md ansible.spec
include README.md packaging/rpm/ansible.spec
include examples/hosts
include packaging/distutils/setup.py
recursive-include docs *
recursive-include library *
include Makefile

125
Makefile
View File

@ -1,23 +1,59 @@
#!/usr/bin/make
########################################################
# Makefile for Ansible
#
# useful targets:
# make sdist ---------------- produce a tarball
# make rpm ----------------- produce RPMs
# 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
########################################################
# variable section
NAME = "ansible"
# Manpages are currently built with asciidoc -- would like to move to markdown
# This doesn't evaluate until it's called. The -D argument is the
# directory of the target file ($@), kinda like `dirname`.
ASCII2MAN = a2x -D $(dir $@) -d manpage -f manpage $<
ASCII2HTMLMAN = a2x -D docs/html/man/ -d manpage -f xhtml
MANPAGES := docs/man/man1/ansible.1
MANPAGES := docs/man/man1/ansible.1 docs/man/man1/ansible-playbook.1
SITELIB = $(shell python -c "from distutils.sysconfig import get_python_lib; print get_python_lib()")
# VERSION file provides one place to update the software version
VERSION := $(shell cat VERSION)
# 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')
RPMNVR = "$(NAME)-$(RPMVERSION)-$(RPMRELEASE)$(RPMDIST)"
########################################################
all: clean python
tests:
PYTHONPATH=./lib nosetests
PYTHONPATH=./lib nosetests -v
docs: manuals
# To force a rebuild of the docs run 'touch VERSION && make docs'
docs: $(MANPAGES)
manuals: $(MANPAGES)
# Regenerate %.1.asciidoc if %.1.asciidoc.in has been modified more
# recently than %.1.asciidoc.
%.1.asciidoc: %.1.asciidoc.in
sed "s/%VERSION%/$(VERSION)/" $< > $@
%.1: %.1.asciidoc
$(ASCII2MAN)
%.5: %.5.asciidoc
# Regenerate %.1 if %.1.asciidoc or VERSION has been modified more
# recently than %.1. (Implicitly runs the %.1.asciidoc recipe)
%.1: %.1.asciidoc VERSION
$(ASCII2MAN)
loc:
@ -27,38 +63,79 @@ pep8:
@echo "#############################################"
@echo "# Running PEP8 Compliance Tests"
@echo "#############################################"
pep8 -r --ignore=E501,E221,W291,W391,E302,E251,E203,W293,E231,E303,E201,E225 lib/ bin/
pep8 -r --ignore=E501,E221,W291,W391,E302,E251,E203,W293,E231,E303,E201,E225,E261 lib/ bin/
pyflakes:
pyflakes lib/ansible/*.py
pyflakes lib/ansible/*.py bin/*
clean:
@echo "Cleaning up distutils stuff"
-rm -rf build
-rm -rf dist
rm -rf build
rm -rf dist
@echo "Cleaning up byte compiled python stuff"
find . -regex ".*\.py[co]$$" -delete
find . -type f -regex ".*\.py[co]$$" -delete
@echo "Cleaning up editor backup files"
find . -type f \( -name "*~" -or -name "#*" \) -delete
find . -type f \( -name "*.swp" \) -delete
@echo "Cleaning up asciidoc to man transformations and results"
find ./docs/man -type f \( -name "*.xml" -or -regex ".*\.[0-9]$$" \) -delete
find ./docs/man -type f -name "*.xml" -delete
find ./docs/man -type f -name "*.asciidoc" -delete
@echo "Cleaning up output from test runs"
-rm -rf test/test_data
@echo "Cleaning up RPM stuff"
-rm MANIFEST
rm -rf test/test_data
@echo "Cleaning up RPM building stuff"
rm -rf MANIFEST rpm-build
@echo "Cleaning up Debian building stuff"
rm -rf debian
rm -rf deb-build
python: docs
python:
python setup.py build
install: docs
install:
mkdir -p /usr/share/ansible
cp ./library/* /usr/share/ansible/
python setup.py install
rpm:
python setup.py sdist
rpmbuild -ta dist/ansible-1.0.tar.gz
sdist: clean
python setup.py sdist -t MANIFEST.in
.PHONEY: docs manual clean pep8
vpath %.asciidoc docs/man/man1
rpmcommon: sdist
@mkdir -p rpm-build
@cp dist/*.gz rpm-build/
srpm: rpmcommon
@rpmbuild --define "_topdir %(pwd)/rpm-build" \
--define "_builddir %{_topdir}" \
--define "_rpmdir %{_topdir}" \
--define "_srcrpmdir %{_topdir}" \
--define "_specdir $(RPMSPECDIR)" \
--define "_sourcedir %{_topdir}" \
-bs $(RPMSPEC)
@echo "#############################################"
@echo "Ansible SRPM is built:"
@echo " rpm-build/$(RPMNVR).src.rpm"
@echo "#############################################"
rpm: rpmcommon
@rpmbuild --define "_topdir %(pwd)/rpm-build" \
--define "_builddir %{_topdir}" \
--define "_rpmdir %{_topdir}" \
--define "_srcrpmdir %{_topdir}" \
--define "_specdir $(RPMSPECDIR)" \
--define "_sourcedir %{_topdir}" \
-ba $(RPMSPEC)
@echo "#############################################"
@echo "Ansible RPM is built:"
@echo " rpm-build/noarch/$(RPMNVR).noarch.rpm"
@echo "#############################################"
debian: sdist
deb: debian
cp -r packaging/debian ./
chmod 755 debian/rules
fakeroot debian/rules clean
fakeroot dh_install
fakeroot debian/rules binary
# for arch or gentoo, read instructions in the appropriate 'packaging' subdirectory directory

View File

@ -24,6 +24,25 @@ Get Involved
* [ansible-project mailing list](http://groups.google.com/group/ansible-project)
* irc.freenode.net: #ansible
Patch Instructions
==================
Contributions to the core and modules are greatly welcome.
* Required Process:
* Submit github pull requests to the "ansible/integration" branch
* (This branch will be renamed 'devel' in the very near future)
* Make sure "make tests" passes before submitting any requests.
* Bonus points:
* Joining the mailing list
* Using squash merges
* Updating the "rst/*" files in the docs project and "docs/" manpage content
* Adding more unit tests
* Avoid:
* Sending patches to the mailing list directly.
* Sending pull requests to the 'master' branch instead of the integration branch.
* Sending pull requests to mpdehaan's personal ansible fork.
Author
======

1
VERSION Normal file
View File

@ -0,0 +1 @@
0.3

View File

@ -1,50 +0,0 @@
%{!?python_sitelib: %global python_sitelib %(%{__python} -c "from distutils.sysconfig import get_python_lib; print(get_python_lib())")}
Summary: Minimal SSH command and control
Name: ansible
Version: 1.0
Release: 1
Source0: ansible-%{version}.tar.gz
License: GPLv3
Group: Development/Libraries
BuildRoot: %{_tmppath}/%{name}-%{version}-%{release}-buildroot
Prefix: %{_prefix}
BuildArch: noarch
Url: http://github.com/mpdehaan/ansible/
BuildRequires: asciidoc
Requires: python-paramiko
Requires: python-jinja2
%description
Ansible is a extra-simple tool/API for doing 'parallel remote things' over SSH
executing commands, running "modules", or executing larger 'playbooks' that
can serve as a configuration management or deployment system.
%prep
%setup -n %{name}-%{version}
%build
python setup.py build
make docs
%install
python setup.py install -O1 --root=$RPM_BUILD_ROOT --record=INSTALLED_FILES
mkdir -p $RPM_BUILD_ROOT/etc/ansible/
%clean
rm -rf $RPM_BUILD_ROOT
%files
%doc README.md AUTHORS.md PKG-INFO
%defattr(-,root,root)
%{_mandir}/man1/*.gz
%{_mandir}/man5/*.gz
%{python_sitelib}/*
%{_bindir}/ansible*
%{_datadir}/ansible/*
%{_sysconfdir}/ansible/
%changelog
* Mon Mar 5 2012 Seth Vidal <skvidal at fedoraproject.org>
- spec file

View File

@ -1,4 +1,4 @@
#!/usr/bin/python
#!/usr/bin/env python
# (c) 2012, Michael DeHaan <michael.dehaan@gmail.com>
#
@ -19,20 +19,16 @@
########################################################
try:
import json
except ImportError:
import simplejson as json
from optparse import OptionParser
import sys
import os
import getpass
import shlex
import time
import ansible.runner
import ansible.playbook
import ansible.constants as C
from optparse import OptionParser
from ansible.utils import *
from ansible import utils
from ansible import errors
from ansible import callbacks
from ansible import inventory
########################################################
@ -42,38 +38,26 @@ class Cli(object):
# ----------------------------------------------
def __init__(self):
pass
self.stats = callbacks.AggregateStats()
self.callbacks = callbacks.CliRunnerCallbacks()
self.silent_callbacks = callbacks.DefaultRunnerCallbacks()
# ----------------------------------------------
def parse(self):
''' create an options parser for bin/ansible '''
parser = OptionParser(usage = 'ansible <host-pattern> [options]')
parser.add_option("-a", "--args", dest="module_args",
help="module arguments", default=C.DEFAULT_MODULE_ARGS)
parser.add_option("-B", "--background", dest="seconds", default=0,
help="run asynchronously, failing after X seconds")
parser.add_option('-f','--forks', dest='forks', default=C.DEFAULT_FORKS, type='int',
help='number of parallel processes to use')
parser.add_option("-i", "--inventory-file", dest="inventory",
help="inventory host file", default=C.DEFAULT_HOST_LIST)
parser.add_option("-k", "--ask-pass", default=False, action="store_true",
help="ask for SSH password")
parser.add_option("-M", "--module-path", dest="module_path",
help="path to module library", default=C.DEFAULT_MODULE_PATH)
parser.add_option("-m", "--module-name", dest="module_name",
help="module name to execute", default=C.DEFAULT_MODULE_NAME)
parser.add_option('-o', '--one-line', dest='one_line', action='store_true',
help="condense output")
parser.add_option('-t', '--tree', dest='tree', default=None,
help="log output to this directory")
parser.add_option('-T', '--timeout', default=C.DEFAULT_TIMEOUT, type='int',
dest='timeout', help="set the SSH timeout in seconds")
parser.add_option('-u', '--user', default=C.DEFAULT_REMOTE_USER,
dest='remote_user', help='connect as this user')
options, args = parser.parse_args()
parser = utils.base_parser(constants=C, runas_opts=True, async_opts=True,
output_opts=True, connect_opts=True, usage='%prog <host-pattern> [options]')
parser.add_option('-a', '--args', dest='module_args',
help="module arguments", default=C.DEFAULT_MODULE_ARGS)
parser.add_option('-m', '--module-name', dest='module_name',
help="module name to execute (default=%s)" % C.DEFAULT_MODULE_NAME,
default=C.DEFAULT_MODULE_NAME)
options, args = parser.parse_args()
self.callbacks.options = options
if len(args) == 0 or len(args) > 1:
parser.print_help()
sys.exit(1)
@ -85,57 +69,109 @@ class Cli(object):
''' use Runner lib to do SSH things '''
pattern = args[0]
inventory_manager = inventory.Inventory(options.inventory)
hosts = inventory_manager.list_hosts(pattern)
if len(hosts) == 0:
print >>sys.stderr, "No hosts matched"
sys.exit(1)
sshpass = None
sudopass = None
if options.ask_pass:
sshpass = getpass.getpass(prompt="SSH password: ")
if options.ask_sudo_pass:
sudopass = getpass.getpass(prompt="sudo password: ")
if options.tree:
utils.prepare_writeable_dir(options.tree)
if options.seconds:
print "background launch...\n\n"
runner = ansible.runner.Runner(
module_name=options.module_name, module_path=options.module_path,
module_args=options.module_args,
remote_user=options.remote_user, remote_pass=sshpass,
inventory=inventory_manager, timeout=options.timeout,
forks=options.forks,
background=options.seconds, pattern=pattern,
callbacks=self.callbacks, sudo=options.sudo,
sudo_pass=sudopass,
transport=options.connection, debug=options.debug
)
return (runner, runner.run())
# ----------------------------------------------
def get_polling_runner(self, old_runner, jid):
return ansible.runner.Runner(
module_name=options.module_name,
module_path=options.module_path,
module_args=shlex.split(options.module_args),
remote_user=options.remote_user,
remote_pass=sshpass,
host_list=options.inventory,
timeout=options.timeout,
forks=options.forks,
background=options.seconds,
pattern=pattern,
verbose=True,
).run()
module_name='async_status', module_path=old_runner.module_path,
module_args="jid=%s" % jid, remote_user=old_runner.remote_user,
remote_pass=old_runner.remote_pass, inventory=old_runner.inventory,
timeout=old_runner.timeout, forks=old_runner.forks,
pattern='*', callbacks=self.silent_callbacks,
)
# ----------------------------------------------
def hosts_to_poll(self, results):
hosts = []
for (host, res) in results['contacted'].iteritems():
if res.get('started',False):
hosts.append(host)
return hosts
# ----------------------------------------------
def output(self, results, options, args):
def poll_if_needed(self, runner, results, options, args):
''' summarize results from Runner '''
if results is None:
exit("No hosts matched")
if options.tree:
prepare_writeable_dir(options.tree)
buf = ''
for hostname in contacted_hosts(results):
msg = host_report_msg(
hostname,
options.module_name,
contacted_host_result(results, hostname),
options.one_line
)
if options.tree:
write_tree_file(options.tree, hostname, bigjson(results))
buf += msg
if has_dark_hosts(results):
buf += dark_hosts_msg(results)
# BACKGROUND POLL LOGIC when -B and -P are specified
# FIXME: refactor
if options.seconds and options.poll_interval > 0:
poll_hosts = results['contacted'].keys()
if len(poll_hosts) == 0:
exit("no jobs were launched successfully")
ahost = poll_hosts[0]
jid = results['contacted'][ahost].get('ansible_job_id', None)
if jid is None:
exit("unexpected error: unable to determine jid")
print buf
clock = options.seconds
while (clock >= 0):
runner.inventory.restrict_to(poll_hosts)
polling_runner = self.get_polling_runner(runner, jid)
poll_results = polling_runner.run()
runner.inventory.lift_restriction()
if poll_results is None:
break
for (host, host_result) in poll_results['contacted'].iteritems():
# override last result with current status result for report
results['contacted'][host] = host_result
print utils.async_poll_status(jid, host, clock, host_result)
for (host, host_result) in poll_results['dark'].iteritems():
print "FAILED: %s => %s" % (host, host_result)
clock = clock - options.poll_interval
time.sleep(options.poll_interval)
poll_hosts = self.hosts_to_poll(poll_results)
if len(poll_hosts)==0:
break
########################################################
if __name__ == '__main__':
cli = Cli()
(options, args) = cli.parse()
results = cli.run(options, args)
cli.output(results, options, args)
try:
(runner, results) = cli.run(options, args)
except errors.AnsibleError, e:
# Generic handler for ansible specific errors
print "ERROR: %s" % str(e)
sys.exit(1)
else:
cli.poll_if_needed(runner, results, options, args)

View File

@ -1,4 +1,4 @@
#!/usr/bin/python -tt
#!/usr/bin/env python
# (C) 2012, Michael DeHaan, <michael.dehaan@gmail.com>
# This file is part of Ansible
@ -19,86 +19,86 @@
#######################################################
import sys
import getpass
import ansible.playbook
import ansible.constants as C
from ansible.utils import *
import getpass
from optparse import OptionParser
#######################################################
class PlaybookCallbacks(object):
def __init__(self):
pass
def set_playbook(self, playbook):
self.playbook = playbook
def on_start(self):
print "\n"
def on_task_start(self, name, is_conditional):
print task_start_msg(name, is_conditional)
def on_unreachable(self, host, msg):
print "unreachable: [%s] => %s" % (host, msg)
def on_failed(self, host, results):
print "failed: [%s] => %s\n" % (host, smjson(results))
def on_ok(self, host):
print "ok: [%s]\n" % (host)
def on_play_start(self, pattern):
print "PLAY [%s] ****************************\n" % pattern
from ansible import errors
from ansible import callbacks
from ansible import utils
def main(args):
''' run ansible-playbook operations '''
# create parser for CLI options
parser = OptionParser()
parser.usage = "ans-playbook playbook.yml ..."
parser.add_option('-f','--forks', dest='forks', default=C.DEFAULT_FORKS, type='int',
help='set the number of forks to start up')
parser.add_option("-i", "--inventory-file", dest="inventory",
help="inventory host file", default=C.DEFAULT_HOST_LIST)
parser.add_option("-k", "--ask-pass", default=False, action="store_true",
help="ask for SSH password")
parser.add_option("-M", "--module-path", dest="module_path",
help="path to module library", default=C.DEFAULT_MODULE_PATH)
usage = "%prog playbook.yml"
parser = utils.base_parser(constants=C, usage=usage, connect_opts=True, runas_opts=True)
parser.add_option('-O', '--override-hosts', dest="override_hosts", default=None,
help="run playbook against these hosts regardless of inventory settings")
options, args = parser.parse_args(args)
if len(args) == 0:
print >> sys.stderr, "playbook path is a required argument"
parser.print_help(file=sys.stderr)
return 1
sshpass = None
sudopass = None
if options.ask_pass:
sshpass = getpass.getpass(prompt="SSH password: ")
if options.ask_sudo_pass:
sudopass = getpass.getpass(prompt="sudo password: ")
override_hosts = None
if options.override_hosts:
override_hosts = options.override_hosts.split(",")
# run all playbooks specified on the command line
for playbook in args:
stats = callbacks.AggregateStats()
playbook_cb = callbacks.PlaybookCallbacks()
runner_cb = callbacks.PlaybookRunnerCallbacks(stats)
pb = ansible.playbook.PlayBook(
playbook=playbook,
host_list=options.inventory,
module_path=options.module_path,
forks=options.forks,
verbose=True,
remote_pass=sshpass,
callbacks=PlaybookCallbacks()
host_list=options.inventory,
override_hosts=override_hosts,
forks=options.forks,
debug=options.debug,
remote_user=options.remote_user,
remote_pass=sshpass,
callbacks=playbook_cb,
runner_callbacks=runner_cb,
stats=stats,
timeout=options.timeout,
transport=options.connection,
sudo=options.sudo,
sudo_pass=sudopass
)
pb.run()
try:
pb.run()
hosts = sorted(pb.stats.processed.keys())
print "\n\nPLAY RECAP **********************\n\n"
for h in hosts:
t = pb.stats.summarize(h)
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:
print >>sys.stderr, "ERROR: %s" % e
return 1
return 0
if __name__ == "__main__":
sys.exit(main(sys.argv[1:]))
try:
sys.exit(main(sys.argv[1:]))
except errors.AnsibleError, e:
print >>sys.stderr, "ERROR: %s" % e
sys.exit(1)

View File

@ -1,203 +0,0 @@
Comparisons
===========
Confused about how Ansible fits in? Here's a comparison with some common tools.
Accuracy is important, so corrections are VERY welcome if we've got something wrong.
For space reasons, we can't list everybody's favorite management tool.
Ansible mainly tries to be different in it's goals of minimalism, while also cutting across
the categories of configuration management, deployment, and ad-hoc task firing.
It strives to be a super-easy to use app, requiring no daemons or databases, and having
a super-simple configuration management language that takes minutes to learn.
Generally speaking, Ansible is answer to the question "why is the world's systems
management software so complicated?" The code does not try to show off. It is an
experiment in radical simplicity and encourages anyone to be able to easily contribute to it.
<table>
<tr>
<td></td>
<td><B><U>Ansible</U></B></font></td>
<td>Puppet</td>
<td>Chef</td>
<td>Func</td>
<td>Capistrano</td>
<td>mCollective</td>
</tr>
<tr>
<td>Purpose</td>
<td><B>Config, Deployment, Ad-Hoc</B></td>
<td>Config, Deployment</td>
<td>Config, Deployment</td>
<td>Ad-Hoc</td>
<td>Deployment</td>
<td>Ad-Hoc</td>
</tr>
<tr>
<td>Inspirations</td>
<td>Cobbler, Puppet, Func, Taboot</B></td>
<td>cfengine</td>
<td>puppet</td>
<td>sysadmin SSH scripts</td>
<td>sysadmin SSH scripts</td>
<td>Func, puppet</td>
</tr>
<tr>
<td>Config Language</td>
<td><B>Simple YAML format</B></td>
<td>Custom DSL</td>
<td>Ruby code</td>
<td>None</td>
<td>None</td>
<td>None</td>
</tr>
<tr>
<td>Config Language Style</td>
<td><B>Very simple</B></td>
<td>Very rich, complex</td>
<td>Very rich, complex</td>
<td>None</td>
<td>None</td>
<td>None</td>
</tr>
<tr>
<td>Config Ordering</td>
<td><B>Simply ordered, with notifiers</B></td>
<td>Strict dependency DAG</td>
<td>Simply ordered</td>
<td>None</td>
<td>Simply ordered</td>
<td>None</td>
</tr>
<tr>
<td>Communication</td>
<td>SSH push</td>
<td>SSL pull or push trigger</td>
<td>SSL pull or push trigger(?)</td>
<td>SSL push</td>
<td>SSH</td>
<td>AMQP bus</td>
</tr>
<tr>
<td>Daemons Required?</td>
<td><B>no</B></td>
<td>yes</td>
<td>yes</td>
<td>yes</td>
<td><B>no</B></td>
<td>yes</td>
</tr>
<tr>
<td>Database Needed?</td>
<td><B>no</B></td>
<td>some features</td>
<td>yes</td>
<td><B>no</B></td>
<td><B>no</B></td>
<td>some features</td>
</tr>
<tr>
<td>Message Bus Required?</td>
<td><B>no</B></td>
<td><B>no</B></td>
<td>yes</td>
<td><B>no</B></td>
<td><B>no</B></td>
<td>yes</td>
</tr>
<tr>
<td>Inventory Features</td>
<td>planned</td>
<td><B>yes</B></td>
<td><B>yes?</B></td>
<td>some</td>
<td>no</td>
<td>no</td>
</tr>
<tr>
<td>Implemented In</td>
<td>Python</td>
<td>Ruby</td>
<td>Ruby, Erlang</td>
<td>Python</td>
<td>Ruby</td>
<td>Ruby</td>
</tr>
<tr>
<td>Extensible In</td>
<td><B>Anything</B></td>
<td>Ruby</td>
<td>Ruby</td>
<td>Python</td>
<td>Ruby</td>
<td>Ruby</td>
</tr>
<tr>
<td>Codebase Size</td>
<td><B>Small</B></td>
<td>Large</td>
<td>Large</td>
<td>Medium</td>
<td>Medium</td>
<td>Medium</td>
</tr>
<tr>
<td>Module Support</td>
<td>Emerging</td>
<td><B>Wide/Established</B></td>
<td><B>Wide/Established</B></td>
<td>Medium/Established</td>
<td>Poor</td>
<td>Medium/Established</td>
</tr>
<tr>
<td>Users Targeted</td>
<td><B>Sysadmins, developers, QA, web admins</B></td>
<td>Sysadmins</td>
<td>Sysadmins, web admins</td>
<td>Sysadmins, developers</td>
<td>Web admins</td>
<td>Sysadmins</td>
</tr>
<tr>
<td>Learning & Setup Curve</td>
<td><B>Minimal<B></td>
<td>High</td>
<td>High</td>
<td>Medium</td>
<td>Medium</td>
<td>Medium</td>
</tr>
<tr>
<td>Can Easily Build Applications On It</td>
<td><B>Yes</B></td>
<td>No</td>
<td>No</td>
<td><B>Yes</B></td>
<td>No</td>
<td>Yes?</td>
</tr>
<tr>
<td>Parallelism & Scaling Tech</td>
<td>Fork/Merge</td>
<td>Add Servers / Staged Commands / No Server</td>
<td>Adding More Erlang</td>
<td>Fork/Merge</td>
<td>No</td>
<td>Message Bus</td>
</tr>
<tr>
<td>Delegated Hierachies</td>
<td>No</td>
<td>No</td>
<td>No</td>
<td><B>Yes</B></td>
<td>No</td>
<td><B>Yes</B></td>
</tr>
</td>

2
docs/man/.gitignore vendored
View File

@ -1,2 +1,2 @@
*.xml
*.[0-9]
*.asciidoc

View File

@ -0,0 +1,124 @@
'\" t
.\" Title: ansible-playbook
.\" Author: [see the "AUTHOR" section]
.\" Generator: DocBook XSL Stylesheets v1.76.1 <http://docbook.sf.net/>
.\" Date: 04/17/2012
.\" Manual: System administration commands
.\" Source: Ansible 0.0.2
.\" Language: English
.\"
.TH "ANSIBLE\-PLAYBOOK" "1" "04/17/2012" "Ansible 0\&.0\&.2" "System administration commands"
.\" -----------------------------------------------------------------
.\" * Define some portability stuff
.\" -----------------------------------------------------------------
.\" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.\" http://bugs.debian.org/507673
.\" http://lists.gnu.org/archive/html/groff/2009-02/msg00013.html
.\" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.ie \n(.g .ds Aq \(aq
.el .ds Aq '
.\" -----------------------------------------------------------------
.\" * set default formatting
.\" -----------------------------------------------------------------
.\" disable hyphenation
.nh
.\" disable justification (adjust text to left margin only)
.ad l
.\" -----------------------------------------------------------------
.\" * MAIN CONTENT STARTS HERE *
.\" -----------------------------------------------------------------
.SH "NAME"
ansible-playbook \- run an ansible playbook
.SH "SYNOPSIS"
.sp
ansible\-playbook <filename\&.yml> \&... [options]
.SH "DESCRIPTION"
.sp
\fBAnsible playbooks\fR are a configuration and multinode deployment system\&. Ansible\-playbook is the tool used to run them\&. See the project home page (link below) for more information\&.
.SH "ARGUMENTS"
.PP
\fBfilename\&.yml\fR
.RS 4
The names of one or more YAML format files to run as ansible playbooks\&.
.RE
.SH "OPTIONS"
.sp
\fB\-D\fR, \fB\-\-debug\fR
.sp
Print any messages the remote module sends to standard error to the console
.PP
\fB\-i\fR \fIPATH\fR, \fB\-\-inventory=\fR\fIPATH\fR
.RS 4
The
\fIPATH\fR
to the inventory hosts file, which defaults to
\fI/etc/ansible/hosts\fR\&.
.RE
.PP
\fB\-M\fR \fIDIRECTORY\fR, \fB\-\-module\-path=\fR\fIDIRECTORY\fR
.RS 4
The
\fIDIRECTORY\fR
to load modules from\&. The default is
\fI/usr/share/ansible\fR\&.
.RE
.PP
\fB\-f\fR \fINUM\fR, \fB\-\-forks=\fR\fINUM\fR
.RS 4
Level of parallelism\&.
\fINUM\fR
is specified as an integer, the default is 5\&.
.RE
.PP
\fB\-k\fR, \fB\-\-ask\-pass\fR
.RS 4
Prompt for the SSH password instead of assuming key\-based authentication with ssh\-agent\&.
.RE
.PP
\fB\-K\fR, \fB\-\-ask\-sudo\-pass\fR
.RS 4
Prompt for the password to use for playbook plays that request sudo access, if any
.RE
.PP
\fB\-T\fR \fISECONDS\fR, \fB\-\-timeout=\fR\fISECONDS\fR
.RS 4
Connection timeout to use when trying to talk to hosts, in
\fISECONDS\fR\&.
.RE
.PP
\fB\-O\fR \fIOVERRIDE_HOSTS\fR, \fB\-\-override\-hosts=\fR\fIOVERRIDE_HOSTS\fR
.RS 4
Ignore the inventory file and run the playbook against only these hosts\&. "hosts:" line in playbook should be set to
\fIall\fR
when using this option\&.
.RE
.PP
\fB\-s\fR, \fB\-\-sudo\fR
.RS 4
Force all plays to use sudo, even if not marked as such\&.
.RE
.PP
\fB\-u\fR \fIUSERNAME\fR, \fB\-\-remote\-user=\fR\fIUSERNAME\fR
.RS 4
Use this remote user name on playbook steps that do not indicate a user name to run as\&.
.RE
.SH "ENVIRONMENT"
.sp
The following environment variables may specified\&.
.sp
ANSIBLE_HOSTS \(em Override the default ansible hosts file
.sp
ANSIBLE_LIBRARY \(em Override the default ansible module library path
.SH "AUTHOR"
.sp
Ansible was originally written by Michael DeHaan\&. See the AUTHORS file for a complete list of contributors\&.
.SH "COPYRIGHT"
.sp
Copyright \(co 2012, Michael DeHaan
.sp
Ansible is released under the terms of the GPLv3 License\&.
.SH "SEE ALSO"
.sp
\fBansible\fR(1)
.sp
Extensive documentation as well as IRC and mailing list info is available on the ansible home page: https://ansible\&.github\&.com/

View File

@ -0,0 +1,122 @@
ansible-playbook(1)
===================
:doctype:manpage
:man source: Ansible
:man version: %VERSION%
:man manual: System administration commands
NAME
----
ansible-playbook - run an ansible playbook
SYNOPSIS
--------
ansible-playbook <filename.yml> ... [options]
DESCRIPTION
-----------
*Ansible playbooks* are a configuration and multinode deployment
system. Ansible-playbook is the tool used to run them. See the
project home page (link below) for more information.
ARGUMENTS
---------
*filename.yml*::
The names of one or more YAML format files to run as ansible playbooks.
OPTIONS
-------
*-D*, *--debug*
Print any messages the remote module sends to standard error to the console
*-i* 'PATH', *--inventory=*'PATH'::
The 'PATH' to the inventory hosts file, which defaults to '/etc/ansible/hosts'.
*-M* 'DIRECTORY', *--module-path=*'DIRECTORY'::
The 'DIRECTORY' to load modules from. The default is '/usr/share/ansible'.
*-f* 'NUM', *--forks=*'NUM'::
Level of parallelism. 'NUM' is specified as an integer, the default is 5.
*-k*, *--ask-pass*::
Prompt for the SSH password instead of assuming key-based authentication with ssh-agent.
*-K*, *--ask-sudo-pass*::
Prompt for the password to use for playbook plays that request sudo access, if any
*-T* 'SECONDS', *--timeout=*'SECONDS'::
Connection timeout to use when trying to talk to hosts, in 'SECONDS'.
*-O* 'OVERRIDE_HOSTS', *--override-hosts=*'OVERRIDE_HOSTS'::
Ignore the inventory file and run the playbook against only these hosts. "hosts:" line
in playbook should be set to 'all' when using this option.
*-s*, *--sudo*::
Force all plays to use sudo, even if not marked as such.
*-u* 'USERNAME', *--remote-user=*'USERNAME'::
Use this remote user name on playbook steps that do not indicate a user name to run as.
ENVIRONMENT
-----------
The following environment variables may specified.
ANSIBLE_HOSTS -- Override the default ansible hosts file
ANSIBLE_LIBRARY -- Override the default ansible module library path
AUTHOR
------
Ansible was originally written by Michael DeHaan. See the AUTHORS file
for a complete list of contributors.
COPYRIGHT
---------
Copyright © 2012, Michael DeHaan
Ansible is released under the terms of the GPLv3 License.
SEE ALSO
--------
*ansible*(1)
Extensive documentation as well as IRC and mailing list info
is available on the ansible home page: <https://ansible.github.com/>

175
docs/man/man1/ansible.1 Normal file
View File

@ -0,0 +1,175 @@
'\" t
.\" Title: ansible
.\" Author: [see the "AUTHOR" section]
.\" Generator: DocBook XSL Stylesheets v1.76.1 <http://docbook.sf.net/>
.\" Date: 04/17/2012
.\" Manual: System administration commands
.\" Source: Ansible 0.0.2
.\" Language: English
.\"
.TH "ANSIBLE" "1" "04/17/2012" "Ansible 0\&.0\&.2" "System administration commands"
.\" -----------------------------------------------------------------
.\" * Define some portability stuff
.\" -----------------------------------------------------------------
.\" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.\" http://bugs.debian.org/507673
.\" http://lists.gnu.org/archive/html/groff/2009-02/msg00013.html
.\" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.ie \n(.g .ds Aq \(aq
.el .ds Aq '
.\" -----------------------------------------------------------------
.\" * set default formatting
.\" -----------------------------------------------------------------
.\" disable hyphenation
.nh
.\" disable justification (adjust text to left margin only)
.ad l
.\" -----------------------------------------------------------------
.\" * MAIN CONTENT STARTS HERE *
.\" -----------------------------------------------------------------
.SH "NAME"
ansible \- run a command somewhere else
.SH "SYNOPSIS"
.sp
ansible <host\-pattern> [\-f forks] [\-m module_name] [\-a args]
.SH "DESCRIPTION"
.sp
\fBAnsible\fR is an extra\-simple tool/framework/API for doing \*(Aqremote things\*(Aq over SSH\&.
.SH "ARGUMENTS"
.PP
\fBhost\-pattern\fR
.RS 4
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\&.
.RE
.SH "OPTIONS"
.PP
\fB\-i\fR \fIPATH\fR, \fB\-\-inventory=\fR\fIPATH\fR
.RS 4
The
\fIPATH\fR
to the inventory hosts file, which defaults to
\fI/etc/ansible/hosts\fR\&.
.RE
.PP
\fB\-f\fR \fINUM\fR, \fB\-\-forks=\fR\fINUM\fR
.RS 4
Level of parallelism\&.
\fINUM\fR
is specified as an integer, the default is 5\&.
.RE
.PP
\fB\-m\fR \fINAME\fR, \fB\-\-module\-name=\fR\fINAME\fR
.RS 4
Execute the module called
\fINAME\fR\&.
.RE
.PP
\fB\-M\fR \fIDIRECTORY\fR, \fB\-\-module\-path=\fR\fIDIRECTORY\fR
.RS 4
The
\fIDIRECTORY\fR
to load modules from\&. The default is
\fI/usr/share/ansible\fR\&.
.RE
.PP
\fB\-a\fR \*(Aq\fIARGUMENTS\fR\*(Aq, \fB\-\-args=\fR\*(Aq\fIARGUMENTS\fR\*(Aq
.RS 4
The
\fIARGUMENTS\fR
to pass to the module\&.
.RE
.PP
\fB\-D\fR, \fB\-\-debug\fR
.RS 4
Print any messages the remote module sends to standard error to the console
.RE
.PP
\fB\-k\fR, \fB\-\-ask\-pass\fR
.RS 4
Prompt for the SSH password instead of assuming key\-based authentication with ssh\-agent\&.
.RE
.PP
\fB\-K\fR, \fB\-\-ask\-sudo\-pass\fR
.RS 4
Prompt for the password to use with \-\-sudo, if any
.RE
.PP
\fB\-o\fR, \fB\-\-one\-line\fR
.RS 4
Try to output everything on one line\&.
.RE
.PP
\fB\-s\fR, \fB\-\-sudo\fR
.RS 4
Run the command as the user given by \-u and sudo to root\&.
.RE
.PP
\fB\-t\fR \fIDIRECTORY\fR, \fB\-\-tree=\fR\fIDIRECTORY\fR
.RS 4
Save contents in this output
\fIDIRECTORY\fR, with the results saved in a file named after each host\&.
.RE
.PP
\fB\-T\fR \fISECONDS\fR, \fB\-\-timeout=\fR\fISECONDS\fR
.RS 4
Connection timeout to use when trying to talk to hosts, in
\fISECONDS\fR\&.
.RE
.PP
\fB\-B\fR \fINUM\fR, \fB\-\-background=\fR\fINUM\fR
.RS 4
Run commands in the background, killing the task after
\fINUM\fR
seconds\&.
.RE
.PP
\fB\-P\fR \fINUM\fR, \fB\-\-poll=\fR\fINUM\fR
.RS 4
Poll a background job every
\fINUM\fR
seconds\&. Requires
\fB\-B\fR\&.
.RE
.PP
\fB\-u\fR \fIUSERNAME\fR, \fB\-\-remote\-user=\fR\fIUSERNAME\fR
.RS 4
Use this remote
\fIUSERNAME\fR
instead of root\&.
.RE
.PP
\fB\-c\fR \fICONNECTION\fR, \fB\-\-connection=\fR\fICONNECTION\fR
.RS 4
Connection type to use\&. Possible options are
\fIparamiko\fR
(SSH) and
\fIlocal\fR\&. Local is mostly useful for crontab or kickstarts\&.
.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\&.
.SH "FILES"
.sp
/etc/ansible/hosts \(em Default inventory file
.sp
/usr/share/ansible/ \(em Default module library
.SH "ENVIRONMENT"
.sp
The following environment variables may specified\&.
.sp
ANSIBLE_HOSTS \(em Override the default ansible hosts file
.sp
ANSIBLE_LIBRARY \(em Override the default ansible module library path
.SH "AUTHOR"
.sp
Ansible was originally written by Michael DeHaan\&. See the AUTHORS file for a complete list of contributors\&.
.SH "COPYRIGHT"
.sp
Copyright \(co 2012, Michael DeHaan
.sp
Ansible is released under the terms of the GPLv3 License\&.
.SH "SEE ALSO"
.sp
\fBansible\-playbook\fR(1)
.sp
Extensive documentation as well as IRC and mailing list info is available on the ansible home page: https://ansible\&.github\&.com/

View File

@ -1,108 +0,0 @@
ansible(1)
=========
:doctype:manpage
:man source: Ansible
:man version: 0.0.1
:man manual: System administration commands
NAME
----
ansible - run a command somewhere else
SYNOPSIS
--------
ansible <host-pattern> [-f forks] [-m module_name] [-a args]
DESCRIPTION
-----------
*Ansible* is an extra-simple tool/framework/API for doing \'remote things' over
SSH.
ARGUMENTS
---------
*host-pattern*::
A name of a group in the inventory file, a shell-like glob selecting hosts in inventory
file, or any combination of the two seperated by semicolons.
OPTIONS
-------
*-i*, *--inventory*::
Path to the inventory hosts file, which defaults to /etc/ansible/hosts.
*-f*, *--forks*::
Level of parallelism. Specify as an integer, the default is 5.
*-m*, *--module-name*::
Module name to execute.
*-p*, *--pattern*::
Hostname pattern. Accepts shell-like globs which can be seperated with ";"
The default is "*" which matches all hosts in the ansible hosts file. Group
names from the ansible inventory file can also be used.
See ansible --help for additional options.
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.
FILES
-----
/etc/ansible/hosts -- Default hosts file
/usr/share/ansible -- Default module library
ENVIRONMENT
-----------
The following environment variables may specified.
ANSIBLE_HOSTS -- Override the default ansible hosts file
ANSIBLE_LIBRARY -- Override the default ansible module library path
AUTHOR
------
Ansible was originally written by Michael DeHaan. See the AUTHORS file
for a complete list of contributors.
COPYRIGHT
---------
Copyright © 2012, Michael DeHaan
Ansible is released under the terms of the GPLv3 License.
SEE ALSO
--------
Ansible home page: <https://ansible.github.com/>

View File

@ -0,0 +1,156 @@
ansible(1)
=========
:doctype:manpage
:man source: Ansible
:man version: %VERSION%
:man manual: System administration commands
NAME
----
ansible - run a command somewhere else
SYNOPSIS
--------
ansible <host-pattern> [-f forks] [-m module_name] [-a args]
DESCRIPTION
-----------
*Ansible* is an extra-simple tool/framework/API for doing \'remote things' over
SSH.
ARGUMENTS
---------
*host-pattern*::
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
-------
*-i* 'PATH', *--inventory=*'PATH'::
The 'PATH' to the inventory hosts file, which defaults to '/etc/ansible/hosts'.
*-f* 'NUM', *--forks=*'NUM'::
Level of parallelism. 'NUM' is specified as an integer, the default is 5.
*-m* 'NAME', *--module-name=*'NAME'::
Execute the module called 'NAME'.
*-M* 'DIRECTORY', *--module-path=*'DIRECTORY'::
The 'DIRECTORY' to load modules from. The default is '/usr/share/ansible'.
*-a* \'_ARGUMENTS_', *--args=*\'_ARGUMENTS_'::
The 'ARGUMENTS' to pass to the module.
*-D*, *--debug*::
Print any messages the remote module sends to standard error to the console
*-k*, *--ask-pass*::
Prompt for the SSH password instead of assuming key-based authentication with ssh-agent.
*-K*, *--ask-sudo-pass*::
Prompt for the password to use with --sudo, if any
*-o*, *--one-line*::
Try to output everything on one line.
*-s*, *--sudo*::
Run the command as the user given by -u and sudo to root.
*-t* 'DIRECTORY', *--tree=*'DIRECTORY'::
Save contents in this output 'DIRECTORY', with the results saved in a
file named after each host.
*-T* 'SECONDS', *--timeout=*'SECONDS'::
Connection timeout to use when trying to talk to hosts, in 'SECONDS'.
*-B* 'NUM', *--background=*'NUM'::
Run commands in the background, killing the task after 'NUM' seconds.
*-P* 'NUM', *--poll=*'NUM'::
Poll a background job every 'NUM' seconds. Requires *-B*.
*-u* 'USERNAME', *--remote-user=*'USERNAME'::
Use this remote 'USERNAME' instead of root.
*-c* 'CONNECTION', *--connection=*'CONNECTION'::
Connection type to use. Possible options are 'paramiko' (SSH) and 'local'.
Local is mostly useful for crontab or kickstarts.
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.
FILES
-----
/etc/ansible/hosts -- Default inventory file
/usr/share/ansible/ -- Default module library
ENVIRONMENT
-----------
The following environment variables may specified.
ANSIBLE_HOSTS -- Override the default ansible hosts file
ANSIBLE_LIBRARY -- Override the default ansible module library path
AUTHOR
------
Ansible was originally written by Michael DeHaan. See the AUTHORS file
for a complete list of contributors.
COPYRIGHT
---------
Copyright © 2012, Michael DeHaan
Ansible is released under the terms of the GPLv3 License.
SEE ALSO
--------
*ansible-playbook*(1)
Extensive documentation as well as IRC and mailing list info
is available on the ansible home page: <https://ansible.github.com/>

View File

@ -1,235 +0,0 @@
ansible-modules(5)
=================
:doctype:manpage
:man source: Ansible-modules
:man version: 0.0.1
:man manual: Ansible
NAME
----
ansible-modules - stock modules shipped with ansible
DESCRIPTION
-----------
Ansible ships with a number of modules that can be executed directly on remote hosts or through
ansible playbooks.
IDEMPOTENCE
-----------
Most modules other than command are idempotent, meaning they will seek to avoid changes
unless a change needs to be made. When using ansible playbooks, these modules can
trigger change events, as described in *ansible-playbooks*(5).
Unless otherwise noted, all modules support change hooks.
command
-------
The command module takes the command name followed by a list of arguments, space delimited.
This is the only module that does not use key=value style parameters.
Example usage::
/sbin/shutdown -t now
This module does not support change hooks.
Returns the return code from the program as well as timing information.
(Async command running and command execution time limits are in plan.)
copy
----
The copy module moves a file on the local box to remote locations.
*src=*::
Local absolute path to a file to copy to the remote server
*dest=*::
Remote absolute path where the file should end up
This module also returns md5sum information about the resultant file.
facter
------
Runs the discovery program 'facter' on the remote system, returning
JSON data that can be useful for inventory purposes.
Requires that 'facter' and 'ruby-json' be installed on the remote end.
This module is informative only - it takes no parameters & does not support change hooks,
nor does it make any changes on the system.
file
----
Ensures the ownership and permissions of files are as desired.
Use copy or template first if you need to make sure a file is on the box.
In plan.
git
---
Deploys software from git checkouts.
*repo=*::
git or http protocol address of the repo to checkout
*dest=*::
where to check it out, an absolute directory path
*version=*::
what version to check out -- either the git SHA, the literal string 'HEAD', or a tag name
ohai
----
Similar to the facter module, this returns JSON inventory data. Ohai
data is a bit more verbose and nested than facter.
Requires that 'ohai' be installed on the remote end.
This module is information only - it takes no parameters & does not
support change hooks, nor does it make any changes on the system.
ping
----
A trivial test module, this module always returns the integer '1' on
successful contact.
This module does not support change hooks.
This module is informative only - it takes no parameters & does not
support change hooks, nor does it make any changes on the system.
service
-------
Controls services on remote machines.
*state=*::
Values are 'started', 'stopped', or 'restarted'. Started/stopped
are idempotent actions that will not run commands unless neccessary.
'restarted' will always bounce the service
*name=*::
The name of the service
setup
-----
Writes a JSON file containing key/value data, for use in templating.
Call this once before using the template modules, usually as the very
first step in your playbook.
If facter or ohai are installed, variables from these programs will also
be snapshotted into the JSON file for usage in templating. These variables
are prefixed with 'facter_' and 'ohai_" so it's easy to tell their source.
*metadata=*::
Optionally overrides the default JSON file location of /etc/ansible/setup.
If used, also supply the metadata parameter to 'template'. Change if
running as a non-root remote user who does not have permissions on /etc/ansible.
*anything=*::
any other parameters can be named basically anything, and set a key=value
pair in the JSON file for use in templating.
template
--------
Templates a file out to a remote server. Call the setup module prior to usage.
*src=*::
path of a Jinja2 formatted template on the local server
*dest*::
location to render the template on the remote server
*metadata*::
location of a JSON file to use to supply template data. Default is /etc/ansible/setup
which is the same as the default for the setup module. Change if running as a non-root
remote user who does not have permissions on /etc/ansible.
This module also returns md5sum information about the resultant file.
user
----
This module is in plan.
yum
---
This module is in plan.
WRITING YOUR OWN MODULES
------------------------
To write your own modules, simply follow the convention of those already available in
/usr/share/ansible. Modules must return JSON but can be written in any language.
To support change hooks, modules should return hashes, with a changed: True/False
element at the top level. Modules can also choose to indicate a failure scenario
by returning a top level 'failure' element with a True value.
ENVIRONMENT
-----------
ANSIBLE_LIBRARY -- Override the default ansible module library path
AUTHOR
------
Ansible was originally written by Michael DeHaan. See the AUTHORS file
for a complete list of contributors.
SEE ALSO
--------
*ansible*(1)
*ansible-playbook*(5)
Ansible home page: <https://github.com/mpdehaan/ansible/>

View File

@ -1,123 +0,0 @@
ansible-modules(5)
=================
:doctype:manpage
:man source: Ansible-playbook
:man version: 0.0.1
:man manual: Ansible
NAME
----
ansible-playbook - format and function of an ansible playbook file
DESCRIPTION
-----------
Ansible ships with 'ansible-playbook', a tool for running playbooks.
Playbooks can represent frequent tasks, desired system configurations,
or deployment processes.
FORMAT
------
Playbooks are written in YAML.
EXAMPLE
-------
See:
- https://github.com/mpdehaan/ansible/blob/master/examples/playbook.yml
- https://github.com/mpdehaan/ansible/blob/master/examples/base.yml
- https://github.com/mpdehaan/ansible/blob/master/examples/handlers.yml
WHAT THE EXAMPLE MEANS
-----------------------
Here's what playbook.yml (above) will do.
The first pattern will select all hosts. The patterns are the same
as supported by /usr/bin/ansible.
First, it will run all the modules specified in base.yml. Includes can
be used to implement classes of things, and if you wanted, a playbook
could consist of nothing but include files. This is an example of an
include.
After processing base.yml, on each host we'll write for
a JSON file into /etc/ansible/setup on each remote system with the
values max_clients and http_port.
Next, we'll use a Jinja2 template locally residing at
/srv/templates/httpd.j2 to write the Apache config file on each host,
using the previous values in that setup file.
Next, We'll ensure that apache is running if stopped.
The template task set up a notifier, which means if the configuration
file actually changed, we have a named handler, in this case, 'restart apache'
to run. In this case, all the notifiers come from handlers.yml, though it's
also ok to express handlers directly in the main yaml file too. Using
the include promotes reuse.
What does the handler say? If and only if the config file changed, note that we need to restart
apache at the end of the run, otherwise, don't bother because we
already know it is running.
HIGH LEVEL EXPLANATION
----------------------
Playbooks are executed top down and can contain multiple references to
patterns. For instance, a playbook could do something to all
webservers, then do something to all database servers, then do
something different to all webservers again.
For each pattern, the tasks in the 'tasks' list are executed in order
for all hosts in the host file matching the pattern.
For each task, a name/action pair describes what the task is and what
ansible module to use to accomplish the task, along with any
arguments. Additional fields like 'comment:' can be added and will
be ignored, so feel free to take notes in the file.
Most modules accept key=value format arguments.
Handlers are like tasks, but are conditionally executed. If a module
reports a 'change', it can notify one or more handler by name. If
notified, it will run only for hosts that changed.
ERROR HANDLING
--------------
If a host has a failure, the host will be ignored for the remainder
of the playbook execution.
ENVIRONMENT
-----------
ANSIBLE_LIBRARY -- Override the default ansible module library path
AUTHOR
------
Ansible was originally written by Michael DeHaan. See the AUTHORS file
for a complete list of contributors.
SEE ALSO
--------
*ansible*(1)
*ansible-modules*(5)
Ansible home page: <https://github.com/mpdehaan/ansible/>

View File

@ -1,12 +0,0 @@
[webservers]
alpha.example.org
beta.example.org
192.168.1.100
192.168.1.110
[dbservers]
192.168.1.200
192.168.1.201
foo.example.org
bar.example.org

View File

@ -1,7 +0,0 @@
---
- name: no selinux
action: command /usr/sbin/setenforce 0
- name: no iptables
action: service name=iptables state=stopped
- name: this is just to show variables work here, favcolor={{ favcolor }}
action: command /bin/true

View File

@ -1,5 +0,0 @@
---
- name: restart apache
action: service name=httpd state=restarted
- name: restart memcached
action: service name=memcached state=restarted

37
examples/hosts Normal file
View File

@ -0,0 +1,37 @@
# This is the default ansible 'hosts' file.
#
# It should live in /etc/ansible/hosts
#
# - Comments begin with the '#' character
# - Blank lines are ignored
# - 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.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]
www01.bikeshed.org
www02.bikeshed.org
wheel.colors.com
192.168.1.100
192.168.1.110
# 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
10.25.1.56
db02.intranet.mydomain.net
10.25.1.57
# Perhaps you serve a db off your personal server too:
myserver.com

View File

@ -1,18 +0,0 @@
---
- hosts: all
user: root
vars:
http_port: 80
max_clients: 200
tasks:
- include: base.yml favcolor=blue
- name: write the foo config file using vars set above
action: template src=foo.j2 dest=/etc/some_random_foo.conf
notify:
- restart apache
- name: ensure apache is running
action: service name=httpd state=started
- name: pointless test action
action: command /bin/echo {{ http_port }}
handlers:
- include: handlers.yml

View File

@ -1,24 +0,0 @@
---
- hosts: '*'
vars:
a: 2
b: 3
c: 4
tasks:
- name: copy comand
action: copy src=/srv/a dest=/srv/b
notify:
- restart apache
- name: template step
action: template src=/srv/template.j2 dest=/srv/file.out
notify:
- restart apache
- name: execute bin false
comment: call something that will fail just to demo failure counts and such
action: command /bin/false
- name: execute bin true
comment: this will never be executed because previous will fail
action: command /bin/true
handlers:
- name: restart apache
action: service name=httpd state=restarted

View File

@ -0,0 +1,51 @@
---
# this is a demo of conditional imports. This is a powerful concept
# and can be used to use the same recipe for different types of hosts,
# based on variables that bubble up from the hosts from tools such
# as ohai or facter.
#
# Here's an example use case:
#
# what to do if the service for apache is named 'httpd' on CentOS
# but is named 'apache' on Debian?
# there is only one play in this playbook, it runs on all hosts
# as root
- hosts: all
user: root
# we have a common list of variables stored in /vars/external_vars.yml
# that we will always import
# next, we want to import files that are different per operating system
# and if no per operating system file is found, load a defaults file.
# for instance, if the OS was "CentOS", we'd try to load vars/CentOS.yml.
# if that was found, we would immediately stop. However if that wasn't
# present, we'd try to load vars/defaults.yml. If that in turn was not
# found, we would fail immediately, because we had gotten to the end of
# the list without importing anything.
vars_files:
- "vars/external_vars.yml"
- [ "vars/$facter_operatingsystem.yml", "vars/defaults.yml" ]
# and this is just a regular task line from a playbook, as we're used to.
# but with variables in it that come from above. Note that the variables
# from above are *also* available in templates
tasks:
- name: ensure apache is latest
action: $packager pkg=$apache state=latest
- name: ensure apache is running
action: service name=$apache state=running
- name: fail
action: command /bin/false

View File

@ -0,0 +1,47 @@
---
# this is a demo of conditional executions using 'only_if', which can skip
# certain tasks on machines/platforms/etc where they do not apply.
- hosts: all
user: root
vars:
favcolor: "red"
ssn: 8675309
# Below we're going to define some expressions.
#
# Not only can we assign variables for reuse, but we can also assign conditional
# expressions. By keeping these in 'vars', the task section remains
# extraordinarily clean, and not littered with programming language
# constructs -- so it's easily skimmed by humans.
#
# Remember to quote any variables if they are not numbers!
#
# Interesting fact: aside from the $variables, these expressions are actually
# tiny bits of Python. They are evaluated in the context of each host, so different
# steps can be skipped on different hosts! They should evaluate to either True
# or False
is_favcolor_blue: "'$favcolor' == 'blue'"
is_centos: "'$facter_operatingsystem' == 'CentOS'"
# NOTE:
#
# facter and ohai variables can be used in only_if statements too
# ex: "'$facter_operatingsystem' == 'CentOS'", which bubble up automatically
# from the managed machines
#
# this example won't do that though, as you might not have facter or ohai,
# but you get the idea...
tasks:
- name: "do this if my favcolor is blue"
action: shell /bin/false
only_if: '$is_favcolor_blue'
- name: "do this if my favcolor is not blue"
action: shell /bin/true
only_if: 'not ($is_favcolor_blue)'

View File

@ -0,0 +1,18 @@
---
# This is a demo of how to manage the selinux context using the file module
- hosts: test
user: root
tasks:
- name: Change setype of /etc/exports to non-default value
action: file path=/etc/exports setype=etc_t
- name: Change seuser of /etc/exports to non-default value
action: file path=/etc/exports seuser=unconfined_u
- name: Set selinux context back to default value
action: file path=/etc/exports context=default
- name: Create empty file
action: command /bin/touch /tmp/foo
- name: Change setype of /tmp/foo
action: file path=/tmp/foo setype=default_t
- name: Try to set secontext to default, but this will fail
because of the lack of a default in the policy
action: file path=/tmp/foo context=default

View File

@ -0,0 +1,17 @@
---
# This is a demo of how the group command works.
- hosts: all
user: root
tasks:
# Walk through group creation, modification, and deletion
- name: create a group
action: group name=tset
# You can only modify the group's gid
- action: group name=tset gid=7777
# And finally remove the group
- action: group name=tset state=absent

View File

@ -0,0 +1,10 @@
---
# this is an example to show that handlers can be included from yaml files,
# to promote reuse between different plays or even playbooks. They work
# just like normal handlers.
- name: restart apache
action: service name=httpd state=restarted
- name: restart memcached
action: service name=memcached state=restarted

View File

@ -0,0 +1,89 @@
---
# see examples.yml first!
# This file explains some more advanced features of playbooks.
# because of the comments it's less concise than it normally is. But feel
# free to comment your playbooks if you like.
- hosts: dbservers
# we can define variables the normal way...
vars:
release: 2.0
# but they can also come from other files. This can be a relative
# or absolute path. This is a good way to store 'secret' variable
# files but still keep the playbook in public source control
vars_files:
- vars/external_vars.yml
# as with before, every play has a list of tasks in it
tasks:
# tasks can be written the normal way...
- name: arbitrary command
action: command /bin/true
# or we can promote reuse and simplicity by including tasks
# from other files, for instance, to reuse common tasks
- include: tasks/base.yml
# we could also have done something like:
# - include: wordpress.yml user=timmy
# and had access to the template variable {{ user }} in the
# included file, if we wanted to. Variables from vars
# and vars_files are also available inside include files
handlers:
# handlers can also be included from files, to promote reuse
# and simpler recipes, you may wish to only have one
# handler file for all your plays and playbooks
- include: handlers/handlers.yml
# you can mix things that are directly in the file with things
# that are included. Order is executed as written, but only
# handlers that have been notified get executed
- name: restart foo
action: service name=foo state=restarted
# ===============================================================
# Here's a second play in the same playbook. This will be run
# after the first playbook completes on all hosts. You may want
# a different play for each class of systems, or may want a different
# play for each stage in a complex multi-node deployment push
# process. How you use them are up to you.
# any play in a playbook can be executed by a user other than root
# if you want. sudo support is coming too.
- hosts: webservers
user: mdehaan
# vars must be specified again for the next play in the playbook
# but can be reused by including from vars_files if you want
# you can use vars, vars_files, or both. vars_files overrides
# those set in vars.
vars:
release: 2.0
vars_files:
- vars/external_vars.yml
# these all runs as the user 'mdehaan'. If there were any handlers
# they would as well.
tasks:
- name: some random command
action: command /bin/true

View File

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

View File

@ -0,0 +1,30 @@
---
# this is an example of how to run repeated task elements over lists
# of items, for example, installing multiple packages or configuring
# multiple users
- hosts: all
user: root
tasks:
- name: install $item
action: yum pkg=$item state=installed
with_items:
- cobbler
- httpd
- name: configure user $item
action: user name=$item state=present groups=wheel
with_items:
- testuser1
- testuser2
- name: remove user $item
action: user name=$item state=absent
with_items:
- testuser1
- testuser2

View File

@ -0,0 +1,33 @@
---
# it is possible to ask for variables from the user at the start
# of a playbook run, for example, as part of a release script.
- hosts: all
user: root
# regular variables are a dictionary of keys and values
vars:
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)
vars_prompt:
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.
- name: imagine this did something interesting with $release_version
action: shell echo foo >> /tmp/$release_version-$alpha

View File

@ -0,0 +1,22 @@
---
# this is the example of an included tasks file. It contains a flat list of tasks
# they can notify other tasks, and have full access to variables from 'vars'
# or 'vars_files' directives. Further, if ohai or facter were installed on
# the remote machines, variables from those tools can be accessed on the 'action'
# line or in templates. Just prefix with 'facter_' or 'ohai_' before the particular
# variable.
# possible uses for a included yaml file might be to represent a 'class' of a system
# like defining what makes up a webserver, or you might have a common 'base.yml'
# (like this) that might be applied to all your systems as well.
- name: no selinux
action: command /usr/sbin/setenforce 0
- name: no iptables
action: service name=iptables state=stopped
- name: made up task just to show variables work here
action: command /bin/echo release is $release

View File

@ -0,0 +1,4 @@
# This is a very simple Jinja2 template representing an imaginary configuration file
# for an imaginary app.
http_port={{ http_port }}

View File

@ -0,0 +1,38 @@
---
# this is a demo of how the user commands work and how to reference salted passwords
# in vars sections. You could also use vars_files if you like (see other examples)
- hosts: all
user: root
vars:
# created with:
# crypt.crypt('This is my Password', '$1$SomeSalt')
password: $1$SomeSalt$UqddPX3r4kH3UL5jq5/ZI.
tasks:
# Walk through account creation, modification, and deletion
- name: test basic user account creation
action: user name=tset comment=TsetUser group=users shell=/sbin/nologin createhome=no
# the following is just a simple example of how you don't have to include
# the 'name' element for each task
- action: user name=tset comment=NyetUser
- action: user name=tset password=$password
# The following will add the user to supplementary groups.
# Add the user to the groups dialout and uucp.
- action: user name=tset groups=dialout,uucp
# Add the user to the groups dialout and wheel,
# This will remove tset from the group uucp.
- action: user name=tset groups=dialout,wheel
# Add the user to the group uucp. Because append=yes, the user
# will not be removed from the groups dialout and wheel.
- action: user name=tset groups=uucp append=yes
# Finally, remove the user.
- action: user name=tset state=absent

View File

@ -0,0 +1,3 @@
---
apache: httpd
packager: yum

View File

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

View File

@ -0,0 +1,3 @@
---
alpha: one
beta: two

View File

@ -0,0 +1,127 @@
#!/usr/bin/python
"""
Cobbler external inventory script
=================================
Ansible has a feature where instead of reading from /etc/ansible/hosts
as a text file, it can query external programs to obtain the list
of hosts, groups the hosts are in, and even variables to assign to each host.
To use this, copy this file over /etc/ansible/hosts and chmod +x the file.
This, more or less, allows you to keep one central database containing
info about all of your managed instances.
This script is an example of sourcing that data from Cobbler
(http://cobbler.github.com). With cobbler each --mgmt-class in cobbler
will correspond to a group in Ansible, and --ks-meta variables will be
passed down for use in templates or even in argument lines.
NOTE: The cobbler system names will not be used. Make sure a
cobbler --dns-name is set for each cobbler system. If a system
appears with two DNS names we do not add it twice because we don't want
ansible talking to it twice. The first one found will be used. If no
--dns-name is set the system will NOT be visible to ansible. We do
not add cobbler system names because there is no requirement in cobbler
that those correspond to addresses.
See http://ansible.github.com/api.html for more info
Tested with Cobbler 2.0.11.
"""
# (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
import xmlrpclib
import shlex
try:
import json
except:
import simplejson as json
# NOTE -- this file assumes Ansible is being accessed FROM the cobbler
# server, so it does not attempt to login with a username and password.
# this will be addressed in a future version of this script.
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) == 1:
systems = conn.get_item_names('system')
groups = { 'ungrouped' : [] }
for system in systems:
data = conn.get_blended_data(None, system)
dns_name = None
interfaces = data['interfaces']
for (iname, ivalue) in interfaces.iteritems():
this_dns_name = ivalue.get('dns_name', None)
if this_dns_name is not None:
dns_name = this_dns_name
if dns_name is None:
continue
classes = data['mgmt_classes']
for cls in classes:
if cls not in groups:
groups[cls] = []
# hostname is not really what we want to insert, really insert the
# first DNS name but no further DNS names
groups[cls].append(dns_name)
print json.dumps(groups)
sys.exit(0)
#####################################################
# executed with a hostname as a parameter, return the
# variables for that 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[1])
system = result.get('name', None)
data = {}
if system is None:
print json.dumps({})
sys.exit(1)
data = conn.get_system_for_koan(system)
# return the ksmeta data for that system
metadata = data['ks_meta']
tokens = shlex.split(metadata)
results = {}
for t in tokens:
if t.find("=") != -1:
(k,v) = t.split("=",1)
results[k]=v
print json.dumps(results)
sys.exit(0)

View File

@ -6,6 +6,3 @@ To use it from the root of a checkout:
$ . ./hacking/env-setup
Note the space between the '.' and the './'
Man pages will not load until you run 'make docs' from the root of the
checkout.

View File

@ -1,18 +1,20 @@
# -*- mode: shell-script -*-
PREFIX_PYTHONPATH="`pwd`/lib"
PREFIX_PATH="`pwd`/bin"
PREFIX_MANPATH="`pwd`/docs/man"
PREFIX_ANS_LIBRARY="`pwd`/library"
#!/bin/bash
# usage: source ./hacking/env-setup
# modifies environment for running Ansible from checkout
PREFIX_PYTHONPATH="$PWD/lib"
PREFIX_PATH="$PWD/bin"
PREFIX_MANPATH="$PWD/docs/man"
echo "Prefixing PYTHONPATH with $PREFIX_PYTHONPATH"
export PYTHONPATH=$PREFIX_PYTHONPATH:$PYTHONPATH
echo "Prefixing PATH with $PREFIX_PATH"
export PATH=$PREFIX_PATH:$PATH
echo $PATH
echo "Prefixing MANPATH with $PREFIX_MANPATH"
export ANSIBLE_LIBRARY="$PWD/library"
export MANPATH=$PREFIX_MANPATH:$MANPATH
echo "Setting ANSIBLE_LIBRARY to ${PREFIX_ANS_LIBRARY}"
export ANSIBLE_LIBRARY=${PREFIX_ANS_LIBRARY}
echo "PATH=$PATH"
echo "PYTHONPATH=$PYTHONPATH"
echo "ANSIBLE_LIBRARY=$ANSIBLE_LIBRARY"
echo "MANPATH=$MANPATH"
echo "Reminder: specify your host file with -i"
echo "Done."

89
hacking/test-module Executable file
View File

@ -0,0 +1,89 @@
#!/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 script is for testing modules without running through the
# entire guts of ansible, and is very helpful for when developing
# modules
#
# example:
# test-module ../library/command /bin/sleep 3
# test-module ../library/service name=httpd ensure=restarted
import sys
import os
import subprocess
import traceback
from ansible import utils
try:
import json
except ImportError:
import simplejson as json
modfile = None
if len(sys.argv) == 1:
print >>sys.stderr, "usage: test-module ./library/command [key=value ...]"
sys.exit(1)
modfile = sys.argv[1]
if len(sys.argv) > 1:
args = " ".join(sys.argv[2:])
else:
args = ""
argspath = os.path.expanduser("/.ansible_test_module_arguments")
argsfile = open(argspath, 'w')
argsfile.write(args)
argsfile.close()
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 "RECIEVED DATA ON STDOUT, WILL IGNORE THIS:"
print err
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)
print "***********************************"
print "PARSED OUTPUT"
print utils.bigjson(results)
sys.exit(0)

2
lib/ansible/__init__.py Executable file → Normal file
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.0.1'
__version__ = '0.3'
__author__ = 'Michael DeHaan'

214
lib/ansible/callbacks.py Normal file
View File

@ -0,0 +1,214 @@
# (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 utils
import sys
import getpass
#######################################################
class AggregateStats(object):
''' holds stats about per-host activity during playbook runs '''
def __init__(self):
self.processed = {}
self.failures = {}
self.ok = {}
self.dark = {}
self.changed = {}
self.skipped = {}
def _increment(self, what, host):
''' helper function to bump a statistic '''
self.processed[host] = 1
prev = (getattr(self, what)).get(host, 0)
getattr(self, what)[host] = prev+1
def compute(self, runner_results, setup=False, poll=False):
''' walk through all results and increment stats '''
for (host, value) in runner_results.get('contacted', {}).iteritems():
if ('failed' in value and bool(value['failed'])) or ('rc' in value and value['rc'] != 0):
self._increment('failures', host)
elif 'skipped' in value and bool(value['skipped']):
self._increment('skipped', host)
elif 'changed' in value and bool(value['changed']):
if not setup:
self._increment('changed', host)
self._increment('ok', host)
else:
if not poll or ('finished' in value and bool(value['finished'])):
self._increment('ok', host)
for (host, value) in runner_results.get('dark', {}).iteritems():
self._increment('dark', host)
def summarize(self, host):
''' return information about a particular host '''
return dict(
ok = self.ok.get(host, 0),
failures = self.failures.get(host, 0),
unreachable = self.dark.get(host,0),
changed = self.changed.get(host, 0),
skipped = self.skipped.get(host, 0)
)
########################################################################
class DefaultRunnerCallbacks(object):
''' no-op callbacks for API usage of Runner() if no callbacks are specified '''
def __init__(self):
pass
def on_failed(self, host, res):
pass
def on_ok(self, host, res):
pass
def on_error(self, host, msg):
pass
def on_skipped(self, host):
pass
def on_unreachable(self, host, res):
pass
def on_no_hosts(self):
pass
########################################################################
class CliRunnerCallbacks(DefaultRunnerCallbacks):
''' callbacks for use by /usr/bin/ansible '''
def __init__(self):
# set by /usr/bin/ansible later
self.options = None
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):
print "%s | FAILED => %s" % (host, res)
if self.options.tree:
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, "stderr: [%s] => %s\n" % (host, err)
def on_no_hosts(self):
print >>sys.stderr, "no hosts matched\n"
def _on_any(self, host, result):
print utils.host_report_msg(host, self.options.module_name, result, self.options.one_line)
if self.options.tree:
utils.write_tree_file(self.options.tree, host, utils.bigjson(result))
########################################################################
class PlaybookRunnerCallbacks(DefaultRunnerCallbacks):
''' callbacks used for Runner() from /usr/bin/ansible-playbook '''
def __init__(self, stats):
self.stats = stats
def on_unreachable(self, host, msg):
print "unreachable: [%s] => %s" % (host, msg)
def on_failed(self, host, results):
invocation = results.get('invocation',None)
if not invocation or invocation.startswith('setup ') or invocation.startswith('async_status '):
print "failed: [%s] => %s\n" % (host, utils.smjson(results))
else:
print "failed: [%s] => %s => %s\n" % (host, invocation, utils.smjson(results))
def on_ok(self, host, host_result):
invocation = host_result.get('invocation','')
if invocation.startswith('async_status'):
pass
elif not invocation or invocation.startswith('setup '):
print "ok: [%s]\n" % (host)
else:
print "ok: [%s] => %s\n" % (host, invocation)
def on_error(self, host, err):
print >>sys.stderr, "stderr: [%s] => %s\n" % (host, err)
def on_skipped(self, host):
print "skipping: [%s]\n" % host
def on_no_hosts(self):
print "no hosts matched or remaining\n"
########################################################################
class PlaybookCallbacks(object):
''' playbook.py callbacks used by /usr/bin/ansible-playbook '''
def __init__(self):
pass
def on_start(self):
print "\n"
def on_notify(self, host, handler):
pass
def on_task_start(self, name, is_conditional):
print utils.task_start_msg(name, is_conditional)
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_primary(self):
print "SETUP PHASE ****************************\n"
def on_setup_secondary(self):
print "\nVARIABLE IMPORT PHASE ******************\n"
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):
print "%s: not importing file: %s" % (host, missing_file)
def on_play_start(self, pattern):
print "PLAY [%s] ****************************\n" % pattern
def on_async_confused(self, msg):
print msg
def on_async_poll(self, jid, host, clock, host_result):
print utils.async_poll_status(jid, host, clock, host_result)

234
lib/ansible/connection.py Normal file
View File

@ -0,0 +1,234 @@
# (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 warnings
# prevent paramiko warning noise
# see http://stackoverflow.com/questions/3920502/
with warnings.catch_warnings():
warnings.simplefilter("ignore")
import paramiko
import traceback
import os
import time
import random
import re
import shutil
import subprocess
from ansible import errors
################################################
class Connection(object):
''' Handles abstract connections to remote hosts '''
_LOCALHOSTRE = re.compile(r"^(127.0.0.1|localhost|%s)$" % os.uname()[1])
def __init__(self, runner, transport):
self.runner = runner
self.transport = transport
def connect(self, host, port=None):
conn = None
if self.transport == 'local' and self._LOCALHOSTRE.search(host):
conn = LocalConnection(self.runner, host, None)
elif self.transport == 'paramiko':
conn = ParamikoConnection(self.runner, host, port)
if conn is None:
raise Exception("unsupported connection type")
return conn.connect()
################################################
# want to implement another connection type?
# follow duck-typing of ParamikoConnection
# you may wish to read config files in __init__
# if you have any. Paramiko does not need any.
class ParamikoConnection(object):
''' SSH based connections with Paramiko '''
def __init__(self, runner, host, port=None):
self.ssh = None
self.runner = runner
self.host = host
self.port = port
if port is None:
self.port = self.runner.remote_port
def _get_conn(self):
ssh = paramiko.SSHClient()
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
try:
ssh.connect(
self.host,
username=self.runner.remote_user,
allow_agent=True,
look_for_keys=True,
password=self.runner.remote_pass,
timeout=self.runner.timeout,
port=self.port
)
except Exception, e:
if str(e).find("PID check failed") != -1:
raise errors.AnsibleError("paramiko version issue, please upgrade paramiko on the machine running ansible")
else:
raise errors.AnsibleConnectionFailed(str(e))
return ssh
def connect(self):
''' connect to the remote host '''
self.ssh = self._get_conn()
return self
def exec_command(self, cmd, tmp_path, sudoable=False):
''' run a command on the remote host '''
if not self.runner.sudo or not sudoable:
stdin, stdout, stderr = self.ssh.exec_command(cmd)
return (stdin, stdout, stderr)
else:
# percalculated tmp_path is ONLY required for sudo usage
if tmp_path is None:
raise Exception("expecting tmp_path")
r = random.randint(0,99999)
# invoke command using a new connection over sudo
result_file = os.path.join(tmp_path, "sudo_result.%s" % r)
self.ssh.close()
ssh_sudo = self._get_conn()
sudo_chan = ssh_sudo.invoke_shell()
sudo_chan.send("sudo -s\n")
# FIXME: using sudo with a password adds more delay, someone may wish
# to optimize to see when the channel is actually ready
if self.runner.sudo_pass:
time.sleep(0.1) # this is conservative
sudo_chan.send("%s\n" % self.runner.sudo_pass)
time.sleep(0.1)
# to avoid ssh expect logic, redirect output to file and move the
# file when we are done with it...
sudo_chan.send("(%s >%s_pre 2>/dev/null ; mv %s_pre %s) &\n" % (cmd, result_file, result_file, result_file))
# FIXME: someone may wish to optimize to not background the launch, and tell when the command
# returns, removing the time.sleep(1) here
time.sleep(1)
sudo_chan.close()
self.ssh = self._get_conn()
# now load the results of the JSON execution...
# FIXME: really need some timeout logic here
# though it doesn't make since to use the SSH timeout or impose any particular
# limit. Upgrades welcome.
sftp = self.ssh.open_sftp()
while True:
# print "waiting on %s" % result_file
time.sleep(1)
try:
sftp.stat(result_file)
break
except IOError:
pass
sftp.close()
# TODO: see if there's a SFTP way to just get the file contents w/o saving
# to disk vs this hack...
stdin, stdout, stderr = self.ssh.exec_command("cat %s" % result_file)
return (stdin, stdout, stderr)
def put_file(self, in_path, out_path):
''' transfer a file from local to remote '''
if not os.path.exists(in_path):
raise errors.AnsibleFileNotFound("file or module does not exist: %s" % in_path)
sftp = self.ssh.open_sftp()
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):
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()
############################################
# add other connection types here
class LocalConnection(object):
''' Local based connections '''
def __init__(self, runner, host):
self.runner = runner
self.host = host
def connect(self, port=None):
''' connect to the local host; nothing to do here '''
return self
def exec_command(self, cmd, tmp_path, sudoable=False):
''' run a command on the local host '''
if self.runner.sudo and sudoable:
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)
stdout, stderr = p.communicate()
return ("", stdout, stderr)
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:
shutil.copyfile(in_path, out_path)
except shutil.Error:
traceback.print_exc()
raise errors.AnsibleError("failed to copy: %s and %s are the same" % (in_path, out_path))
except IOError:
traceback.print_exc()
raise errors.AnsibleError("failed to transfer file to %s" % out_path)
def fetch_file(self, in_path, out_path):
''' fetch a file from local to local -- for copatibility '''
self.put_file(in_path, out_path)
def close(self):
''' terminate the connection; nothing to do here '''
pass

View File

@ -19,15 +19,21 @@
import os
# control side (aka 'overlord')
DEFAULT_HOST_LIST = os.environ.get('ANSIBLE_HOSTS',
'/etc/ansible/hosts')
'/etc/ansible/hosts')
DEFAULT_MODULE_PATH = os.environ.get('ANSIBLE_LIBRARY',
'/usr/share/ansible')
'/usr/share/ansible')
DEFAULT_MODULE_NAME = 'command'
DEFAULT_PATTERN = '*'
DEFAULT_FORKS = 5
DEFAULT_MODULE_ARGS = ''
DEFAULT_TIMEOUT = 10
DEFAULT_POLL_INTERVAL = 15
DEFAULT_REMOTE_USER = 'root'
DEFAULT_REMOTE_PASS = None
DEFAULT_SUDO_PASS = None
DEFAULT_REMOTE_PORT = 22
DEFAULT_TRANSPORT = 'paramiko'
DEFAULT_TRANSPORT_OPTS = ['local', 'paramiko']

37
lib/ansible/errors.py Normal file
View File

@ -0,0 +1,37 @@
# (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/>.
class AnsibleError(Exception):
"""
The base Ansible exception from which all others should subclass.
"""
def __init__(self, msg):
self.msg = msg
def __str__(self):
return self.msg
class AnsibleFileNotFound(AnsibleError):
pass
class AnsibleConnectionFailed(AnsibleError):
pass

292
lib/ansible/inventory.py Normal file
View File

@ -0,0 +1,292 @@
# (c) 2012, Michael DeHaan <michael.dehaan@gmail.com>
#
# This file is part of Ansible
#
# Ansible is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Ansible is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
#############################################
import fnmatch
import os
import subprocess
import constants as C
from ansible import errors
from ansible import utils
class Inventory(object):
""" Host inventory for ansible.
The inventory is either a simple text file with systems and [groups] of
systems, or a script that will be called with --list or --host.
"""
def __init__(self, host_list=C.DEFAULT_HOST_LIST):
self._restriction = None
self._variables = {}
if type(host_list) == list:
self.host_list = host_list
self.groups = dict(ungrouped=host_list)
self._is_script = False
return
inventory_file = os.path.expanduser(host_list)
if not os.path.exists(inventory_file):
raise errors.AnsibleFileNotFound("inventory file not found: %s" % host_list)
self.inventory_file = os.path.abspath(inventory_file)
if os.access(self.inventory_file, os.X_OK):
self.host_list, self.groups = self._parse_from_script()
self._is_script = True
else:
self.host_list, self.groups = self._parse_from_file()
self._is_script = False
# *****************************************************
# Public API
def list_hosts(self, pattern="all"):
""" Return a list of hosts [matching the pattern] """
if self._restriction is None:
host_list = self.host_list
else:
host_list = [ h for h in self.host_list if h in self._restriction ]
return [ h for h in host_list if self._matches(h, pattern) ]
def restrict_to(self, restriction):
""" Restrict list operations to the hosts given in restriction """
if type(restriction)!=list:
restriction = [ restriction ]
self._restriction = restriction
def lift_restriction(self):
""" Do not restrict list operations """
self._restriction = None
def get_variables(self, host):
""" Return the variables associated with this host. """
if host in self._variables:
return self._variables[host].copy()
if not self._is_script:
return {}
return self._get_variables_from_script(host)
# *****************************************************
def _parse_from_file(self):
''' parse a textual host file '''
results = []
groups = dict(ungrouped=[])
lines = file(self.inventory_file).read().split("\n")
if "---" in lines:
return self._parse_yaml()
group_name = 'ungrouped'
for item in lines:
item = item.lstrip().rstrip()
if item.startswith("#"):
# ignore commented out lines
pass
elif item.startswith("["):
# looks like a group
group_name = item.replace("[","").replace("]","").lstrip().rstrip()
groups[group_name] = []
elif item != "":
# looks like a regular host
if ":" in item:
# a port was specified
item, port = item.split(":")
try:
port = int(port)
except ValueError:
raise errors.AnsibleError("SSH port for %s in inventory (%s) should be numerical."%(item, port))
self._set_variable(item, "ansible_ssh_port", port)
groups[group_name].append(item)
if not item in results:
results.append(item)
return (results, groups)
# *****************************************************
def _parse_from_script(self):
''' evaluate a script that returns list of hosts by groups '''
results = []
groups = dict(ungrouped=[])
cmd = [self.inventory_file, '--list']
cmd = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=False)
out, err = cmd.communicate()
rc = cmd.returncode
if rc:
raise errors.AnsibleError("%s: %s" % self.inventory_file, err)
try:
groups = utils.json_loads(out)
except:
raise errors.AnsibleError("invalid JSON response from script: %s" % self.inventory_file)
for (groupname, hostlist) in groups.iteritems():
for host in hostlist:
if host not in results:
results.append(host)
return (results, groups)
# *****************************************************
def _parse_yaml(self):
""" Load the inventory from a yaml file.
returns hosts and groups"""
data = utils.parse_yaml_from_file(self.inventory_file)
if type(data) != list:
raise errors.AnsibleError("YAML inventory should be a list.")
hosts = []
groups = {}
ungrouped = []
for item in data:
if type(item) == dict:
if "group" in item:
group_name = item["group"]
group_vars = []
if "vars" in item:
group_vars = item["vars"]
group_hosts = []
if "hosts" in item:
for host in item["hosts"]:
host_name = self._parse_yaml_host(host, group_vars)
group_hosts.append(host_name)
groups[group_name] = group_hosts
hosts.extend(group_hosts)
elif "host" in item:
host_name = self._parse_yaml_host(item)
hosts.append(host_name)
ungrouped.append(host_name)
else:
host_name = self._parse_yaml_host(item)
hosts.append(host_name)
ungrouped.append(host_name)
# filter duplicate hosts
output_hosts = []
for host in hosts:
if host not in output_hosts:
output_hosts.append(host)
if len(ungrouped) > 0 :
# hosts can be defined top-level, but also in a group
really_ungrouped = []
for host in ungrouped:
already_grouped = False
for name, group_hosts in groups.items():
if host in group_hosts:
already_grouped = True
if not already_grouped:
really_ungrouped.append(host)
groups["ungrouped"] = really_ungrouped
return output_hosts, groups
def _parse_yaml_host(self, item, variables=[]):
def set_variables(host, variables):
if type(variables) == list:
for variable in variables:
if len(variable) != 1:
raise errors.AnsibleError("Only one item expected in %s"%(variable))
k, v = variable.items()[0]
self._set_variable(host, k, v)
elif type(variables) == dict:
for k, v in variables.iteritems():
self._set_variable(host, k, v)
if type(item) in [str, unicode]:
set_variables(item, variables)
return item
elif type(item) == dict:
if "host" in item:
host_name = item["host"]
set_variables(host_name, variables)
if "vars" in item:
set_variables(host_name, item["vars"])
return host_name
else:
raise errors.AnsibleError("Unknown item in inventory: %s"%(item))
def _get_variables_from_script(self, host):
''' support per system variabes from external variable scripts, see web docs '''
cmd = [self.inventory_file, '--host', host]
cmd = subprocess.Popen(cmd,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
shell=False
)
out, err = cmd.communicate()
variables = {}
try:
variables = utils.json_loads(out)
except:
raise errors.AnsibleError("%s returned invalid result when called with hostname %s" % (
self.inventory_file,
host
))
return variables
def _set_variable(self, host, key, value):
if not host in self._variables:
self._variables[host] = {}
self._variables[host][key] = value
def _matches(self, host_name, pattern):
''' returns if a hostname is matched by the pattern '''
# a pattern is in fnmatch format but more than one pattern
# can be strung together with semicolons. ex:
# atlanta-web*.example.com;dc-web*.example.com
if host_name == '':
return False
pattern = pattern.replace(";",":")
subpatterns = pattern.split(":")
for subpattern in subpatterns:
if subpattern == 'all':
return True
if fnmatch.fnmatch(host_name, subpattern):
return True
elif subpattern in self.groups:
if host_name in self.groups[subpattern]:
return True
return False

609
lib/ansible/playbook.py Executable file → Normal file
View File

@ -17,13 +17,13 @@
#############################################
import ansible.inventory
import ansible.runner
import ansible.constants as C
from ansible.utils import *
import yaml
import shlex
from ansible import utils
from ansible import errors
import os
import jinja2
import time
# used to transfer variables to Runner
SETUP_CACHE={ }
@ -38,84 +38,148 @@ class PlayBook(object):
management, or automation based set of commands to
run in series.
multiple patterns do not execute simultaneously,
multiple plays/tasks do not execute simultaneously,
but tasks in each pattern do execute in parallel
according to the number of forks requested.
(according to the number of forks requested) among
the hosts they address
'''
# *****************************************************
def __init__(self,
playbook =None,
host_list =C.DEFAULT_HOST_LIST,
module_path =C.DEFAULT_MODULE_PATH,
forks =C.DEFAULT_FORKS,
timeout =C.DEFAULT_TIMEOUT,
remote_user =C.DEFAULT_REMOTE_USER,
remote_pass =C.DEFAULT_REMOTE_PASS,
verbose=False,
callbacks=None):
playbook = None,
host_list = C.DEFAULT_HOST_LIST,
module_path = C.DEFAULT_MODULE_PATH,
forks = C.DEFAULT_FORKS,
timeout = C.DEFAULT_TIMEOUT,
remote_user = C.DEFAULT_REMOTE_USER,
remote_pass = C.DEFAULT_REMOTE_PASS,
sudo_pass = C.DEFAULT_SUDO_PASS,
remote_port = C.DEFAULT_REMOTE_PORT,
transport = C.DEFAULT_TRANSPORT,
override_hosts = None,
debug = False,
callbacks = None,
runner_callbacks = None,
stats = None,
sudo = False):
# TODO, once ansible-playbook is it's own script this will
# have much LESS parameters to the constructor and will
# read most everything per pattern from the playbook
# and this will be greatly simplified
"""
playbook: path to a playbook file
host_list: path to a file like /etc/ansible/hosts
module_path: path to ansible modules, like /usr/share/ansible/
forks: desired level of paralellism
timeout: connection timeout
remote_user: run as this user if not specified in a particular play
remote_pass: use this remote password (for all plays) vs using SSH keys
sudo_pass: if sudo==True, and a password is required, this is the sudo password
remote_port: default remote port to use if not specified with the host or play
transport: how to connect to hosts that don't specify a transport (local, paramiko, etc)
override_hosts: skip the inventory file, just talk to these hosts
callbacks output callbacks for the playbook
runner_callbacks: more callbacks, this time for the runner API
stats: holds aggregrate data about events occuring to each host
sudo: if not specified per play, requests all plays use sudo mode
"""
self.host_list = host_list
self.module_path = module_path
self.forks = forks
self.timeout = timeout
self.remote_user = remote_user
self.remote_pass = remote_pass
self.verbose = verbose
self.callbacks = callbacks
self.callbacks.set_playbook(self)
if playbook is None or callbacks is None or runner_callbacks is None or stats is None:
raise Exception('missing required arguments')
# store the list of changes/invocations/failure counts
# as a dictionary of integers keyed off the hostname
self.module_path = module_path
self.forks = forks
self.timeout = timeout
self.remote_user = remote_user
self.remote_pass = remote_pass
self.remote_port = remote_port
self.transport = transport
self.debug = debug
self.callbacks = callbacks
self.runner_callbacks = runner_callbacks
self.override_hosts = override_hosts
self.stats = stats
self.sudo = sudo
self.sudo_pass = sudo_pass
self.processed = {}
self.dark = {}
self.changed = {}
self.invocations = {}
self.failures = {}
self.basedir = os.path.dirname(playbook)
self.playbook = self._parse_playbook(playbook)
# playbook file can be passed in as a path or
# as file contents (to support API usage)
if override_hosts is not None:
if type(override_hosts) != list:
raise errors.AnsibleError("override hosts must be a list")
self.inventory = ansible.inventory.Inventory(override_hosts)
else:
self.inventory = ansible.inventory.Inventory(host_list)
self.basedir = os.path.dirname(playbook)
self.playbook = self._parse_playbook(playbook)
# *****************************************************
def _get_vars(self, play, dirname):
''' load the vars section from a play '''
if play.get('vars') is None:
play['vars'] = {}
vars = play['vars']
if type(vars) not in [dict, list]:
raise errors.AnsibleError("'vars' section must contain only key/value pairs")
# translate a list of vars into a dict
if type(vars) == list:
varlist = vars
vars = {}
for item in varlist:
k, v = item.items()[0]
vars[k] = v
play['vars'] = vars
vars_prompt = play.get('vars_prompt', {})
if type(vars_prompt) != dict:
raise errors.AnsibleError("'vars_prompt' section must contain only key/value pairs")
for vname in vars_prompt:
print vars_prompt[vname]
# FIXME - need some way to know that this prompt should be getpass or raw_input
vars[vname] = self.callbacks.on_vars_prompt(vname)
return vars
# *****************************************************
def _include_tasks(self, play, task, dirname, new_tasks):
# an include line looks like:
''' load tasks included from external files. '''
# include: some.yml a=2 b=3 c=4
include_tokens = task['include'].split()
path = path_dwim(dirname, include_tokens[0])
inject_vars = play.get('vars', {})
path = utils.path_dwim(dirname, include_tokens[0])
play_vars = self._get_vars(play, dirname)
include_vars = {}
for i,x in enumerate(include_tokens):
if x.find("=") != -1:
(k,v) = x.split("=")
inject_vars[k] = v
included = file(path).read()
template = jinja2.Template(included)
included = template.render(inject_vars)
included = yaml.load(included)
for x in included:
new_tasks.append(x)
include_vars[k] = v
inject_vars = play_vars.copy()
inject_vars.update(include_vars)
included = utils.template_from_file(path, inject_vars)
included = utils.parse_yaml(included)
for x in included:
if len(include_vars):
x["vars"] = include_vars
new_tasks.append(x)
# *****************************************************
def _include_handlers(self, play, handler, dirname, new_handlers):
path = path_dwim(dirname, handler['include'])
included = file(path).read()
inject_vars = play.get('vars', {})
template = jinja2.Template(included)
included = template.render(inject_vars)
included = yaml.load(included)
''' load handlers from external files '''
path = utils.path_dwim(dirname, handler['include'])
inject_vars = self._get_vars(play, dirname)
included = utils.template_from_file(path, inject_vars)
included = utils.parse_yaml(included)
for x in included:
new_handlers.append(x)
# *****************************************************
def _parse_playbook(self, playbook):
''' load YAML file, including handling for imported files '''
dirname = os.path.dirname(playbook)
playbook = yaml.load(file(playbook).read())
playbook = utils.parse_yaml_from_file(playbook)
for play in playbook:
tasks = play.get('tasks',[])
@ -124,22 +188,47 @@ class PlayBook(object):
# process tasks in this file as well as imported tasks
new_tasks = []
for task in tasks:
if 'include' in task:
self._include_tasks(play, task, dirname, new_tasks)
else:
new_tasks.append(task)
play['tasks'] = new_tasks
if 'include' in task:
self._include_tasks(play, task, dirname, new_tasks)
else:
new_tasks.append(task)
# now new_tasks contains a list of tasks, but tasks may contain
# lists of with_items to loop over. Do that.
# TODO: refactor into subfunction
new_tasks2 = []
for task in new_tasks:
if 'with_items' in task:
for item in task['with_items']:
produced_task = {}
name = task.get('name', task.get('action', 'unnamed task'))
action = task.get('action', None)
only_if = task.get('only_if', None)
if action is None:
raise errors.AnsibleError('action is required')
produced_task = task.copy()
produced_task['action'] = utils.template(action, dict(item=item), SETUP_CACHE)
produced_task['name'] = utils.template(name, dict(item=item), SETUP_CACHE)
if only_if:
produced_task['only_if'] = utils.template(only_if, dict(item=item), SETUP_CACHE)
new_tasks2.append(produced_task)
else:
new_tasks2.append(task)
play['tasks'] = new_tasks2
# process handlers as well as imported handlers
new_handlers = []
for handler in handlers:
if 'include' in handler:
self._include_handlers(play, handler, dirname, new_handlers)
else:
new_handlers.append(handler)
if 'include' in handler:
self._include_handlers(play, handler, dirname, new_handlers)
else:
new_handlers.append(handler)
play['handlers'] = new_handlers
return playbook
# *****************************************************
def run(self):
''' run all patterns in the playbook '''
@ -147,67 +236,153 @@ class PlayBook(object):
# loop through all patterns and run them
self.callbacks.on_start()
for pattern in self.playbook:
self._run_pattern(pattern)
self._run_play(pattern)
# summarize the results
results = {}
for host in self.processed.keys():
results[host] = {
'resources' : self.invocations.get(host, 0),
'changed' : self.changed.get(host, 0),
'dark' : self.dark.get(host, 0),
'failed' : self.failures.get(host, 0)
}
for host in self.stats.processed.keys():
results[host] = self.stats.summarize(host)
return results
def _prune_failed_hosts(self, host_list):
new_hosts = []
for x in host_list:
if not x in self.failures and not x in self.dark:
new_hosts.append(x)
return new_hosts
# *****************************************************
def _run_module(self, pattern, module, args, hosts, remote_user):
def hosts_to_poll(self, results):
''' which hosts need more polling? '''
hosts = []
for (host, res) in results['contacted'].iteritems():
if (host in self.stats.failures) or (host in self.stats.dark):
continue
if not 'finished' in res and not 'skipped' in res and 'started' in res:
hosts.append(host)
return hosts
# *****************************************************
def _async_poll(self, runner, hosts, async_seconds, async_poll_interval, only_if):
''' launch an async job, if poll_interval is set, wait for completion '''
runner.background = async_seconds
results = runner.run()
self.stats.compute(results, poll=True)
if async_poll_interval <= 0:
# if not polling, playbook requested fire and forget
# trust the user wanted that and return immediately
return results
poll_hosts = results['contacted'].keys()
if len(poll_hosts) == 0:
# no hosts launched ok, return that.
return results
ahost = poll_hosts[0]
jid = results['contacted'][ahost].get('ansible_job_id', None)
if jid is None:
# note: this really shouldn't happen, ever
self.callbacks.on_async_confused("unexpected error: unable to determine jid")
return results
clock = async_seconds
host_list = self.hosts_to_poll(results)
poll_results = results
while (clock >= 0):
# poll/loop until polling duration complete
runner.module_args = "jid=%s" % jid
runner.module_name = 'async_status'
runner.background = 0
runner.pattern = '*'
self.inventory.restrict_to(host_list)
poll_results = runner.run()
self.stats.compute(poll_results, poll=True)
host_list = self.hosts_to_poll(poll_results)
self.inventory.lift_restriction()
if len(host_list) == 0:
break
if poll_results is None:
break
# mention which hosts we're going to poll again...
for (host, host_result) in poll_results['contacted'].iteritems():
results['contacted'][host] = host_result
if not host in self.stats.dark and not host in self.stats.failures:
self.callbacks.on_async_poll(jid, host, clock, host_result)
# run down the clock
clock = clock - async_poll_interval
time.sleep(async_poll_interval)
# mark any hosts that are still listed as started as failed
# since these likely got killed by async_wrapper
for (host, host_result) in poll_results['contacted'].iteritems():
if 'started' in host_result:
reason = { 'failed' : 1, 'rc' : None, 'msg' : 'timed out' }
self.runner_callbacks.on_failed(host, reason)
results['contacted'][host] = reason
return results
# *****************************************************
def _run_module(self, pattern, module, args, vars, remote_user,
async_seconds, async_poll_interval, only_if, sudo, transport, port):
''' run a particular module step in a playbook '''
return ansible.runner.Runner(
pattern=pattern,
module_name=module,
module_args=args,
host_list=hosts,
forks=self.forks,
remote_pass=self.remote_pass,
module_path=self.module_path,
timeout=self.timeout,
remote_user=remote_user,
setup_cache=SETUP_CACHE,
basedir=self.basedir
).run()
def _run_task(self, pattern=None, task=None, host_list=None,
remote_user=None, handlers=None, conditional=False):
'''
run a single task in the playbook and
recursively run any subtasks.
'''
hosts = [ h for h in self.inventory.list_hosts() if (h not in self.stats.failures) and (h not in self.stats.dark)]
self.inventory.restrict_to(hosts)
if host_list is None:
# pruned host lists occur when running triggered
# actions where not all hosts have changed
# though top-level tasks will pass in "None" here
host_list = self.host_list
(host_list, groups) = ansible.runner.Runner.parse_hosts(host_list)
if port is None:
port=self.remote_port
# do not continue to run tasks on hosts that have had failures
host_list = self._prune_failed_hosts(host_list)
runner = ansible.runner.Runner(
pattern=pattern, inventory=self.inventory, module_name=module,
module_args=args, forks=self.forks,
remote_pass=self.remote_pass, module_path=self.module_path,
timeout=self.timeout, remote_user=remote_user,
remote_port=port, module_vars=vars,
setup_cache=SETUP_CACHE, basedir=self.basedir,
conditional=only_if, callbacks=self.runner_callbacks,
debug=self.debug, sudo=sudo,
transport=transport, sudo_pass=self.sudo_pass, is_playbook=True
)
if async_seconds == 0:
results = runner.run()
else:
results = self._async_poll(runner, hosts, async_seconds, async_poll_interval, only_if)
self.inventory.lift_restriction()
return results
# *****************************************************
def _run_task(self, pattern=None, task=None,
remote_user=None, handlers=None, conditional=False, sudo=False, transport=None, port=None):
''' run a single task in the playbook and recursively run any subtasks. '''
# load the module name and parameters from the task entry
name = task['name']
action = task['action']
# comment = task.get('comment', '')
name = task.get('name', None)
action = task.get('action', None)
if action is None:
raise errors.AnsibleError("action is required for each item in tasks: offending task is %s" % name if name else "unknown")
if name is None:
name = action
only_if = task.get('only_if', 'True')
async_seconds = int(task.get('async', 0)) # not async by default
async_poll_interval = int(task.get('poll', 10)) # default poll = 10 seconds
tokens = shlex.split(action)
tokens = action.split(None, 1)
module_name = tokens[0]
module_args = tokens[1:]
module_args = ''
if len(tokens) > 1:
module_args = tokens[1]
# include task specific vars
module_vars = task.get('vars')
# tasks can be direct (run on all nodes matching
# the pattern) or conditional, where they ran
@ -219,49 +394,16 @@ class PlayBook(object):
# load up an appropriate ansible runner to
# run the task in parallel
results = self._run_module(pattern, module_name,
module_args, host_list, remote_user)
module_args, module_vars, remote_user, async_seconds,
async_poll_interval, only_if, sudo, transport, port)
self.stats.compute(results)
# if no hosts are matched, carry on, unlike /bin/ansible
# which would warn you about this
if results is None:
results = {}
# walk through the results and build up
# summary information about successes and
# failures. TODO: split into subfunction
dark = results.get("dark", {})
contacted = results.get("contacted", {})
for host, msg in dark.items():
self.processed[host] = 1
self.callbacks.on_unreachable(host, msg)
if not host in self.dark:
self.dark[host] = 1
else:
self.dark[host] = self.dark[host] + 1
for host, results in contacted.items():
self.processed[host] = 1
if is_failed(results):
self.callbacks.on_failed(host, results)
if not host in self.failures:
self.failures[host] = 1
else:
self.failures[host] = self.failures[host] + 1
else:
self.callbacks.on_ok(host)
if not host in self.invocations:
self.invocations[host] = 1
else:
self.invocations[host] = self.invocations[host] + 1
if results.get('changed', False):
if not host in self.changed:
self.changed[host] = 1
else:
self.changed[host] = self.changed[host] + 1
# flag which notify handlers need to be run
# this will be on a SUBSET of the actual host list. For instance
# a file might need to be written on only half of the nodes so
@ -269,11 +411,13 @@ class PlayBook(object):
subtasks = task.get('notify', [])
if len(subtasks) > 0:
for host, results in contacted.items():
for host, results in results.get('contacted',{}).iteritems():
if results.get('changed', False):
for subtask in subtasks:
self._flag_handler(handlers, subtask, host)
# *****************************************************
def _flag_handler(self, handlers, match_name, host):
'''
if a task has any notify elements, flag handlers for run
@ -284,64 +428,148 @@ class PlayBook(object):
# for all registered handlers in the ansible playbook
# for this particular pattern group
found = False
for x in handlers:
name = x['name']
name = x.get('name', None)
if name is None:
raise errors.AnsibleError('handler is missing a name')
if match_name == name:
# flag the handler with the list of hosts
# it needs to be run on, it will be run later
found = True
self.callbacks.on_notify(host, name)
# flag the handler with the list of hosts it needs to be run on, it will be run later
if not 'run' in x:
x['run'] = []
x['run'].append(host)
if not found:
raise errors.AnsibleError("change handler (%s) is not defined" % match_name)
def _run_pattern(self, pg):
'''
run a list of tasks for a given pattern, in order
'''
# *****************************************************
# get configuration information about the pattern
pattern = pg['hosts']
vars = pg.get('vars', {})
tasks = pg['tasks']
handlers = pg['handlers']
user = pg.get('user', C.DEFAULT_REMOTE_USER)
def _do_conditional_imports(self, vars_files):
''' handle the vars_files section, which can contain variables '''
# FIXME: save parsed variable results in memory to avoid excessive re-reading/parsing
# FIXME: currently parses imports for hosts not in the pattern, that is not wrong, but it's
# not super optimized yet either, because we wouldn't have hit them, ergo
# it will raise false errors if there is no defaults variable file without any $vars
# in it, which could happen on uncontacted hosts.
if type(vars_files) != list:
raise errors.AnsibleError("vars_files must be a list")
for host in self.inventory.list_hosts():
cache_vars = SETUP_CACHE.get(host,{})
SETUP_CACHE[host] = cache_vars
for filename in vars_files:
if type(filename) == list:
# loop over all filenames, loading the first one, and failing if # none found
found = False
sequence = []
for real_filename in filename:
filename2 = utils.path_dwim(self.basedir, utils.template(real_filename, cache_vars, SETUP_CACHE))
sequence.append(filename2)
if os.path.exists(filename2):
found = True
data = utils.parse_yaml_from_file(filename2)
SETUP_CACHE[host].update(data)
self.callbacks.on_import_for_host(host, filename2)
break
else:
self.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)
)
self.host_list, groups = ansible.runner.Runner.parse_hosts(self.host_list)
else:
filename2 = utils.path_dwim(self.basedir, utils.template(filename, cache_vars, SETUP_CACHE))
if not os.path.exists(filename2):
raise errors.AnsibleError("no file matched for vars_file import: %s" % filename2)
data = utils.parse_yaml_from_file(filename2)
SETUP_CACHE[host].update(data)
self.callbacks.on_import_for_host(host, filename2)
self.callbacks.on_play_start(pattern)
# *****************************************************
# first run the setup task on every node, which gets the variables
# written to the JSON file and will also bubble facts back up via
# magic in Runner()
push_var_str=''
for (k,v) in vars.items():
push_var_str += "%s=%s " % (k,v)
def _do_setup_step(self, pattern, vars, user, port, sudo, transport, vars_files=None):
''' push variables down to the systems and get variables+facts back up '''
# this enables conditional includes like $facter_os.yml and is only done
# after the original pass when we have that data.
#
if vars_files is not None:
self.callbacks.on_setup_secondary()
self._do_conditional_imports(vars_files)
else:
self.callbacks.on_setup_primary()
host_list = [ h for h in self.inventory.list_hosts(pattern)
if not (h in self.stats.failures or h in self.stats.dark) ]
self.inventory.restrict_to(host_list)
# push any variables down to the system
setup_results = ansible.runner.Runner(
pattern=pattern,
module_name='setup',
module_args=push_var_str,
host_list=self.host_list,
forks=self.forks,
module_path=self.module_path,
timeout=self.timeout,
remote_user=user,
setup_cache=SETUP_CACHE
pattern=pattern, module_name='setup',
module_args=vars, inventory=self.inventory,
forks=self.forks, module_path=self.module_path,
timeout=self.timeout, remote_user=user,
remote_pass=self.remote_pass, remote_port=port,
setup_cache=SETUP_CACHE,
callbacks=self.runner_callbacks, sudo=sudo, debug=self.debug,
transport=transport, sudo_pass=self.sudo_pass, is_playbook=True
).run()
self.stats.compute(setup_results, setup=True)
self.inventory.lift_restriction()
# 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.items():
SETUP_CACHE[host] = result
if vars_files is None:
# first pass only or we'll erase good work
for (host, result) in setup_ok.iteritems():
SETUP_CACHE[host] = result
# *****************************************************
def _run_play(self, pg):
''' run a list of tasks for a given pattern, in order '''
# get configuration information about the pattern
pattern = pg.get('hosts',None)
if self.override_hosts:
pattern = 'all'
if pattern is None:
raise errors.AnsibleError('hosts declaration is required')
vars = self._get_vars(pg, self.basedir)
vars_files = pg.get('vars_files', {})
tasks = pg.get('tasks', [])
handlers = pg.get('handlers', [])
user = pg.get('user', self.remote_user)
port = pg.get('port', self.remote_port)
sudo = pg.get('sudo', self.sudo)
transport = pg.get('connection', self.transport)
self.callbacks.on_play_start(pattern)
# push any variables down to the system # and get facts/ohai/other data back up
self._do_setup_step(pattern, vars, user, port, sudo, transport, None)
# now with that data, handle contentional variable file imports!
if len(vars_files) > 0:
self._do_setup_step(pattern, vars, user, port, sudo, transport, vars_files)
# run all the top level tasks, these get run on every node
for task in tasks:
self._run_task(
pattern=pattern,
pattern=pattern,
task=task,
handlers=handlers,
remote_user=user
remote_user=user,
sudo=sudo,
transport=transport,
port=port
)
# handlers only run on certain nodes, they are flagged by _flag_handlers
@ -351,15 +579,20 @@ class PlayBook(object):
# but Apache will only be restarted once (at the end).
for task in handlers:
if type(task.get("run", None)) == list:
triggered_by = task.get('run', None)
if type(triggered_by) == list:
self.inventory.restrict_to(triggered_by)
self._run_task(
pattern=pattern,
task=task,
handlers=handlers,
host_list=task.get('run',[]),
handlers=[],
conditional=True,
remote_user=user
remote_user=user,
sudo=sudo,
transport=transport,
port=port
)
self.inventory.lift_restriction()
# end of execution for this particular pattern. Multiple patterns
# can be in a single playbook file

895
lib/ansible/runner.py Executable file → Normal file

File diff suppressed because it is too large Load Diff

219
lib/ansible/utils.py Executable file → Normal file
View File

@ -19,11 +19,20 @@
import sys
import os
import shlex
import re
import jinja2
import yaml
import optparse
from operator import methodcaller
try:
import json
except ImportError:
import simplejson as json
from ansible import errors
import ansible.constants as C
###############################################################
# UTILITY FUNCTIONS FOR COMMAND LINE TOOLS
###############################################################
@ -39,24 +48,32 @@ def exit(msg, rc=1):
def bigjson(result):
''' format JSON output (uncompressed) '''
return json.dumps(result, sort_keys=True, indent=4)
# hide some internals magic from command line userland
result2 = result.copy()
if 'invocation' in result2:
del result2['invocation']
return json.dumps(result2, sort_keys=True, indent=4)
def smjson(result):
''' format JSON output (compressed) '''
return json.dumps(result, sort_keys=True)
# hide some internals magic from command line userland
result2 = result.copy()
if 'invocation' in result2:
del result2['invocation']
return json.dumps(result2, sort_keys=True)
def task_start_msg(name, conditional):
if conditional:
return "NOTIFIED: [%s] **********\n" % name
return "\nNOTIFIED: [%s] **********\n" % name
else:
return "TASK: [%s] *********\n" % name
return "\nTASK: [%s] *********\n" % 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 >>\n%s" % (hostname, caption, bigjson(result))
return "%s | %s >> %s\n" % (hostname, caption, bigjson(result))
else:
return "%s | %s >> %s" % (hostname, caption, smjson(result))
return "%s | %s >> %s\n" % (hostname, caption, smjson(result))
def regular_success_msg(hostname, result, oneline):
''' output the result of a successful module run '''
@ -80,12 +97,13 @@ def command_generic_msg(hostname, result, oneline, caption):
buf += stderr
if msg:
buf += msg
buf += "\n"
return buf
else:
if stderr:
return "%s | %s | rc=%s | (stdout) %s (stderr) %s" % (hostname, caption, rc, stdout, stderr)
return "%s | %s | rc=%s | (stdout) %s (stderr) %s\n" % (hostname, caption, rc, stdout, stderr)
else:
return "%s | %s | rc=%s | (stdout) %s" % (hostname, caption, rc, stdout)
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 '''
@ -119,7 +137,7 @@ 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 == 'command':
if module_name in [ 'command', 'shell' ] and 'ansible_job_id' not in result:
if not failed:
buf = command_success_msg(hostname, result, oneline)
else:
@ -131,28 +149,6 @@ def host_report_msg(hostname, module_name, result, oneline):
buf = regular_failure_msg(hostname, result, oneline)
return buf
def dark_hosts_msg(results):
''' summarize the results of all uncontactable hosts '''
buf = ''
if len(results['dark'].keys()) > 0:
buf += "*** Hosts which could not be contacted or did not respond: ***"
for hostname in results['dark'].keys():
buf += "%s:\n%s\n" % (hostname, results['dark'][hostname])
buf += "\n"
return buf
def has_dark_hosts(results):
''' are there any uncontactable hosts? '''
return len(results['dark'].keys()) > 0
def contacted_hosts(results):
''' what are the contactable hosts? '''
return sorted(results['contacted'])
def contacted_host_result(results, hostname):
''' what are the results for a given host? '''
return results['contacted'][hostname]
def prepare_writeable_dir(tree):
''' make sure a directory exists and is writeable '''
if tree != '/':
@ -166,6 +162,7 @@ def prepare_writeable_dir(tree):
exit("Cannot write to path %s" % tree)
def path_dwim(basedir, given):
''' make relative paths work like folks expect '''
if given.startswith("/"):
return given
elif given.startswith("~/"):
@ -173,4 +170,164 @@ def path_dwim(basedir, given):
else:
return os.path.join(basedir, given)
def async_poll_status(jid, host, clock, result):
if 'finished' in result:
return "<job %s> finished on %s" % (jid, host)
elif 'failed' in result:
return "<job %s> FAILED on %s" % (jid, host)
else:
return "<job %s> polling on %s, %s remaining" % (jid, host, clock)
def json_loads(data):
return json.loads(data)
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 = {}
tokens = shlex.split(data)
for t in tokens:
if t.find("=") == -1:
raise errors.AnsibleError("failed to parse: %s" % data)
(key,value) = t.split("=", 1)
if key == 'changed' or 'failed':
if value.lower() in [ 'true', '1' ]:
value = True
elif value.lower() in [ 'false', '0' ]:
value = False
if key == 'rc':
value = int(value)
results[key] = value
if len(results.keys()) == 0:
return { "failed" : True, "parsed" : False, "msg" : data }
return results
_KEYCRE = re.compile(r"\$(\w+)")
def varReplace(raw, vars):
'''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
while raw:
m = _KEYCRE.search(raw)
if not m:
done.append(raw)
break
# Determine replacement value (if unknown variable then preserve
# original)
varname = m.group(1).lower()
replacement = str(vars.get(varname, m.group()))
start, end = m.span()
done.append(raw[:start]) # Keep stuff leading up to token
done.append(replacement) # Append replacement value
raw = raw[end:] # Continue with remainder of string
return ''.join(done)
def template(text, vars, setup_cache):
''' run a text buffer through the templating engine '''
vars = vars.copy()
text = varReplace(str(text), vars)
vars['hostvars'] = setup_cache
template = jinja2.Template(text)
return template.render(vars)
def double_template(text, vars, setup_cache):
return template(template(text, vars, setup_cache), vars, setup_cache)
def template_from_file(path, vars):
''' run a file through the templating engine '''
data = file(path).read()
return template(data, vars)
def parse_yaml(data):
return yaml.load(data)
def parse_yaml_from_file(path):
try:
data = file(path).read()
except IOError:
raise errors.AnsibleError("file not found: %s" % path)
return parse_yaml(data)
def parse_kv(args):
''' convert a string of key/value items to a dict '''
options = {}
vargs = shlex.split(args, posix=True)
for x in vargs:
if x.find("=") != -1:
k, v = x.split("=")
options[k]=v
return options
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=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)
parser.add_option('-D','--debug', default=False, action="store_true",
help='debug standard error output of remote modules')
parser.add_option('-f','--forks', dest='forks', default=constants.DEFAULT_FORKS, type='int',
help="specify number of parallel processes to use (default=%s)" % constants.DEFAULT_FORKS)
parser.add_option('-i', '--inventory-file', dest='inventory',
help="specify inventory host file (default=%s)" % constants.DEFAULT_HOST_LIST,
default=constants.DEFAULT_HOST_LIST)
parser.add_option('-k', '--ask-pass', default=False, dest='ask_pass', action='store_true',
help='ask for SSH password')
parser.add_option('-K', '--ask-sudo-pass', default=False, dest='ask_sudo_pass', action='store_true',
help='ask for sudo password')
parser.add_option('-M', '--module-path', dest='module_path',
help="specify path to module library (default=%s)" % constants.DEFAULT_MODULE_PATH,
default=constants.DEFAULT_MODULE_PATH)
parser.add_option('-T', '--timeout', default=constants.DEFAULT_TIMEOUT, type='int',
dest='timeout',
help="override the SSH timeout in seconds (default=%s)" % constants.DEFAULT_TIMEOUT)
if output_opts:
parser.add_option('-o', '--one-line', dest='one_line', action='store_true',
help='condense output')
parser.add_option('-t', '--tree', dest='tree', default=None,
help='log output to this directory')
if runas_opts:
parser.add_option("-s", "--sudo", default=False, action="store_true",
dest='sudo', help="run operations with sudo (nopasswd)")
parser.add_option('-u', '--user', default=constants.DEFAULT_REMOTE_USER,
dest='remote_user',
help='connect as this user (default=%s)' % constants.DEFAULT_REMOTE_USER)
if connect_opts:
parser.add_option('-c', '--connection', dest='connection',
choices=C.DEFAULT_TRANSPORT_OPTS,
default=C.DEFAULT_TRANSPORT,
help="connection type to use (default=%s)" % C.DEFAULT_TRANSPORT)
if async_opts:
parser.add_option('-P', '--poll', default=constants.DEFAULT_POLL_INTERVAL, type='int',
dest='poll_interval',
help="set the poll interval if using -B (default=%s)" % constants.DEFAULT_POLL_INTERVAL)
parser.add_option('-B', '--background', dest='seconds', type='int', default=0,
help='run asynchronously, failing after X seconds (default=N/A)')
return parser

174
library/apt Executable file
View File

@ -0,0 +1,174 @@
#!/usr/bin/python -tt
# (c) 2012, Flowroute LLC
# Written by Matthew Williams <matthew@flowroute.com>
# Based on yum module written by Seth Vidal <skvidal at fedoraproject.org>
#
# 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/>.
#
try:
import json
except ImportError:
import simplejson as json
import os
import sys
import shlex
import subprocess
import traceback
APT_PATH = "/usr/bin/apt-get"
APT = "DEBIAN_PRIORITY=critical %s" % APT_PATH
def debug(msg):
print >>sys.stderr, msg
def exit_json(rc=0, **kwargs):
print json.dumps(kwargs)
sys.exit(rc)
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,
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
out, err = cmd.communicate()
except (OSError, IOError), e:
rc = 1
err = str(e)
out = ''
except:
rc = 1
err = traceback.format_exc()
out = ''
else:
rc = cmd.returncode
return rc, out, err
def package_split(pkgspec):
parts = pkgspec.split('=')
if len(parts) > 1:
return parts[0], parts[1]
else:
return parts[0], None
def package_status(pkgname, version, cache):
try:
pkg = cache[pkgname]
except KeyError:
fail_json(msg="No package matching '%s' is available" % pkgname)
if version:
return pkg.is_installed and pkg.installed.version == version, False
else:
return pkg.is_installed, pkg.is_upgradable
def install(pkgspec, cache, upgrade=False, default_release=None):
name, version = package_split(pkgspec)
installed, upgradable = package_status(name, version, cache)
if not installed or (upgrade and upgradable):
cmd = "%s -q -y install '%s'" % (APT, pkgspec)
if default_release:
cmd += " -t '%s'" % (default_release,)
rc, out, err = run_apt(cmd)
if rc:
fail_json(msg="'apt-get install %s' failed: %s" % (pkgspec, err))
return True
else:
return 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, name)
rc, out, err = run_apt(cmd)
if rc:
fail_json(msg="'apt-get remove %s' failed: %s" % (name, err))
return True
# ===========================================
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)
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)

View File

@ -30,12 +30,12 @@ import datetime
import traceback
# ===========================================
# convert arguments of form a=b c=d
# to a dictionary
# FIXME: make more idiomatic
args = " ".join(sys.argv[1:])
items = shlex.split(args)
# FIXME: better error handling
argsfile = sys.argv[1]
items = shlex.split(file(argsfile).read())
params = {}
for x in items:
(k, v) = x.split("=")
@ -81,12 +81,22 @@ if mode == 'cleanup':
data = file(log_path).read()
try:
data = json.loads(data)
except:
print json.dumps({
"failed" : True,
"msg" : "Could not parse job output"
})
sys.exit(1)
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

View File

@ -31,19 +31,50 @@ import traceback
import signal
import time
def daemonize_self():
# daemonizing code: http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/66012
# logger.info("cobblerd started")
try:
pid = os.fork()
if pid > 0:
# exit first parent
sys.exit(0)
except OSError, e:
print >>sys.stderr, "fork #1 failed: %d (%s)" % (e.errno, e.strerror)
sys.exit(1)
# decouple from parent environment
os.chdir("/")
os.setsid()
os.umask(022)
# do second fork
try:
pid = os.fork()
if pid > 0:
# print "Daemon PID %d" % pid
sys.exit(0)
except OSError, e:
print >>sys.stderr, "fork #2 failed: %d (%s)" % (e.errno, e.strerror)
sys.exit(1)
dev_null = file('/dev/null','rw')
os.dup2(dev_null.fileno(), sys.stdin.fileno())
os.dup2(dev_null.fileno(), sys.stdout.fileno())
os.dup2(dev_null.fileno(), sys.stderr.fileno())
if len(sys.argv) < 3:
print json.dumps({
"failed" : True,
"msg" : "usage: async_wrapper <jid> <module_script> <time_limit> <args>. Humans, do not call directly!"
"msg" : "usage: async_wrapper <jid> <time_limit> <modulescript> <argsfile>. Humans, do not call directly!"
})
sys.exit(1)
jid = sys.argv[1]
time_limit = sys.argv[2]
wrapped_module = sys.argv[3]
args = sys.argv[4:]
cmd = "%s %s" % (wrapped_module, " ".join(args))
argsfile = sys.argv[4]
cmd = "%s %s" % (wrapped_module, argsfile)
# setup logging directory
logdir = os.path.expanduser("~/.ansible_async")
@ -60,65 +91,89 @@ if not os.path.exists(logdir):
def _run_command(wrapped_cmd, jid, log_path):
logfile = open(log_path, "w+")
logfile = open(log_path, "w")
logfile.write(json.dumps({ "started" : 1, "ansible_job_id" : jid }))
logfile.close()
logfile = open(log_path, "w")
result = {}
outdata = ''
try:
cmd = shlex.split(wrapped_cmd)
script = subprocess.Popen(cmd, shell=False,
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
out, err = script.communicate()
result = json.loads(out)
stdin=None, stdout=logfile, stderr=logfile)
script.communicate()
outdata = file(log_path).read()
result = json.loads(outdata)
except (OSError, IOError), e:
result = {
"failed": 1,
"cmd" : wrapped_cmd,
"msg": str(e),
}
result['ansible_job_id'] = jid
logfile.write(json.dumps(result))
except:
result = {
"failed" : 1,
"cmd" : wrapped_cmd,
"data" : outdata, # temporary debug only
"msg" : traceback.format_exc()
}
result['ansible_job_id'] = jid
logfile = open(log_path, "w+")
logfile.write(json.dumps(result))
result['ansible_job_id'] = jid
logfile.write(json.dumps(result))
logfile.close()
# immediately exit this process, leaving an orphaned process
# running which immediately forks a supervisory timing process
pid = os.fork()
if pid == 0:
# "RETURNING SUCCESS IN UNO"
print json.dumps({ "started" : 1, "ansible_job_id" : jid })
if pid != 0:
# the parent indicates the job has started
# print "RETURNING SUCCESS IN PARENT"
print json.dumps({ "started" : 1, "ansible_job_id" : jid, "results_file" : log_path })
sys.stdout.flush()
# we need to not return immmediately such that the launched command has an attempt
# to initialize PRIOR to ansible trying to clean up the launch directory (and argsfile)
# this probably could be done with some IPC later. Modules should always read
# the argsfile at the very first start of their execution anyway
time.sleep(1)
sys.exit(0)
else:
# "DAEMONIZED DOS"
# the kid manages the job
# WARNING: the following call may be total overkill
daemonize_self()
# we are now daemonized in this other fork but still
# want to create a supervisory process
#print "DAEMONIZED KID MAKING MORE KIDS"
sub_pid = os.fork()
if sub_pid == 0:
# "RUNNING IN KID A"
#print "RUNNING IN KID A"
_run_command(cmd, jid, log_path)
#print "KID A COMPLETE"
sys.stdout.flush()
sys.exit(0)
else:
# "WATCHING IN KID B"
#print "WATCHING IN KID B"
remaining = int(time_limit)
if os.path.exists("/proc/%s" % sub_pid):
# "STILL RUNNING"
#print "STILL RUNNING"
time.sleep(1)
remaining = remaining - 1
else:
# "DONE IN KID B"
#print "DONE IN KID B"
sys.stdout.flush()
sys.exit(0)
if remaining == 0:
# "SLAYING IN KID B"
#print "SLAYING IN KID B"
os.kill(sub_pid, signals.SIGKILL)
sys.stdout.flush()
sys.exit(1)
sys.stdout.flush()
sys.exit(0)

View File

@ -27,25 +27,52 @@ import subprocess
import sys
import datetime
import traceback
import shlex
import os
if len(sys.argv) == 1:
print json.dumps({
"failed" : True,
"msg" : "the command module requires arguments (-a)"
})
sys.exit(1)
argfile = sys.argv[1]
args = open(argfile, 'r').read()
shell = False
if args.find("#USE_SHELL") != -1:
args = args.replace("#USE_SHELL", "")
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):
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)
args = sys.argv[1:]
startd = datetime.datetime.now()
try:
cmd = subprocess.Popen(args, shell=False,
cmd = subprocess.Popen(args, shell=shell,
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
out, err = cmd.communicate()
except (OSError, IOError), e:
print json.dumps({
"failed": 1,
"msg": str(e),
"cmd" : args,
"failed" : 1,
"msg" : str(e),
})
sys.exit(1)
except:
@ -64,12 +91,14 @@ if err is None:
err = ''
result = {
"stdout" : out.strip(),
"stderr" : err.strip(),
"rc" : cmd.returncode,
"start" : str(startd),
"end" : str(endd),
"delta" : str(delta),
"cmd" : args,
"stdout" : out.strip(),
"stderr" : err.strip(),
"rc" : cmd.returncode,
"start" : str(startd),
"end" : str(endd),
"delta" : str(delta),
"changed" : True
}
print json.dumps(result)

View File

@ -22,18 +22,19 @@ import sys
import os
import shlex
try:
import json
except ImportError:
import simplejson as json
# ===========================================
# convert arguments of form a=b c=d
# to a dictionary
# FIXME: make more idiomatic
args = " ".join(sys.argv[1:])
items = shlex.split(args)
if len(sys.argv) == 1:
sys.exit(1)
argfile = sys.argv[1]
if not os.path.exists(argfile):
sys.exit(1)
items = shlex.split(open(argfile, 'r').read())
params = {}
for x in items:
(k, v) = x.split("=")
@ -41,14 +42,14 @@ for x in items:
src = params['src']
dest = params['dest']
if src:
src = os.path.expanduser(src)
if dest:
dest = os.path.expanduser(dest)
# raise an error if there is no src file
if not os.path.exists(src):
print json.dumps({
"failed" : 1,
"msg" : "Source %s failed to transfer" % src
})
print "failed=1 msg='Source %s failed to transfer'" % src
sys.exit(1)
md5sum = None
@ -56,17 +57,13 @@ changed = False
if os.path.exists(dest):
md5sum = os.popen("md5sum %s" % dest).read().split()[0]
os.system("cp %s %s" % (src, dest))
md5sum2 = os.popen("md5sum %s" % dest).read().split()[0]
md5sum2 = os.popen("md5sum %s" % src).read().split()[0]
if md5sum != md5sum2:
os.system("cp %s %s" % (src, dest))
changed = True
# mission accomplished
print json.dumps({
"md5sum" : md5sum2,
"changed" : changed
})
print "md5sum=%s changed=%s" % (md5sum2, changed)

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"
})

24
library/fetch Executable file
View File

@ -0,0 +1,24 @@
#!/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 #

349
library/file Executable file
View File

@ -0,0 +1,349 @@
#!/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/>.
try:
import json
except ImportError:
import simplejson as json
import os
import sys
import shlex
import subprocess
import shutil
import stat
import grp
import pwd
try:
import selinux
HAVE_SELINUX=True
except ImportError:
HAVE_SELINUX=False
def debug(msg):
# ansible ignores stderr, so it's safe to use for debug
# print >>sys.stderr, msg
pass
def exit_json(rc=0, **kwargs):
if 'path' in kwargs:
debug("adding path info")
add_path_info(kwargs)
print json.dumps(kwargs)
sys.exit(rc)
def fail_json(**kwargs):
kwargs['failed'] = True
exit_json(rc=1, **kwargs)
def add_path_info(kwargs):
path = kwargs['path']
if os.path.exists(path):
(user, group) = user_and_group(path)
kwargs['user'] = user
kwargs['group'] = group
st = os.stat(path)
kwargs['mode'] = stat.S_IMODE(st[stat.ST_MODE])
# secontext not yet supported
if os.path.islink(path):
kwargs['state'] = 'link'
elif os.path.isfile(path):
kwargs['state'] = 'file'
else:
kwargs['state'] = 'directory'
if HAVE_SELINUX:
kwargs['secontext'] = ':'.join(selinux_context(path))
else:
kwargs['state'] = 'absent'
return kwargs
# If selinux fails to find a default, return an array of None
def selinux_default_context(path, mode=0):
context = [None, None, None, None]
if not HAVE_SELINUX:
return context
try:
ret = selinux.matchpathcon(path, mode)
except OSError:
return context
if ret[0] == -1:
return context
context = ret[1].split(':')
debug("got default secontext=%s" % ret[1])
return context
# ===========================================
argfile = sys.argv[1]
args = open(argfile, 'r').read()
items = shlex.split(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','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')
context = params.get('context', None)
secontext = [seuser, serole, setype, selevel]
if context is not None:
if context != 'default':
fail_json(msg='invalid context: %s' % context)
if seuser is not None or serole is not None or setype is not None:
fail_json(msg='cannot define context=default and seuser, serole or setype')
secontext = selinux_default_context(path)
if state not in [ 'file', 'directory', 'link', 'absent']:
fail_json(msg='invalid state: %s' % state)
if state == 'link' and (src is None or dest is None):
fail_json(msg='src and dest are required for "link" state')
elif path is None:
fail_json(msg='path is required')
changed = False
# ===========================================
# support functions
def md5sum(filename):
return os.popen("/usr/bin/md5sum %s" % f).read()
def user_and_group(filename):
st = os.stat(filename)
uid = st.st_uid
gid = st.st_gid
user = pwd.getpwuid(uid)[0]
group = grp.getgrgid(gid)[0]
debug("got user=%s and group=%s" % (user, group))
return (user, group)
def selinux_context(path):
context = [None, None, None, None]
if not HAVE_SELINUX:
return context
try:
ret = selinux.lgetfilecon(path)
except:
fail_json(path=path, msg='failed to retrieve selinux context')
if ret[0] == -1:
return context
context = ret[1].split(':')
debug("got current secontext=%s" % ret[1])
return context
def set_context_if_different(path, context, changed):
if not HAVE_SELINUX:
return changed
cur_context = selinux_context(path)
new_context = list(cur_context)
for i in range(len(context)):
if context[i] is not None and context[i] != cur_context[i]:
new_context[i] = context[i]
debug("current secontext is %s" % ':'.join(cur_context))
debug("new secontext is %s" % ':'.join(new_context))
if cur_context != new_context:
try:
rc = selinux.lsetfilecon(path, ':'.join(new_context))
except OSError:
fail_json(path=path, msg='invalid selinux context')
if rc != 0:
fail_json(path=path, msg='set selinux context failed')
changed = True
return changed
def set_owner_if_different(path, owner, changed):
if owner is None:
debug('not tweaking owner')
return changed
user, group = user_and_group(path)
if owner != user:
debug('setting owner')
rc = os.system("/bin/chown -R %s %s" % (owner, path))
if rc != 0:
fail_json(path=path, msg='chown failed')
return True
return changed
def set_group_if_different(path, group, changed):
if group is None:
debug('not tweaking group')
return changed
old_user, old_group = user_and_group(path)
if old_group != group:
debug('setting group')
rc = os.system("/bin/chgrp -R %s %s" % (group, path))
if rc != 0:
fail_json(path=path, msg='chgrp failed')
return True
return changed
def set_mode_if_different(path, mode, changed):
if mode is None:
debug('not tweaking mode')
return changed
try:
# FIXME: support English modes
mode = int(mode, 8)
except Exception, e:
fail_json(path=path, msg='mode needs to be something octalish', details=str(e))
st = os.stat(path)
prev_mode = stat.S_IMODE(st[stat.ST_MODE])
if prev_mode != mode:
# FIXME: comparison against string above will cause this to be executed
# every time
try:
debug('setting mode')
os.chmod(path, mode)
except Exception, e:
fail_json(path=path, msg='chmod failed', details=str(e))
st = os.stat(path)
new_mode = stat.S_IMODE(st[stat.ST_MODE])
if new_mode != prev_mode:
return True
return changed
def rmtree_error(func, path, exc_info):
fail_json(path=path, msg='failed to remove directory')
# ===========================================
# go...
prev_state = 'absent'
if os.path.exists(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':
debug('requesting absent')
try:
if prev_state == 'directory':
if os.path.islink(path):
os.unlink(path)
else:
shutil.rmtree(path, ignore_errors=False, onerror=rmtree_error)
else:
os.unlink(path)
except Exception, e:
fail_json(path=path, msg=str(e))
exit_json(path=path, changed=True)
sys.exit(0)
if prev_state != 'absent' and prev_state != state:
fail_json(path=path, msg='refusing to convert between %s and %s' % (prev_state, state))
if prev_state == 'absent' and state == 'absent':
exit_json(path=path, changed=False)
if state == 'file':
debug('requesting file')
if prev_state == 'absent':
fail_json(path=path, msg='file does not exist, use copy or template module to create')
# 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_json(path=path, changed=changed)
elif state == 'directory':
debug('requesting 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)
exit_json(path=path, changed=changed)
elif state == 'link':
if os.path.isabs(src):
abs_src = src
else:
abs_src = os.path.join(os.path.dirname(dest))
if not os.path.exists(abssrc):
fail_json(dest=dest, src=src, msg='src file does not exist')
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_json(dest=dest, src=src, msg='unexpected position reached')
# 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)
exit_json(dest=dest, src=src, changed=changed)
fail_json(path=path, msg='unexpected position reached')
sys.exit(0)

View File

@ -37,8 +37,31 @@ import subprocess
# to a dictionary
# FIXME: make more idiomatic
args = " ".join(sys.argv[1:])
if len(sys.argv) == 1:
print json.dumps({
"failed" : True,
"msg" : "the command module requires arguments (-a)"
})
sys.exit(1)
argfile = sys.argv[1]
if not os.path.exists(argfile):
print json.dumps({
"failed" : True,
"msg" : "Argument file not found"
})
sys.exit(1)
args = open(argfile, 'r').read()
items = shlex.split(args)
if not len(items):
print json.dumps({
"failed" : True,
"msg" : "the command module requires arguments (-a)"
})
sys.exit(1)
params = {}
for x in items:
(k, v) = x.split("=")

174
library/group Executable file
View File

@ -0,0 +1,174 @@
#!/usr/bin/env python
# (c) 2012, Stephen Fromm <sfromm@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/>.
try:
import json
except ImportError:
import simplejson as json
import os
import grp
import shlex
import subprocess
import sys
GROUPADD = "/usr/sbin/groupadd"
GROUPDEL = "/usr/sbin/groupdel"
GROUPMOD = "/usr/sbin/groupmod"
def debug(msg):
# ansible ignores stderr, so it's safe to use for debug
print >>sys.stderr, msg
#pass
def exit_json(rc=0, **kwargs):
if 'name' in kwargs:
debug("add group info to exit_json")
add_group_info(kwargs)
print json.dumps(kwargs)
sys.exit(rc)
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:
kwargs['state'] = 'absent'
return kwargs
def group_del(group):
cmd = [GROUPDEL, group]
debug("Arguments to groupdel: %s" % (" ".join(cmd)))
rc = subprocess.call(cmd, shell=False, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
if rc == 0:
return True
else:
return False
def group_add(group, **kwargs):
cmd = [GROUPADD]
for key in kwargs:
if key == 'gid' and kwargs[key] is not None:
cmd.append('-g')
cmd.append(kwargs[key])
cmd.append(group)
debug("Arguments to groupadd: %s" % (" ".join(cmd)))
rc = subprocess.call(cmd, shell=False, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
if rc == 0:
return True
else:
return False
def group_mod(group, **kwargs):
cmd = [GROUPMOD]
info = group_info(group)
for key in kwargs:
if key == 'gid':
if kwargs[key] is not None and info[2] != int(kwargs[key]):
cmd.append('-g')
cmd.append(kwargs[key])
if len(cmd) == 1:
return False
cmd.append(group)
debug("Arguments to groupmod: %s" % (" ".join(cmd)))
rc = subprocess.call(cmd, shell=False, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
if rc == 0:
return True
else:
return False
def group_exists(group):
try:
if grp.getgrnam(group):
return True
except KeyError:
return False
def group_info(group):
if not group_exists(group):
return False
try:
info = list(grp.getgrnam(group))
except KeyError:
return False
return info
# ===========================================
if not os.path.exists(GROUPADD):
if os.path.exists("/sbin/groupadd"):
GROUPADD = "/sbin/groupadd"
else:
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)
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)
if state not in [ 'present', 'absent' ]:
fail_json(msg='invalid state')
if name is None:
fail_json(msg='name is required')
changed = False
rc = 0
if state == 'absent':
if group_exists(name):
changed = group_del(name)
exit_json(name=name, changed=changed)
elif state == 'present':
if not group_exists(name):
changed = group_add(name, gid=gid)
else:
changed = group_mod(name, gid=gid)
exit_json(name=name, changed=changed)
fail_json(name=name, msg='Unexpected position reached')

View File

@ -27,24 +27,32 @@ import shlex
import subprocess
# ===========================================
# convert arguments of form a=b c=d
# to a dictionary
# FIXME: make more idiomatic
args = " ".join(sys.argv[1:])
argfile = sys.argv[1]
args = open(argfile, 'r').read()
items = shlex.split(args)
if not len(items):
print json.dumps(dict(failed=True, msg='this module requires arguments (-a)'))
sys.exit(1)
params = {}
for x in items:
(k, v) = x.split("=")
params[k] = v
name = params['name']
state = params.get('state','running')
state = params.get('state','unknown')
# running and started are the same
if state not in [ 'running', 'started', 'stopped', 'restarted' ]:
print json.dumps(dict(failed=True, msg='invalid state'))
sys.exit(1)
# ===========================================
# get service status
status = os.popen("/sbin/service %s status" % name).read()
status = os.popen("service %s status" % name).read()
# ===========================================
# determine if we are going to change anything
@ -71,20 +79,17 @@ elif state == "restarted":
# run change commands if we need to
def _run(cmd):
return subprocess.call(cmd,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE, shell=True)
return subprocess.call(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True)
rc = 0
if changed:
if state == 'started':
rc = _run("/sbin/service %s start" % name)
if state in ('started', 'running'):
rc = _run("service %s start" % name)
elif state == 'stopped':
rc = _run("/sbin/service %s stop" % name)
rc = _run("service %s stop" % name)
elif state == 'restarted':
rc1 = _run("/sbin/service %s stop" % name)
rc2 = _run("/sbin/service %s start" % name)
rc1 = _run("service %s stop" % name)
rc2 = _run("service %s start" % name)
rc = rc1 and rc2
if rc != 0:

View File

@ -19,21 +19,283 @@
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
try:
import json
except ImportError:
import simplejson as json
_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' }
# 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' }
def get_file_content(path):
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('/proc/ide/hd*/model'):
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():
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.gethostname()
facts['hostname'] = facts['fqdn'].split('.')[0]
facts['interfaces'] = get_interfaces()
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] }
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_service_facts(facts):
get_public_ssh_host_keys(facts)
def ansible_facts():
facts = {}
get_platform_facts(facts)
get_hardware_facts(facts)
get_virtual_facts(facts)
get_network_facts(facts)
get_service_facts(facts)
return facts
# load config & template variables
input_data = sys.argv[1:]
new_options = dict([ x.split('=') for x in input_data ])
ansible_file = new_options.get('metadata', DEFAULT_ANSIBLE_SETUP)
if len(sys.argv) == 1:
sys.exit(1)
argfile = sys.argv[1]
if not os.path.exists(argfile):
sys.exit(1)
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
ansible_file = setup_options.get('metadata', DEFAULT_ANSIBLE_SETUP)
ansible_dir = os.path.dirname(ansible_file)
# create the config dir if it doesn't exist
@ -48,6 +310,10 @@ if not os.path.exists(ansible_file):
else:
md5sum = os.popen("md5sum %s" % ansible_file).read().split()[0]
# 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
@ -62,7 +328,7 @@ if os.path.exists("/usr/bin/facter"):
facter = False
if facter:
for (k,v) in facter_ds.items():
new_options["facter_%s" % k] = v
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
@ -81,13 +347,13 @@ if os.path.exists("/usr/bin/ohai"):
for (k,v) in ohai_ds.items():
if type(v) == str or type(v) == unicode:
k2 = "ohai_%s" % k
new_options[k2] = v
setup_options[k2] = v
# write the template/settings file using
# instructions from server
f = open(ansible_file, "w+")
reformat = json.dumps(new_options, sort_keys=True, indent=4)
reformat = json.dumps(setup_options, sort_keys=True, indent=4)
f.write(reformat)
f.close()
@ -96,9 +362,9 @@ md5sum2 = os.popen("md5sum %s" % ansible_file).read().split()[0]
if md5sum != md5sum2:
changed = True
new_options['written'] = ansible_file
new_options['changed'] = changed
new_options['md5sum'] = md5sum2
setup_options['written'] = ansible_file
setup_options['changed'] = changed
setup_options['md5sum'] = md5sum2
print json.dumps(new_options)
print json.dumps(setup_options)

6
library/shell Normal file
View File

@ -0,0 +1,6 @@
# 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".

71
library/slurp Executable file
View File

@ -0,0 +1,71 @@
#!/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
import os
import shlex
import base64
try:
import json
except ImportError:
import simplejson as json
# ===========================================
# convert arguments of form a=b c=d
# to a dictionary
if len(sys.argv) == 1:
sys.exit(1)
argfile = sys.argv[1]
if not os.path.exists(argfile):
sys.exit(1)
items = shlex.split(open(argfile, 'r').read())
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

@ -17,94 +17,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 jinja2
import shlex
try:
import json
except ImportError:
import simplejson as json
# ===========================================
# convert arguments of form a=b c=d
# to a dictionary
# FIXME: make more idiomatic
args = " ".join(sys.argv[1:])
items = shlex.split(args)
params = {}
for x in items:
(k, v) = x.split("=")
params[k] = v
source = params['src']
dest = params['dest']
metadata = params.get('metadata', '/etc/ansible/setup')
# raise an error if there is no template metadata
if not os.path.exists(metadata):
print json.dumps({
"failed" : 1,
"msg" : "Missing %s, did you run the setup module yet?" % metadata
})
sys.exit(1)
# raise an error if we can't parse the template metadata
try:
f = open(metadata)
data = json.loads(f.read())
f.close()
except:
print json.dumps({
"failed" : 1,
"msg" : "Failed to parse/load %s, rerun the setup module?" % metadata
})
sys.exit(1)
if not os.path.exists(source):
print json.dumps({
"failed" : 1,
"msg" : "Source template could not be read: %s" % source
})
sys.exit(1)
source = file(source).read()
if os.path.isdir(dest):
print json.dumps({
"failed" : 1,
"msg" : "Destination is a directory"
})
sys.exit(1)
# record md5sum of original source file so we can report if it changed
changed = False
md5sum = None
if os.path.exists(dest):
md5sum = os.popen("md5sum %s" % dest).read().split()[0]
# call Jinja2 here and save the new template file
template = jinja2.Template(source)
data_out = template.render(data)
f = open(dest, "w+")
f.write(data_out)
f.close()
# TODO: catch templating errors and do not clobber the file on the
# other end unless things were successful
# record m5sum and return success and whether things have changed
md5sum2 = os.popen("md5sum %s" % dest).read().split()[0]
if md5sum != md5sum2:
changed = True
# mission accomplished
print json.dumps({
"md5sum" : md5sum2,
"changed" : changed
})
# 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.

317
library/user Executable file
View File

@ -0,0 +1,317 @@
#!/usr/bin/python
# (c) 2012, Stephen Fromm <sfromm@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/>.
try:
import json
except ImportError:
import simplejson as json
import os
import pwd
import grp
import shlex
import spwd
import subprocess
import sys
USERADD = "/usr/sbin/useradd"
USERMOD = "/usr/sbin/usermod"
USERDEL = "/usr/sbin/userdel"
def debug(msg):
# ansible ignores stderr, so it's safe to use for debug
print >>sys.stderr, msg
#pass
def exit_json(rc=0, **kwargs):
if 'name' in kwargs:
debug("add user info to exit_json")
add_user_info(kwargs)
print json.dumps(kwargs)
sys.exit(rc)
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]:
cmd.append('-f')
elif key == 'remove' and kwargs[key]:
cmd.append('-r')
cmd.append(user)
debug("Arguments to userdel: %s" % (" ".join(cmd)))
rc = subprocess.call(cmd, shell=False, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
if rc == 0:
return True
else:
return False
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]):
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):
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:
cmd.append('-c')
cmd.append(kwargs[key])
elif key == 'home' and kwargs[key] is not None:
cmd.append('-d')
cmd.append(kwargs[key])
elif key == 'shell' and kwargs[key] is not None:
cmd.append('-s')
cmd.append(kwargs[key])
elif key == 'password' and kwargs[key] is not None:
cmd.append('-p')
cmd.append(kwargs[key])
elif key == 'createhome':
if kwargs[key] is not None:
if kwargs[key] == 'yes':
cmd.append('-m')
else:
cmd.append('-M')
cmd.append(user)
debug("Arguments to useradd: %s" % (" ".join(cmd)))
rc = subprocess.call(cmd, shell=False, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
if rc == 0:
return True
else:
return False
def user_mod(user, **kwargs):
cmd = [USERMOD]
info = user_info(user)
for key in kwargs:
if key == 'uid':
if kwargs[key] is not None and info[2] != int(kwargs[key]):
cmd.append('-u')
cmd.append(kwargs[key])
elif key == 'group' and kwargs[key] is not None:
if not group_exists(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])
elif key == 'groups' and kwargs[key] is not None:
for g in kwargs[key].split(','):
if not group_exists(g):
fail_json(msg="Group %s does not exist" % (g))
groups = ",".join(user_group_membership(user))
if groups != kwargs[key]:
cmd.append('-G')
cmd.append(kwargs[key])
elif key == 'comment':
if kwargs[key] is not None and info[4] != kwargs[key]:
cmd.append('-c')
cmd.append(kwargs[key])
elif key == 'home':
if kwargs[key] is not None and info[5] != kwargs[key]:
cmd.append('-d')
cmd.append(kwargs[key])
elif key == 'shell':
if kwargs[key] is not None and info[6] != kwargs[key]:
cmd.append('-s')
cmd.append(kwargs[key])
elif key == 'password':
if kwargs[key] is not None and info[1] != kwargs[key]:
cmd.append('-p')
cmd.append(kwargs[key])
elif key == 'append':
if kwargs[key] is not None and kwargs[key] == 'yes':
if 'groups' in kwargs and kwargs['groups'] is not None:
cmd.append('-a')
# skip if no changes to be made
if len(cmd) == 1:
return False
cmd.append(user)
debug("Arguments to usermod: %s" % (" ".join(cmd)))
rc = subprocess.call(cmd, shell=False, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
if rc == 0:
return True
else:
return False
def group_exists(group):
try:
if group.isdigit():
if grp.getgrgid(group):
return True
else:
if grp.getgrnam(group):
return True
except KeyError:
return False
def group_info(group):
if not group_exists(group):
return False
if group.isdigit():
return list(grp.getgrgid(group))
else:
return list(grp.getgrnam(group))
def user_group_membership(user):
groups = []
info = get_pwd_info(user)
for group in grp.getgrall():
if user in group[3] and info[3] != group[2]:
groups.append(group[0])
return groups
def user_exists(user):
try:
if pwd.getpwnam(user):
return True
except KeyError:
return False
def get_pwd_info(user):
if not user_exists(user):
return False
return list(pwd.getpwnam(user))
def user_info(user):
if not user_exists(user):
return False
try:
info = get_pwd_info(user)
sinfo = spwd.getspnam(user)
except KeyError:
return False
info[1] = sinfo[1]
return info
# ===========================================
if not os.path.exists(USERADD):
if os.path.exists("/sbin/useradd"):
USERADD = "/sbin/useradd"
else:
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)
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')
# ===========================================
# 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 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):
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)
else:
changed = user_mod(name, uid=uid, group=group, groups=groups,
comment=comment, home=home, shell=shell,
password=password, append=append)
if password is not None:
exit_json(name=name, changed=changed, password="XXXXXXXX")
else:
exit_json(name=name, changed=changed)
fail_json(name=name, msg='Unexpected position reached')
sys.exit(0)

450
library/virt Executable file
View File

@ -0,0 +1,450 @@
#!/usr/bin/python
"""
Virt management features
Copyright 2007, 2012 Red Hat, Inc
Michael DeHaan <mdehaan@redhat.com>
Seth Vidal <skvidal@fedoraproject.org>
This software may be freely redistributed under the terms of the GNU
general public license.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
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
try:
import libvirt
except ImportError:
print json.dumps({
"failed" : True,
"msg" : "libvirt python module unavailable",
"rc": VIRT_UNAVAILABLE,
})
sys.exit(1)
import shlex
VIRT_STATE_NAME_MAP = {
0 : "running",
1 : "running",
2 : "running",
3 : "paused",
4 : "shutdown",
5 : "shutdown",
6 : "crashed"
}
class LibvirtConnection(object):
def __init__(self):
cmd = subprocess.Popen("uname -r", shell=True, stdout=subprocess.PIPE,
close_fds=True)
output = cmd.communicate()[0]
if output.find("xen") != -1:
conn = libvirt.open(None)
else:
conn = libvirt.open("qemu:///system")
if not conn:
raise Exception("hypervisor connection failure")
self.conn = conn
def find_vm(self, vmid):
"""
Extra bonus feature: vmid = -1 returns a list of everything
"""
conn = self.conn
vms = []
# this block of code borrowed from virt-manager:
# get working domain's name
ids = conn.listDomainsID()
for id in ids:
vm = conn.lookupByID(id)
vms.append(vm)
# get defined domain
names = conn.listDefinedDomains()
for name in names:
vm = conn.lookupByName(name)
vms.append(vm)
if vmid == -1:
return vms
for vm in vms:
if vm.name() == vmid:
return vm
raise Exception("virtual machine %s not found" % vmid)
def shutdown(self, vmid):
return self.find_vm(vmid).shutdown()
def pause(self, vmid):
return self.suspend(self.conn,vmid)
def unpause(self, vmid):
return self.resume(self.conn,vmid)
def suspend(self, vmid):
return self.find_vm(vmid).suspend()
def resume(self, vmid):
return self.find_vm(vmid).resume()
def create(self, vmid):
return self.find_vm(vmid).create()
def destroy(self, vmid):
return self.find_vm(vmid).destroy()
def undefine(self, vmid):
return self.find_vm(vmid).undefine()
def get_status2(self, vm):
state = vm.info()[0]
# print "DEBUG: state: %s" % state
return VIRT_STATE_NAME_MAP.get(state,"unknown")
def get_status(self, vmid):
state = self.find_vm(vmid).info()[0]
return VIRT_STATE_NAME_MAP.get(state,"unknown")
def nodeinfo(self):
return self.conn.getInfo()
def get_type(self):
return self.conn.getType()
def get_maxVcpus(self, vmid):
vm = self.conn.lookupByName(vmid)
return vm.maxVcpus()
def get_maxMemory(self, vmid):
vm = self.conn.lookupByName(vmid)
return vm.maxMemory()
def getFreeMemory(self):
return self.conn.getFreeMemory()
def get_autostart(self, vmid):
vm = self.conn.lookupByName(vmid)
return vm.autostart()
def set_autostart(self, vmid, val):
vm = self.conn.lookupByName(vmid)
return vm.setAutostart(val)
class Virt(object):
def __get_conn(self):
self.conn = LibvirtConnection()
return self.conn
def get_vm(self, vmid):
self.__get_conn()
return self.conn.find_vm(vmid)
def state(self):
vms = self.list_vms()
state = []
for vm in vms:
state_blurb = self.conn.get_status(vm)
state.append("%s %s" % (vm,state_blurb))
return state
def info(self):
vms = self.list_vms()
info = dict()
for vm in vms:
data = self.conn.find_vm(vm).info()
# libvirt returns maxMem, memory, and cpuTime as long()'s, which
# xmlrpclib tries to convert to regular int's during serialization.
# This throws exceptions, so convert them to strings here and
# assume the other end of the xmlrpc connection can figure things
# out or doesn't care.
info[vm] = {
"state" : VIRT_STATE_NAME_MAP.get(data[0],"unknown"),
"maxMem" : str(data[1]),
"memory" : str(data[2]),
"nrVirtCpu" : data[3],
"cpuTime" : str(data[4]),
}
info[vm]["autostart"] = self.conn.get_autostart(vm)
return info
def nodeinfo(self):
self.__get_conn()
info = dict()
data = self.conn.nodeinfo()
info = {
"cpumodel" : str(data[0]),
"phymemory" : str(data[1]),
"cpus" : str(data[2]),
"cpumhz" : str(data[3]),
"numanodes" : str(data[4]),
"sockets" : str(data[5]),
"cpucores" : str(data[6]),
"cputhreads" : str(data[7])
}
return info
def list_vms(self):
self.conn = self.__get_conn()
vms = self.conn.find_vm(-1)
results = []
for x in vms:
try:
results.append(x.name())
except:
pass
return results
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)
def freemem(self):
self.conn = self.__get_conn()
return self.conn.getFreeMemory()
def shutdown(self, vmid):
"""
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.
"""
self.__get_conn()
return self.conn.suspend(vmid)
def unpause(self, 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 mac address.
"""
self.__get_conn()
return self.conn.create(vmid)
def start(self, vmid):
"""
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.
"""
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.
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')
try:
domV = conn.lookupByName(vmid)
except:
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
"""
self.__get_conn()
return self.conn.get_maxVcpus(vmid)
def get_max_memory(self, vmid):
"""
Gets the max memory on a guest
"""
self.__get_conn()
return self.conn.get_MaxMemory(vmid)
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
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)
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:
msg = "state change requires a guest specified"
return VIRT_FAILED, msg
res['changed'] = False
if state == 'running':
if v.status(guest) is not 'running':
res['changed'] = True
res['msg'] = v.start(guest)
elif state == 'shutdown':
if v.status(guest) is not 'shutdown':
res['changed'] = True
res['msg'] = v.shutdown(guest)
return VIRT_SUCCESS, res
if command:
if command in vm_commands:
if not guest:
msg = "%s requires 1 argument: guest" % command
return VIRT_FAILED, msg
res = getattr(v, command)(guest)
if type(res) != dict:
res = { command: res }
return rc, res
elif hasattr(v, command):
res = getattr(v, command)()
if type(res) != dict:
res = { command: res }
return rc, res
else:
msg = "Command %s not recognized" % basecmd
rc = VIRT_FAILED
return rc, msg
if __name__ == "__main__":
try:
rc, result = main()
except Exception, e:
rc = 1
result = str(e)
if rc != 0: # something went wrong emit the msg
print json.dumps({
"failed" : rc,
"msg" : result,
"rc": rc,
})
sys.exit(rc)
else:
print json.dumps(result)

344
library/yum Executable file
View File

@ -0,0 +1,344 @@
#!/usr/bin/python -tt
# (c) 2012, Red Hat, Inc
# Written by Seth Vidal <skvidal at fedoraproject.org>
#
# 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
import yum
import subprocess
import datetime
import shlex
import re
import traceback
try:
import json
except ImportError:
import simplejson as json
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 my
def pkg_to_dict(po):
d = {
'name':po.name,
'arch':po.arch,
'epoch':po.epoch,
'release':po.release,
'version':po.version,
'repo':po.ui_from_repo,
'_nevra':po.ui_nevra,
}
if type(po) == yum.rpmsack.RPMInstalledPackage:
d['yumstate'] = 'installed'
else:
d['yumstate'] = 'available'
return d
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(po) for po in my.rpmdb ]
elif stuff == 'updates':
return [ pkg_to_dict(po) for
po in my.doPackageLists(pkgnarrow='updates').updates ]
elif stuff == 'available':
return [ pkg_to_dict(po) for po in my.pkgSack ]
elif stuff == 'repos':
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:
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_yum(command):
try:
cmd = subprocess.Popen(command, shell=True,
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
out, err = cmd.communicate()
except (OSError, IOError), e:
rc = 1
err = str(e)
out = ''
except:
rc = 1
err = traceback.format_exc()
out = ''
if out is None:
out = ''
if err is None:
err = ''
else:
rc = cmd.returncode
return rc, out, err
def ensure(my, state, pkgspec):
yumconf = my.conf.config_file_path
res = {}
if state == 'installed':
# check if pkgspec is installed
if re.search('[></=]',pkgspec):
try:
pkgs = my.returnInstalledPackagesByDep(pkgspec)
except yum.Errors.YumBaseError:
pkgs = None
else:
e,m,u = my.rpmdb.matchPackageNames([pkgspec])
pkgs = e +m
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:
changed = False
failed = True
else:
changed = True
failed = False
res['changed'] = changed
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:
e,m,u = my.rpmdb.matchPackageNames([pkgspec])
pkgs = e +m
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
if rc:
changed = False
failed = True
else:
changed = True
failed = False
res['changed'] = changed
if failed:
res['failed'] = failed
res['msg'] = err
res['results'] = out
return res
if state == 'latest':
updates = my.doPackageLists(pkgnarrow='updates', patterns=[pkgspec]).updates
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,}
# 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
if rc:
changed = False
failed = True
else:
changed = True
failed = False
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 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
pass
def main():
# state=installed pkg=pkgspec
# state=removed pkg=pkgspec
# state=latest pkg=pkgspec
#
# informational commands:
# list=installed
# list=updates
# list=available
# list=repos
# list=pkgspec
if len(sys.argv) == 1:
msg = "the yum module requires arguments (-a)"
return 1, msg
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)
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 '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.get('pkg', params.get('package', params.get('name', None)))
if 'pkg' is None:
results['msg'] = "No pkg specified"
else:
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)
print json.dumps(results)
return 0, None
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)

44
packaging/arch/PKGBUILD Normal file
View File

@ -0,0 +1,44 @@
#Maintainer: Michel Blanc <mblanc@erasme.org>
pkgname=ansible-git
pkgver=20120419
pkgrel=1
pkgdesc="A radically simple deployment, model-driven configuration management, and command execution framework"
arch=('any')
url="https://github.com/ansible/ansible"
license=('GPL3')
depends=('python2' 'python2-yaml' 'python-paramiko>=1.7.7' 'python2-jinja' 'python-simplejson')
makedepends=('git' 'asciidoc' 'fakeroot')
_gitroot="https://github.com/ansible/ansible"
_gitname="ansible"
build() {
cd "$srcdir"
msg "Connecting to GIT server...."
if [ -d $_gitname ] ; then
cd $_gitname && git pull origin
msg "The local files are updated."
else
git clone $_gitroot $_gitname
fi
msg "GIT checkout done or server timeout"
cd "$srcdir/$_gitname"
make
}
package() {
cd "$srcdir/$_gitname"
mkdir -p ${pkgdir}/usr/share/ansible
cp ./library/* ${pkgdir}/usr/share/ansible/
python setup.py install -O1 --root=${pkgdir}
install -D docs/man/man1/ansible.1 ${pkgdir}/usr/share/man/man1/ansible.1
install -D docs/man/man1/ansible-playbook.1 ${pkgdir}/usr/share/man/man1/ansible-playbook.1
gzip -9 ${pkgdir}/usr/share/man/man1/ansible.1
gzip -9 ${pkgdir}/usr/share/man/man1/ansible-playbook.1
}

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

@ -0,0 +1,3 @@
etc/ansible
usr/lib/python2.7/site-packages
usr/share/ansible

View File

@ -0,0 +1,5 @@
examples/hosts etc/ansible
library/* usr/share/ansible
docs/man/man1/ansible.1 usr/share/man/man1
docs/man/man1/ansible-playbook.1 usr/share/man/man1
bin/* usr/bin

View File

@ -0,0 +1,11 @@
ansible (0.3) debian; urgency=low
* 0.3 update
-- Michael DeHaan <michael.dehaan@gmail.com> Mon, 23 Apr 2012 11:08:00 -0400
ansible (0.0.2) debian; urgency=low
* Initial Release
-- Henry Graham (hzgraham) <Henry.Graham@mail.wvu.edu> Tue, 17 Apr 2012 17:17:01 -0400

1
packaging/debian/compat Normal file
View File

@ -0,0 +1 @@
5

13
packaging/debian/control Normal file
View File

@ -0,0 +1,13 @@
Source: ansible
Section: admin
Priority: optional
Maintainer: Henry Graham (hzgraham) <Henry.Graham@mail.wvu.edu>
Build-Depends: cdbs, debhelper (>= 5.0.0)
Standards-Version: 3.9.1
Homepage: http://ansible.github.com/
Package: ansible
Architecture: all
Depends: python, python-support (>= 0.90), python-jinja2, python-yaml, python-paramiko
Description: Ansible Application
Ansible is a extra-simple tool/API for doing 'parallel remote things' over SSH executing commands, running "modules", or executing larger 'playbooks' that can serve as a configuration management or deployment system.

View File

@ -0,0 +1,26 @@
This package was debianized by Henry Graham (hzgraham) <Henry.Graham@mail.wvu.edu> on
Tue, 17 Apr 2012 12:19:47 -0400.
It was downloaded from https://github.com/ansible/ansible.git
Copyright: Henry Graham (hzgraham) <Henry.Graham@mail.wvu.edu>
License:
This package 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; version 2 dated June, 1991.
This package 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 package; if not, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301,
USA.
On Debian systems, the complete text of the GNU General
Public License can be found in `/usr/share/common-licenses/GPL'.

1
packaging/debian/docs Normal file
View File

@ -0,0 +1 @@
README.md

View File

@ -0,0 +1 @@
2

6
packaging/debian/rules Executable file
View File

@ -0,0 +1,6 @@
#!/usr/bin/make -f
# -- makefile --
include /usr/share/cdbs/1/rules/debhelper.mk
DEB_PYTHON_SYSTEM = pysupport
include /usr/share/cdbs/1/class/python-distutils.mk

View File

@ -0,0 +1,3 @@
Gentoo ebuilds are available here:
https://github.com/uu/ubuilds

View File

@ -0,0 +1,68 @@
%if 0%{?rhel} <= 5
%{!?python_sitelib: %global python_sitelib %(%{__python} -c "from distutils.sysconfig import get_python_lib; print(get_python_lib())")}
BuildRoot: %{_tmppath}/%{name}-%{version}-%{release}-buildroot
%endif
Name: ansible
Release: 1%{?dist}
Summary: Minimal SSH command and control
Version: 0.3
Group: Development/Libraries
License: GPLv3
Source0: https://github.com/downloads/ansible/ansible/%{name}-%{version}.tar.gz
Url: http://ansible.github.com
BuildArch: noarch
BuildRequires: python2-devel
Requires: PyYAML
Requires: python-paramiko
Requires: python-jinja2
%description
Ansible is a radically simple model-driven configuration management,
multi-node deployment, and remote task execution system. Ansible works
over SSH and does not require any software or daemons to be installed
on remote nodes. Extension modules can be written in any language and
are transferred to managed machines automatically.
%prep
%setup -q
%build
%{__python} setup.py build
%install
%{__python} setup.py install -O1 --root=$RPM_BUILD_ROOT
mkdir -p $RPM_BUILD_ROOT/etc/ansible/
cp examples/hosts $RPM_BUILD_ROOT/etc/ansible/
mkdir -p $RPM_BUILD_ROOT/%{_mandir}/man1/
cp -v docs/man/man1/*.1 $RPM_BUILD_ROOT/%{_mandir}/man1/
mkdir -p $RPM_BUILD_ROOT/%{_datadir}/ansible
cp -v library/* $RPM_BUILD_ROOT/%{_datadir}/ansible/
%clean
rm -rf $RPM_BUILD_ROOT
%files
%defattr(-,root,root)
%{python_sitelib}/ansible*
%{_bindir}/ansible*
%{_datadir}/ansible
%config(noreplace) %{_sysconfdir}/ansible
%doc README.md PKG-INFO
%doc %{_mandir}/man1/ansible*
%changelog
* Mon Apr 23 2012 Michael DeHaan <mdehaan@redhat.com> - 0.3-1
- Release of 0.3
* Tue Apr 3 2012 John Eckersberg <jeckersb@redhat.com> - 0.0.2-1
- Release of 0.0.2
* Sat Mar 10 2012 <tbielawa@redhat.com> - 0.0.1-1
- Release of 0.0.1

View File

@ -1,34 +1,28 @@
#!/usr/bin/env python
# NOTE: setup.py does NOT install the contents of the library dir
# for you, you should go through "make install" or "make RPMs"
# for that, or manually copy modules over.
import os
import sys
sys.path.insert(0, os.path.abspath('lib'))
from ansible import __version__, __author__
from distutils.core import setup
setup(name='ansible',
version='1.0',
version=__version__,
description='Minimal SSH command and control',
author='Michael DeHaan',
author=__author__,
author_email='michael.dehaan@gmail.com',
url='http://github.com/mpdehaan/ansible/',
url='http://ansible.github.com/',
license='GPLv3',
package_dir = { 'ansible' : 'lib/ansible' },
install_requires=['paramiko', 'jinja2', "PyYAML"],
package_dir={ 'ansible': 'lib/ansible' },
packages=[
'ansible',
],
data_files=[
('/usr/share/ansible', [
'library/ping',
'library/command',
'library/facter',
'library/ohai',
'library/copy',
'library/setup',
'library/service',
'library/template',
'library/git',
]),
('/usr/share/man/man1', [
'docs/man/man1/ansible.1'
]),
],
scripts=[
'bin/ansible',
'bin/ansible-playbook'

4
test/CentOS.yml Normal file
View File

@ -0,0 +1,4 @@
---
# could test something different here but want people running tests on
# different OS platforms to still have passing tests
testing: default

252
test/TestInventory.py Normal file
View File

@ -0,0 +1,252 @@
import os
import unittest
from ansible.inventory import Inventory
from ansible.runner import Runner
class TestInventory(unittest.TestCase):
def setUp(self):
self.cwd = os.getcwd()
self.test_dir = os.path.join(self.cwd, 'test')
self.inventory_file = os.path.join(self.test_dir, 'simple_hosts')
self.inventory_script = os.path.join(self.test_dir, 'inventory_api.py')
self.inventory_yaml = os.path.join(self.test_dir, 'yaml_hosts')
os.chmod(self.inventory_script, 0755)
def tearDown(self):
os.chmod(self.inventory_script, 0644)
### Simple inventory format tests
def simple_inventory(self):
return Inventory( self.inventory_file )
def script_inventory(self):
return Inventory( self.inventory_script )
def yaml_inventory(self):
return Inventory( self.inventory_yaml )
def test_simple(self):
inventory = self.simple_inventory()
hosts = inventory.list_hosts()
expected_hosts=['jupiter', 'saturn', 'zeus', 'hera', 'poseidon', 'thor', 'odin', 'loki']
assert hosts == expected_hosts
def test_simple_all(self):
inventory = self.simple_inventory()
hosts = inventory.list_hosts('all')
expected_hosts=['jupiter', 'saturn', 'zeus', 'hera', 'poseidon', 'thor', 'odin', 'loki']
assert hosts == expected_hosts
def test_simple_norse(self):
inventory = self.simple_inventory()
hosts = inventory.list_hosts("norse")
expected_hosts=['thor', 'odin', 'loki']
assert hosts == expected_hosts
def test_simple_ungrouped(self):
inventory = self.simple_inventory()
hosts = inventory.list_hosts("ungrouped")
expected_hosts=['jupiter', 'saturn']
assert hosts == expected_hosts
def test_simple_combined(self):
inventory = self.simple_inventory()
hosts = inventory.list_hosts("norse:greek")
expected_hosts=['zeus', 'hera', 'poseidon', 'thor', 'odin', 'loki']
assert hosts == expected_hosts
def test_simple_restrict(self):
inventory = self.simple_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")
assert hosts == restricted_hosts
inventory.lift_restriction()
hosts = inventory.list_hosts("norse:greek")
assert hosts == expected_hosts
def test_simple_vars(self):
inventory = self.simple_inventory()
vars = inventory.get_variables('thor')
assert vars == {}
def test_simple_port(self):
inventory = self.simple_inventory()
vars = inventory.get_variables('hera')
assert vars == {'ansible_ssh_port': 3000}
### Inventory API tests
def test_script(self):
inventory = self.script_inventory()
hosts = inventory.list_hosts()
expected_hosts=['jupiter', 'saturn', 'zeus', 'hera', 'poseidon', 'thor', 'odin', 'loki']
print "Expected: %s"%(expected_hosts)
print "Got : %s"%(hosts)
assert sorted(hosts) == sorted(expected_hosts)
def test_script_all(self):
inventory = self.script_inventory()
hosts = inventory.list_hosts('all')
expected_hosts=['jupiter', 'saturn', 'zeus', 'hera', 'poseidon', 'thor', 'odin', 'loki']
assert sorted(hosts) == sorted(expected_hosts)
def test_script_norse(self):
inventory = self.script_inventory()
hosts = inventory.list_hosts("norse")
expected_hosts=['thor', 'odin', 'loki']
assert sorted(hosts) == sorted(expected_hosts)
def test_script_combined(self):
inventory = self.script_inventory()
hosts = inventory.list_hosts("norse:greek")
expected_hosts=['zeus', 'hera', 'poseidon', 'thor', 'odin', 'loki']
assert sorted(hosts) == sorted(expected_hosts)
def test_script_restrict(self):
inventory = self.script_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")
assert sorted(hosts) == sorted(restricted_hosts)
inventory.lift_restriction()
hosts = inventory.list_hosts("norse:greek")
assert sorted(hosts) == sorted(expected_hosts)
def test_script_vars(self):
inventory = self.script_inventory()
vars = inventory.get_variables('thor')
assert vars == {"hammer":True}
### Tests for yaml inventory file
def test_yaml(self):
inventory = self.yaml_inventory()
hosts = inventory.list_hosts()
print hosts
expected_hosts=['jupiter', 'saturn', 'zeus', 'hera', 'poseidon', 'thor', 'odin', 'loki']
assert hosts == expected_hosts
def test_yaml_all(self):
inventory = self.yaml_inventory()
hosts = inventory.list_hosts('all')
expected_hosts=['jupiter', 'saturn', 'zeus', 'hera', 'poseidon', 'thor', 'odin', 'loki']
assert hosts == expected_hosts
def test_yaml_norse(self):
inventory = self.yaml_inventory()
hosts = inventory.list_hosts("norse")
expected_hosts=['thor', 'odin', 'loki']
assert hosts == expected_hosts
def test_simple_ungrouped(self):
inventory = self.yaml_inventory()
hosts = inventory.list_hosts("ungrouped")
expected_hosts=['jupiter']
assert hosts == expected_hosts
def test_yaml_combined(self):
inventory = self.yaml_inventory()
hosts = inventory.list_hosts("norse:greek")
expected_hosts=['zeus', 'hera', 'poseidon', 'thor', 'odin', 'loki']
assert hosts == expected_hosts
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")
assert hosts == restricted_hosts
inventory.lift_restriction()
hosts = inventory.list_hosts("norse:greek")
assert hosts == expected_hosts
def test_yaml_vars(self):
inventory = self.yaml_inventory()
vars = inventory.get_variables('thor')
assert vars == {"hammer":True}
def test_yaml_change_vars(self):
inventory = self.yaml_inventory()
vars = inventory.get_variables('thor')
vars["hammer"] = False
vars = inventory.get_variables('thor')
assert vars == {"hammer":True}
def test_yaml_host_vars(self):
inventory = self.yaml_inventory()
vars = inventory.get_variables('saturn')
assert vars == {"moon":"titan", "moon2":"enceladus"}
def test_yaml_port(self):
inventory = self.yaml_inventory()
vars = inventory.get_variables('hera')
assert vars == {'ansible_ssh_port': 3000, 'ntp_server': 'olympus.example.com'}
### Test Runner class method
def test_class_method(self):
hosts, groups = Runner.parse_hosts(self.inventory_file)
expected_hosts = ['jupiter', 'saturn', 'zeus', 'hera', 'poseidon', 'thor', 'odin', 'loki']
assert hosts == expected_hosts
expected_groups= {
'ungrouped': ['jupiter', 'saturn'],
'greek': ['zeus', 'hera', 'poseidon'],
'norse': ['thor', 'odin', 'loki']
}
assert groups == expected_groups
def test_class_override(self):
override_hosts = ['thor', 'odin']
hosts, groups = Runner.parse_hosts(self.inventory_file, override_hosts)
assert hosts == override_hosts
assert groups == { 'ungrouped': override_hosts }

170
test/TestPlayBook.py Normal file
View File

@ -0,0 +1,170 @@
# tests are fairly 'live' (but safe to run)
# setup authorized_keys for logged in user such
# that the user can log in as themselves before running tests
import unittest
import getpass
import ansible.playbook
import ansible.utils as utils
import ansible.callbacks as ans_callbacks
import os
import shutil
import time
try:
import json
except:
import simplejson as json
EVENTS = []
class TestCallbacks(object):
# using same callbacks class for both runner and playbook
def __init__(self):
pass
def set_playbook(self, playbook):
self.playbook = playbook
def on_start(self):
EVENTS.append('start')
def on_setup_primary(self):
EVENTS.append([ 'primary_setup' ])
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):
EVENTS.append([ 'import', [ host, filename ]])
def on_error(self, host, msg):
EVENTS.append([ 'stderr', [ host, msg ]])
def on_not_import_for_host(self, host, missing_filename):
pass
def on_notify(self, host, handler):
EVENTS.append([ 'notify', [ host, handler ]])
def on_task_start(self, name, is_conditional):
EVENTS.append([ 'task start', [ name, is_conditional ]])
def on_unreachable(self, host, msg):
EVENTS.append([ 'unreachable', [ host, msg ]])
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
host_result = result.copy()
for k in [ 'ansible_job_id', 'results_file', 'invocation', 'md5sum', 'delta', 'start', 'end' ]:
if k in host_result:
del host_result[k]
for k in host_result.keys():
if k.startswith('facter_') or k.startswith('ohai_'):
del host_result[k]
EVENTS.append([ 'ok', [ host, host_result ]])
def on_play_start(self, pattern):
EVENTS.append([ 'play start', [ pattern ]])
def on_async_confused(self, msg):
EVENTS.append([ 'async confused', [ msg ]])
def on_async_poll(self, jid, host, clock, host_result):
EVENTS.append([ 'async poll', [ host ]])
def on_unreachable(self, host, msg):
EVENTS.append([ 'failed/dark', [ host, msg ]])
def on_setup_primary(self):
pass
def on_setup_secondary(self):
pass
def on_no_hosts(self):
pass
class TestPlaybook(unittest.TestCase):
def setUp(self):
self.user = getpass.getuser()
self.cwd = os.getcwd()
self.test_dir = os.path.join(self.cwd, 'test')
self.stage_dir = self._prepare_stage_dir()
if os.path.exists('/tmp/ansible_test_data_copy.out'):
os.unlink('/tmp/ansible_test_data_copy.out')
if os.path.exists('/tmp/ansible_test_data_template.out'):
os.unlink('/tmp/ansible_test_data_template.out')
def _prepare_stage_dir(self):
stage_path = os.path.join(self.test_dir, 'test_data')
if os.path.exists(stage_path):
shutil.rmtree(stage_path, ignore_errors=False)
assert not os.path.exists(stage_path)
os.makedirs(stage_path)
assert os.path.exists(stage_path)
return stage_path
def _get_test_file(self, filename):
# get a file inside the test input directory
filename = os.path.join(self.test_dir, filename)
assert os.path.exists(filename)
return filename
def _get_stage_file(self, filename):
# get a file inside the test output directory
filename = os.path.join(self.stage_dir, filename)
return filename
def _run(self, test_playbook):
''' run a module and get the localhost results '''
self.test_callbacks = TestCallbacks()
self.playbook = ansible.playbook.PlayBook(
playbook = test_playbook,
host_list = 'test/ansible_hosts',
module_path = 'library/',
forks = 1,
timeout = 5,
remote_user = self.user,
remote_pass = None,
stats = ans_callbacks.AggregateStats(),
callbacks = self.test_callbacks,
runner_callbacks = self.test_callbacks
)
result = self.playbook.run()
print utils.bigjson(dict(events=EVENTS))
return result
def test_one(self):
pb = os.path.join(self.test_dir, 'playbook1.yml')
actual = self._run(pb)
# if different, this will output to screen
print "**ACTUAL**"
print utils.bigjson(actual)
expected = {
"127.0.0.2": {
"changed": 9,
"failures": 0,
"ok": 12,
"skipped": 1,
"unreachable": 0
}
}
print "**EXPECTED**"
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()
assert data.find("ears") != -1, "template success"

View File

@ -14,6 +14,15 @@ try:
except:
import simplejson as json
from nose.plugins.skip import SkipTest
def get_binary(name):
for directory in os.environ["PATH"].split(os.pathsep):
path = os.path.join(directory, name)
if os.path.isfile(path) and os.access(path, os.X_OK):
return path
return None
class TestRunner(unittest.TestCase):
def setUp(self):
@ -21,15 +30,14 @@ class TestRunner(unittest.TestCase):
self.runner = ansible.runner.Runner(
module_name='ping',
module_path='library/',
module_args=[],
remote_user='root',
module_args='',
remote_user=self.user,
remote_pass=None,
host_list='test/ansible_hosts',
timeout=5,
forks=1,
background=0,
pattern='all',
verbose=True,
)
self.cwd = os.getcwd()
self.test_dir = os.path.join(self.cwd, 'test')
@ -58,26 +66,34 @@ class TestRunner(unittest.TestCase):
def _run(self, module_name, module_args, background=0):
''' run a module and get the localhost results '''
self.runner.module_name = module_name
self.runner.module_args = module_args
args = ' '.join(module_args)
print "DEBUG: using args=%s" % args
self.runner.module_args = args
self.runner.background = background
results = self.runner.run()
# when using nosetests this will only show up on failure
# which is pretty useful
print "RESULTS=%s" % results
assert "127.0.0.1" in results['contacted']
return results['contacted']['127.0.0.1']
assert "127.0.0.2" in results['contacted']
return results['contacted']['127.0.0.2']
def test_ping(self):
result = self._run('ping',[])
assert "ping" in result
def test_facter(self):
if not get_binary("facter"):
raise SkipTest
result = self._run('facter',[])
assert "hostname" in result
def test_ohai(self):
result = self._run('ohai',[])
assert "hostname" in result
# temporarily disbabled since it occasionally hangs
# ohai's fault, setup module doesn't actually run this
# to get ohai's "facts" anyway
#
#def test_ohai(self):
# result = self._run('ohai',[])
# assert "hostname" in result
def test_copy(self):
# test copy module, change trigger, etc
@ -127,6 +143,7 @@ class TestRunner(unittest.TestCase):
assert result['changed'] == False
def test_command(self):
# test command module, change trigger, etc
result = self._run('command', [ "/bin/echo", "hi" ])
assert "failed" not in result
@ -134,14 +151,21 @@ class TestRunner(unittest.TestCase):
assert result['rc'] == 0
assert result['stdout'] == 'hi'
assert result['stderr'] == ''
result = self._run('command', [ "/bin/false" ])
assert result['rc'] == 1
assert 'failed' not in result
result = self._run('command', [ "/usr/bin/this_does_not_exist", "splat" ])
assert 'msg' in result
assert 'failed' in result
assert 'rc' not in result
result = self._run('shell', [ "/bin/echo", "$HOME" ])
assert 'failed' not in result
assert result['rc'] == 0
def test_setup(self):
output = self._get_stage_file('output.json')
result = self._run('setup', [ "metadata=%s" % output, "a=2", "b=3", "c=4" ])
@ -154,18 +178,19 @@ class TestRunner(unittest.TestCase):
# 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
result = self._run('command', [ "/bin/sleep", "3" ], background=20)
result = self._run('command', [ get_binary("sleep"), "3" ], background=20)
assert 'ansible_job_id' in result
assert 'started' in result
jid = result['ansible_job_id']
# no real chance of this op taking a while, but whatever
time.sleep(5)
# CLI will abstract this, but this is how it works internally
# CLI will abstract this (when polling), but this is how it works internally
result = self._run('async_status', [ "jid=%s" % jid ])
# TODO: would be nice to have tests for supervisory process
# killing job after X seconds
@ -175,6 +200,19 @@ class TestRunner(unittest.TestCase):
assert 'stdout' in result
assert result['ansible_job_id'] == jid
def test_fetch(self):
input = self._get_test_file('sample.j2')
output = os.path.join(self.stage_dir, '127.0.0.2', input)
result = self._run('fetch', [ "src=%s" % input, "dest=%s" % self.stage_dir ])
assert os.path.exists(output)
assert open(input).read() == open(output).read()
def test_yum(self):
if not get_binary("yum"):
raise SkipTest
result = self._run('yum', [ "list=repos" ])
assert 'failed' not in result
def test_git(self):
# TODO: tests for the git module
pass

View File

@ -1,2 +1,2 @@
[somegroup]
127.0.0.1
127.0.0.2

3
test/common_vars.yml Normal file
View File

@ -0,0 +1,3 @@
---
duck: quack
cow: moo

2
test/default_os.yml Normal file
View File

@ -0,0 +1,2 @@
---
testing: default

39
test/inventory_api.py Normal file
View File

@ -0,0 +1,39 @@
#!/usr/bin/env python
import json
import sys
from optparse import OptionParser
parser = OptionParser()
parser.add_option('-l', '--list', default=False, dest="list_hosts", action="store_true")
parser.add_option('-H', '--host', default=None, dest="host")
parser.add_option('-e', '--extra-vars', default=None, dest="extra")
options, args = parser.parse_args()
systems = {
"ungouped": [ "jupiter", "saturn" ],
"greek": [ "zeus", "hera", "poseidon" ],
"norse": [ "thor", "odin", "loki" ]
}
variables = {
"thor": {
"hammer": True
}
}
if options.list_hosts == True:
print json.dumps(systems)
sys.exit(0)
if options.host is not None:
if options.extra:
k,v = options.extra.split("=")
variables[options.host][k] = v
print json.dumps(variables[options.host])
sys.exit(0)
parser.print_help()
sys.exit(1)

78
test/playbook1.yml Normal file
View File

@ -0,0 +1,78 @@
# extremely simple test of the most basic of playbook engine/functions
---
- hosts: all
# the 'weasels' string should show up in the output
vars:
answer: "Wuh, I think so, Brain, but if we didn't have ears, we'd look like weasels."
port: 5150
# we should have import events for common_vars and CentOS.yml (if run on CentOS)
# sorry, tests are a bit platform specific just for now
vars_files:
- common_vars.yml
- [ '$facter_operatingsystem.yml', 'default_os.yml' ]
tasks:
- name: test basic success command
action: command /bin/true
- name: test basic success command 2
action: command /bin/true
- name: test basic shell, plus two ways to dereference a variable
action: shell echo $HOME $port {{ port }}
- name: test vars_files imports
action: shell echo $duck $cow $testing
# in the command below, the test file should contain a valid template
# and trigger the change handler
- name: test copy
action: copy src=sample.j2 dest=/tmp/ansible_test_data_copy.out
notify:
- on change 1
# this should trigger two change handlers, but the 2nd should
# not be triggered twice because it's already triggered
- name: test template
action: template src=sample.j2 dest=/tmp/ansible_test_data_template.out
notify:
- on change 1
- on change 2
# there should be various poll events within the range
- name: async poll test
action: shell sleep 5
async: 10
poll: 3
# the following command should be skipped
- name: this should be skipped
action: shell echo 'if you see this, this is wrong ($facter_operatingsystem)'
only_if: "'$facter_operatingsystem' == 'Imaginary'"
handlers:
# in the above test example, this should fire ONCE (at the end)
- name: on change 1
action: shell echo 'this should fire once'
# in the above test example, this should fire ONCE (at the end)
- name: on change 2
action: shell echo 'this should fire once also'
# in the above test example, this should NOT FIRE
- name: on change 3
action: shell echo 'if you see this, this is wrong'

71
test/playbook2.yml Normal file
View File

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

71
test/playbook3.yml Normal file
View File

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

12
test/simple_hosts Normal file
View File

@ -0,0 +1,12 @@
jupiter
saturn
[greek]
zeus
hera:3000
poseidon
[norse]
thor
odin
loki

30
test/yaml_hosts Normal file
View File

@ -0,0 +1,30 @@
---
- jupiter
- host: saturn
vars:
moon: titan
moon2: enceladus
- zeus
- 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: multiple
hosts:
- saturn