mirror of https://github.com/ansible/ansible.git
Merge branch 'master' of github.com:mpdehaan/ansible
commit
9b3d2b97a7
@ -0,0 +1,3 @@
|
|||||||
|
*~
|
||||||
|
*.py[co]
|
||||||
|
build
|
@ -0,0 +1,12 @@
|
|||||||
|
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
|
||||||
|
|
||||||
|
Send in a github pull request to get your name here.
|
||||||
|
|
||||||
|
Upstream: github.com/mpdehaan/ansible
|
||||||
|
|
@ -0,0 +1,32 @@
|
|||||||
|
#!/usr/bin/make
|
||||||
|
|
||||||
|
ASCII2MAN = a2x -D $(dir $@) -d manpage -f manpage $<
|
||||||
|
ASCII2HTMLMAN = a2x -D docs/html/man/ -d manpage -f xhtml
|
||||||
|
MANPAGES := docs/man/man1/ansible.1
|
||||||
|
SITELIB = $(shell python -c "from distutils.sysconfig import get_python_lib; print get_python_lib()")
|
||||||
|
|
||||||
|
docs: manuals
|
||||||
|
|
||||||
|
manuals: $(MANPAGES)
|
||||||
|
|
||||||
|
%.1: %.1.asciidoc
|
||||||
|
$(ASCII2MAN)
|
||||||
|
|
||||||
|
%.5: %.5.asciidoc
|
||||||
|
$(ASCII2MAN)
|
||||||
|
|
||||||
|
pep8:
|
||||||
|
@echo "#############################################"
|
||||||
|
@echo "# Running PEP8 Compliance Tests"
|
||||||
|
@echo "#############################################"
|
||||||
|
pep8 lib/
|
||||||
|
|
||||||
|
clean:
|
||||||
|
find . -type f -name "*.pyc" -delete
|
||||||
|
find . -type f -name "*.pyo" -delete
|
||||||
|
find . -type f -name "*~" -delete
|
||||||
|
find ./docs/ -type f -name "*.xml" -delete
|
||||||
|
find . -type f -name "#*" -delete
|
||||||
|
|
||||||
|
.PHONEY: docs manual clean pep8
|
||||||
|
vpath %.asciidoc docs/man/man1
|
@ -0,0 +1,44 @@
|
|||||||
|
TODO list and plans
|
||||||
|
===================
|
||||||
|
|
||||||
|
Playbook TODO:
|
||||||
|
|
||||||
|
* error codes and failure summaries
|
||||||
|
* create modules that return 'changed' attributes
|
||||||
|
* fail nodes on errors, i.e. remove from host list, rather than continuing to pound them
|
||||||
|
* further improve output
|
||||||
|
* more conditional capability
|
||||||
|
* very good logging
|
||||||
|
|
||||||
|
Command module:
|
||||||
|
* allow additional key/value options to be passed to any module (via ENV vars?)
|
||||||
|
* allow this to be expressed in playbook as a 4th option after the array options list
|
||||||
|
* use this to pass timeout and async params to the command module
|
||||||
|
default timeouts will be infinite, async False
|
||||||
|
|
||||||
|
General:
|
||||||
|
|
||||||
|
* logging
|
||||||
|
* async options
|
||||||
|
* modules for users, groups, and files, using puppet style ensure mechanics
|
||||||
|
* very simple option constructing/parsing for modules
|
||||||
|
* templating module (how might this work syntax wise?) with facter/ohai awareness
|
||||||
|
* probably could lay down a values.json file
|
||||||
|
* use copy capabilities to move files to tmp, run python templating
|
||||||
|
* maybe support templating engine of choice
|
||||||
|
* think about how to build idempotency guards around command module?
|
||||||
|
* think about how to feed extra JSON data onto system
|
||||||
|
|
||||||
|
Bonus utilities:
|
||||||
|
|
||||||
|
* ansible-inventory - gathering fact/hw info, storing in git, adding RSS
|
||||||
|
* ansible-slurp - recursively rsync file trees for each host
|
||||||
|
* maybe it's own fact engine, not required, that also feeds from facter
|
||||||
|
|
||||||
|
Not so interested really, but maybe:
|
||||||
|
|
||||||
|
* list available modules from command line
|
||||||
|
* add/remove/list hosts from the command line
|
||||||
|
* filter exclusion (run this only if fact is true/false)
|
||||||
|
-- should be doable with playbooks (i.e. not neccessary)
|
||||||
|
|
@ -0,0 +1 @@
|
|||||||
|
*.xml
|
@ -0,0 +1,108 @@
|
|||||||
|
'\" t
|
||||||
|
.\" Title: ansible
|
||||||
|
.\" Author: [see the "AUTHOR" section]
|
||||||
|
.\" Generator: DocBook XSL Stylesheets v1.76.1 <http://docbook.sf.net/>
|
||||||
|
.\" Date: 02/24/2012
|
||||||
|
.\" Manual: System administration commands
|
||||||
|
.\" Source: Ansible 0.0.1
|
||||||
|
.\" Language: English
|
||||||
|
.\"
|
||||||
|
.TH "ANSIBLE" "1" "02/24/2012" "Ansible 0\&.0\&.1" "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 [\-H hosts_path] [\-L library_path] [\-f forks] [\-n module_name] [\-a [args1 [args2 \&...]]] [\-p host_pattern] [\-u remote_user]
|
||||||
|
.SH "DESCRIPTION"
|
||||||
|
.sp
|
||||||
|
\fBAnsible\fR is an extra\-simple Python API for doing \*(Aqremote things\*(Aq over SSH\&.
|
||||||
|
.SH "OPTIONS"
|
||||||
|
.PP
|
||||||
|
\fB\-P\fR, \fB\-\-askpass\fR
|
||||||
|
.RS 4
|
||||||
|
Ask the user to input the ssh password for connecting\&.
|
||||||
|
.RE
|
||||||
|
.PP
|
||||||
|
\fB\-H\fR, \fB\-\-host\-list\fR
|
||||||
|
.RS 4
|
||||||
|
Path to hosts list\&.
|
||||||
|
.RE
|
||||||
|
.PP
|
||||||
|
\fB\-L\fR, \fB\-\-library\fR
|
||||||
|
.RS 4
|
||||||
|
Path to module library\&.
|
||||||
|
.RE
|
||||||
|
.PP
|
||||||
|
\fB\-f\fR, \fB\-\-forks\fR
|
||||||
|
.RS 4
|
||||||
|
Level of parallelism\&. Specify as an integer\&.
|
||||||
|
.RE
|
||||||
|
.PP
|
||||||
|
\fB\-n\fR, \fB\-\-name\fR
|
||||||
|
.RS 4
|
||||||
|
Module name to execute\&.
|
||||||
|
.RE
|
||||||
|
.PP
|
||||||
|
\fB\-a\fR, \fB\-\-args\fR
|
||||||
|
.RS 4
|
||||||
|
Arguments to module\&.
|
||||||
|
.RE
|
||||||
|
.PP
|
||||||
|
\fB\-p\fR, \fB\-\-pattern\fR
|
||||||
|
.RS 4
|
||||||
|
Hostname pattern\&. Accepts shell\-like globs\&.
|
||||||
|
.RE
|
||||||
|
.PP
|
||||||
|
\fB\-r\fR, \fB\-\-run\-playbook\fR
|
||||||
|
.RS 4
|
||||||
|
Playbook file to run\&. Replaces the
|
||||||
|
\fB\-n\fR
|
||||||
|
and
|
||||||
|
\fB\-a\fR
|
||||||
|
options\&.
|
||||||
|
.RE
|
||||||
|
.PP
|
||||||
|
\fB\-u\fR, \fB\-\-remote\-user\fR
|
||||||
|
.RS 4
|
||||||
|
Remote user to connect as\&. Uses
|
||||||
|
\fIroot\fR
|
||||||
|
by default\&.
|
||||||
|
.RE
|
||||||
|
.SH "INVENTORY"
|
||||||
|
.sp
|
||||||
|
Ansible stores the hosts it can potentially operate on in an inventory file\&. The syntax is simple: one host per line\&. Organize your hosts into multiple groups by separating them into multiple inventory files\&.
|
||||||
|
.SH "FILES"
|
||||||
|
.sp
|
||||||
|
/etc/ansible/hosts \(em Default hosts file
|
||||||
|
.sp
|
||||||
|
/usr/share/ansible \(em Default module library
|
||||||
|
.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 MIT license\&.
|
||||||
|
.SH "SEE ALSO"
|
||||||
|
.sp
|
||||||
|
Ansible home page: https://github\&.com/mpdehaan/ansible/
|
@ -0,0 +1,109 @@
|
|||||||
|
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 [-H hosts_path] [-L library_path] [-f forks] [-n module_name]
|
||||||
|
[-a [args1 [args2 ...]]] [-p host_pattern] [-u remote_user]
|
||||||
|
|
||||||
|
|
||||||
|
DESCRIPTION
|
||||||
|
-----------
|
||||||
|
|
||||||
|
*Ansible* is an extra-simple Python API for doing \'remote things' over
|
||||||
|
SSH.
|
||||||
|
|
||||||
|
|
||||||
|
OPTIONS
|
||||||
|
-------
|
||||||
|
|
||||||
|
*-P*, *--askpass*::
|
||||||
|
|
||||||
|
Ask the user to input the ssh password for connecting.
|
||||||
|
|
||||||
|
|
||||||
|
*-H*, *--host-list*::
|
||||||
|
|
||||||
|
Path to hosts list.
|
||||||
|
|
||||||
|
|
||||||
|
*-L*, *--library*::
|
||||||
|
|
||||||
|
Path to module library.
|
||||||
|
|
||||||
|
|
||||||
|
*-f*, *--forks*::
|
||||||
|
|
||||||
|
Level of parallelism. Specify as an integer.
|
||||||
|
|
||||||
|
|
||||||
|
*-n*, *--name*::
|
||||||
|
|
||||||
|
Module name to execute.
|
||||||
|
|
||||||
|
|
||||||
|
*-a*, *--args*::
|
||||||
|
|
||||||
|
Arguments to module.
|
||||||
|
|
||||||
|
|
||||||
|
*-p*, *--pattern*::
|
||||||
|
|
||||||
|
Hostname pattern. Accepts shell-like globs.
|
||||||
|
|
||||||
|
|
||||||
|
*-r*, *--run-playbook*::
|
||||||
|
|
||||||
|
Playbook file to run. Replaces the *-n* and *-a* options.
|
||||||
|
|
||||||
|
|
||||||
|
*-u*, *--remote-user*::
|
||||||
|
|
||||||
|
Remote user to connect as. Uses __root__ by default.
|
||||||
|
|
||||||
|
|
||||||
|
INVENTORY
|
||||||
|
---------
|
||||||
|
|
||||||
|
Ansible stores the hosts it can potentially operate on in an inventory
|
||||||
|
file. The syntax is simple: one host per line. Organize your hosts
|
||||||
|
into multiple groups by separating them into multiple inventory files.
|
||||||
|
|
||||||
|
|
||||||
|
FILES
|
||||||
|
-----
|
||||||
|
|
||||||
|
/etc/ansible/hosts -- Default hosts file
|
||||||
|
|
||||||
|
/usr/share/ansible -- Default module library
|
||||||
|
|
||||||
|
|
||||||
|
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 MIT license.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
SEE ALSO
|
||||||
|
--------
|
||||||
|
|
||||||
|
Ansible home page: <https://github.com/mpdehaan/ansible/>
|
@ -0,0 +1,16 @@
|
|||||||
|
- pattern: '*'
|
||||||
|
tasks:
|
||||||
|
- do:
|
||||||
|
- update apache
|
||||||
|
- command
|
||||||
|
- [/usr/bin/yum, update, apache]
|
||||||
|
onchange:
|
||||||
|
- do:
|
||||||
|
- restart apache
|
||||||
|
- command
|
||||||
|
- [/sbin/service, apache, restart]
|
||||||
|
- do:
|
||||||
|
- run bin false
|
||||||
|
- command
|
||||||
|
- [/bin/false]
|
||||||
|
|
@ -1,153 +0,0 @@
|
|||||||
# Copyright (c) 2012 Michael DeHaan <michael.dehaan@gmail.com>
|
|
||||||
#
|
|
||||||
# Permission is hereby granted, free of charge, to any person
|
|
||||||
# obtaining a copy of this software and associated documentation
|
|
||||||
# files (the "Software"), to deal in the Software without restriction,
|
|
||||||
# including without limitation the rights to use, copy, modify, merge,
|
|
||||||
# publish, distribute, sublicense, and/or sell copies of the Software,
|
|
||||||
# and to permit persons to whom the Software is furnished to do so,
|
|
||||||
# subject to the following conditions:
|
|
||||||
#
|
|
||||||
# The above copyright notice and this permission notice shall be
|
|
||||||
# included in all copies or substantial portions of the Software.
|
|
||||||
#
|
|
||||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
||||||
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
||||||
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
|
||||||
# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR
|
|
||||||
# ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
|
|
||||||
# CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|
||||||
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
||||||
|
|
||||||
import fnmatch
|
|
||||||
from multiprocessing import Process, Pipe
|
|
||||||
from itertools import izip
|
|
||||||
import os
|
|
||||||
import json
|
|
||||||
|
|
||||||
# non-core
|
|
||||||
import paramiko
|
|
||||||
|
|
||||||
# TODO -- library should have defaults, not just CLI
|
|
||||||
# update Runner constructor below to use
|
|
||||||
|
|
||||||
DEFAULT_HOST_LIST = '~/.ansible_hosts'
|
|
||||||
DEFAULT_MODULE_PATH = '~/ansible'
|
|
||||||
DEFAULT_MODULE_NAME = 'ping'
|
|
||||||
DEFAULT_PATTERN = '*'
|
|
||||||
DEFAULT_FORKS = 3
|
|
||||||
DEFAULT_MODULE_ARGS = ''
|
|
||||||
|
|
||||||
class Pooler(object):
|
|
||||||
|
|
||||||
# credit: http://stackoverflow.com/questions/3288595/multiprocessing-using-pool-map-on-a-function-defined-in-a-class
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def spawn(cls, f):
|
|
||||||
def fun(pipe,x):
|
|
||||||
pipe.send(f(x))
|
|
||||||
pipe.close()
|
|
||||||
return fun
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def parmap(cls, f, X):
|
|
||||||
pipe=[Pipe() for x in X]
|
|
||||||
proc=[Process(target=cls.spawn(f),args=(c,x)) for x,(p,c) in izip(X,pipe)]
|
|
||||||
[p.start() for p in proc]
|
|
||||||
[p.join() for p in proc]
|
|
||||||
return [p.recv() for (p,c) in pipe]
|
|
||||||
|
|
||||||
class Runner(object):
|
|
||||||
|
|
||||||
def __init__(self, host_list=[], module_path=None,
|
|
||||||
module_name=None, module_args=[],
|
|
||||||
forks=3, timeout=60, pattern='*'):
|
|
||||||
|
|
||||||
self.host_list = host_list
|
|
||||||
self.module_path = module_path
|
|
||||||
self.module_name = module_name
|
|
||||||
self.forks = forks
|
|
||||||
self.pattern = pattern
|
|
||||||
self.module_args = module_args
|
|
||||||
self.timeout = timeout
|
|
||||||
|
|
||||||
|
|
||||||
def _matches(self, host_name):
|
|
||||||
if host_name == '':
|
|
||||||
return False
|
|
||||||
if fnmatch.fnmatch(host_name, self.pattern):
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
def _connect(self, host):
|
|
||||||
ssh = paramiko.SSHClient()
|
|
||||||
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
|
||||||
try:
|
|
||||||
ssh.connect(host, username='root',
|
|
||||||
allow_agent=True, look_for_keys=True)
|
|
||||||
return ssh
|
|
||||||
except:
|
|
||||||
return None
|
|
||||||
|
|
||||||
def _executor(self, host):
|
|
||||||
# TODO: try/catch returning none
|
|
||||||
conn = self._connect(host)
|
|
||||||
if not conn:
|
|
||||||
return [ host, None ]
|
|
||||||
if self.module_name != "copy":
|
|
||||||
outpath = self._copy_module(conn)
|
|
||||||
self._exec_command(conn, "chmod +x %s" % outpath)
|
|
||||||
cmd = self._command(outpath)
|
|
||||||
result = self._exec_command(conn, cmd)
|
|
||||||
result = json.loads(result)
|
|
||||||
else:
|
|
||||||
ftp = conn.open_sftp()
|
|
||||||
ftp.put(self.module_args[0], self.module_args[1])
|
|
||||||
ftp.close()
|
|
||||||
return [ host, 1 ]
|
|
||||||
|
|
||||||
return [ host, result ]
|
|
||||||
|
|
||||||
def _command(self, outpath):
|
|
||||||
cmd = "%s %s" % (outpath, " ".join(self.module_args))
|
|
||||||
return cmd
|
|
||||||
|
|
||||||
def _exec_command(self, conn, cmd):
|
|
||||||
stdin, stdout, stderr = conn.exec_command(cmd)
|
|
||||||
results = stdout.read()
|
|
||||||
return results
|
|
||||||
|
|
||||||
def _copy_module(self, conn):
|
|
||||||
inpath = os.path.expanduser(os.path.join(self.module_path, self.module_name))
|
|
||||||
outpath = os.path.join("/var/spool/", "ansible_%s" % self.module_name)
|
|
||||||
ftp = conn.open_sftp()
|
|
||||||
ftp.put(inpath, outpath)
|
|
||||||
ftp.close()
|
|
||||||
return outpath
|
|
||||||
|
|
||||||
def run(self):
|
|
||||||
hosts = [ h for h in self.host_list if self._matches(h) ]
|
|
||||||
def executor(x):
|
|
||||||
return self._executor(x)
|
|
||||||
results = Pooler.parmap(executor, hosts)
|
|
||||||
by_host = dict(results)
|
|
||||||
return by_host
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
|
|
||||||
|
|
||||||
# TODO: if host list is string load from file
|
|
||||||
|
|
||||||
r = Runner(
|
|
||||||
host_list = [ '127.0.0.1' ],
|
|
||||||
module_path='~/ansible',
|
|
||||||
module_name='ping',
|
|
||||||
module_args='',
|
|
||||||
pattern='*',
|
|
||||||
forks=3
|
|
||||||
)
|
|
||||||
print r.run()
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -0,0 +1,10 @@
|
|||||||
|
DEFAULT_HOST_LIST = '/etc/ansible/hosts'
|
||||||
|
DEFAULT_MODULE_PATH = '/usr/share/ansible'
|
||||||
|
DEFAULT_MODULE_NAME = 'ping'
|
||||||
|
DEFAULT_PATTERN = '*'
|
||||||
|
DEFAULT_FORKS = 3
|
||||||
|
DEFAULT_MODULE_ARGS = ''
|
||||||
|
DEFAULT_TIMEOUT = 60
|
||||||
|
DEFAULT_REMOTE_USER = 'root'
|
||||||
|
DEFAULT_REMOTE_PASS = None
|
||||||
|
|
@ -0,0 +1,175 @@
|
|||||||
|
# Copyright (c) 2012 Michael DeHaan <michael.dehaan@gmail.com>
|
||||||
|
#
|
||||||
|
# Permission is hereby granted, free of charge, to any person
|
||||||
|
# obtaining a copy of this software and associated documentation
|
||||||
|
# files (the "Software"), to deal in the Software without restriction,
|
||||||
|
# including without limitation the rights to use, copy, modify, merge,
|
||||||
|
# publish, distribute, sublicense, and/or sell copies of the Software,
|
||||||
|
# and to permit persons to whom the Software is furnished to do so,
|
||||||
|
# subject to the following conditions:
|
||||||
|
#
|
||||||
|
# The above copyright notice and this permission notice shall be
|
||||||
|
# included in all copies or substantial portions of the Software.
|
||||||
|
#
|
||||||
|
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||||
|
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||||
|
# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR
|
||||||
|
# ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
|
||||||
|
# CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
|
||||||
|
import ansible.runner
|
||||||
|
import ansible.constants as C
|
||||||
|
import json
|
||||||
|
import yaml
|
||||||
|
|
||||||
|
# TODO: make a constants file rather than
|
||||||
|
# duplicating these
|
||||||
|
|
||||||
|
class PlayBook(object):
|
||||||
|
'''
|
||||||
|
runs an ansible playbook, given as a datastructure
|
||||||
|
or YAML filename. a playbook is a deployment, config
|
||||||
|
management, or automation based set of commands to
|
||||||
|
run in series.
|
||||||
|
|
||||||
|
multiple patterns do not execute simultaneously,
|
||||||
|
but tasks in each pattern do execute in parallel
|
||||||
|
according to the number of forks requested.
|
||||||
|
'''
|
||||||
|
|
||||||
|
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):
|
||||||
|
|
||||||
|
# runner is reused between calls
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
if type(playbook) == str:
|
||||||
|
playbook = yaml.load(file(playbook).read())
|
||||||
|
self.playbook = playbook
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
''' run against all patterns in the playbook '''
|
||||||
|
|
||||||
|
for pattern in self.playbook:
|
||||||
|
self._run_pattern(pattern)
|
||||||
|
|
||||||
|
# TODO: return a summary of success & failure counts per node
|
||||||
|
# TODO: in bin/ancible, ensure return codes are appropriate
|
||||||
|
|
||||||
|
return "complete"
|
||||||
|
|
||||||
|
def _get_task_runner(self,
|
||||||
|
pattern=None,
|
||||||
|
host_list=None,
|
||||||
|
module_name=None,
|
||||||
|
module_args=None):
|
||||||
|
|
||||||
|
'''
|
||||||
|
return a runner suitable for running this task, using
|
||||||
|
preferences from the constructor
|
||||||
|
'''
|
||||||
|
|
||||||
|
if host_list is None:
|
||||||
|
host_list = self.host_list
|
||||||
|
|
||||||
|
return ansible.runner.Runner(
|
||||||
|
pattern=pattern,
|
||||||
|
module_name=module_name,
|
||||||
|
module_args=module_args,
|
||||||
|
host_list=host_list,
|
||||||
|
forks=self.forks,
|
||||||
|
remote_user=self.remote_user,
|
||||||
|
remote_pass=self.remote_pass,
|
||||||
|
module_path=self.module_path,
|
||||||
|
timeout=self.timeout
|
||||||
|
)
|
||||||
|
|
||||||
|
def _run_task(self, pattern, task, host_list=None, conditional=False):
|
||||||
|
'''
|
||||||
|
run a single task in the playbook and
|
||||||
|
recursively run any subtasks.
|
||||||
|
'''
|
||||||
|
|
||||||
|
if host_list is None:
|
||||||
|
host_list = self.host_list
|
||||||
|
|
||||||
|
instructions = task['do']
|
||||||
|
(comment, module_name, module_args) = instructions
|
||||||
|
|
||||||
|
namestr = "%s/%s" % (pattern, comment)
|
||||||
|
if conditional:
|
||||||
|
namestr = "subset/%s" % namestr
|
||||||
|
print "TASK [%s]" % namestr
|
||||||
|
|
||||||
|
runner = self._get_task_runner(
|
||||||
|
pattern=pattern,
|
||||||
|
host_list=host_list,
|
||||||
|
module_name=module_name,
|
||||||
|
module_args=module_args
|
||||||
|
)
|
||||||
|
results = runner.run()
|
||||||
|
|
||||||
|
dark = results.get("dark", [])
|
||||||
|
|
||||||
|
contacted = results.get("contacted", [])
|
||||||
|
|
||||||
|
# TODO: filter based on values that indicate
|
||||||
|
# they have changed events to emulate Puppet
|
||||||
|
# 'notify' behavior, super easy -- just
|
||||||
|
# a list comprehension -- but we need complaint
|
||||||
|
# modules first
|
||||||
|
|
||||||
|
ok_hosts = contacted.keys()
|
||||||
|
|
||||||
|
for host, msg in dark.items():
|
||||||
|
print "DARK: [%s] => %s" % (host, msg)
|
||||||
|
|
||||||
|
for host, results in contacted.items():
|
||||||
|
if module_name == "command":
|
||||||
|
if results.get("rc", 0) != 0:
|
||||||
|
print "FAIL: [%s/%s] => %s" % (host, comment, results)
|
||||||
|
elif results.get("failed", 0) == 1:
|
||||||
|
print "FAIL: [%s/%s]" % (host, comment, results)
|
||||||
|
|
||||||
|
|
||||||
|
subtasks = task.get('onchange', [])
|
||||||
|
if len(subtasks) > 0:
|
||||||
|
for subtask in subtasks:
|
||||||
|
self._run_task(pattern, subtask, ok_hosts, conditional=True)
|
||||||
|
|
||||||
|
# TODO: if a host fails in task 1, add it to an excludes
|
||||||
|
# list such that no other tasks in the list ever execute
|
||||||
|
# unlike Puppet, do not allow partial failure of the tree
|
||||||
|
# and continuing as far as possible. Fail fast.
|
||||||
|
|
||||||
|
|
||||||
|
def _run_pattern(self, pg):
|
||||||
|
'''
|
||||||
|
run a list of tasks for a given pattern, in order
|
||||||
|
'''
|
||||||
|
|
||||||
|
pattern = pg['pattern']
|
||||||
|
tasks = pg['tasks']
|
||||||
|
for task in tasks:
|
||||||
|
self._run_task(pattern, task)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -0,0 +1,198 @@
|
|||||||
|
# Copyright (c) 2012 Michael DeHaan <michael.dehaan@gmail.com>
|
||||||
|
#
|
||||||
|
# Permission is hereby granted, free of charge, to any person
|
||||||
|
# obtaining a copy of this software and associated documentation
|
||||||
|
# files (the "Software"), to deal in the Software without restriction,
|
||||||
|
# including without limitation the rights to use, copy, modify, merge,
|
||||||
|
# publish, distribute, sublicense, and/or sell copies of the Software,
|
||||||
|
# and to permit persons to whom the Software is furnished to do so,
|
||||||
|
# subject to the following conditions:
|
||||||
|
#
|
||||||
|
# The above copyright notice and this permission notice shall be
|
||||||
|
# included in all copies or substantial portions of the Software.
|
||||||
|
#
|
||||||
|
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||||
|
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||||
|
# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR
|
||||||
|
# ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
|
||||||
|
# CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
|
||||||
|
import fnmatch
|
||||||
|
import multiprocessing
|
||||||
|
import os
|
||||||
|
import json
|
||||||
|
import traceback
|
||||||
|
|
||||||
|
# non-core
|
||||||
|
import paramiko
|
||||||
|
|
||||||
|
import constants as C
|
||||||
|
|
||||||
|
def _executor_hook(x):
|
||||||
|
''' callback used by multiprocessing pool '''
|
||||||
|
(runner, host) = x
|
||||||
|
return runner._executor(host)
|
||||||
|
|
||||||
|
class Runner(object):
|
||||||
|
|
||||||
|
def __init__(self,
|
||||||
|
host_list=C.DEFAULT_HOST_LIST,
|
||||||
|
module_path=C.DEFAULT_MODULE_PATH,
|
||||||
|
module_name=C.DEFAULT_MODULE_NAME,
|
||||||
|
module_args=C.DEFAULT_MODULE_ARGS,
|
||||||
|
forks=C.DEFAULT_FORKS,
|
||||||
|
timeout=C.DEFAULT_TIMEOUT,
|
||||||
|
pattern=C.DEFAULT_PATTERN,
|
||||||
|
remote_user=C.DEFAULT_REMOTE_USER,
|
||||||
|
remote_pass=C.DEFAULT_REMOTE_PASS,
|
||||||
|
verbose=False):
|
||||||
|
|
||||||
|
|
||||||
|
'''
|
||||||
|
Constructor.
|
||||||
|
'''
|
||||||
|
|
||||||
|
self.host_list = self._parse_hosts(host_list)
|
||||||
|
self.module_path = module_path
|
||||||
|
self.module_name = module_name
|
||||||
|
self.forks = forks
|
||||||
|
self.pattern = pattern
|
||||||
|
self.module_args = module_args
|
||||||
|
self.timeout = timeout
|
||||||
|
self.verbose = verbose
|
||||||
|
self.remote_user = remote_user
|
||||||
|
self.remote_pass = remote_pass
|
||||||
|
|
||||||
|
def _parse_hosts(self, host_list):
|
||||||
|
''' parse the host inventory file if not sent as an array '''
|
||||||
|
if type(host_list) != list:
|
||||||
|
host_list = os.path.expanduser(host_list)
|
||||||
|
return file(host_list).read().split("\n")
|
||||||
|
return host_list
|
||||||
|
|
||||||
|
|
||||||
|
def _matches(self, host_name):
|
||||||
|
''' returns if a hostname is matched by the pattern '''
|
||||||
|
if host_name == '':
|
||||||
|
return False
|
||||||
|
if fnmatch.fnmatch(host_name, self.pattern):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _connect(self, host):
|
||||||
|
'''
|
||||||
|
obtains a paramiko connection to the host.
|
||||||
|
on success, returns (True, connection)
|
||||||
|
on failure, returns (False, traceback str)
|
||||||
|
'''
|
||||||
|
ssh = paramiko.SSHClient()
|
||||||
|
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||||
|
try:
|
||||||
|
ssh.connect(host, username=self.remote_user, allow_agent=True,
|
||||||
|
look_for_keys=True, password=self.remote_pass)
|
||||||
|
return [ True, ssh ]
|
||||||
|
except:
|
||||||
|
return [ False, traceback.format_exc() ]
|
||||||
|
|
||||||
|
def _executor(self, host):
|
||||||
|
'''
|
||||||
|
callback executed in parallel for each host.
|
||||||
|
returns (hostname, connected_ok, extra)
|
||||||
|
where extra is the result of a successful connect
|
||||||
|
or a traceback string
|
||||||
|
'''
|
||||||
|
# TODO: try/catch around JSON handling
|
||||||
|
|
||||||
|
ok, conn = self._connect(host)
|
||||||
|
if not ok:
|
||||||
|
return [ host, False, conn ]
|
||||||
|
|
||||||
|
if self.module_name != "copy":
|
||||||
|
# transfer a module, set it executable, and run it
|
||||||
|
outpath = self._copy_module(conn)
|
||||||
|
self._exec_command(conn, "chmod +x %s" % outpath)
|
||||||
|
cmd = self._command(outpath)
|
||||||
|
result = self._exec_command(conn, cmd)
|
||||||
|
self._exec_command(conn, "rm -f %s" % outpath)
|
||||||
|
conn.close()
|
||||||
|
return [ host, True, json.loads(result) ]
|
||||||
|
else:
|
||||||
|
# SFTP file copy module is not really a module
|
||||||
|
ftp = conn.open_sftp()
|
||||||
|
ftp.put(self.module_args[0], self.module_args[1])
|
||||||
|
ftp.close()
|
||||||
|
conn.close()
|
||||||
|
return [ host, True, 1 ]
|
||||||
|
|
||||||
|
|
||||||
|
def _command(self, outpath):
|
||||||
|
''' form up a command string '''
|
||||||
|
cmd = "%s %s" % (outpath, " ".join(self.module_args))
|
||||||
|
return cmd
|
||||||
|
|
||||||
|
def _exec_command(self, conn, cmd):
|
||||||
|
''' execute a command over SSH '''
|
||||||
|
stdin, stdout, stderr = conn.exec_command(cmd)
|
||||||
|
results = "\n".join(stdout.readlines())
|
||||||
|
return results
|
||||||
|
|
||||||
|
def _get_tmp_path(self, conn, file_name):
|
||||||
|
output = self._exec_command(conn, "mktemp /tmp/%s.XXXXXX" % file_name)
|
||||||
|
return output.split("\n")[0]
|
||||||
|
|
||||||
|
def _copy_module(self, conn):
|
||||||
|
''' transfer a module over SFTP '''
|
||||||
|
in_path = os.path.expanduser(
|
||||||
|
os.path.join(self.module_path, self.module_name)
|
||||||
|
)
|
||||||
|
out_path = self._get_tmp_path(conn, "ansible_%s" % self.module_name)
|
||||||
|
|
||||||
|
sftp = conn.open_sftp()
|
||||||
|
sftp.put(in_path, out_path)
|
||||||
|
sftp.close()
|
||||||
|
return out_path
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
''' xfer & run module on all matched hosts '''
|
||||||
|
|
||||||
|
# find hosts that match the pattern
|
||||||
|
hosts = [ h for h in self.host_list if self._matches(h) ]
|
||||||
|
|
||||||
|
# attack pool of hosts in N forks
|
||||||
|
pool = multiprocessing.Pool(self.forks)
|
||||||
|
hosts = [ (self,x) for x in hosts ]
|
||||||
|
results = pool.map(_executor_hook, hosts)
|
||||||
|
|
||||||
|
# sort hosts by ones we successfully contacted
|
||||||
|
# and ones we did not
|
||||||
|
results2 = {
|
||||||
|
"contacted" : {},
|
||||||
|
"dark" : {}
|
||||||
|
}
|
||||||
|
for x in results:
|
||||||
|
(host, is_ok, result) = x
|
||||||
|
if not is_ok:
|
||||||
|
results2["dark"][host] = result
|
||||||
|
else:
|
||||||
|
results2["contacted"][host] = result
|
||||||
|
|
||||||
|
return results2
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
|
||||||
|
# test code...
|
||||||
|
|
||||||
|
r = Runner(
|
||||||
|
host_list = DEFAULT_HOST_LIST,
|
||||||
|
module_name='ping',
|
||||||
|
module_args='',
|
||||||
|
pattern='*',
|
||||||
|
forks=3
|
||||||
|
)
|
||||||
|
print r.run()
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -0,0 +1,16 @@
|
|||||||
|
#!/usr/bin/python
|
||||||
|
|
||||||
|
# requires 'ohai' to be installed
|
||||||
|
|
||||||
|
try:
|
||||||
|
import json
|
||||||
|
except ImportError:
|
||||||
|
import simplejson as json
|
||||||
|
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
cmd = subprocess.Popen("/usr/bin/ohai", stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||||
|
out, err = cmd.communicate()
|
||||||
|
|
||||||
|
# try to cleanup the JSON, for some reason facter --json doesn't need this hack
|
||||||
|
print json.dumps(json.loads(out))
|
@ -1,5 +1,8 @@
|
|||||||
#!/usr/bin/python
|
#!/usr/bin/python
|
||||||
|
|
||||||
|
try:
|
||||||
import json
|
import json
|
||||||
|
except ImportError:
|
||||||
|
import simplejson as json
|
||||||
|
|
||||||
print json.dumps(1)
|
print json.dumps(1)
|
||||||
|
@ -0,0 +1,30 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
|
||||||
|
from distutils.core import setup
|
||||||
|
|
||||||
|
setup(name='ansible',
|
||||||
|
version='1.0',
|
||||||
|
description='Minimal SSH command and control',
|
||||||
|
author='Michael DeHaan',
|
||||||
|
author_email='michael.dehaan@gmail.com',
|
||||||
|
url='http://github.com/mpdehaan/ansible/',
|
||||||
|
license='MIT',
|
||||||
|
package_dir = { 'ansible' : 'lib/ansible' },
|
||||||
|
packages=[
|
||||||
|
'ansible',
|
||||||
|
],
|
||||||
|
data_files=[
|
||||||
|
('/usr/share/ansible', [
|
||||||
|
'library/ping',
|
||||||
|
'library/command',
|
||||||
|
'library/facter',
|
||||||
|
'library/copy',
|
||||||
|
]),
|
||||||
|
('man/man1', [
|
||||||
|
'docs/man/man1/ansible.1'
|
||||||
|
])
|
||||||
|
],
|
||||||
|
scripts=[
|
||||||
|
'bin/ansible',
|
||||||
|
]
|
||||||
|
)
|
Loading…
Reference in New Issue