import repository from arizona
[raven.git] / apps / tempest / tempestd.py
1 #! /usr/bin/env python
2
3 # John H. Hartman
4 # Parts copied from storkupdated
5 #
6 # tempestd -- tempest daemon
7 # Runs tempest periodically
8 #
9 #           [option, long option,                     variable,                      action,       data,     default,                       metavar,                       description]
10 """arizonaconfig
11     options=[
12              ["",    "--tempestdconfigfile",          "tempestdconfigfile",          "store",      "string",    "/usr/local/stork/etc/stork.conf",      "FILENAME",     "configuration file"],
13              ["",    "--tempestdsync",                "tempestdsync",                "store_true", None,     False,                         None,                          "run synchronously (don't detach)"],
14              ["",    "--tempestdverbose",             "tempestdverbose",             "store_true", None,     False,                         None,                          "tempestd verbose mode"],
15              ["",    "--timer",                       "pactimer",                    "store",      "int",    300,                           "pactimer",                    "Set time between tempest calls when not using the poller"],
16              ["",    "--staletimer",                  "pacstaletimer",               "store",      "int",    120,                           "pacstaletimer",               "Timer after which poller is considered stale"],
17              ["",    "--pollmetadatafile",            "pollfile",                    "store",      "string", "/usr/local/stork/var/packageinfo/stork-repository.cs.arizona.edu/packageinfo/metafile",        "pollfile",                         "poll the filesystem for a new metadata file to indicate a tempest update."],
18              ["",    "--tempestd-no-act",             "tempestdnoact",               "store_true",      None,           False,                                  None,           "Prevent tempestd from running tempest"],
19              ["",    "--tempestdsubscribe",           "tempestdsubscribe",           "store",      "string", "stork-repository.cs.arizona.edu:4321", None, "Hostname:port to subscribe to tempest pubsub feed"],
20             ]
21     includes=[]
22 """
23
24 import logging
25 import logging.handlers
26 import sys,os,signal,time
27 import arizonaconfig, arizonareport
28 import arizonageneral
29 import ravenlib.stats
30 import threading
31 import sha
32 import subprocess
33 import tempestversion
34 import tempestpubsub
35
36 from ravenlib.pubsub.receiver import PubSubReceiver
37
38 glo_tempestd_logger = None
39
40 def init_logging(verbose, enable_stdout):
41     global glo_tempestd_logger
42
43     if verbose:
44         level = logging.DEBUG
45     else:
46         level = logging.INFO
47
48     glo_tempestd_logger = logging.Logger("TempestdLogger", level)
49     glo_tempestd_logger.setLevel(level)
50
51     file_handler = logging.handlers.RotatingFileHandler("/var/log/tempestd.log", maxBytes=1*1024*1024, backupCount=1)
52     file_handler.setLevel(level)
53     file_handler.setFormatter(logging.Formatter("%(asctime)s - %(message)s"))
54     glo_tempestd_logger.addHandler(file_handler)
55
56     if enable_stdout:
57         stdout_handler = logging.StreamHandler(sys.stdout)
58         stdout_handler.setLevel(level)
59         stdout_handler.setFormatter(logging.Formatter("[tempestd] %(message)s"))
60         glo_tempestd_logger.addHandler(stdout_handler)
61         glo_stdout_handler = stdout_handler
62
63 def Debug(x):
64     if glo_tempestd_logger:
65        glo_tempestd_logger.debug(x)
66     else:
67        print x
68
69 def Info(x):
70     if glo_tempestd_logger:
71         glo_tempestd_logger.info(x)
72     else:
73         print x
74
75 def Warning(x):
76     if glo_tempestd_logger:
77         glo_tempestd_logger.warning(x)
78     else:
79         print x
80
81 class FixedPoller(threading.Thread):
82    def __init__(self, interval):
83       threading.Thread.__init__(self)
84       self.interval = interval
85       self.start()
86
87    def run(self):
88       while True:
89           ravenlib.stats.update("tempestd_fixedpoller_update")
90           tempest_signal_event("update")
91           time.sleep(self.interval)
92
93
94 class FilePoller(threading.Thread):
95    """
96    <Purpose>
97       Initialize the thread to begin polling the FS for a new metadata file
98    """
99    def __init__(self, metadata_filename, interval, stale_timeout):
100       threading.Thread.__init__(self)
101       self.metadata_filename = metadata_filename
102       self.mtime = 0;            # mtime
103       self.file_hash = ""        # file hash
104       self.interval = interval;
105       self.stale_timeout = stale_timeout
106       self.stale_time = 0
107       Debug("[tempestd.filepoller]: Starting filepoller; will look at " + metadata_filename)
108       self.start()
109
110    """
111    <Purpose>
112       Get the SHA1 hash of the metadata file
113    """
114    def get_hash(self):
115       sha1_hasher = sha.new()
116
117       metadata_file = open( self.metadata_filename, "r" )
118       for file_line in metadata_file.readlines():
119          sha1_hasher.update( file_line )
120
121       metadata_file.close()
122       # calculate the hash and return
123       return sha1_hasher.hexdigest()
124
125
126    """
127    <Purpose>
128       Get the modify time of the metadata file
129    """
130    def get_mtime(self):
131       try:
132           return os.stat(self.metadata_filename).st_mtime
133       except OSError:
134           return 0
135
136
137    """
138    <Purpose>
139       Poll the FS for the file matching self.metadata_filename
140       If the file exists AND has a different timestamp than
141       the currently-recognized file, then invoke tempest on it.
142    """
143    def run(self):
144       while True:
145          time.sleep(self.interval)
146          self.stale_time += self.interval
147
148          # check to see if we've been declared stale
149          if self.stale_time > self.stale_timeout:
150              Debug("[tempestd.filepoller] declaring staleness")
151              ravenlib.stats.update("tempestd_filepoller_stale")
152              tempest_signal_event("stale")
153              self.stale_time = 0
154              continue
155
156          Debug("[tempestd.filepoller] polling for " + self.metadata_filename)
157
158          # does the file exist?  Bail if not...
159          if not os.path.exists( self.metadata_filename ):
160              Debug("[tempestd.filepoller] file " + self.metadata_filename + " does not exist!")
161              continue
162
163          # check the modify time
164          mtime = self.get_mtime()
165          if (mtime == self.mtime):
166              Debug("[tempestd.filepoller] file " + self.metadata_filename + " mtime is unchanged")
167              continue
168
169          self.mtime = mtime
170          self.stale_time = 0
171
172          # check the hash
173          sha1_hash = self.get_hash()
174          if (sha1_hash == self.file_hash):
175              Debug("[tempestd.filepoller] file " + self.metadata_filename + " hash is unchanged")
176              ravenlib.stats.update("tempestd_filepoller_samehash")
177              continue
178
179          self.file_hash = sha1_hash
180
181          Debug("[tempestd.filepoller] file changed; invoking tempest")
182
183          ravenlib.stats.update("tempestd_filepoller_update")
184
185          # signal an update
186          tempest_signal_event("update")
187
188
189 def check_initscript_running():
190     """
191     <Purpose>
192        Return True if the stork initscript is running, false otherwise.
193     """
194     if not os.path.exists("/var/run/stork_initscript.pid"):
195         return False
196
197     try:
198         file = open("/var/run/stork_initscript.pid")
199     except:
200         Warning("failed to open the stork_initscript pid file")
201         return False
202
203     try:
204         initscript_pid = file.readline().strip()
205     except:
206         file.close()
207         Warning("failed to get the pid from the stork_initscript pid file")
208         return False
209
210     file.close()
211
212     # "ps -p <pid>" should return 0 if the process exists, or nonzero if it
213     # does not exist
214
215     try:
216         result = os.system("ps -p " + initscript_pid + " > /dev/null")
217     except:
218         Warning("exception while running ps to check stork_initscript pid")
219         return False
220
221     if result == 0:
222         return True
223     else:
224         return False
225
226 def wait_if_initscript_running():
227     """
228     <Purpose>
229        Wait for the stork initscript to finish running.
230     """
231     sent = False
232
233     while check_initscript_running():
234         if not sent:
235             Warning("stork initscript is running. delaying in 30-second increments")
236             sent = True
237         time.sleep(30)
238
239     if sent:
240         Info("stork initscript is no longer running")
241
242 def handler_sighup(signum, frame):
243     """
244     <Purpose>
245        Intercepts the "hangup" signal, but doesn't do anything.
246        Simply causes the sleep to return.
247     """
248     pass
249
250 def tempest_signal_event(event_name):
251    global tempest_event, tempest_event_name
252
253    tempest_event_name = event_name
254    tempest_event.set()
255
256 def run_command(cmd):
257     Info("Executing: " + cmd)
258
259     if (arizonaconfig.get_option("tempestdnoact")):
260         Info("Skipping command because --tempestd-no-act was specified.")
261         return
262
263     # Open filedescriptors must be closed because otherwise the child will
264     # inhert tempest'd mutex file and cause all sorts of problems during an
265     # upgrade.
266
267     p = subprocess.Popen(cmd, shell=True, close_fds=True)
268     rc = os.waitpid(p.pid, 0)[1]
269
270     if rc == 0:
271         Info("command returned with status " + str(rc))
272     else:
273         Warning("Error executing '%s'" % cmd)
274
275
276 def tempest_update():
277     Info("doing update")
278     run_command("/usr/local/stork/bin/storkconfigsync.py")
279     run_command("/usr/local/stork/bin/storkreposync.py")
280     run_command("/usr/local/stork/bin/tempest.py")
281
282 def tempest_stale():
283     Info("stale - polling for a metafile")
284     run_command("/usr/local/stork/bin/storkmetapoll.py")
285
286 def get_config_modify_time():
287     fn = arizonaconfig.get_option("tempestdconfigfile")
288     if not os.path.exists(fn):
289         return -1
290     return os.stat(fn).st_mtime
291
292 def restart():
293     os.execv(sys.argv[0], sys.argv)
294
295 def Main():
296     global tempest_event, tempest_event_name
297
298     args = arizonaconfig.init_options("tempestd.py",version=tempestversion.VERREL, configfile_optvar="tempestdconfigfile")
299
300     if os.geteuid() > 0:
301         arizonareport.send_error(0, "You must be root to run this program...")
302         sys.exit(1)
303
304     config_mtime = get_config_modify_time()
305
306     daemonized = False
307     timer = arizonaconfig.get_option("pactimer")
308     stale_timer = arizonaconfig.get_option("pacstaletimer")
309
310     # set the hangup signal handler
311     signal.signal(signal.SIGHUP, handler_sighup)
312
313     if not arizonaconfig.get_option("tempestdsync"):
314         # run as a daemon
315         Info("daemonizing")
316         arizonageneral.make_daemon("tempestd")
317         daemonized = True
318
319     # we have to do logging and mutex down here, because make_daemon() will
320     # close our open files otherwise. annoying.
321
322     init_logging(arizonaconfig.get_option("tempestdverbose"), not daemonized)
323
324     Info("starting up - version: " + str(tempestversion.VERREL) + " pid: " + str(os.getpid()))
325
326     tempestd_lock = arizonageneral.mutex_lock("tempestd", arizonaconfig.get_option("lockdir"))
327     if not tempestd_lock:
328         Warning("Another copy of tempestd is already running. Exiting.")
329         sys.exit(-1)
330
331     Info("obtained mutex")
332
333     # create an event used to signal pacman updates
334     tempest_event = threading.Event()
335     tempest_event_name = "update"
336
337     pubsub = arizonaconfig.get_option("tempestdsubscribe")
338     if pubsub:
339         Debug("configuring pubsub")
340         parts = pubsub.split(":")
341         if len(parts) == 2:
342             Debug("creating pubsubReceiver")
343             pubsubReceiver = tempestpubsub.TempestReceiver((parts[0], int(parts[1])), arizonaconfig.get_option("pollfile"))
344         else:
345             Warning("invalid format for --tempestdpubsub")
346
347     pollfilename = arizonaconfig.get_option("pollfile")
348     if pollfilename != "":
349         Debug("forking thread to monitor " + pollfilename)
350         # create a file poller that polls when a file changes
351         FilePoller(pollfilename, 5, stale_timer)
352         # add in a fixed poller once an hour to serve as a failsafe
353         FixedPoller(60*60)
354     else:
355         Debug("forking thread for fixed interval polling")
356         # create a fixed poller than polls on an interval
357         FixedPoller(arizonaconfig.get_option("pactimer"))
358
359     last_time = 0
360
361     # update no more often than once every 30 seconds
362     minimum_time_since_last = 30
363
364     while True:
365         if (config_mtime != get_config_modify_time()):
366             Info("config file has changed; restarting.")
367             arizonageneral.mutex_unlock(tempestd_lock)
368             restart()
369             Info("XXX XXX XXX Nobody should ever see this message XXX XXX XXX")
370
371         Info("waiting for event")
372         tempest_event.wait()
373         Info("got event: " + tempest_event_name)
374
375         # block if an initscript is running
376         wait_if_initscript_running()
377
378         # clear the update event, so that it can be signalled again
379         tempest_event.clear()
380
381         # do our thing
382         if tempest_event_name == "update":
383             # check and see if we just ran. If we did, then delay a bit
384             time_since_last = time.time() - last_time
385             if time_since_last < minimum_time_since_last:
386                delay = minimum_time_since_last - time_since_last
387                Info("events too frequent; delaying " + str(delay))
388                time.sleep(delay)
389
390             time_since_last = time.time() - last_time
391
392             Info("time since last update " + str(int(time_since_last)) + " seconds")
393
394             last_time = time.time()
395
396             tempest_update()
397         elif tempest_event_name == "stale":
398             tempest_stale()
399
400 if __name__ == "__main__":
401     Main()