Merge branch 'master' of github.com:mpdehaan/ansible

This commit is contained in:
Michael DeHaan 2012-02-25 09:39:18 -05:00
commit 9b3d2b97a7
18 changed files with 873 additions and 228 deletions

3
.gitignore vendored Normal file
View file

@ -0,0 +1,3 @@
*~
*.py[co]
build

12
AUTHORS.md Normal file
View file

@ -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

32
Makefile Normal file
View file

@ -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

113
README.md
View file

@ -1,26 +1,37 @@
Ansible
=======
Ansible is a extra-simple Python API for doing 'remote things' over SSH.
Ansible is a extra-simple tool/API for doing 'parallel remote things' over SSH -- whether
executing commands, running declarative 'modules', or executing larger 'playbooks' that
can serve as a configuration management or deployment system.
While [Func](http://fedorahosted.org/func), which I co-wrote, aspired to avoid using SSH and have it's own daemon infrastructure, Ansible aspires to be quite different and more minimal, but still able to grow more modularly over time. This is based on talking to a lot of users of various tools and wishing to eliminate problems with connectivity and long running daemons, or not picking tool X because they preferred to code in Y.
While [Func](http://fedorahosted.org/func), which I co-wrote,
aspired to avoid using SSH and have it's own daemon infrastructure,
Ansible aspires to be quite different and more minimal, but still able
to grow more modularly over time. This is based on talking to a lot of
users of various tools and wishing to eliminate problems with connectivity
and long running daemons, or not picking tool X because they preferred to
code in Y.
Why use Ansible versus something else? (Fabric, Capistrano, mCollective, Func, SaltStack, etc?) It will have far less code, it will be more correct, and it will be the easiest thing to hack on and use you'll ever see -- regardless of your favorite language of choice. Want to only code plugins in bash or clojure? Ansible doesn't care. The docs will fit on one page and the source will be blindingly obvious.
Why use Ansible versus something else? (Fabric, Capistrano, mCollective,
Func, SaltStack, etc?) It will have far less code, it will be more correct,
and it will be the easiest thing to hack on and use you'll ever see --
regardless of your favorite language of choice. Want to only code plugins
in bash or clojure? Ansible doesn't care. The docs will fit on one page
and the source will be blindingly obvious.
Principles
==========
Design Principles
=================
* Dead simple setup
* Super fast & parallel by default
* No server or client daemons, uses existing SSHd
* No additional software required on client boxes
* Everything is self updating on the clients. "Modules" are remotely transferred to target boxes and exec'd, and do not stay active or consume resources.
* Only SSH keys are allowed for authentication
* usage of ssh-agent is more or less required (no passwords)
* plugins can be written in ANY language
* as with Func, API usage is an equal citizen to CLI usage
* use Python's multiprocessing capabilities to emulate Func's forkbomb logic
* all file paths can be specified as command line options easily allowing non-root usage
* Everything is self updating on the clients
* Encourages use of ssh-agent
* Plugins can be written in ANY language
* API usage is an equal citizen to CLI usage
* Can be controlled/installed/used as non-root
Requirements
============
@ -28,16 +39,17 @@ Requirements
For the server the tool is running from, *only*:
* python 2.6 -- or the 2.4/2.5 backport of the multiprocessing module
* PyYAML (if using playbooks)
* paramiko
Inventory file
==============
The inventory file is a required list of hostnames that can be potentially managed by
ansible. Eventually this file may be editable via the CLI, but for now, is
edited with your favorite text editor.
The inventory file is a required list of hostnames that can be
potentially managed by ansible. Eventually this file may be editable
via the CLI, but for now, is edited with your favorite text editor.
The default inventory file (-H) is ~/.ansible_hosts and is a list
The default inventory file (-H) is /etc/ansible/hosts and is a list
of all hostnames to target with ansible, one per line. These
can be hostnames or IPs
@ -66,32 +78,40 @@ Run a module by name with arguments
API Example
===========
The API is simple and returns basic datastructures.
The API is simple and returns basic datastructures. Ansible will keep
track of which hosts were successfully contacted seperately from hosts
that had communication problems. The format of the return, if successful,
is entirely up to the module.
import ansible
runner = ansible.Runner(
runner = ansible.runner.Runner(
pattern='*',
module_name='inventory',
host_list=['xyz.example.com', '...']
module_name='inventory',
module_args='...'
)
data = runner.run()
{
'xyz.example.com' : [ 'any kind of datastructure is returnable' ],
'foo.example.com' : None, # failed to connect,
...
{
'contacted' : {
'xyz.example.com' : [ 'any kind of datastructure is returnable' ],
'foo.example.com' : [ '...' ]
},
'dark' : {
'bar.example.com' : [ 'failure message' ]
}
}
Additional options to Runner include the number of forks, hostname
exclusion pattern, library path, arguments, and so on. Read the source, it's not
complicated.
exclusion pattern, library path, arguments, and so on.
Read the source, it's not complicated.
Patterns
========
To target only hosts starting with "rtp", for example:
* ansible "rtp*" -n command -a "yum update apache"
* ansible -p "rtp*" -n command -a "yum update apache"
Parallelism
===========
@ -107,38 +127,41 @@ File Transfer
Ansible can SCP lots of files to lots of places in parallel.
* ansible -f 10 -n copy -a "/etc/hosts /tmp/hosts"
* ansible -p "web-*.acme.net" -f 10 -n copy -a "/etc/hosts /tmp/hosts"
Bundled Modules
===============
Ansible Library (Bundled Modules)
=================================
See the example library for modules, they can be written in any language
and simply return JSON to stdout. The path to your ansible library is
specified with the "-L" flag should you wish to use a different location
than "~/ansible". There is potential for a sizeable community to build
than "/usr/share/ansible". This means anyone can use Ansible, even without
root permissions.
There is potential for a sizeable community to build
up around the library scripts.
Existing library modules
========================
Modules include:
* command -- runs commands, giving output, return codes, and run time info
* ping - just returns if the system is up or not
* facter - retrieves facts about the host OS
* ohai - similar to facter, but returns structured data
* copy - add files to remote systems
Playbooks
=========
Playbooks are loosely equivalent to recipes or manifests in most configuration
management or deployment tools and describe a set of operations to run on
a set of hosts. Some tasks can choose to only fire when certain
conditions are true, and if a task in a chain fails the dependent tasks
will not proceed. Playbooks are described in (YAML)[http://yaml.org] format.
Future plans
============
* modules for users, groups, and files, using puppet style ensure mechanics
* ansible-inventory -- gathering fact/hw info, storing in git, adding RSS
* ansible-slurp ------ recursively rsync file trees for each host
* very simple option constructing/parsing for modules
* Dead-simple declarative configuration management engine using
a runbook style recipe file, written in JSON or YAML
* maybe it's own fact engine, not required, that also feeds from facter
* add/remove/list hosts from the command line
* list available modules from command line
* filter exclusion (run this only if fact is true/false)
* see TODO.md
License
=======
@ -170,8 +193,8 @@ Mailing List
Author
======
Michael DeHaan -- michael.dehaan@gmail.com
Michael DeHaan -- michael.dehaan@gmail.com
[http://michaeldehaan.net](http://michaeldehaan.net/)
[http://michaeldehaan.net](http://michaeldehaan.net/)

44
TODO.md Normal file
View file

@ -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)

View file

@ -24,14 +24,10 @@
from optparse import OptionParser
import json
import os
import ansible
DEFAULT_HOST_LIST = '~/.ansible_hosts'
DEFAULT_MODULE_PATH = '~/ansible'
DEFAULT_MODULE_NAME = 'ping'
DEFAULT_PATTERN = '*'
DEFAULT_FORKS = 3
DEFAULT_MODULE_ARGS = ''
import getpass
import ansible.runner
import ansible.playbook
import ansible.constants as C
class Cli(object):
@ -40,37 +36,56 @@ class Cli(object):
def runner(self):
parser = OptionParser()
parser.add_option("-P", "--askpass", default=False, action="store_true",
help="ask the user to input the ssh password for connecting")
parser.add_option("-H", "--host-list", dest="host_list",
help="path to hosts list", default=DEFAULT_HOST_LIST)
help="path to hosts list", default=C.DEFAULT_HOST_LIST)
parser.add_option("-L", "--library", dest="module_path",
help="path to module library", default=DEFAULT_MODULE_PATH)
parser.add_option("-F", "--forks", dest="forks",
help="level of parallelism", default=DEFAULT_FORKS)
help="path to module library", default=C.DEFAULT_MODULE_PATH)
parser.add_option("-f", "--forks", dest="forks", type="int",
help="level of parallelism", default=C.DEFAULT_FORKS)
parser.add_option("-n", "--name", dest="module_name",
help="module name to execute", default=DEFAULT_MODULE_NAME)
help="module name to execute", default=C.DEFAULT_MODULE_NAME)
parser.add_option("-a", "--args", dest="module_args",
help="module arguments", default=DEFAULT_MODULE_ARGS)
help="module arguments", default=C.DEFAULT_MODULE_ARGS)
parser.add_option("-p", "--pattern", dest="pattern",
help="hostname pattern", default=DEFAULT_PATTERN)
help="hostname pattern", default=C.DEFAULT_PATTERN)
parser.add_option("-u", "--remote-user", dest="remote_user",
help="remote username", default=C.DEFAULT_REMOTE_USER)
parser.add_option("-r", "--run-playbook", dest="playbook",
help="playbook file, instead of -n and -a", default=None)
options, args = parser.parse_args()
host_list = self._host_list(options.host_list)
# TODO: more shell like splitting on module_args would
# be a good idea
return ansible.Runner(
module_name=options.module_name,
module_path=options.module_path,
module_args=options.module_args.split(' '),
host_list=host_list,
forks=options.forks,
pattern=options.pattern,
)
sshpass = None
if options.askpass:
sshpass = getpass.getpass(prompt="SSH password: ")
def _host_list(self, host_list):
host_list = os.path.expanduser(host_list)
return file(host_list).read().split("\n")
if options.playbook is None:
return ansible.runner.Runner(
module_name=options.module_name,
module_path=options.module_path,
module_args=options.module_args.split(' '),
remote_user=options.remote_user,
remote_pass=sshpass,
host_list=options.host_list,
forks=options.forks,
pattern=options.pattern,
verbose=False,
)
else:
return ansible.playbook.PlayBook(
playbook=options.playbook,
module_path=options.module_path,
remote_user=options.remote_user,
remote_pass=sshpass,
host_list=options.host_list,
forks=options.forks,
verbose=False,
)
if __name__ == '__main__':

1
docs/man/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
*.xml

108
docs/man/man1/ansible.1 Normal file
View file

@ -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/

View file

@ -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/>

16
examples/playbook.yml Normal file
View file

@ -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]

View file

@ -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()

10
lib/ansible/constants.py Normal file
View file

@ -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

175
lib/ansible/playbook.py Executable file
View file

@ -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)

198
lib/ansible/runner.py Executable file
View file

@ -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()

View file

@ -1,6 +1,9 @@
#!/usr/bin/python
import json
try:
import json
except ImportError:
import simplejson as json
import subprocess
import sys
import datetime
@ -8,7 +11,7 @@ import datetime
args = sys.argv[1:]
startd = datetime.datetime.now()
cmd = subprocess.Popen(args, shell=True,
cmd = subprocess.Popen(args, shell=False,
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
out, err = cmd.communicate()

16
library/ohai Normal file
View file

@ -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))

View file

@ -1,5 +1,8 @@
#!/usr/bin/python
import json
try:
import json
except ImportError:
import simplejson as json
print json.dumps(1)

30
setup.py Normal file
View file

@ -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',
]
)