]> git.ipfire.org Git - ipfire.org.git/blob - build/build.py
edaf98cf8227206d5c50a4eaa1ff56088df23a92
[ipfire.org.git] / build / build.py
1 #!/usr/bin/python
2
3 import os
4 import time
5 import cgi
6
7 from stat import ST_MTIME
8 from rhpl.simpleconfig import SimpleConfigFile
9
10 DB_DIR = "db"
11
12 def format_time(seconds, use_hours=1):
13 if seconds is None or seconds < 0:
14 if use_hours: return '--:--:--'
15 else: return '--:--'
16 else:
17 seconds = int(seconds)
18 minutes = seconds / 60
19 seconds = seconds % 60
20 if use_hours:
21 hours = minutes / 60
22 minutes = minutes % 60
23 return '%02i:%02i:%02i' % (hours, minutes, seconds)
24 else:
25 return '%02i:%02i' % (minutes, seconds)
26
27 def format_number(number, SI=0, space=' '):
28 """Turn numbers into human-readable metric-like numbers"""
29 symbols = ['', # (none)
30 'k', # kilo
31 'M', # mega
32 'G', # giga
33 'T', # tera
34 'P', # peta
35 'E', # exa
36 'Z', # zetta
37 'Y'] # yotta
38
39 if SI: step = 1000.0
40 else: step = 1024.0
41
42 thresh = 999
43 depth = 0
44 max_depth = len(symbols) - 1
45
46 # we want numbers between 0 and thresh, but don't exceed the length
47 # of our list. In that event, the formatting will be screwed up,
48 # but it'll still show the right number.
49 while number > thresh and depth < max_depth:
50 depth = depth + 1
51 number = number / step
52
53 if type(number) == type(1) or type(number) == type(1L):
54 # it's an int or a long, which means it didn't get divided,
55 # which means it's already short enough
56 format = '%i%s%s'
57 elif number < 9.95:
58 # must use 9.95 for proper sizing. For example, 9.99 will be
59 # rounded to 10.0 with the .1f format string (which is too long)
60 format = '%.1f%s%s'
61 else:
62 format = '%.0f%s%s'
63
64 return(format % (float(number or 0), space, symbols[depth]))
65
66 stage2desc = {
67 "unknown" : "Dunno what the host is doing at the moment...",
68 "compiling" : "The host is really hard working at the moment...",
69 "error" : "Oops! The host had an error...",
70 "idle" : "The host is idle at the moment...",
71 }
72
73 sys2desc = {
74 "CPU_NAME" : "CPU model",
75 "CPU_MIPS" : "Bogomips",
76 "CPU_MHZ" : "CPU MHz",
77 "MEM_SIZE" : "Memory size",
78 }
79
80 class Site:
81 def __init__(self):
82 print "Content-type: text/html"
83 print
84
85 def __call__(self):
86 print '''<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
87 "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
88 <html xmlns="http://www.w3.org/1999/xhtml" lang="en" xml:lang="en">
89 <head>
90 <title>IPFire - Builder</title>
91 <style type="text/css">
92 body { font-family: Verdana; font-size: 9pt; background-color:#f0f0f0; }
93 a:link { color: black; text-decoration: none; }
94 a:visited { color: black; text-decoration: none; }
95 table { font-family: Verdana; font-size: 8pt; border: 1px solid; }
96 td.header { background-color: #a0a0a0; text-align: left; color: white;
97 font-size: 11pt; }
98 td.date { text-align: center; }
99 font.installed { color: green; }
100 font.deleted { color: red; }
101 font.update { color: blue; }
102 font.error { color: red; }
103 td.log { background: pink; }
104 td.update { background: darkgrey; }
105 /* td.distcc { background: darkgrey; } */
106 td.online { background: green; color: white; text-align: center; }
107 td.offline { background: red; color: white; text-align: center; }
108 </style>
109 <meta http-equiv="refresh" content="90; URL=%s" />
110 </head>
111 <body>
112 <h1>IPFire Builder</h1>
113 <p>This site is for monitoring the build machines...</p>
114 <p>Made: %s</p>''' % (os.environ['SCRIPT_NAME'], time.ctime())
115
116 self.content()
117
118 print "\t\t</body>\n</html>"
119
120 def content(self):
121 for builder in builders:
122 builder()
123
124
125 class Builder:
126 def __init__(self, uuid=None):
127 self.uuid = uuid
128 self.db = os.path.join(DB_DIR, self.uuid)
129
130 if not os.path.isdir(self.db):
131 os.mkdir(self.db)
132
133 self.fn = {
134 'state' : os.path.join(self.db, "state"),
135 'distcc' : os.path.join(self.db, "distcc"),
136 'hostname' : os.path.join(self.db, "hostname"),
137 'build_durations' : os.path.join(self.db, "build_durations"),
138 'log' : os.path.join(self.db, "log"),
139 'profile' : os.path.join(self.db, "profile"),
140 }
141
142 self.sys = SimpleConfigFile()
143 self.sys.read(self.fn['profile'])
144
145 self.state = self._state()
146
147 def _state(self, state="unknown"):
148 """State says what the host is doing at the moment.
149 This can be:
150 - unknown - Didn't talk with host for more than two hours.
151 - compiling - Host is working
152 - error - Host had an error
153 - idle - Host is idle
154 - hidden - Host was idle or unknown for more than 12 hours"""
155
156 if not state == "unknown":
157 f = open(self.fn['state'], "w")
158 f.write("%s\n" % state)
159 f.close()
160
161 if not os.access(self.fn['state'], os.R_OK):
162 return state
163
164 six_h_ago = int(time.time()) - (3600 * 6)
165 twelve_h_ago = int(time.time()) - (3600 * 12)
166 mtime = os.stat(self.fn['state'])[ST_MTIME]
167
168 if mtime > six_h_ago:
169 try:
170 f = open(self.fn['state'])
171 state = f.readline().rstrip("\n")
172 f.close()
173 except:
174 pass
175 elif mtime > twelve_h_ago:
176 state = "hidden"
177
178 return state
179
180 def _build_durations(self, duration=None):
181 """This returns a 4x-tupel:
182 First value is best build duration.
183 Second value is average build duration.
184 Third value is worst build duration.
185 Fourth value is the whole build duration."""
186
187 ## set duration
188 if duration:
189 f = open(self.fn['build_durations'], "a")
190 f.write("%s\n" % int(duration))
191 f.close()
192 return
193
194 ## get duration
195 durations = []
196 all_build_duration = 0
197 try:
198 f = open(self.fn['build_durations'])
199 except IOError:
200 return (None, None, None, None)
201 else:
202 while True:
203 duration = f.readline().rstrip("\n")
204 if not duration:
205 break # EOF
206 durations.append(int(duration))
207 f.close()
208 durations.sort()
209 for duration in durations:
210 if duration < 3600: continue
211 all_build_duration += duration
212
213 avg = all_build_duration / len(durations)
214
215 return (durations[0], avg, durations[-1], all_build_duration)
216
217 def _log(self, log=[]):
218
219 if log:
220 f = open(self.fn['log'], "w")
221 f.write("%s\n" % log)
222 f.close()
223 return
224
225 try:
226 f = open(self.fn['log'])
227 log = f.readlines()
228 f.close()
229 except:
230 pass
231
232 return log
233
234 def _last_update(self):
235 if not os.access(self.fn['state'], os.R_OK):
236 return
237
238 return os.stat(self.fn['state'])[ST_MTIME]
239
240 def _profile_get(self, what=None):
241 data = self.sys.get(what)
242 if data and what.endswith("_SIZE"):
243 data = format_number(int(data))
244 return data or None
245
246 def _profile_set(self, what):
247 self.sys.set(what)
248 self.sys.write(self.fn['profile'])
249
250 def __repr__(self):
251 return "<Builder %s>" % self.uuid
252
253 def __call__(self):
254 if self.state == "hidden":
255 return
256
257 self.hostname = self._profile_get("HOSTNAME")
258 self.distcc = self._profile_get("DISTCC")
259 self.build_durations = self._build_durations()
260 self.last_update = self._last_update()
261
262 print "<table width='66%'>"
263
264 ## give hostname or uuid
265 print "\t<tr>"
266 print "\t\t<td class='header' colspan='3' width='100%'>",
267 if self.hostname:
268 print self.hostname,
269 else:
270 print self.uuid,
271 print "</td>"
272 print "\t</tr>"
273
274 ## give state
275 print "\t<tr>"
276 print "\t\t<td class='state' colspan='2' width='80%'><b>",
277 print stage2desc[self.state],
278 print "</b></td>"
279 print "\t\t<td class='state' rowspan='7' width='20%'>",
280 print "<img alt='%s' width='128px' height='128px' src='/images/%s.png' />" % (self.state, self.state,),
281 print "</td>"
282 print "\t</tr>"
283
284 ## give sys info
285 for key in [ "CPU_NAME", "CPU_MHZ", "CPU_MIPS", "MEM_SIZE", ]:
286 print "\t<tr>"
287 print "\t\t<td class='sys' width='60%'><b>",
288 print sys2desc[key]
289 print "</b></td>"
290 print "\t\t<td class='durations' width='40%'>",
291 print self._profile_get(key) or "N/A"
292 print "</td>"
293 print "\t</tr>"
294
295 ## give durations
296 (min, avg, max, all) = self.build_durations
297 print "\t<tr>"
298 print "\t\t<td class='durations' width='60%'>",
299 print "<b>Build durations</b>",
300 print "</td>"
301 print "\t\t<td class='durations' width='40%'>",
302 if avg:
303 print "<b>Average:</b> %s h<br />" % format_time(avg),
304 if min:
305 print "<b>Minimum:</b> %s h<br />" % format_time(min),
306 if max:
307 print "<b>Maximum:</b> %s h<br />" % format_time(max),
308 if all:
309 print "<b>As a whole:</b> %s h" % format_time(all),
310
311 if not avg and not min and not max and not all:
312 print "N/A",
313
314 print "</td>"
315 print "\t</tr>"
316
317 ## give distcc
318 print "\t<tr>"
319 print "\t\t<td class='distcc' width='60%'>",
320 print "<b>Distcc capable</b>",
321 print "</td>"
322 print "\t\t<td class='distcc' width='40%'>",
323 if self.distcc == None or self.distcc == "0":
324 print "No",
325 else:
326 print "Yes (port: %s)" \
327 % (self.distcc),
328 print "</td>"
329 print "\t</tr>"
330
331 ## give log
332 if self.state == "error":
333 print "\t<tr>"
334 print "\t\t<td class='log' colspan='3' width='100%'>",
335 for line in self._log():
336 print "%s<br/>" % (line,)
337 print "</td>"
338 print "\t</tr>"
339
340 ## give lastupdate
341 if self.last_update:
342 print "\t<tr>"
343 print "\t\t<td class='update' colspan='3' width='100%'>",
344 print "Last update:",
345 print time.strftime("%Y-%b-%d %I:%M %p", time.localtime(self.last_update)),
346 print " - ",
347 print format_time(int(time.time() - self.last_update)),
348 print "ago </td>"
349 print "\t</tr>"
350
351 print "</table>"
352
353 print "<br />"
354
355 form = cgi.FieldStorage()
356 action = form.getfirst('action')
357
358 if action in [ "compiling", "error", "idle", "set" ]:
359 builder = Builder(form.getfirst('uuid'))
360 if not action == "set":
361 builder._state(action)
362 if action == "set":
363 key = form.getfirst('key')
364 val = form.getfirst('val')
365 if key == "duration":
366 builder._build_durations(val)
367 else:
368 builder._profile_set((key, val))
369 elif action == "error":
370 log = form.getfirst('log')
371 builder._log(log)
372 else:
373 builders = []
374 for uuid in os.listdir(DB_DIR):
375 if not os.path.isdir(os.path.join(DB_DIR, uuid)): continue
376 builders.append(Builder(uuid))
377
378 site = Site()
379 site()