~smoser/cloud-init/finalcmd

« back to all changes in this revision

Viewing changes to cloudinit/config/cc_power_state_change.py

  • Committer: Scott Moser
  • Date: 2012-11-13 16:18:22 UTC
  • Revision ID: smoser@ubuntu.com-20121113161822-7yh0v9b9qaz0kwoz
implement power_state with tests.

Show diffs side-by-side

added added

removed removed

Lines of Context:
21
21
 
22
22
import errno
23
23
import os
 
24
import re
24
25
import subprocess
25
26
import sys
26
27
import time
28
29
frequency = PER_INSTANCE
29
30
 
30
31
 
31
 
def handle(_name, cfg, cloud, log, _args):
32
 
 
33
 
    finalcmds = cfg.get("finalcmd")
34
 
 
35
 
    if not finalcmds:
36
 
        log.debug("No final commands")
 
32
def handle(_name, cfg, _cloud, log, _args):
 
33
 
 
34
    try:
 
35
        (args, timeout) = load_power_state(cfg)
 
36
        if args is None:
 
37
            log.debug("no power_state provided. doing nothing")
 
38
            return
 
39
    except Exception as e:
 
40
        log.warn("%s Not performing power state change!" % str(e))
37
41
        return
38
42
 
39
43
    mypid = os.getpid()
40
44
    cmdline = util.load_file("/proc/%s/cmdline")
41
45
 
42
46
    if not cmdline:
43
 
        log.warn("Failed to get cmdline of current process")
 
47
        log.warn("power_state: failed to get cmdline of current process")
44
48
        return
45
49
 
 
50
    devnull_fp = open("/dev/null", "w")
 
51
 
 
52
    log.debug("After pid %s ends, will execute: %s" % (mypid, ' '.join(args)))
 
53
 
 
54
    util.fork_cb(run_after_pid_gone, mypid, cmdline, timeout, log, execmd,
 
55
                 [args, devnull_fp])
 
56
 
 
57
 
 
58
def load_power_state(cfg):
 
59
    # returns a tuple of shutdown_command, timeout
 
60
    # shutdown_command is None if no config found
 
61
    pstate = cfg.get('power_state')
 
62
 
 
63
    if pstate is None:
 
64
        return (None, None)
 
65
 
 
66
    if not isinstance(pstate, dict):
 
67
        raise TypeError("power_state is not a dict.")
 
68
 
 
69
    opt_map = {'halt': '-H', 'poweroff': '-P', 'reboot': '-r'}
 
70
 
 
71
    mode = pstate.get("mode")
 
72
    if mode not in opt_map:
 
73
        raise TypeError("power_state[mode] required, must be one of: %s." %
 
74
                        ','.join(opt_map.keys()))
 
75
 
 
76
    delay = pstate.get("delay", "now")
 
77
    if delay != "now" and not re.match("\+[0-9]+", delay):
 
78
        raise TypeError("power_state[delay] must be 'now' or '+m' (minutes).")
 
79
 
 
80
    args = ["shutdown", opt_map[mode], delay]
 
81
    if pstate.get("message"):
 
82
        args.append(pstate.get("message"))
 
83
 
46
84
    try:
47
 
        timeout = float(cfg.get("finalcmd_timeout", 30.0))
 
85
        timeout = float(pstate.get('timeout', 30.0))
48
86
    except ValueError:
49
 
        log.warn("failed to convert finalcmd_timeout '%s' to float" %
50
 
                 cfg.get("finalcmd_timeout", 30.0))
51
 
        return
52
 
 
53
 
    devnull_fp = open("/dev/null", "w")
54
 
 
55
 
    shellcode = util.shellify(finalcmds)
56
 
 
57
 
    # note, after the fork, we do not use any of cloud-init's functions
58
 
    # that would attempt to log.  The primary reason for that is
59
 
    # to allow the 'finalcmd' the ability to do just about anything
60
 
    # and not depend on syslog services.
61
 
    # Basically, it should "just work" to have finalcmd of:
