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 as published by
      8  # 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 openPrivilegedPort(self, *address):
     82          """Execute under zensocket, providing the args to zensocket"""
     83          zensocket = binPath('zensocket')
     84          cmd = [zensocket, zensocket] + list(address) + ['--'] + \
     85                [sys.executable] + sys.argv + \
     86                ['--useFileDescriptor=$privilegedSocket']
     87          os.execlp(*cmd)
     88
     89
     90 -    def writePidFile(self):
     91          """
     92          Write the PID file to disk
     93          """
     94          myname = sys.argv[0].split(os.sep)[-1]
     95          if myname.endswith('.py'): myname = myname[:-3]
     96          monitor = getattr(self.options, 'monitor', 'localhost')
     97          myname = "%s-%s.pid" % (myname, monitor)
     98          if self.options.watchdog and not self.options.watchdogPath:
     99             self.pidfile =  zenPath("var", 'watchdog-%s' % myname)
    100          else:
    101             self.pidfile =  zenPath("var", myname)
    102          fp = open(self.pidfile, 'w')
    103          fp.write(str(os.getpid()))
    104          fp.close()
    105
    106 -    def setupLogging(self):
    107          """
    108          Create formating for log entries and set default log level
    109          """
    110
    111          # Setup python logging module
    112          rlog = logging.getLogger()
    113          rlog.setLevel(logging.WARN)
    114          if hasattr(self, 'mname'): mname = self.mname
    115          else: mname = self.__class__.__name__
    116          self.log = logging.getLogger("zen."+ mname)
    117          zlog = logging.getLogger("zen")
    118          zlog.setLevel(self.options.logseverity)
    119          if self.options.watchdogPath or \
    120             self.options.daemon:
    121              logdir = self.checkLogpath()
    122              if not logdir:
    123                  logdir = zenPath("log")
    124              logfile = os.path.join(logdir, mname.lower()+".log")
    125              maxBytes = self.options.maxLogKiloBytes * 1024
    126              backupCount = self.options.maxBackupLogs
    127              h = logging.handlers.RotatingFileHandler(filename=logfile, maxBytes=maxBytes, backupCount=backupCount)
    128              h.setFormatter(logging.Formatter(
    129                  "%(asctime)s %(levelname)s %(name)s: %(message)s"))
    130              rlog.addHandler(h)
    131          else:
    132              logging.basicConfig()
    133              if self.options.weblog:
    134                  [ h.setFormatter(HtmlFormatter()) for h in rlog.handlers ]
    135              else:
    136                  f = logging.Formatter("%(asctime)s %(levelname)s %(name)s: %(message)s")
    137                  [ h.setFormatter(f) for h in rlog.handlers ]
    138
    139          # Allow the user to dynamically lower and raise the logging
    140          # level without restarts.
    141          import signal
    142          try:
    143              signal.signal(signal.SIGUSR1, self.sighandler_USR1)
    144          except ValueError:
    145              # If we get called multiple times, this will generate an exception:
    146              # ValueError: signal only works in main thread
    147              # Ignore it as we've already set up the signal handler.
    148              pass
    149
    150 -    def sighandler_USR1(self, signum, frame):
    151          """
    152          Switch to debug level if signaled by the user, and to
    153          default when signaled again.
    154          """
    155          log = logging.getLogger('zen')
    156          currentLevel = log.getEffectiveLevel()
    157          if currentLevel == logging.DEBUG:
    158              log.setLevel(self.options.logseverity)
    159              log.info("Restoring logging level back to %s (%d)",
    160                       logging.getLevelName(self.options.logseverity) or "unknown",
    161                       self.options.logseverity)
    162              # Stop twisted logging
    163              if hasattr(self, 'mname'): mname = self.mname
    164              else: mname = self.__class__.__name__
    165              observer = twisted_log.PythonLoggingObserver(loggerName="zen."+ mname)
    166              observer.stop()
    167          else:
    168              log.setLevel(logging.DEBUG)
    169              log.info("Setting logging level to DEBUG")
    170
    171              # Setup twisted logging
    172              if hasattr(self, 'mname'): mname = self.mname
    173              else: mname = self.__class__.__name__
    174              observer = twisted_log.PythonLoggingObserver(loggerName="zen."+ mname)
    175              observer.start()
    176
    177
    178 -    def changeUser(self):
    179          """
    180          Switch identity to the appropriate Unix user
    181          """
    182          if not self.keeproot:
    183              try:
    184                  cname = pwd.getpwuid(os.getuid())[0]
    185                  pwrec = pwd.getpwnam(self.options.uid)
    186                  os.setuid(pwrec.pw_uid)
    187                  os.environ['HOME'] = pwrec.pw_dir
    188              except (KeyError, OSError):
    189                  print >>sys.stderr, "WARN: user:%s not found running as:%s"%(
    190                                      self.options.uid,cname)
    191
    192
    193 -    def becomeDaemon(self):
    194          """Code below comes from the excellent recipe by Chad J. Schroeder.
    195          """
    196          try:
    197              pid = os.fork()
    198          except OSError, e:
    199              raise Exception( "%s [%d]" % (e.strerror, e.errno) )
    200
    201          if (pid == 0):  # The first child.
    202              os.setsid()
    203              try:
    204                  pid = os.fork() # Fork a second child.
    205              except OSError, e:
    206                  raise Exception( "%s [%d]" % (e.strerror, e.errno) )
    207
    208              if (pid == 0):      # The second child.
    209                  os.chdir(WORKDIR)
    210                  os.umask(UMASK)
    211              else:
    212                  os._exit(0)     # Exit parent (the first child) of the second child.
    213          else:
    214              os._exit(0) # Exit parent of the first child.
    215
    216          # Iterate through and close all stdin/out/err
    217          for fd in range(0, MAXFD):
    218              try:
    219                  os.close(fd)
    220              except OSError:     # ERROR, fd wasn't open to begin with (ignored)
    221                  pass
    222
    223          os.open(REDIRECT_TO, os.O_RDWR) # standard input (0)
    224          # Duplicate standard input to standard output and standard error.
    225          os.dup2(0, 1)                   # standard output (1)
    226          os.dup2(0, 2)                   # standard error (2)
    227
    228
    229 -    def sigTerm(self, signum=None, frame=None):
    230          """
    231          Signal handler for the SIGTERM signal.
    232          """
    233          # This probably won't be called when running as daemon.
    234          # See ticket #1757
    235          from Products.ZenUtils.Utils import unused
    236          unused(signum, frame)
    237          stop = getattr(self, "stop", None)
    238          if callable(stop): stop()
    239          if self.pidfile and os.path.exists(self.pidfile):
    240              self.log.info("Deleting PID file %s ...", self.pidfile)
    241              os.remove(self.pidfile)
    242          self.log.info('Daemon %s shutting down' % self.__class__.__name__)
    243          raise SystemExit
    244
    245
    246 -    def watchdogCycleTime(self):
    247          """
    248          Return our cycle time (in minutes)
    249
    250          @return: cycle time
    251          @rtype: integer
    252          """
    253          # time between child reports: default to 2x the default cycle time
    254          default = 1200
    255          cycleTime = getattr(self.options, 'cycleTime', default)
    256          if not cycleTime:
    257              cycleTime = default
    258          return cycleTime
    259
    260 -    def watchdogStartTimeout(self):
    261          """
    262          Return our watchdog start timeout (in minutes)
    263
    264          @return: start timeout
    265          @rtype: integer
    266          """
    267          # Default start timeout should be cycle time plus a couple of minutes
    268          default = self.watchdogCycleTime() + 120
    269          startTimeout = getattr(self.options, 'starttimeout', default)
    270          if not startTimeout:
    271              startTimeout = default
    272          return startTimeout
    273
    274
    275 -    def watchdogMaxRestartTime(self):
    276          """
    277          Return our watchdog max restart time (in minutes)
    278
    279          @return: maximum restart time
    280          @rtype: integer
    281          """
    282          default = 600
    283          maxTime = getattr(self.options, 'maxRestartTime', default)
    284          if not maxTime:
    285              maxTime = default
    286          return default
    287
    288
    289 -    def becomeWatchdog(self):
    290          """
    291          Watch the specified daemon and restart it if necessary.
    292          """
    293          from Products.ZenUtils.Watchdog import Watcher, log
    294          log.setLevel(self.options.logseverity)
    295          cmd = sys.argv[:]
    296          if '--watchdog' in cmd:
    297              cmd.remove('--watchdog')
    298          if '--daemon' in cmd:
    299              cmd.remove('--daemon')
    300
    301          socketPath = '%s/.%s-watchdog-%d' % (
    302              zenPath('var'), self.__class__.__name__, os.getpid())
    303
    304          cycleTime = self.watchdogCycleTime()
    305          startTimeout = self.watchdogStartTimeout()
    306          maxTime = self.watchdogMaxRestartTime()
    307          self.log.debug("Watchdog cycleTime=%d startTimeout=%d maxTime=%d",
    308                         cycleTime, startTimeout, maxTime)
    309
    310          watchdog = Watcher(socketPath,
    311                             cmd,
    312                             startTimeout,
    313                             cycleTime,
    314                             maxTime)
    315          watchdog.run()
    316          sys.exit(0)
    317
    318 -    def niceDoggie(self, timeout):
    319          # defer creation of the reporter until we know we're not going
    320          # through zensocket or other startup that results in closing
    321          # this socket
    322          if not self.reporter and self.options.watchdogPath:
    323              from Watchdog import Reporter
    324              self.reporter = Reporter(self.options.watchdogPath)
    325          if self.reporter:
    326             self.reporter.niceDoggie(timeout)
    327
    328 -    def buildOptions(self):
    329          """
    330          Standard set of command-line options.
    331          """
    332          CmdBase.buildOptions(self)
    333          self.parser.add_option('--uid',dest='uid',default="zenoss",
    334                  help='User to become when running default:zenoss')
    335          self.parser.add_option('-c', '--cycle',dest='cycle',
    336                  action="store_true", default=False,
    337                  help="Cycle continuously on cycleInterval from Zope")
    338          self.parser.add_option('-D', '--daemon', default=False,
    339                  dest='daemon',action="store_true",
    340                  help="Launch into the background")
    341          self.parser.add_option('--weblog', default=False,
    342                  dest='weblog',action="store_true",
    343                  help="output log info in HTML table format")
    344          self.parser.add_option('--watchdog', default=False,
    345                                 dest='watchdog', action="store_true",
    346                                 help="Run under a supervisor which will restart it")
    347          self.parser.add_option('--watchdogPath', default=None,
    348                                 dest='watchdogPath',
    349                                 help="The path to the watchdog reporting socket")
    350          self.parser.add_option('--starttimeout',
    351                                 dest='starttimeout',
    352                                 type="int",
    353                                 help="Wait seconds for initial heartbeat")
    354