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