62
 
    #  - sleep 30
63
 
    #  - /sbin/poweroff
64
 
    finalcmd_d = os.path.join(cloud.get_ipath_cur(), "finalcmds")
65
 
 
66
 
    util.fork_cb(run_after_pid_gone, mypid, cmdline, timeout,
67
 
                 runfinal, (shellcode, finalcmd_d, devnull_fp))
68
 
 
69
 
 
70
 
def execmd(exe_args, data_in=None, output=None):
 
87
        raise ValueError("failed to convert timeout '%s' to float." %
 
88
                         pstate['timeout'])
 
89
 
 
90
    return (args, timeout)
 
91
 
 
92
 
 
93
def execmd(exe_args, output=None, data_in=None):
71
94
    try:
72
95
        proc = subprocess.Popen(exe_args, stdin=subprocess.PIPE,
73
96
                                stdout=output, stderr=subprocess.STDOUT)
74
97
        proc.communicate(data_in)
75
98
    except Exception:
76
 
        return 254
77
 
    return proc.returncode()
78
 
 
79
 
 
80
 
def runfinal(shellcode, finalcmd_d, output=None):
81
 
    ret = execmd(["/bin/sh"], data_in=shellcode, output=output)
82
 
    if not (finalcmd_d and os.path.isdir(finalcmd_d)):
83
 
        sys.exit(ret)
84
 
 
85
 
    fails = 0
86
 
    if ret != 0:
87
 
        fails = 1
88
 
 
89
 
    # now runparts the final command dir
90
 
    for exe_name in sorted(os.listdir(finalcmd_d)):
91
 
        exe_path = os.path.join(finalcmd_d, exe_name)
92
 
        if os.path.isfile(exe_path) and os.access(exe_path, os.X_OK):
93
 
            ret = execmd([exe_path], data_in=None, output=output)
94
 
            if ret != 0:
95
 
                fails += 1
96
 
    sys.exit(fails)
97
 
 
98
 
 
99
 
def run_after_pid_gone(pid, pidcmdline, timeout, func, args):
 
99
        sys.exit(254)
 
100
    sys.exit(proc.returncode())
 
101
 
 
102
 
 
103
def run_after_pid_gone(pid, pidcmdline, timeout, log, func, args):
100
104
    # wait until pid, with /proc/pid/cmdline contents of pidcmdline
101
105
    # is no longer alive.  After it is gone, or timeout has passed
102
106
    # execute func(args)
103
 
    msg = "ERROR: Uncaught error"
 
107
    msg = None
104
108
    end_time = time.time() + timeout
105
109
 
106
110
    cmdline_f = "/proc/%s/cmdline" % pid
107
111
 
 
112
    def fatal(msg):
 
113
        if log:
 
114
            log.warn(msg)
 
115
        sys.exit(254)
 
116
 
108
117
    while True:
109
118
        if time.time() > end_time:
110
119
            msg = "timeout reached before %s ended" % pid
122
131
            if ioerr.errno == errno.ENOENT:
123
132
                msg = "pidfile '%s' gone" % cmdline_f
124
133
            else:
125
 
                msg = "ERROR: IOError: %s" % ioerr
126
 
                raise
 
134
                fatal("IOError during wait: %s" % ioerr)
127
135
            break
128
136
 
129
137
        except Exception as e:
130
 
            msg = "ERROR: Exception: %s" % e
131
 
            raise
132
 
 
133
 
    if msg.startswith("ERROR:"):
134
 
        sys.stderr.write(msg)
135
 
        sys.stderr.write("Not executing finalcmd")
136
 
        sys.exit(1)
137
 
 
138
 
    sys.stderr.write("calling %s with %s\n" % (func, args))
139
 
    sys.exit(func(*args))
 
138
            fatal("Unexpected Exception: %s" % e)
 
139
 
 
140
    if not msg:
 
141
        fatal("Unexpected error in run_after_pid_gone")
 
142
 
 
143
    if log:
 
144
        log.debug(msg)
 
145
    func(*args)