From 01ad8fc5b1928c01367b32022ceab09f1dc5dc26 Mon Sep 17 00:00:00 2001 From: Joshua Harlow Date: Thu, 6 Sep 2012 18:12:21 -0700 Subject: Add capturing of basic signal handlers to handle those signals more gracefully and with better messaging than what comes builtin. --- bin/cloud-init | 4 +++ cloudinit/signal_handler.py | 65 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+) create mode 100644 cloudinit/signal_handler.py diff --git a/bin/cloud-init b/bin/cloud-init index 1f017475..a6ad14a6 100755 --- a/bin/cloud-init +++ b/bin/cloud-init @@ -35,6 +35,7 @@ if os.path.exists(os.path.join(possible_topdir, "cloudinit", "__init__.py")): from cloudinit import log as logging from cloudinit import netinfo +from cloudinit import signal_handler from cloudinit import sources from cloudinit import stages from cloudinit import templater @@ -494,6 +495,9 @@ def main(): if args.debug: logging.setupBasicLogging() + # Setup signal handlers before running + signal_handler.attach_handlers() + (name, functor) = args.action return functor(name, args) diff --git a/cloudinit/signal_handler.py b/cloudinit/signal_handler.py new file mode 100644 index 00000000..244293b4 --- /dev/null +++ b/cloudinit/signal_handler.py @@ -0,0 +1,65 @@ +# vi: ts=4 expandtab +# +# Copyright (C) 2012 Canonical Ltd. +# Copyright (C) 2012 Yahoo! Inc. +# +# Author: Scott Moser +# Author: Joshua Harlow +# +# 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 . + +import inspect +import signal +import sys + +from StringIO import StringIO + +from cloudinit import util +from cloudinit import version as vr + + +EXIT_FOR = { + signal.SIGINT: ('Cloud-init %(version)s interrupted, exiting...', 1), + signal.SIGTERM: ('Cloud-init %(version)s terminated, exiting...', 1), + # signal.SIGKILL: ('Cloud-init killed, exiting...', 1), - Can't be caught... + signal.SIGABRT: ('Cloud-init %(version)s aborted, exiting...', 1), +} + + +def _pprint_frame(frame, depth=1, max_depth=3): + if depth > max_depth or not frame: + return + frame_info = inspect.getframeinfo(frame) + prefix = " " * (depth * 2) + contents = StringIO() + contents.write("%sFilename: %s\n" % (prefix, frame_info.filename)) + contents.write("%sFunction: %s\n" % (prefix, frame_info.function)) + contents.write("%sLine number: %s\n" % (prefix, frame_info.lineno)) + util.multi_log(contents.getvalue()) + _pprint_frame(frame.f_back, depth + 1, max_depth) + + +def _handle_exit(signum, frame): + (msg, rc) = EXIT_FOR[signum] + msg = msg % ({'version': vr.version()}) + util.multi_log("%s\n" % (msg)) + _pprint_frame(frame) + sys.exit(rc) + + +def attach_handlers(): + sigs_attached = 0 + for signum in EXIT_FOR.keys(): + signal.signal(signum, _handle_exit) + sigs_attached += len(EXIT_FOR) + return sigs_attached -- cgit v1.2.3 From 0f385445522cf8fe76391c0c0c855b6fa7ad9f50 Mon Sep 17 00:00:00 2001 From: Joshua Harlow Date: Thu, 6 Sep 2012 18:14:03 -0700 Subject: Fix pylintness. --- cloudinit/signal_handler.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cloudinit/signal_handler.py b/cloudinit/signal_handler.py index 244293b4..96b618f3 100644 --- a/cloudinit/signal_handler.py +++ b/cloudinit/signal_handler.py @@ -31,7 +31,8 @@ from cloudinit import version as vr EXIT_FOR = { signal.SIGINT: ('Cloud-init %(version)s interrupted, exiting...', 1), signal.SIGTERM: ('Cloud-init %(version)s terminated, exiting...', 1), - # signal.SIGKILL: ('Cloud-init killed, exiting...', 1), - Can't be caught... + # Can't be caught... + # signal.SIGKILL: ('Cloud-init killed, exiting...', 1), signal.SIGABRT: ('Cloud-init %(version)s aborted, exiting...', 1), } -- cgit v1.2.3 From 2899c638c04907461024dff0d32c1cc57ae4b6d4 Mon Sep 17 00:00:00 2001 From: Joshua Harlow Date: Fri, 14 Sep 2012 14:32:31 -0700 Subject: Add a basic signal test and write out all the signal information in one block instead of many. --- cloudinit/signal_handler.py | 28 +++++++++++++++++++------ tests/unittests/test_signal.py | 46 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 68 insertions(+), 6 deletions(-) create mode 100644 tests/unittests/test_signal.py diff --git a/cloudinit/signal_handler.py b/cloudinit/signal_handler.py index 96b618f3..b6ea592b 100644 --- a/cloudinit/signal_handler.py +++ b/cloudinit/signal_handler.py @@ -24,10 +24,14 @@ import sys from StringIO import StringIO +from cloudinit import log as logging from cloudinit import util from cloudinit import version as vr +LOG = logging.getLogger(__name__) + +BACK_FRAME_TRACE_DEPTH = 3 EXIT_FOR = { signal.SIGINT: ('Cloud-init %(version)s interrupted, exiting...', 1), signal.SIGTERM: ('Cloud-init %(version)s terminated, exiting...', 1), @@ -37,24 +41,36 @@ EXIT_FOR = { } -def _pprint_frame(frame, depth=1, max_depth=3): +def _pprint_frame(frame, depth, max_depth, contents): if depth > max_depth or not frame: return frame_info = inspect.getframeinfo(frame) prefix = " " * (depth * 2) - contents = StringIO() contents.write("%sFilename: %s\n" % (prefix, frame_info.filename)) contents.write("%sFunction: %s\n" % (prefix, frame_info.function)) contents.write("%sLine number: %s\n" % (prefix, frame_info.lineno)) - util.multi_log(contents.getvalue()) - _pprint_frame(frame.f_back, depth + 1, max_depth) + _pprint_frame(frame.f_back, depth + 1, max_depth, contents) def _handle_exit(signum, frame): (msg, rc) = EXIT_FOR[signum] + # Reset logging so that only the basic logging + # is active since the state of syslog or other + # logging processes is unknown if we are being + # signaled by a reboot process which is external and + # killing other processes while this process is being + # finished off... + try: + logging.resetLogging() + logging.setupBasicLogging() + except: + pass msg = msg % ({'version': vr.version()}) - util.multi_log("%s\n" % (msg)) - _pprint_frame(frame) + contents = StringIO() + contents.write("%s\n" % (msg)) + _pprint_frame(frame, 1, BACK_FRAME_TRACE_DEPTH, contents) + util.multi_log(contents.getvalue(), + console=True, stderr=False, log=LOG) sys.exit(rc) diff --git a/tests/unittests/test_signal.py b/tests/unittests/test_signal.py new file mode 100644 index 00000000..02fd1ef1 --- /dev/null +++ b/tests/unittests/test_signal.py @@ -0,0 +1,46 @@ +"""Tests for handling of signals within cloud init.""" + +import os +import subprocess +import sys +import time + +from StringIO import StringIO + +from mocker import MockerTestCase + + +class TestSignal(MockerTestCase): + + def test_signal_output(self): + + # This is done since nose/unittest is actually setting up + # output capturing, signal handling itself, and its easier + # to just call out to cloudinit with a loop and see what the result is + run_what = [sys.executable, + '-c', ('import time; from cloudinit import signal_handler;' + 'signal_handler.attach_handlers(); time.sleep(120)')] + + pc_info = subprocess.Popen(run_what, stderr=subprocess.PIPE, stdout=subprocess.PIPE) + + # Let it start up + time.sleep(0.5) + dead = None + while dead is None: + pc_info.terminate() + # Ok not dead yet. try again + time.sleep(0.5) + dead = pc_info.poll() + + outputs = StringIO() + if pc_info.stdout: + outputs.write(pc_info.stdout.read()) + if pc_info.stderr: + outputs.write(pc_info.stderr.read()) + val = outputs.getvalue() + print val + + # Check some of the outputs that should of happened + self.assertEquals(1, pc_info.wait()) + self.assertTrue(len(val) != 0) + self.assertTrue(val.find("terminated") != -1) -- cgit v1.2.3 From 5f33046a1e059f23ce1f4aceeec33813c57a7b70 Mon Sep 17 00:00:00 2001 From: Joshua Harlow Date: Fri, 14 Sep 2012 14:35:32 -0700 Subject: Cleanup pylint warnings. --- tests/unittests/test_signal.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/tests/unittests/test_signal.py b/tests/unittests/test_signal.py index 02fd1ef1..8cb31fd7 100644 --- a/tests/unittests/test_signal.py +++ b/tests/unittests/test_signal.py @@ -1,6 +1,5 @@ """Tests for handling of signals within cloud init.""" -import os import subprocess import sys import time @@ -17,11 +16,14 @@ class TestSignal(MockerTestCase): # This is done since nose/unittest is actually setting up # output capturing, signal handling itself, and its easier # to just call out to cloudinit with a loop and see what the result is - run_what = [sys.executable, - '-c', ('import time; from cloudinit import signal_handler;' - 'signal_handler.attach_handlers(); time.sleep(120)')] + run_what = [sys.executable, + '-c', + ('import time; from cloudinit import signal_handler;' + 'signal_handler.attach_handlers(); time.sleep(120)')] - pc_info = subprocess.Popen(run_what, stderr=subprocess.PIPE, stdout=subprocess.PIPE) + pc_info = subprocess.Popen(run_what, + stderr=subprocess.PIPE, + stdout=subprocess.PIPE) # Let it start up time.sleep(0.5) @@ -38,7 +40,6 @@ class TestSignal(MockerTestCase): if pc_info.stderr: outputs.write(pc_info.stderr.read()) val = outputs.getvalue() - print val # Check some of the outputs that should of happened self.assertEquals(1, pc_info.wait()) -- cgit v1.2.3 From a637b5f74948324173219a2e7817c7f19d864281 Mon Sep 17 00:00:00 2001 From: Scott Moser Date: Wed, 19 Sep 2012 16:26:45 -0400 Subject: change exit messages to include signal name --- cloudinit/signal_handler.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cloudinit/signal_handler.py b/cloudinit/signal_handler.py index b6ea592b..34e075a2 100644 --- a/cloudinit/signal_handler.py +++ b/cloudinit/signal_handler.py @@ -33,11 +33,11 @@ LOG = logging.getLogger(__name__) BACK_FRAME_TRACE_DEPTH = 3 EXIT_FOR = { - signal.SIGINT: ('Cloud-init %(version)s interrupted, exiting...', 1), - signal.SIGTERM: ('Cloud-init %(version)s terminated, exiting...', 1), + signal.SIGINT: ('Cloud-init %(version)s received SIGINT, exiting...', 1), + signal.SIGTERM: ('Cloud-init %(version)s received SIGTERM, exiting...', 1), # Can't be caught... # signal.SIGKILL: ('Cloud-init killed, exiting...', 1), - signal.SIGABRT: ('Cloud-init %(version)s aborted, exiting...', 1), + signal.SIGABRT: ('Cloud-init %(version)s received SIGABRT, exiting...', 1), } -- cgit v1.2.3 From 24d2457c2eab89f135e2b483f05d52eee0169af7 Mon Sep 17 00:00:00 2001 From: Scott Moser Date: Wed, 19 Sep 2012 16:26:58 -0400 Subject: remove unit test. the unit test required access to /dev/console due to the logging. --- tests/unittests/test_signal.py | 47 ------------------------------------------ 1 file changed, 47 deletions(-) delete mode 100644 tests/unittests/test_signal.py diff --git a/tests/unittests/test_signal.py b/tests/unittests/test_signal.py deleted file mode 100644 index 8cb31fd7..00000000 --- a/tests/unittests/test_signal.py +++ /dev/null @@ -1,47 +0,0 @@ -"""Tests for handling of signals within cloud init.""" - -import subprocess -import sys -import time - -from StringIO import StringIO - -from mocker import MockerTestCase - - -class TestSignal(MockerTestCase): - - def test_signal_output(self): - - # This is done since nose/unittest is actually setting up - # output capturing, signal handling itself, and its easier - # to just call out to cloudinit with a loop and see what the result is - run_what = [sys.executable, - '-c', - ('import time; from cloudinit import signal_handler;' - 'signal_handler.attach_handlers(); time.sleep(120)')] - - pc_info = subprocess.Popen(run_what, - stderr=subprocess.PIPE, - stdout=subprocess.PIPE) - - # Let it start up - time.sleep(0.5) - dead = None - while dead is None: - pc_info.terminate() - # Ok not dead yet. try again - time.sleep(0.5) - dead = pc_info.poll() - - outputs = StringIO() - if pc_info.stdout: - outputs.write(pc_info.stdout.read()) - if pc_info.stderr: - outputs.write(pc_info.stderr.read()) - val = outputs.getvalue() - - # Check some of the outputs that should of happened - self.assertEquals(1, pc_info.wait()) - self.assertTrue(len(val) != 0) - self.assertTrue(val.find("terminated") != -1) -- cgit v1.2.3 From cef4e846797ed7aee29e18b00810887c67591abc Mon Sep 17 00:00:00 2001 From: Scott Moser Date: Wed, 19 Sep 2012 16:33:56 -0400 Subject: do not disable logging on signal I had previously asked for this, but we're hoping to handle it in a more generic way. Just because we receive a signal doesn't mean that all logging is broken. The more general solution we'll chase is to catch a failure of a log message and fall back if necessary across the board. That way cloud-init will still send logging to the right places on a user interupt. --- cloudinit/signal_handler.py | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/cloudinit/signal_handler.py b/cloudinit/signal_handler.py index 34e075a2..40b0c94c 100644 --- a/cloudinit/signal_handler.py +++ b/cloudinit/signal_handler.py @@ -54,17 +54,6 @@ def _pprint_frame(frame, depth, max_depth, contents): def _handle_exit(signum, frame): (msg, rc) = EXIT_FOR[signum] - # Reset logging so that only the basic logging - # is active since the state of syslog or other - # logging processes is unknown if we are being - # signaled by a reboot process which is external and - # killing other processes while this process is being - # finished off... - try: - logging.resetLogging() - logging.setupBasicLogging() - except: - pass msg = msg % ({'version': vr.version()}) contents = StringIO() contents.write("%s\n" % (msg)) -- cgit v1.2.3