Blame cloudinit/config/cc_power_state_change.py

Packit Service a04d08
# Copyright (C) 2011 Canonical Ltd.
Packit Service a04d08
#
Packit Service a04d08
# Author: Scott Moser <scott.moser@canonical.com>
Packit Service a04d08
#
Packit Service a04d08
# This file is part of cloud-init. See LICENSE file for license information.
Packit Service a04d08
Packit Service a04d08
"""
Packit Service a04d08
Power State Change
Packit Service a04d08
------------------
Packit Service a04d08
**Summary:** change power state
Packit Service a04d08
Packit Service a04d08
This module handles shutdown/reboot after all config modules have been run. By
Packit Service a04d08
default it will take no action, and the system will keep running unless a
Packit Service a04d08
package installation/upgrade requires a system reboot (e.g. installing a new
Packit Service a04d08
kernel) and ``package_reboot_if_required`` is true. The ``power_state`` config
Packit Service a04d08
key accepts a dict of options. If ``mode`` is any value other than
Packit Service a04d08
``poweroff``, ``halt``, or ``reboot``, then no action will be taken.
Packit Service a04d08
Packit Service a04d08
The system
Packit Service a04d08
can be shutdown before cloud-init has finished using the ``timeout`` option.
Packit Service a04d08
The ``delay`` key specifies a duration to be added onto any shutdown command
Packit Service a04d08
used. Therefore, if a 5 minute delay and a 120 second shutdown are specified,
Packit Service a04d08
the maximum amount of time between cloud-init starting and the system shutting
Packit Service a04d08
down is 7 minutes, and the minimum amount of time is 5 minutes. The ``delay``
Packit Service 9bfd13
key must have an argument in either the form ``+5`` for 5 minutes or ``now``
Packit Service 9bfd13
for immediate shutdown.
Packit Service a04d08
Packit Service a04d08
Optionally, a command can be run to determine whether or not
Packit Service a04d08
the system should shut down. The command to be run should be specified in the
Packit Service a04d08
``condition`` key. For command formatting, see the documentation for
Packit Service a04d08
``cc_runcmd``. The specified shutdown behavior will only take place if the
Packit Service a04d08
``condition`` key is omitted or the command specified by the ``condition``
Packit Service a04d08
key returns 0.
Packit Service a04d08
Packit Service 9bfd13
.. note::
Packit Service 9bfd13
    With Alpine Linux any message value specified is ignored as Alpine's halt,
Packit Service 9bfd13
    poweroff, and reboot commands do not support broadcasting a message.
Packit Service 9bfd13
Packit Service a04d08
**Internal name:** ``cc_power_state_change``
Packit Service a04d08
Packit Service a04d08
**Module frequency:** per instance
Packit Service a04d08
Packit Service a04d08
**Supported distros:** all
Packit Service a04d08
Packit Service a04d08
**Config keys**::
Packit Service a04d08
Packit Service a04d08
    power_state:
Packit Service a04d08
        delay: <now/'+minutes'>
Packit Service a04d08
        mode: <poweroff/halt/reboot>
Packit Service a04d08
        message: <shutdown message>
Packit Service a04d08
        timeout: <seconds>
Packit Service a04d08
        condition: <true/false/command>
Packit Service a04d08
"""
Packit Service a04d08
Packit Service a04d08
import errno
Packit Service a04d08
import os
Packit Service a04d08
import re
Packit Service a04d08
import subprocess
Packit Service a04d08
import time
Packit Service a04d08
Packit Service 9bfd13
from cloudinit.settings import PER_INSTANCE
Packit Service 9bfd13
from cloudinit import subp
Packit Service 9bfd13
from cloudinit import util
Packit Service 9bfd13
Packit Service a04d08
frequency = PER_INSTANCE
Packit Service a04d08
Packit Service a04d08
EXIT_FAIL = 254
Packit Service a04d08
Packit Service a04d08
Packit Service a04d08
def givecmdline(pid):
Packit Service a04d08
    # Returns the cmdline for the given process id. In Linux we can use procfs
