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

# Source Code for Module Products.ZenUtils.ZenDaemon

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