async_wrapper 6.01 KB
Newer Older
1
#!/usr/bin/python
2
# -*- coding: utf-8 -*-
3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31

# (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
32 33
import signal
import time
34
import syslog
35

36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65
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())
66
    os.dup2(dev_null.fileno(), sys.stderr.fileno())
67

68 69 70
if len(sys.argv) < 3:
    print json.dumps({
        "failed" : True,
71
        "msg"    : "usage: async_wrapper <jid> <time_limit> <modulescript> <argsfile>.  Humans, do not call directly!"
72 73 74 75
    })
    sys.exit(1)

jid = sys.argv[1]
Michael DeHaan committed
76 77
time_limit = sys.argv[2]
wrapped_module = sys.argv[3]
78 79
argsfile = sys.argv[4]
cmd = "%s %s" % (wrapped_module, argsfile)
80

81 82 83
syslog.openlog('ansible-%s' % os.path.basename(__file__))
syslog.syslog(syslog.LOG_NOTICE, 'Invoked with %s' % " ".join(sys.argv[1:]))

84 85 86 87 88 89 90 91 92 93 94 95 96 97 98
# 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):

99
    logfile = open(log_path, "w")
100
    logfile.write(json.dumps({ "started" : 1, "ansible_job_id" : jid }))
101 102
    logfile.close()
    logfile = open(log_path, "w")
103
    result = {}
104

105
    outdata = ''
106 107
    try:
        cmd = shlex.split(wrapped_cmd)
108
        script = subprocess.Popen(cmd, shell=False,
109 110
            stdin=None, stdout=logfile, stderr=logfile)
        script.communicate()
111 112
        outdata = file(log_path).read()
        result = json.loads(outdata)
113

114 115 116
    except (OSError, IOError), e:
        result = {
            "failed": 1,
Michael DeHaan committed
117
            "cmd" : wrapped_cmd,
118 119
            "msg": str(e),
        }
120 121
        result['ansible_job_id'] = jid
        logfile.write(json.dumps(result))
122 123 124
    except:
        result = {
            "failed" : 1,
Michael DeHaan committed
125
            "cmd" : wrapped_cmd,
126
            "data" : outdata, # temporary debug only
127
            "msg" : traceback.format_exc()
128
        }
129 130
        result['ansible_job_id'] = jid
        logfile.write(json.dumps(result))
131 132
    logfile.close()

133 134
# immediately exit this process, leaving an orphaned process
# running which immediately forks a supervisory timing process
Michael DeHaan committed
135

136 137
#import logging
#import logging.handlers
138

139 140 141 142 143 144
#logger = logging.getLogger("ansible_async")
#logger.setLevel(logging.WARNING)
#logger.addHandler( logging.handlers.SysLogHandler("/dev/log") )
def debug(msg):
    #logger.warning(msg)
    pass
145

146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200
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