1
2
3
4
5
6
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
31
32 UMASK = 0022
33
34 WORKDIR = "/"
35
36
37 MAXFD = 3
38
39
40 if (hasattr(os, "devnull")):
41 REDIRECT_TO = os.devnull
42 else:
43 REDIRECT_TO = "/dev/null"
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
85
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:
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
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
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
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
149 return getattr(self, 'mname', self.__class__.__name__)
150
152 """
153 Create formating for log entries and set default log level
154 """
155
156
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
177
178 if self.options.weblog:
179 formatter = HtmlFormatter()
180
181 if not rootLog.handlers:
182
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
193
194 import signal
195 try:
196 signal.signal(signal.SIGUSR1, self.sighandler_USR1)
197 except ValueError:
198
199
200
201 pass
202
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:
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
235
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
252 """Code below comes from the excellent recipe by Chad J. Schroeder.
253 """
254
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):
265 os.setsid()
266 try:
267 pid = os.fork()
268 except OSError, e:
269 raise Exception( "%s [%d]" % (e.strerror, e.errno) )
270
271 if (pid == 0):
272 os.chdir(WORKDIR)
273 os.umask(UMASK)
274 else:
275 os._exit(0)
276 else:
277 os._exit(0)
278
279
280 for fd in range(0, MAXFD):
281 try:
282 os.close(fd)
283 except OSError:
284 pass
285
286 os.open(REDIRECT_TO, os.O_RDWR)
287
288 os.dup2(0, 1)
289 os.dup2(0, 2)
290
291
292 - def sigTerm(self, signum=None, frame=None):
308
309
323
337
338
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
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
383
384
385
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
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