-
Notifications
You must be signed in to change notification settings - Fork 23.7k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit f314215
Showing
2 changed files
with
253 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,88 @@ | ||
Ansible | ||
======= | ||
|
||
Ansible is a extra-simple Python API for doing 'remote things' over SSH. | ||
|
||
As 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. | ||
|
||
Principles | ||
========== | ||
|
||
* Dead simple setup | ||
* No server or client daemons, uses existing SSHd | ||
* Only SSH keys are allowed for authentication | ||
* usage of ssh-agent is more or less required | ||
* plugins can be written in ANY language | ||
* as with Func, API usage is an equal citizen to CLI usage | ||
|
||
Requirements | ||
============ | ||
|
||
* python 2.6 -- or a backport of the multiprocessing module | ||
* paramiko | ||
|
||
Inventory file | ||
============== | ||
|
||
The default inventory file (-H) is ~/.ansible_hosts and is a list | ||
of all hostnames to target with ansible, one per line. | ||
|
||
This list is further filtered by the pattern wildcard (-P) to target | ||
specific hosts. | ||
|
||
Comamnd line usage example | ||
========================== | ||
|
||
Run a module by name with arguments | ||
|
||
ansible -p "*.example.com" -m modName -a "arg1 arg2" | ||
|
||
API Example | ||
=========== | ||
|
||
The API is simple and returns basic datastructures. | ||
|
||
import ansible | ||
runner = ansible.Runner(command='inventory', host_list=['xyz.example.com', '...']) | ||
data = runner.run() | ||
|
||
{ | ||
'xyz.example.com' : [ 'any kind of datastructure is returnable' ], | ||
'foo.example.com' : None, # failed to connect, | ||
... | ||
} | ||
|
||
Additional options to runner include the number of forks, hostname | ||
exclusion pattern, library path, and so on. | ||
|
||
Parallelism | ||
=========== | ||
|
||
Specify the number of forks to use, to run things in greater parallelism. | ||
|
||
ansible -f 10 "*.example.com" -m modName -a "arg1 arg2" | ||
|
||
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". | ||
|
||
Features not supported from Func (by design) | ||
============================================ | ||
|
||
* Delegation for treeish topologies | ||
* Asynchronous modes for long running tasks -- background tasks on your own | ||
|
||
Future plans | ||
============ | ||
|
||
* Dead-simple declarative configuration management & facts engine, with | ||
probes implementable in any language. | ||
|
||
Author | ||
====== | ||
|
||
* Michael DeHaan <michael.dehaan@gmail.com> | http://michaeldehaan.net/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,165 @@ | ||
# core | ||
from optparse import OptionParser | ||
import fnmatch | ||
from multiprocessing import Process, Pipe | ||
from itertools import izip | ||
import os | ||
import json | ||
|
||
# non-core | ||
import paramiko | ||
|
||
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 Cli(object): | ||
|
||
def __init__(self): | ||
pass | ||
|
||
def runner(self): | ||
parser = OptionParser() | ||
parser.add_option("-H", "--host-list", dest="host_list", | ||
help="path to hosts list", default=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) | ||
parser.add_option("-n", "--name", dest="module_name", | ||
help="module name to execute", default=DEFAULT_MODULE_NAME) | ||
parser.add_option("-a", "--args", dest="module_args", | ||
help="module arguments", default=DEFAULT_MODULE_ARGS) | ||
parser.add_option("-p", "--pattern", dest="pattern", | ||
help="hostname pattern", default=DEFAULT_PATTERN) | ||
|
||
options, args = parser.parse_args() | ||
host_list = self._host_list(options.host_list) | ||
|
||
return Runner( | ||
module_name=options.module_name, | ||
module_path=options.module_path, | ||
module_args=options.module_args, | ||
host_list=host_list, | ||
forks=options.forks, | ||
pattern=options.pattern, | ||
) | ||
|
||
def _host_list(self, host_list): | ||
host_list = os.path.expanduser(host_list) | ||
return file(host_list).read().split("\n") | ||
|
||
|
||
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 ] | ||
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) | ||
return [ host, result ] | ||
|
||
def _command(self, outpath): | ||
cmd = "%s %s" % (outpath, 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__': | ||
|
||
# comamnd line usage example: | ||
|
||
result = Cli().runner().run() | ||
print json.dumps(result, sort_keys=True, indent=4) | ||
|
||
# API usage example: | ||
|
||
#r = Runner( | ||
# host_list = [ '127.0.0.1' ], | ||
# module_path='~/.ansible', | ||
# module_name='ping', | ||
# module_args='', | ||
# pattern='*', | ||
# forks=3 | ||
#) | ||
#print r.run() | ||
|
||
|
||
|