# Copyright 2012 Hewlett-Packard Development Company, L.P. # Copyright 2013 OpenStack Foundation # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import argparse import daemon import errno import extras import io import logging import logging.config import os import signal import sys import threading import traceback from nodepool.version import release_string from nodepool import logconfig yappi = extras.try_import('yappi') objgraph = extras.try_import('objgraph') # as of python-daemon 1.6 it doesn't bundle pidlockfile anymore # instead it depends on lockfile-0.9.1 which uses pidfile. pid_file_module = extras.try_imports(['daemon.pidlockfile', 'daemon.pidfile']) def is_pidfile_stale(pidfile): """ Determine whether a PID file is stale. Return 'True' ("stale") if the contents of the PID file are valid but do not match the PID of a currently-running process; otherwise return 'False'. """ result = False pidfile_pid = pidfile.read_pid() if pidfile_pid is not None: try: os.kill(pidfile_pid, 0) except OSError as exc: if exc.errno == errno.ESRCH: # The specified PID does not exist result = True return result def stack_dump_handler(signum, frame): signal.signal(signal.SIGUSR2, signal.SIG_IGN) log = logging.getLogger("nodepool.stack_dump") log.debug("Beginning debug handler") try: threads = {} for t in threading.enumerate(): threads[t.ident] = t log_str = "" for thread_id, stack_frame in sys._current_frames().items(): thread = threads.get(thread_id) if thread: thread_name = thread.name thread_is_daemon = str(thread.daemon) else: thread_name = '(Unknown)' thread_is_daemon = '(Unknown)' log_str += "Thread: %s %s d: %s\n"\ % (thread_id, thread_name, thread_is_daemon) log_str += "".join(traceback.format_stack(stack_frame)) log.debug(log_str) except Exception: log.exception("Thread dump error:") try: if yappi: if not yappi.is_running(): log.debug("Starting Yappi") yappi.start() else: log.debug("Stopping Yappi") yappi.stop() yappi_out = io.StringIO() yappi.get_func_stats().print_all(out=yappi_out) yappi.get_thread_stats().print_all(out=yappi_out) log.debug(yappi_out.getvalue()) yappi_out.close() yappi.clear_stats() except Exception: log.exception("Yappi error:") try: if objgraph: log.debug("Most common types:") objgraph_out = io.StringIO() objgraph.show_growth(limit=100, file=objgraph_out) log.debug(objgraph_out.getvalue()) objgraph_out.close() except Exception: log.exception("Objgraph error:") log.debug("End debug handler") signal.signal(signal.SIGUSR2, stack_dump_handler) class NodepoolApp(object): app_name = None app_description = 'Node pool.' def __init__(self): self.parser = None self.args = None def get_path(self, path): if path is None: return None return os.path.abspath(os.path.expanduser(path)) def _get_version(self): return "Nodepool version: %s" % release_string def create_parser(self): parser = argparse.ArgumentParser( description=self.app_description, formatter_class=argparse.ArgumentDefaultsHelpFormatter) parser.add_argument('-l', dest='logconfig', help='path to log config file') parser.add_argument('--version', dest='version', action='version', version=self._get_version(), help='show nodepool version') return parser def parse_args(self): self.args = self.parser.parse_args() self.logconfig = self.get_path(self.args.logconfig) # The arguments debug and foreground both lead to nodaemon mode so # set nodaemon if one of them is set. if ((hasattr(self.args, 'debug') and self.args.debug) or (hasattr(self.args, 'foreground') and self.args.foreground)): self.args.nodaemon = True else: self.args.nodaemon = False def setup_logging(self): if self.logconfig: logging_config = logconfig.load_config(self.logconfig) else: # If someone runs in the foreground and doesn't give a logging # config, leave the config set to emit to stdout. if hasattr(self.args, 'nodaemon') and self.args.nodaemon: logging_config = logconfig.ServerLoggingConfig() if hasattr(self.args, 'debug') and self.args.debug: logging_config.setDebug() else: # Setting a server value updates the defaults to use # WatchedFileHandler on /var/log/nodepool/{server}-debug.log # and /var/log/nodepool/{server}.log logging_config = logconfig.ServerLoggingConfig( server=self.app_name) logging_config.apply() def _main(self, argv=None): if argv is None: argv = sys.argv[1:] self.parser = self.create_parser() self.parse_args() return self._do_run() def _do_run(self): # NOTE(jamielennox): setup logging a bit late so it's not done until # after a DaemonContext is created. self.setup_logging() return self.run() @classmethod def main(cls, argv=None): return cls()._main(argv=argv) def run(self): """The app's primary function, override it with your logic.""" raise NotImplementedError() class NodepoolDaemonApp(NodepoolApp): def create_parser(self): parser = super(NodepoolDaemonApp, self).create_parser() parser.add_argument('-p', dest='pidfile', help='path to pid file', default='/var/run/nodepool/%s.pid' % self.app_name) parser.add_argument('-d', dest='debug', action='store_true', help='do not run as a daemon with debug logging') parser.add_argument('-f', dest='foreground', action='store_true', help='do not run as a daemon') return parser def parse_args(self): super().parse_args() self.pidfile = self.get_path(self.args.pidfile) def _do_run(self): if self.args.nodaemon: return super(NodepoolDaemonApp, self)._do_run() else: pid = pid_file_module.TimeoutPIDLockFile(self.pidfile, 10) if is_pidfile_stale(pid): pid.break_lock() # Exercise the pidfile before we do anything else (including # logging or daemonizing) with pid: pass with daemon.DaemonContext(pidfile=pid): return super(NodepoolDaemonApp, self)._do_run() @classmethod def main(cls, argv=None): signal.signal(signal.SIGUSR2, stack_dump_handler) return super(NodepoolDaemonApp, cls).main(argv)