summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorScott Moser <smoser@ubuntu.com>2012-11-12 22:15:29 -0500
committerScott Moser <smoser@ubuntu.com>2012-11-12 22:15:29 -0500
commit949e1759342b1e60c100855aaf250165bcb9997e (patch)
tree5515fda8889d42bedd0f3be16cf3f8ee86663e30
parent9c64c5d0e01e48612fe37d3304b1f6eb70181cae (diff)
downloadvyos-cloud-init-949e1759342b1e60c100855aaf250165bcb9997e.tar.gz
vyos-cloud-init-949e1759342b1e60c100855aaf250165bcb9997e.zip
add 'finalcmd' module for running code after cloud-init-final
This allows the user to easily run stuff even after cloud-init-final has finished. The initial reason for it is to be able to run /sbin/poweroff and not have cloud-init complain loudly that it is being killed. LP: #1064665
-rw-r--r--ChangeLog2
-rw-r--r--cloudinit/config/cc_finalcmd.py139
-rw-r--r--config/cloud.cfg1
-rw-r--r--doc/examples/cloud-config.txt18
4 files changed, 160 insertions, 0 deletions
diff --git a/ChangeLog b/ChangeLog
index 4cae8b32..93c3af04 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -46,6 +46,8 @@
dictionary and force it to full expand so that if cloud-init blocks the ec2
metadata port the lazy loaded dictionary will continue working properly
instead of trying to make additional url calls which will fail (LP: #1068801)
+ - add 'finalcmd' config module to execute 'finalcmd' entries like
+ 'runcmd' but detached from cloud-init (LP: #1064665)
0.7.0:
- add a 'exception_cb' argument to 'wait_for_url'. If provided, this
method will be called back with the exception received and the message.
diff --git a/cloudinit/config/cc_finalcmd.py b/cloudinit/config/cc_finalcmd.py
new file mode 100644
index 00000000..442ad12b
--- /dev/null
+++ b/cloudinit/config/cc_finalcmd.py
@@ -0,0 +1,139 @@
+# vi: ts=4 expandtab
+#
+# Copyright (C) 2011 Canonical Ltd.
+#
+# Author: Scott Moser <scott.moser@canonical.com>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 3, as
+# published by the Free Software Foundation.
+#
+# This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
+
+from cloudinit.settings import PER_INSTANCE
+from cloudinit import util
+
+import errno
+import os
+import subprocess
+import sys
+import time
+
+frequency = PER_INSTANCE
+
+
+def handle(_name, cfg, _cloud, log, _args):
+
+ finalcmds = cfg.get("finalcmd")
+
+ if not finalcmds:
+ log.debug("No final commands")
+ return
+
+ mypid = os.getpid()
+ cmdline = util.load_file("/proc/%s/cmdline")
+
+ if not cmdline:
+ log.warn("Failed to get cmdline of current process")
+ return
+
+ try:
+ timeout = float(cfg.get("finalcmd_timeout", 30.0))
+ except ValueError:
+ log.warn("failed to convert finalcmd_timeout '%s' to float" %
+ cfg.get("finalcmd_timeout", 30.0))
+ return
+
+ devnull_fp = open("/dev/null", "w")
+
+ shellcode = util.shellify(finalcmds)
+
+ # note, after the fork, we do not use any of cloud-init's functions
+ # that would attempt to log. The primary reason for that is
+ # to allow the 'finalcmd' the ability to do just about anything
+ # and not depend on syslog services.
+ # Basically, it should "just work" to have finalcmd of:
+ # - sleep 30
+ # - /sbin/poweroff
+ finalcmd_d = os.path.join(cloud.get_ipath_cur(), "finalcmds")
+
+ util.fork_cb(run_after_pid_gone, mypid, cmdline, timeout,
+ runfinal, (shellcode, finalcmd_d, devnull_fp))
+
+
+def execmd(exe_args, data_in=None, output=None):
+ try:
+ proc = subprocess.Popen(exe_args, stdin=subprocess.PIPE,
+ stdout=output, stderr=subprocess.STDERR)
+ proc.communicate(data_in)
+ except Exception as e:
+ return 254
+ return proc.returncode()
+
+
+def runfinal(shellcode, finalcmd_d, output=None):
+ ret = execmd(("/bin/sh",), data_in=shellcode, output=output)
+ if not (finalcmd_d and os.path.isdir(finalcmd_d)):
+ sys.exit(ret)
+
+ fails = 0
+ if ret != 0:
+ fails = 1
+
+ # now runparts the final command dir
+ for exe_name in sorted(os.listdir(finalcmd_d)):
+ exe_path = os.path.join(finalcmd_d, exe_name)
+ if os.path.isfile(exe_path) and os.access(exe_path, os.X_OK):
+ ret = execmd(exe_path, data_in=None, output=output)
+ if ret != 0:
+ fails += 1
+ sys.exit(fails)
+
+
+def run_after_pid_gone(pid, pidcmdline, timeout, func, args):
+ # wait until pid, with /proc/pid/cmdline contents of pidcmdline
+ # is no longer alive. After it is gone, or timeout has passed
+ # execute func(args)
+ msg = "ERROR: Uncaught error"
+ end_time = time.time() + timeout
+
+ cmdline_f = "/proc/%s/cmdline" % pid
+
+ while True:
+ if time.time() > end_time:
+ msg = "timeout reached before %s ended" % pid
+ break
+
+ try:
+ cmdline = ""
+ with open(cmdline_f) as fp:
+ cmdline = fp.read()
+ if cmdline != pidcmdline:
+ msg = "cmdline changed for %s [now: %s]" % (pid, cmdline)
+ break
+
+ except IOError as ioerr:
+ if ioerr.errno == errno.ENOENT:
+ msg = "pidfile '%s' gone" % cmdline_f
+ else:
+ msg = "ERROR: IOError: %s" % ioerr
+ raise
+ break
+
+ except Exception as e:
+ msg = "ERROR: Exception: %s" % e
+ raise
+
+ if msg.startswith("ERROR:"):
+ sys.stderr.write(msg)
+ sys.stderr.write("Not executing finalcmd")
+ sys.exit(1)
+
+ sys.stderr.write("calling %s with %s\n" % (func, args))
+ sys.exit(func(*args))
diff --git a/config/cloud.cfg b/config/cloud.cfg
index ad100fff..249a593d 100644
--- a/config/cloud.cfg
+++ b/config/cloud.cfg
@@ -69,6 +69,7 @@ cloud_final_modules:
- keys-to-console
- phone-home
- final-message
+ - finalcmd
# System and/or distro specific settings
# (not accessible to handlers/transforms)
diff --git a/doc/examples/cloud-config.txt b/doc/examples/cloud-config.txt
index 12bf2c91..4fc5f351 100644
--- a/doc/examples/cloud-config.txt
+++ b/doc/examples/cloud-config.txt
@@ -256,6 +256,24 @@ bootcmd:
- echo 192.168.1.130 us.archive.ubuntu.com > /etc/hosts
- [ cloud-init-per, once, mymkfs, mkfs, /dev/vdb ]
+# final commands
+# default: none
+# This can be used to execute commands after and fully detached from
+# a cloud-init stage. The initial purpose of it was to allow 'poweroff'
+# detached from cloud-init. If poweroff was run from 'runcmd' or userdata
+# then messages may be spewed from cloud-init about logging failing or other
+# issues as a result of the system being turned off.
+#
+# You probably are better off using 'runcmd' for this.
+#
+# The output of finalcmd will redirected redirected to /dev/null
+# If you want output to be seen, take care to do so in your commands
+# themselves. See example.
+finalcmd:
+ - sleep 30
+ - "echo $(date -R): powering off > /dev/console"
+ - /sbin/poweroff
+
# cloud_config_modules:
# default:
# cloud_config_modules: