← Back to team overview

cloud-init-dev team mailing list archive

[Merge] lp:~smoser/cloud-init/finalcmd into lp:cloud-init

 

Scott Moser has proposed merging lp:~smoser/cloud-init/finalcmd into lp:cloud-init.

Requested reviews:
  cloud init development team (cloud-init-dev)
Related bugs:
  Bug #1064665 in cloud-init: "should have some way to indicate shutdown or reboot"
  https://bugs.launchpad.net/cloud-init/+bug/1064665

For more details, see:
https://code.launchpad.net/~smoser/cloud-init/finalcmd/+merge/134029
-- 
https://code.launchpad.net/~smoser/cloud-init/finalcmd/+merge/134029
Your team cloud init development team is requested to review the proposed merge of lp:~smoser/cloud-init/finalcmd into lp:cloud-init.
=== modified file 'ChangeLog'
--- ChangeLog	2012-11-12 21:53:46 +0000
+++ ChangeLog	2012-11-13 03:18:20 +0000
@@ -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.

=== added file 'cloudinit/config/cc_finalcmd.py'
--- cloudinit/config/cc_finalcmd.py	1970-01-01 00:00:00 +0000
+++ cloudinit/config/cc_finalcmd.py	2012-11-13 03:18:20 +0000
@@ -0,0 +1,139 @@
+# vi: ts=4 expandtab
+#
+#    Copyright (C) 2011 Canonical Ltd.
+#
+#    Author: Scott Moser <scott.moser@xxxxxxxxxxxxx>
+#
+#    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))

=== modified file 'config/cloud.cfg'
--- config/cloud.cfg	2012-11-08 05:34:41 +0000
+++ config/cloud.cfg	2012-11-13 03:18:20 +0000
@@ -69,6 +69,7 @@
  - keys-to-console
  - phone-home
  - final-message
+ - finalcmd
 
 # System and/or distro specific settings
 # (not accessible to handlers/transforms)

=== modified file 'doc/examples/cloud-config.txt'
--- doc/examples/cloud-config.txt	2012-11-08 05:00:33 +0000
+++ doc/examples/cloud-config.txt	2012-11-13 03:18:20 +0000
@@ -256,6 +256,24 @@
  - 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:


Follow ups