Packit Service a04d08
    # for this but on BSD there is /usr/bin/procstat.
Packit Service a04d08
    try:
Packit Service a04d08
        # Example output from procstat -c 1
Packit Service a04d08
        #   PID COMM             ARGS
Packit Service a04d08
        #     1 init             /bin/init --
Packit Service a04d08
        if util.is_FreeBSD():
Packit Service 9bfd13
            (output, _err) = subp.subp(['procstat', '-c', str(pid)])
Packit Service a04d08
            line = output.splitlines()[1]
Packit Service a04d08
            m = re.search(r'\d+ (\w|\.|-)+\s+(/\w.+)', line)
Packit Service a04d08
            return m.group(2)
Packit Service a04d08
        else:
Packit Service a04d08
            return util.load_file("/proc/%s/cmdline" % pid)
Packit Service a04d08
    except IOError:
Packit Service a04d08
        return None
Packit Service a04d08
Packit Service a04d08
Packit Service a04d08
def check_condition(cond, log=None):
Packit Service a04d08
    if isinstance(cond, bool):
Packit Service a04d08
        if log:
Packit Service a04d08
            log.debug("Static Condition: %s" % cond)
Packit Service a04d08
        return cond
Packit Service a04d08
Packit Service a04d08
    pre = "check_condition command (%s): " % cond
Packit Service a04d08
    try:
Packit Service a04d08
        proc = subprocess.Popen(cond, shell=not isinstance(cond, list))
Packit Service a04d08
        proc.communicate()
Packit Service a04d08
        ret = proc.returncode
Packit Service a04d08
        if ret == 0:
Packit Service a04d08
            if log:
Packit Service a04d08
                log.debug(pre + "exited 0. condition met.")
Packit Service a04d08
            return True
Packit Service a04d08
        elif ret == 1:
Packit Service a04d08
            if log:
Packit Service a04d08
                log.debug(pre + "exited 1. condition not met.")
Packit Service a04d08
            return False
Packit Service a04d08
        else:
Packit Service a04d08
            if log:
Packit Service a04d08
                log.warning(pre + "unexpected exit %s. " % ret +
Packit Service a04d08
                            "do not apply change.")
Packit Service a04d08
            return False
Packit Service a04d08
    except Exception as e:
Packit Service a04d08
        if log:
Packit Service a04d08
            log.warning(pre + "Unexpected error: %s" % e)
Packit Service a04d08
        return False
Packit Service a04d08
Packit Service a04d08
Packit Service 9bfd13
def handle(_name, cfg, cloud, log, _args):
Packit Service a04d08
    try:
Packit Service 9bfd13
        (args, timeout, condition) = load_power_state(cfg, cloud.distro.name)
Packit Service a04d08
        if args is None:
Packit Service a04d08
            log.debug("no power_state provided. doing nothing")
Packit Service a04d08
            return
Packit Service a04d08
    except Exception as e:
Packit Service a04d08
        log.warning("%s Not performing power state change!" % str(e))
Packit Service a04d08
        return
Packit Service a04d08
Packit Service a04d08
    if condition is False:
Packit Service a04d08
        log.debug("Condition was false. Will not perform state change.")
Packit Service a04d08
        return
Packit Service a04d08
Packit Service a04d08
    mypid = os.getpid()
Packit Service a04d08
Packit Service a04d08
    cmdline = givecmdline(mypid)
Packit Service a04d08
    if not cmdline:
Packit Service a04d08
        log.warning("power_state: failed to get cmdline of current process")
Packit Service a04d08
        return
Packit Service a04d08
Packit Service a04d08
    devnull_fp = open(os.devnull, "w")
Packit Service a04d08
Packit Service a04d08
    log.debug("After pid %s ends, will execute: %s" % (mypid, ' '.join(args)))
Packit Service a04d08
Packit Service a04d08
    util.fork_cb(run_after_pid_gone, mypid, cmdline, timeout, log,
Packit Service a04d08
                 condition, execmd, [args, devnull_fp])
