library/async_wrapper
eaa7714f
 #!/usr/bin/python
7e9e2901
 # -*- coding: utf-8 -*-
eaa7714f
 
 # (c) 2012, Michael DeHaan <michael.dehaan@gmail.com>, and others
 #
 # This file is part of Ansible
 #
 # Ansible is free software: you can redistribute it and/or modify
 # it under the terms of the GNU General Public License as published by
 # the Free Software Foundation, either version 3 of the License, or
 # (at your option) any later version.
 #
 # Ansible is distributed in the hope that it will be useful,
 # but WITHOUT ANY WARRANTY; without even the implied warranty of
 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 # GNU General Public License for more details.
 #
 # You should have received a copy of the GNU General Public License
 # along with Ansible.  If not, see <http://www.gnu.org/licenses/>.
 #
 
 try:
     import json
 except ImportError:
     import simplejson as json
 import shlex
 import os
 import subprocess
 import sys
 import datetime
 import traceback
8e07d83a
 import signal
 import time
aea022b0
 import syslog
eaa7714f
 
49a636d8
 def daemonize_self():
     # daemonizing code: http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/66012
     # logger.info("cobblerd started")
     try:
         pid = os.fork()
         if pid > 0:
             # exit first parent
             sys.exit(0)
     except OSError, e:
         print >>sys.stderr, "fork #1 failed: %d (%s)" % (e.errno, e.strerror)
         sys.exit(1)
 
     # decouple from parent environment
     os.chdir("/")
     os.setsid()
     os.umask(022)
 
     # do second fork
     try:
         pid = os.fork()
         if pid > 0:
             # print "Daemon PID %d" % pid
             sys.exit(0)
     except OSError, e:
         print >>sys.stderr, "fork #2 failed: %d (%s)" % (e.errno, e.strerror)
         sys.exit(1)
 
     dev_null = file('/dev/null','rw')
     os.dup2(dev_null.fileno(), sys.stdin.fileno())
     os.dup2(dev_null.fileno(), sys.stdout.fileno())
faed4b5a
     os.dup2(dev_null.fileno(), sys.stderr.fileno())
49a636d8
 
eaa7714f
 if len(sys.argv) < 3:
     print json.dumps({
         "failed" : True,
696b67f9
         "msg"    : "usage: async_wrapper <jid> <time_limit> <modulescript> <argsfile>.  Humans, do not call directly!"
eaa7714f
     })
     sys.exit(1)
 
 jid = sys.argv[1]
718e2930
 time_limit = sys.argv[2]
 wrapped_module = sys.argv[3]
696b67f9
 argsfile = sys.argv[4]
 cmd = "%s %s" % (wrapped_module, argsfile)
eaa7714f
 
aea022b0
 syslog.openlog('ansible-%s' % os.path.basename(__file__))
 syslog.syslog(syslog.LOG_NOTICE, 'Invoked with %s' % " ".join(sys.argv[1:]))
 
eaa7714f
 # setup logging directory
 logdir = os.path.expanduser("~/.ansible_async")
 log_path = os.path.join(logdir, jid)
 
 if not os.path.exists(logdir):
     try:
         os.makedirs(logdir)
     except:
         print json.dumps({
             "failed" : 1,
             "msg" : "could not create: %s" % logdir
         })
 
 def _run_command(wrapped_cmd, jid, log_path):
 
49a636d8
     logfile = open(log_path, "w")
eaa7714f
     logfile.write(json.dumps({ "started" : 1, "ansible_job_id" : jid }))
49a636d8
     logfile.close()
     logfile = open(log_path, "w")
eaa7714f
     result = {}
faed4b5a
 
696b67f9
     outdata = ''
eaa7714f
     try:
         cmd = shlex.split(wrapped_cmd)
faed4b5a
         script = subprocess.Popen(cmd, shell=False,
49a636d8
             stdin=None, stdout=logfile, stderr=logfile)
         script.communicate()
696b67f9
         outdata = file(log_path).read()
         result = json.loads(outdata)
49a636d8
 
eaa7714f
     except (OSError, IOError), e:
         result = {
             "failed": 1,
718e2930
             "cmd" : wrapped_cmd,
eaa7714f
             "msg": str(e),
         }
49a636d8
         result['ansible_job_id'] = jid
         logfile.write(json.dumps(result))
eaa7714f
     except:
         result = {
             "failed" : 1,
718e2930
             "cmd" : wrapped_cmd,
696b67f9
             "data" : outdata, # temporary debug only
eaa7714f
             "msg" : traceback.format_exc()
faed4b5a
         }
49a636d8
         result['ansible_job_id'] = jid
         logfile.write(json.dumps(result))
eaa7714f
     logfile.close()
 
8e07d83a
 # immediately exit this process, leaving an orphaned process
 # running which immediately forks a supervisory timing process
718e2930
 
d9676334
 #import logging
 #import logging.handlers
eaa7714f
 
d9676334
 #logger = logging.getLogger("ansible_async")
 #logger.setLevel(logging.WARNING)
 #logger.addHandler( logging.handlers.SysLogHandler("/dev/log") )
 def debug(msg):
     #logger.warning(msg)
     pass
eaa7714f
 
d9676334
 try:
     pid = os.fork()
     if pid:
         # Notify the overlord that the async process started
 
         # we need to not return immmediately such that the launched command has an attempt
         # to initialize PRIOR to ansible trying to clean up the launch directory (and argsfile)
         # this probably could be done with some IPC later.  Modules should always read
         # the argsfile at the very first start of their execution anyway
         time.sleep(1)
         debug("Return async_wrapper task started.")
         print json.dumps({ "started" : 1, "ansible_job_id" : jid, "results_file" : log_path })
         sys.stdout.flush()
         sys.exit(0)
     else:
         # The actual wrapper process
 
         # Daemonize, so we keep on running
         daemonize_self()
 
         # we are now daemonized, create a supervisory process
         debug("Starting module and watcher")
 
         sub_pid = os.fork()
         if sub_pid:
             # the parent stops the process after the time limit
             remaining = int(time_limit)
 
             # set the child process group id to kill all children
             os.setpgid(sub_pid, sub_pid)
 
             debug("Start watching %s (%s)"%(sub_pid, remaining))
             time.sleep(5)
             while os.waitpid(sub_pid, os.WNOHANG) == (0, 0):
                 debug("%s still running (%s)"%(sub_pid, remaining))
                 time.sleep(5)
                 remaining = remaining - 5
                 if remaining == 0:
                     debug("Now killing %s"%(sub_pid))
                     os.killpg(sub_pid, signal.SIGKILL)
                     debug("Sent kill to group %s"%sub_pid)
                     time.sleep(1)
                     sys.exit(0)
             debug("Done in kid B.")
             os._exit(0)
         else:
             # the child process runs the actual module
             debug("Start module (%s)"%os.getpid())
             _run_command(cmd, jid, log_path)
             debug("Module complete (%s)"%os.getpid())
             sys.exit(0)
 
 except Exception, err:
     debug("error: %s"%(err))
     raise err