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