Packit Service a04d08
Packit Service a04d08
Packit Service 9bfd13
def convert_delay(delay, fmt=None, scale=None):
Packit Service 9bfd13
    if not fmt:
Packit Service 9bfd13
        fmt = "+%s"
Packit Service 9bfd13
    if not scale:
Packit Service 9bfd13
        scale = 1
Packit Service 9bfd13
Packit Service 9bfd13
    if delay != "now":
Packit Service 9bfd13
        delay = fmt % int(int(delay) * int(scale))
Packit Service 9bfd13
Packit Service 9bfd13
    return delay
Packit Service 9bfd13
Packit Service 9bfd13
Packit Service 9bfd13
def load_power_state(cfg, distro_name):
Packit Service a04d08
    # returns a tuple of shutdown_command, timeout
Packit Service a04d08
    # shutdown_command is None if no config found
Packit Service a04d08
    pstate = cfg.get('power_state')
Packit Service a04d08
Packit Service a04d08
    if pstate is None:
Packit Service a04d08
        return (None, None, None)
Packit Service a04d08
Packit Service a04d08
    if not isinstance(pstate, dict):
Packit Service a04d08
        raise TypeError("power_state is not a dict.")
Packit Service a04d08
Packit Service a04d08
    opt_map = {'halt': '-H', 'poweroff': '-P', 'reboot': '-r'}
Packit Service a04d08
Packit Service a04d08
    mode = pstate.get("mode")
Packit Service a04d08
    if mode not in opt_map:
Packit Service a04d08
        raise TypeError(
Packit Service a04d08
            "power_state[mode] required, must be one of: %s. found: '%s'." %
Packit Service a04d08
            (','.join(opt_map.keys()), mode))
Packit Service a04d08
Packit Service a04d08
    delay = pstate.get("delay", "now")
Packit Service 9bfd13
    message = pstate.get("message")
Packit Service 9bfd13
    scale = 1
Packit Service 9bfd13
    fmt = "+%s"
Packit Service 9bfd13
    command = ["shutdown", opt_map[mode]]
Packit Service 9bfd13
Packit Service 9bfd13
    if distro_name == 'alpine':
Packit Service 9bfd13
        # Convert integer 30 or string '30' to '1800' (seconds) as Alpine's
Packit Service 9bfd13
        # halt/poweroff/reboot commands take seconds rather than minutes.
Packit Service 9bfd13
        scale = 60
Packit Service 9bfd13
        # No "+" in front of delay value as not supported by Alpine's commands.
Packit Service 9bfd13
        fmt = "%s"
Packit Service 9bfd13
        if delay == "now":
Packit Service 9bfd13
            # Alpine's commands do not understand "now".
Packit Service 9bfd13
            delay = "0"
Packit Service 9bfd13
        command = [mode, "-d"]
Packit Service 9bfd13
        # Alpine's commands don't support a message.
Packit Service 9bfd13
        message = None
Packit Service b1601c
Packit Service 9bfd13
    try:
Packit Service 9bfd13
        delay = convert_delay(delay, fmt=fmt, scale=scale)
Packit Service 9bfd13
    except ValueError as e:
Packit Service a04d08
        raise TypeError(
Packit Service a04d08
            "power_state[delay] must be 'now' or '+m' (minutes)."
Packit Service 9bfd13
            " found '%s'." % delay
Packit Service 9bfd13
        ) from e
Packit Service a04d08
Packit Service 9bfd13
    args = command + [delay]
Packit Service 9bfd13
    if message:
Packit Service 9bfd13
        args.append(message)
Packit Service a04d08
Packit Service a04d08
    try:
Packit Service a04d08
        timeout = float(pstate.get('timeout', 30.0))
Packit Service 9bfd13
    except ValueError as e:
Packit Service 9bfd13
        raise ValueError(
Packit Service 9bfd13
            "failed to convert timeout '%s' to float." % pstate['timeout']
Packit Service 9bfd13
        ) from e
