Package Products :: Package ZenUtils :: Module ZenDaemon |
[hide private]
[frames] | no frames]

# Source Code for Module Products.ZenUtils.ZenDaemon

      1  ###########################################################################
      2  #
      3  # This program is part of Zenoss Core, an open source monitoring platform.
      4  # Copyright (C) 2007, Zenoss Inc.
      5  #
      6  # This program is free software; you can redistribute it and/or modify it
      7  # under the terms of the GNU General Public License version 2 or (at your
      8  # option) any later version as published by the Free Software Foundation.
      9  #
     10  # For complete information please visit: http://www.zenoss.com/oss/
     11  #
     12  ###########################################################################
     13
     14  __doc__="""ZenDaemon
     15
     16  Base class for making deamon programs
     17  """
     18
     19
     20  import sys
     21  import os
     22  import pwd
     23  import socket
     24  import logging
     25  from logging import handlers
     26  from twisted.python import log as twisted_log
     27
     28  from CmdBase import CmdBase
     29  from Utils import zenPath, HtmlFormatter, binPath
     30
     31  # Daemon creation code below based on Recipe by Chad J. Schroeder
     32  # File mode creation mask of the daemon.
     33  UMASK = 0022
     34  # Default working directory for the daemon.
     35  WORKDIR = "/"
     36
     37  # only close stdin/out/err
     38  MAXFD = 3
     39
     40  # The standard I/O file descriptors are redirected to /dev/null by default.
     41  if (hasattr(os, "devnull")):
     42     REDIRECT_TO = os.devnull
     43  else:
     44     REDIRECT_TO = "/dev/null"
     45
     46
     47 -class ZenDaemon(CmdBase):
     48      """
     49      Base class for creating daemons
     50      """
     51
     52      pidfile = None
     53
     54 -    def __init__(self, noopts=0, keeproot=False):
     55          """
     56          Initializer that takes care of basic daemon options.
     57          Creates a PID file.
     58          """
     59          super(ZenDaemon, self).__init__(noopts)
     60          self.pidfile = None
     61          self.keeproot=keeproot
     62          self.reporter = None
     63          self.fqdn = socket.getfqdn()
     64          from twisted.internet import reactor
     65          reactor.addSystemEventTrigger('before', 'shutdown', self.sigTerm)
     66          if not noopts:
     67              if self.options.daemon:
     68                  self.changeUser()
     69                  self.becomeDaemon()
     70              if self.options.daemon or self.options.watchdogPath:
     71                  try:
     72                     self.writePidFile()
     73                  except OSError:
     74                     msg= "ERROR: unable to open PID file %s" % \
     75                                      (self.pidfile or '(unknown)')
     76                     raise SystemExit(msg)
     77
     78          if self.options.watchdog and not self.options.watchdogPath:
     79              self.becomeWatchdog()
     80
     81 -    def convertSocketOption(self, optString):
     82          """
     83          Given a socket option string (eg 'so_rcvbufforce=1') convert
     84          to a C-friendly command-line option for passing to zensocket.
     85          """
     86          optString = optString.upper()
     87          if '=' not in optString: # Assume boolean
     88              flag = optString
     89              value = 1
     90          else:
     91              flag, value = optString.split('=', 1)
     92              try:
     93                  value = int(value)
     94              except ValueError:
     95                  self.log.warn("The value %s for flag %s cound not be converted",
     96                            value, flag)
     97                  return None
     98
     99          # Check to see if we can find the option
    100          if flag not in dir(socket):
    101              self.log.warn("The flag %s is not a valid socket option",
    102                            flag)
    103              return None
    104
    105          numericFlag = getattr(socket, flag)
    106          return '--socketOpt=%s:%s' % (numericFlag, value)
    107
    108 -    def openPrivilegedPort(self, *address):
    109          """
    110          Execute under zensocket, providing the args to zensocket
    111          """
    112          socketOptions = []
    113          for optString in set(self.options.socketOption):
    114              arg = self.convertSocketOption(optString)
    115              if arg:
    116                  socketOptions.append(arg)
    117
    118          zensocket = binPath('zensocket')
    119          cmd = [zensocket, zensocket] + list(address) + socketOptions +  ['--',
    120                sys.executable] + sys.argv + \
    121                ['--useFileDescriptor=$privilegedSocket']
    122          self.log.debug(cmd)
    123          os.execlp(*cmd)
    124
    125
    126 -    def writePidFile(self):
    127          """
    128          Write the PID file to disk
    129          """
    130          myname = sys.argv[0].split(os.sep)[-1]
    131          if myname.endswith('.py'): myname = myname[:-3]
    132          monitor = getattr(self.options, 'monitor', 'localhost')
    133          myname = "%s-%s.pid" % (myname, monitor)
    134          if self.options.watchdog and not self.options.watchdogPath:
    135             self.pidfile =  zenPath("var", 'watchdog-%s' % myname)
    136          else:
    137             self.pidfile =  zenPath("var", myname)
    138          fp = open(self.pidfile, 'w')
    139          fp.write(str(os.getpid()))
    140          fp.close()
    141
    142      @property
    143 -    def logname(self):
    144          return getattr(self, 'mname', self.__class__.__name__)
    145
    146 -    def setupLogging(self):
    147          """
    148          Create formating for log entries and set default log level
    149          """
    150
    151          # Setup python logging module
    152          rootLog = logging.getLogger()
    153          rootLog.setLevel(logging.WARN)
    154
    155          zenLog = logging.getLogger('zen')
    156          zenLog.setLevel(self.options.logseverity)
    157
    158          formatter = logging.Formatter('%(asctime)s %(levelname)s %(name)s: %(message)s')
    159
    160          if self.options.watchdogPath or self.options.daemon or self.options.duallog:
    161              logdir = self.checkLogpath() or zenPath("log")
    162
    163              handler = logging.handlers.RotatingFileHandler(
    164                   filename = os.path.join(logdir, '%s.log' % self.logname.lower()),
    165                   maxBytes = self.options.maxLogKiloBytes * 1024,
    166                   backupCount = self.options.maxBackupLogs
    167              )
    168              handler.setFormatter(formatter)
    169              rootLog.addHandler(handler)
    170          if not (self.options.watchdogPath or self.options.daemon):
    171              # We are logging to the console
    172              # Find the stream handler and make it match our desired log level
    173              if self.options.weblog:
    174                  formatter = HtmlFormatter()
    175
    176              if not rootLog.handlers:
    177                  # Add a stream handler to stream to the console
    178                  consoleHandler = logging.StreamHandler(sys.stderr)
    179                  rootLog.addHandler(consoleHandler)
    180
    181              for handler in (h for h in rootLog.handlers if isinstance(h, logging.StreamHandler)):
    182                  handler.setLevel(self.options.logseverity)
    183                  handler.setFormatter(formatter)
    184
    185          self.log = logging.getLogger('zen.%s' % self.logname)
    186
    187          # Allow the user to dynamically lower and raise the logging
    188          # level without restarts.
    189          import signal
    190          try:
    191              signal.signal(signal.SIGUSR1, self.sighandler_USR1)
    192          except ValueError:
    193              # If we get called multiple times, this will generate an exception:
    194              # ValueError: signal only works in main thread
    195              # Ignore it as we've already set up the signal handler.
    196              pass
    197
    198 -    def sighandler_USR1(self, signum, frame):
    199          """
    200          Switch to debug level if signaled by the user, and to
    201          default when signaled again.
    202          """
    203          def getTwistedLogger():
    204              loggerName = "zen.%s.twisted" %  self.logname
    205              return twisted_log.PythonLoggingObserver(loggerName=loggerName)
    206
    207          log = logging.getLogger('zen')
    208          currentLevel = log.getEffectiveLevel()
    209          if currentLevel == logging.DEBUG:
    210              if self.options.logseverity == logging.DEBUG:
    211                  return
    212              log.setLevel(self.options.logseverity)
    213              log.info("Restoring logging level back to %s (%d)",
    214                       logging.getLevelName(self.options.logseverity) or "unknown",
    215                       self.options.logseverity)
    216              try:
    217                  getTwistedLogger().stop()
    218              except ValueError: # Twisted logging is somewhat broken
    219                  log.info("Unable to remove Twisted logger -- "
    220                           "expect Twisted logging to continue.")
    221          else:
    222              log.setLevel(logging.DEBUG)
    223              log.info("Setting logging level to DEBUG")
    224              getTwistedLogger().start()
    225          self._sigUSR1_called(signum, frame)
    226
    227 -    def _sigUSR1_called(self, signum, frame):
    228          pass
    229
    230 -    def changeUser(self):
    231          """
    232          Switch identity to the appropriate Unix user
    233          """
    234          if not self.keeproot:
    235              try:
    236                  cname = pwd.getpwuid(os.getuid())[0]
    237                  pwrec = pwd.getpwnam(self.options.uid)
    238                  os.setuid(pwrec.pw_uid)
    239                  os.environ['HOME'] = pwrec.pw_dir
    240              except (KeyError, OSError):
    241                  print >>sys.stderr, "WARN: user:%s not found running as:%s"%(
    242                                      self.options.uid,cname)
    243
    244
    245 -    def becomeDaemon(self):
    246          """Code below comes from the excellent recipe by Chad J. Schroeder.
    247          """
    248          # Workaround for http://bugs.python.org/issue9405 on Mac OS X
    249          from platform import system
    250          if system() == 'Darwin':
    251              from urllib import getproxies
    252              getproxies()
    253          try:
    254              pid = os.fork()
    255          except OSError, e:
    256              raise Exception( "%s [%d]" % (e.strerror, e.errno) )
    257
    258          if (pid == 0):  # The first child.
    259              os.setsid()
    260              try:
    261                  pid = os.fork() # Fork a second child.
    262              except OSError, e:
    263                  raise Exception( "%s [%d]" % (e.strerror, e.errno) )
    264
    265              if (pid == 0):      # The second child.
    266                  os.chdir(WORKDIR)
    267                  os.umask(UMASK)
    268              else:
    269                  os._exit(0)     # Exit parent (the first child) of the second child.
    270          else:
    271              os._exit(0) # Exit parent of the first child.
    272
    273          # Iterate through and close all stdin/out/err
    274          for fd in range(0, MAXFD):
    275              try:
    276                  os.close(fd)
    277              except OSError:     # ERROR, fd wasn't open to begin with (ignored)
    278                  pass
    279
    280          os.open(REDIRECT_TO, os.O_RDWR) # standard input (0)
    281          # Duplicate standard input to standard output and standard error.
    282          os.dup2(0, 1)                   # standard output (1)
    283          os.dup2(0, 2)                   # standard error (2)
    284
    285
    286 -    def sigTerm(self, signum=None, frame=None):
    287          """
    288          Signal handler for the SIGTERM signal.
    289          """
    290          # This probably won't be called when running as daemon.
    291          # See ticket #1757
    292          from Products.ZenUtils.Utils import unused
    293          unused(signum, frame)
    294          stop = getattr(self, "stop", None)
    295          if callable(stop): stop()
    296          if self.pidfile and os.path.exists(self.pidfile):
    297              self.log.info("Deleting PID file %s ...", self.pidfile)
    298              os.remove(self.pidfile)
    299          self.log.info('Daemon %s shutting down' % self.__class__.__name__)
    300          raise SystemExit
    301
    302
    303 -    def watchdogCycleTime(self):
    304          """
    305          Return our cycle time (in minutes)
    306
    307          @return: cycle time
    308          @rtype: integer
    309          """
    310          # time between child reports: default to 2x the default cycle time
    311          default = 1200
    312          cycleTime = getattr(self.options, 'cycleTime', default)
    313          if not cycleTime:
    314              cycleTime = default
    315          return cycleTime
    316
    317 -    def watchdogStartTimeout(self):
    318          """
    319          Return our watchdog start timeout (in minutes)
    320
    321          @return: start timeout
    322          @rtype: integer
    323          """
    324          # Default start timeout should be cycle time plus a couple of minutes
    325          default = self.watchdogCycleTime() + 120
    326          startTimeout = getattr(self.options, 'starttimeout', default)
    327          if not startTimeout:
    328              startTimeout = default
    329          return startTimeout
    330
    331
    332 -    def watchdogMaxRestartTime(self):
    333          """
    334          Return our watchdog max restart time (in minutes)
    335
    336          @return: maximum restart time
    337          @rtype: integer
    338          """
    339          default = 600
    340          maxTime = getattr(self.options, 'maxRestartTime', default)
    341          if not maxTime:
    342              maxTime = default
    343          return default
    344
    345
    346 -    def becomeWatchdog(self):
    347          """
    348          Watch the specified daemon and restart it if necessary.
    349          """
    350          from Products.ZenUtils.Watchdog import Watcher, log
    351          log.setLevel(self.options.logseverity)
    352          cmd = sys.argv[:]
    353          if '--watchdog' in cmd:
    354              cmd.remove('--watchdog')
    355          if '--daemon' in cmd:
    356              cmd.remove('--daemon')
    357
    358          socketPath = '%s/.%s-watchdog-%d' % (
    359              zenPath('var'), self.__class__.__name__, os.getpid())
    360
    361          cycleTime = self.watchdogCycleTime()
    362          startTimeout = self.watchdogStartTimeout()
    363          maxTime = self.watchdogMaxRestartTime()
    364          self.log.debug("Watchdog cycleTime=%d startTimeout=%d maxTime=%d",
    365                         cycleTime, startTimeout, maxTime)
    366
    367          watchdog = Watcher(socketPath,
    368                             cmd,
    369                             startTimeout,
    370                             cycleTime,
    371                             maxTime)
    372          watchdog.run()
    373          sys.exit(0)
    374
    375 -    def niceDoggie(self, timeout):
    376          # defer creation of the reporter until we know we're not going
    377          # through zensocket or other startup that results in closing
    378          # this socket
    379          if not self.reporter and self.options.watchdogPath:
    380              from Watchdog import Reporter
    381              self.reporter = Reporter(self.options.watchdogPath)
    382          if self.reporter:
    383             self.reporter.niceDoggie(timeout)
    384
    385 -    def buildOptions(self):
    386          """
    387          Standard set of command-line options.
    388          """
    389          CmdBase.buildOptions(self)
    390          self.parser.add_option('--uid',dest='uid',default="zenoss",
    391                  help='User to become when running default:zenoss')
    392          self.parser.add_option('-c', '--cycle',dest='cycle',
    393                  action="store_true", default=False,
    394                  help="Cycle continuously on cycleInterval from Zope")
    395          self.parser.add_option('-D', '--daemon', default=False,
    396                  dest='daemon',action="store_true",
    397                  help="Launch into the background")
    398          self.parser.add_option('--duallog', default=False,
    399                  dest='duallog',action="store_true",
    400                  help="Log to console and log file")
    401          self.parser.add_option('--weblog', default=False,
    402                  dest='weblog',action="store_true",
    403                  help="output log info in HTML table format")
    404          self.parser.add_option('--watchdog', default=False,
    405                                 dest='watchdog', action="store_true",
    406                                 help="Run under a supervisor which will restart it")
    407          self.parser.add_option('--watchdogPath', default=None,
    408                                 dest='watchdogPath',
    409                                 help="The path to the watchdog reporting socket")
    410          self.parser.add_option('--starttimeout',
    411                                 dest='starttimeout',
    412                                 type="int",
    413                                 help="Wait seconds for initial heartbeat")
    414          self.parser.add_option('--socketOption',
    415                                 dest='socketOption', default=[], action='append',
    416                                 help="Set listener socket options." \
    417                                  "For option details: man 7 socket")
    418