import repository from arizona
[raven.git] / owl / client / owl
1 #! /usr/bin/env python
2
3 # owl -- Owl daemon
4
5 version = "0.31"
6
7 import sys,os,signal,time,subprocess,urllib2,socket,urllib,ConfigParser
8 from optparse import OptionParser
9 from logging import *
10 from logging.handlers import *
11 import time
12 import owl
13 import traceback
14
15 def error_exc(msg, exc):
16     # fancy printing for a message and exception
17     # Msg: Exception
18     #  | tb line 1
19     #  | tb line 2
20     #  ...
21     msg = [msg + ":" + str(exc)] + traceback.format_exc().splitlines()
22     error("\n | ".join(msg))
23
24 # Config file parser with error handling
25
26 class OwlParserError(owl.OwlError):
27     pass
28
29 class MyParser(ConfigParser.SafeConfigParser):
30     def GetOpt(self, section, option, default):
31         try:
32             value = self.get(section, option)
33         except ConfigParser.NoOptionError:
34             value = default
35         except ConfigParser.NoSectionError:
36             raise OwlParserError("Section '%s' missing from config file '%s'" % (section,self.path))
37         return value
38
39     def Get(self, section, option):
40         try:
41             value = self.get(section, option)
42         except ConfigParser.NoOptionError:
43             raise OwlParserError("No option '%s' in section '%s' in  file '%s'" % (option,section,self.path))
44         except ConfigParser.NoSectionError:
45             raise OwlParserError("No section '%s' in config file '%s'" % (section,self.path))
46         return value
47
48     def Read(self, path, bail):
49         self.path = path
50         f = []
51         try:
52             f = self.read(path)
53         except ConfigParser.ParsingError :
54             print "Unable to parse '%s'" % (self.path)
55             if (bail):
56                 raise OwlParserError("Unable to parse '%s'" % (self.path))
57         return f
58
59 # Adapted from arizonageneral
60
61 def make_daemon(program):
62    debug("Forking daemon")
63    pid = os.fork()
64
65    # if fork was successful, exit the parent process so it returns
66    try:
67       if pid > 0:
68          os._exit(0)
69    except OSError:
70         print "fork failed"
71         exit(1)
72
73    # Print my pid into /var/run/PROGRAM.pid
74    pid = str(os.getpid())
75    filename = "/var/run/%s.pid" % program
76    try:
77       out_file = open(filename, "w")
78       out_file.write(pid)
79       out_file.close()
80    except IOError:
81        error("error writing %s" % filename)
82        exit(1)
83
84    # we don't want to close the fd's from the logging module
85    logging_fds=[]
86    for handler in getLogger().handlers:
87        if hasattr(handler, "stream"):
88            logging_fds.append(handler.stream.fileno())
89
90    # close any open files
91    sys.stdin.close()
92    sys.stdout.close()
93    sys.stderr.close()
94    for fd in xrange(0,1023):
95        if not (fd in logging_fds):
96            try:
97                os.close(fd)
98            except OSError:
99                pass
100
101    # redirect stdin/out/err to /dev/null    
102    sys.stdin = open('/dev/null')       # fd 0
103    sys.stdout = open('/dev/null', 'w') # fd 1
104    sys.stderr = open('/dev/null', 'w') # fd 2  
105    
106    # disassociate from parent 
107    os.chdir("/")
108    os.setsid()
109    os.umask(0)
110
111    return pid
112
113 #
114 # Configuration defaults
115 #
116
117 config = {}
118 config["script_dir"] = "/etc/owl/scripts.d"
119 config["url"] = "http://owl.cs.arizona.edu/owl/"
120 config["conf_dir"] = "/etc/owl/conf.d"
121 config["databases"] = []
122 config["log_file"] = "/var/log/owl"
123 config["verbosity"] = 1
124 config["interval"] = 60
125 config["planetlab"] = False
126 config["max_register_backoff"] = 600
127 config["max_update_backoff"] = 600
128 config["timeout"] = 10
129 config["key"] = "basic.id"
130 config["owner"] = None
131
132 # Parse the command line arguments 
133
134 oparser = OptionParser(version="%prog " + str(version))
135 oparser.set_defaults(vebosity = config["verbosity"])
136 oparser.add_option("-q", "--quiet", action="store_const", const=0, dest="verbosity",
137     help="print no output")
138 oparser.add_option("-v", "--verbose", action="store_const", const=1, dest="verbosity",
139     help="print verbose output")
140 oparser.add_option("-d", "--debug", action="store_const", const=2, dest="verbosity",
141     help="print debugging output")
142 oparser.add_option("-f", "--config", default="/etc/owl/owl.conf",
143     help="config file location [default: %default]")
144 oparser.add_option("-s", "--scriptdir", default=config["script_dir"],
145     help="script directory [default: %default]")
146 oparser.add_option("-c", "--confdir", default=config["conf_dir"],
147     help="config file directory [default: %default]")
148 oparser.add_option("-u", "--url", default=config["url"],
149     help="URL of Owl server [default: %default]")
150 oparser.add_option("-n", "--dry-run", action="store_true", dest="printonly", default=False,
151     help="don't update Owl server [default: %default]")
152 oparser.add_option("-b", "--database", action="append", dest="databases", metavar="NAME",
153     help="database to update")
154 oparser.add_option("-S", "--sync", action="store_true", default=False,
155     help="run synchronously (don't detach) [default: %default]")
156 oparser.add_option("-l", "--logfile", default=config["log_file"],
157     help="log file [default: %default]")
158 oparser.add_option("-i", "--interval", type="int", default=config["interval"],
159     help="interval to run scripts in daemon mode [default: %default]")
160 oparser.add_option("-p", "--planetlab", action="store_true", default=config["planetlab"],
161     help= "PlanetLab mode. Sets database to slice name. [default: %default]")
162
163 (options,args) = oparser.parse_args()
164
165 # Now parse the owl config file
166
167 parser = MyParser()
168 parser.Read([options.config], 1)
169
170 for item in parser.items("Owl"):
171     (key, value) = item
172     # Hack that databases is a list for now
173     if key == "databases":
174         value = value.split(',')
175     config[key] = value
176
177 # Apply the command-line arguments to the configuration so that they override the
178 # contents of the config file
179
180 def override(parser, opt, key):
181     global config
182     option = parser.get_option(opt)
183     value = getattr(parser.values, option.dest)
184     if value != None and value != option.default:
185         config[key] = value
186
187 override(oparser, "-s", "script_dir")
188 override(oparser, "-u", "url")
189 override(oparser, "-c", "conf_dir")
190 override(oparser, "-b", "databases")
191 override(oparser, "-v", "verbosity")
192 override(oparser, "-l", "log_file")
193 override(oparser, "-i", "interval")
194 override(oparser, "-p", "planetlab")
195
196 if len(config["databases"]) == 0:
197     if config["planetlab"] == True:
198         try:
199             fd = open("/etc/slicename", "r")
200             config["databases"] = [fd.readline()]
201             fd.close()
202         except:
203             error("unable to read slicename from '/etc/slicename'")
204             exit(1)
205     else:
206         print "You must specify at least one database"
207         exit(1)
208
209 scriptDir = config["script_dir"]
210 baseUrl = config["url"]
211 configDir = config["conf_dir"]
212 databases = config["databases"]
213 maxRegisterBackoff = config["max_register_backoff"]
214 maxUpdateBackoff = config["max_update_backoff"]
215 key = config["key"]
216
217 # Set up logging. Keep track of which fd was opened so we don't close it when forking
218 # the daemon. There is probably a better way to do this but I don't know it.
219
220 def setup_logging():
221     global level
222
223     level = INFO
224     if config["verbosity"] == 0:
225         level = ERROR
226     elif config["verbosity"] == 1:
227         level = INFO
228     elif config["verbosity"] == 2:
229         level = DEBUG
230
231     if config["log_file"] == "-":
232         basicConfig(stream=sys.stdout, level=level,
233             format='%(asctime)s %(levelname)s %(message)s',
234             datefmt='%Y-%m-%d %H:%M:%S')
235     else:
236         # if using a log file, setup a rotating handler, so as not to fill up
237         # the disk.
238         l = getLogger()
239         l.setLevel(level)\r
240         fileHandler = RotatingFileHandler(config["log_file"], maxBytes=1*1024*1024, backupCount=1)\r
241         fileHandler.setLevel(level)\r
242         fileHandler.setFormatter(Formatter('%(asctime)s %(levelname)s %(message)s', '%Y-%m-%d %H:%M:%S'))\r
243         l.addHandler(fileHandler)
244
245 def do_owl():
246     global config, options, version, baseUrl, databases
247
248     info("Owl %s starting" % (version))
249     debug("URL = %s" % (baseUrl))
250
251     if options.sync == False:
252         make_daemon(os.path.basename(sys.argv[0]))
253
254
255     info("Interval is %d seconds" % options.interval)
256     interval = options.interval
257
258     owlObj = owl.Owl(baseUrl, config["timeout"], config["max_register_backoff"],
259              config["max_update_backoff"], config["owner"])
260
261     # Create the databases
262
263     owlObj.create(databases, config["key"])
264
265     configFiles = {}
266
267     while 1:
268         #
269         # Parse module configuration files in the conf directory. Only parse those that
270         # have been added or changed.
271         #
272
273         start = time.time()
274
275         info("Processing config files in %s" % configDir)
276
277         configs = []
278         tmp = os.listdir(configDir)
279         for f in tmp:
280             if f.endswith(".conf"):
281                 configs.append(f)
282         configs.sort()
283
284         for c in configs :
285             path = os.path.join(configDir, c)
286             debug("Considering config file %s" % path)
287             doit = False
288             if c in configFiles:
289                 if configFiles[c] != os.stat(path).st_mtime :
290                     doit = True
291             else:
292                 doit = True
293             if doit == False:
294                 debug("Skipping config file " + path)
295                 continue
296             debug("Processing config file %s" % path)
297             configFiles[c] = os.stat(path).st_mtime
298             parser = MyParser()
299             f = parser.Read(path, 0)
300             if f == []:
301                 continue
302             module = parser.Get("Module", "name")
303             version = parser.Get("Module", "version")
304             heading = parser.GetOpt("Module", "heading", None)
305             fields = [x.strip() for x in parser.Get("Module", "fields").split(',')]
306             sections = parser.sections()
307             for s in sections:
308                 if s == "Module":
309                     continue
310                 if s not in fields:
311                     error("Section %s does not appear in list of fields" % (s))
312             fieldList = []
313             for field in fields:
314                 f = owl.Field(module=module,name=field)
315                 f.type = parser.GetOpt(field, "type", f.type)
316                 f.heading = parser.GetOpt(field, "heading", f.heading)
317                 f.headingAlign = parser.GetOpt(field, "headingAlign",
318                                                f.headingAlign)
319                 f.align = parser.GetOpt(field, "align", f.align)
320                 f.description = parser.GetOpt(field, "description", f.description)
321                 f.stats = parser.GetOpt(field, "stats", f.stats)
322                 if f.stats:
323                     f.stats = [x.strip() for x in f.stats.split(",")]
324                 fieldList.append(f)
325             owlObj.register(databases, module, version, fieldList, heading)
326
327         info("Running scripts")
328         inputs = MyParser()
329         scripts = os.listdir(scriptDir)
330         scripts.sort()
331
332         for s in scripts:
333             path = os.path.join(scriptDir, s)
334             if os.access(path, os.X_OK) == False:
335                 warning("Skipped executing \"" + path + "\": executable bit is not set")
336                 continue
337             debug("Running %s" % path)
338             try:
339                 p = subprocess.Popen(path, stdout=subprocess.PIPE).stdout
340             except Exception, e:
341                 error_exc("Error executing \"" + path + "\"", e)
342                 continue
343             try:
344                 inputs.readfp(p)
345             except ConfigParser.ParsingError, e:
346                 error_exc("Error parsing output from " + path + "\"", e)
347             p.close()
348
349         # Update the Owl server with the output from the scripts
350
351         values = {}
352         for module in inputs.sections():
353             for item in inputs.items(module):
354                 (field, value) = item
355                 fullname = "%s.%s" % (module, field)
356                 values[fullname] = value
357
358         if options.printonly:
359             shutdown()
360             exit(0)
361
362         try:
363             owlObj.update(databases, values)
364         except owl.OwlServerError,e:
365             error_exc("OwlServerError while updating", e)
366
367         interval = options.interval
368         now = time.time()
369         debug("Now = %f, start = %f, interval = %d" % (now,start,interval))
370         tmp = float(interval) - (now - start)
371         if tmp > 0:
372             debug("Sleeping %f seconds" % (tmp))
373             while (tmp > 0):
374                 if os.path.isfile("/tmp/owl_run_now"):
375                     try:
376                         os.remove("/tmp/owl_run_now")
377                         # only break the while loop if we were able to successfuly
378                         # delete the signal file
379                         break
380                     except OSError:
381                         pass
382                 time.sleep(1)
383                 tmp = tmp - 1
384
385 if __name__=="__main__":
386    setup_logging()
387
388    try:
389        do_owl()
390    except Exception, e:
391        # catch exceptions and send them to the log
392
393        try:
394            error_exc("Exception in main", e)
395        except:
396            # Uh oh, logging was broken. Just send a message to the console.
397            print "Exception while trying to log an exception!"
398
399        # re-raise it so the stack dump, etc goes to the console
400        raise
401
402