Packit Service a04d08
Packit Service a04d08
    condition = pstate.get("condition", True)
Packit Service 9bfd13
    if not isinstance(condition, (str, list, bool)):
Packit Service a04d08
        raise TypeError("condition type %s invalid. must be list, bool, str")
Packit Service a04d08
    return (args, timeout, condition)
Packit Service a04d08
Packit Service a04d08
Packit Service a04d08
def doexit(sysexit):
Packit Service a04d08
    os._exit(sysexit)
Packit Service a04d08
Packit Service a04d08
Packit Service a04d08
def execmd(exe_args, output=None, data_in=None):
Packit Service a04d08
    ret = 1
Packit Service a04d08
    try:
Packit Service a04d08
        proc = subprocess.Popen(exe_args, stdin=subprocess.PIPE,
Packit Service a04d08
                                stdout=output, stderr=subprocess.STDOUT)
Packit Service a04d08
        proc.communicate(data_in)
Packit Service a04d08
        ret = proc.returncode
Packit Service a04d08
    except Exception:
Packit Service a04d08
        doexit(EXIT_FAIL)
Packit Service a04d08
    doexit(ret)
Packit Service a04d08
Packit Service a04d08
Packit Service a04d08
def run_after_pid_gone(pid, pidcmdline, timeout, log, condition, func, args):
Packit Service a04d08
    # wait until pid, with /proc/pid/cmdline contents of pidcmdline
Packit Service a04d08
    # is no longer alive.  After it is gone, or timeout has passed
Packit Service a04d08
    # execute func(args)
Packit Service a04d08
    msg = None
Packit Service a04d08
    end_time = time.time() + timeout
Packit Service a04d08
Packit Service a04d08
    def fatal(msg):
Packit Service a04d08
        if log:
Packit Service a04d08
            log.warning(msg)
Packit Service a04d08
        doexit(EXIT_FAIL)
Packit Service a04d08
Packit Service a04d08
    known_errnos = (errno.ENOENT, errno.ESRCH)
Packit Service a04d08
Packit Service a04d08
    while True:
Packit Service a04d08
        if time.time() > end_time:
Packit Service a04d08
            msg = "timeout reached before %s ended" % pid
Packit Service a04d08
            break
Packit Service a04d08
Packit Service a04d08
        try:
Packit Service a04d08
            cmdline = givecmdline(pid)
Packit Service a04d08
            if cmdline != pidcmdline:
Packit Service a04d08
                msg = "cmdline changed for %s [now: %s]" % (pid, cmdline)
Packit Service a04d08
                break
Packit Service a04d08
Packit Service a04d08
        except IOError as ioerr:
Packit Service a04d08
            if ioerr.errno in known_errnos:
Packit Service a04d08
                msg = "pidfile gone [%d]" % ioerr.errno
Packit Service a04d08
            else:
Packit Service a04d08
                fatal("IOError during wait: %s" % ioerr)
Packit Service a04d08
            break
Packit Service a04d08
Packit Service a04d08
        except Exception as e:
Packit Service a04d08
            fatal("Unexpected Exception: %s" % e)
Packit Service a04d08
Packit Service a04d08
        time.sleep(.25)
Packit Service a04d08
Packit Service a04d08
    if not msg:
Packit Service a04d08
        fatal("Unexpected error in run_after_pid_gone")
Packit Service a04d08
Packit Service a04d08
    if log:
Packit Service a04d08
        log.debug(msg)
Packit Service a04d08
Packit Service a04d08
    try:
Packit Service a04d08
        if not check_condition(condition, log):
Packit Service a04d08
            return
Packit Service a04d08
    except Exception as e:
Packit Service a04d08
        fatal("Unexpected Exception when checking condition: %s" % e)
Packit Service a04d08
Packit Service a04d08
    func(*args)
Packit Service a04d08
Packit Service a04d08
# vi: ts=4 expandtab