Permalink
Browse files

Genesis.

  • Loading branch information...
0 parents commit f31421576b00f0b167cdbe61217c31c21a41ac02 @mpdehaan mpdehaan committed Feb 23, 2012
Showing with 253 additions and 0 deletions.
  1. +88 −0 README.md
  2. +165 −0 lib/ansible/__init__.py
@@ -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/
@@ -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()
+
+
+

0 comments on commit f314215

Please sign in to comment.