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
|
||||
|
||||
import json
|
||||
try:
|
||||
import json
|
||||
except ImportError:
|
||||
import simplejson as json
|
||||
|
||||
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