|
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
|