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