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