Merge lp:~linaro-infrastructure/tcwg-web/cbuild-lava into lp:tcwg-web
- cbuild-lava
- Merge into trunk
Status: | Merged |
---|---|
Approved by: | Matthew Gretton-Dann |
Approved revision: | 298 |
Merged at revision: | 235 |
Proposed branch: | lp:~linaro-infrastructure/tcwg-web/cbuild-lava |
Merge into: | lp:tcwg-web |
Diff against target: |
792 lines (+524/-34) 14 files modified
common.py (+309/-4) index.py (+1/-1) lava.py (+79/-0) lava_polling_cron.py (+44/-0) schedulejob.py (+10/-0) scheduler.py (+36/-22) tcwg-web.ini.local (+34/-0) templates/buildlog.html (+1/-1) templates/proposals/index.html (+1/-1) templates/recent/index.html (+1/-1) templates/scheduler.html (+5/-1) templates/testcompare.html (+1/-1) templates/testlog.html (+1/-1) templates/testtimeline/index.html (+1/-1) |
To merge this branch: | bzr merge lp:~linaro-infrastructure/tcwg-web/cbuild-lava |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Matthew Gretton-Dann | Approve | ||
Linaro Validation Team | Pending | ||
Linaro Infrastructure | Pending | ||
Paul Sokolovsky | Pending | ||
Review via email: mp+143295@code.launchpad.net |
Commit message
Description of the change
Contains:
- support for new lava-backed build queues.
- lava link integration support on scheduler page.
- trivial LAVA API library wrapper.
- .lock file support for LAVA.
- LAVA polling support with multiple functions.
- 276. By Milo Casagrande
-
Added tarball file extraction.
- 277. By Stevan Radaković
-
Remove deletion of .job and .lock files.
- 278. By Milo Casagrande
-
Fixed regex to retrieve dir name.
- 279. By Milo Casagrande
-
Write bundle id to lock file to mark job as parsed.
- 280. By Milo Casagrande
-
Fixed typo in dictionary get operation.
- 281. By Milo Casagrande
-
Added newline at the end of the string.
Michael Hudson-Doyle (mwhudson) wrote : | # |
Milo Casagrande (milo) wrote : | # |
Hi,
thanks for taking a look!
On Wed, Jan 16, 2013 at 4:55 AM, Michael Hudson-Doyle
<email address hidden> wrote:
>> +
>> +
>> +def get_log_
>> + """Returns the dir name found in the LAVA log file.
>> +
>> + :param lava_log: The name of the saved LAVA log file.
>> + :type str
>> + """
>> + # We need to match strings in the form of:
>> + # + lava-test-
>> + matcher = re.compile(
>> + path = None
>> + if lava_log:
>> + lines = None
>> + try:
>> + # XXX Probably not the most efficient way of parsing the file.
>> + # Might be better to read it backwards, since look like the strings
>> + # we need are closer to the bottom of the file.
>> + with open(lava_log, 'r') as log:
>> + lines = log.readlines()
>> + for line in lines:
>> + if matcher.
>> + match = line.strip(
>> + break
>> + if match:
>> + # We treat it like a path.
>> + path = os.path.
>> + finally:
>> + os.unlink(lava_log)
>> + return path
>
> Wow, this seems SUPER fragile.
It is, indeed. At the moment that was the first thing, and actually
the only place, where we could find that information.
> Can't you create an attachment that has
> the name of this directory in it?
I honestly have no idea, for I have yet to find out in cbuild code
where that directory name is created.
> Test cases can have attributes too,
> which would be more appropriate somehow, but there is no api for
> lava-test-shell tests to create them yet. Adding one will be easier
> than working out what this function is doing :-)
It would indeed, but I guess first we need to find out where that
directory name comes out.
Some of the other comments have been addressed, some are in the process.
Thanks again!
--
Milo Casagrande | Infrastructure Team
Linaro.org <www.linaro.org> │ Open source software for ARM SoCs
- 282. By Milo Casagrande
-
Fixed code as per review comments.
- 283. By Milo Casagrande
-
Fixed as per review comments.
Milo Casagrande (milo) wrote : | # |
Hello Michael,
all the comments have been addressed in the code.
The only missing part is the "weak" one, unfortunately: we need to
find another way of exporting that directory name, but we also need to
find where that directory name is defined in the first place.
Thanks again for your review!
--
Milo Casagrande | Infrastructure Team
Linaro.org <www.linaro.org> │ Open source software for ARM SoCs
- 284. By Paul Sokolovsky
-
lava_polling_cron: Add verbose operation mode.
- 285. By Paul Sokolovsky
-
Update parse_lock_file() to support arbitrary fields, simplify naming scheme.
This was triggered by the fact that FINISHED field wasn't parsed, so
lava_polling_cron.py processed same jobs again and again. - 286. By Stevan Radaković
-
Fix infinite .lock file update with 'FINISHED'.
- 287. By Paul Sokolovsky
-
Fix case where literal field value instead of constant was used previously.
- 288. By Paul Sokolovsky
-
Add debug logging for lava_polling_
cron.py diagnostics. - 289. By Paul Sokolovsky
-
Few minor fixes for get_result_
bundle_ logs(). - 290. By Paul Sokolovsky
-
Debug logging for save_lava_
log_file( ). - 291. By Paul Sokolovsky
-
Fix thinko from os.path.join() refactor.
- 292. By Paul Sokolovsky
-
We also need to pass job content down to builder, as it contains extra config.
- 293. By Paul Sokolovsky
-
Strip CRs from webform's string.
- 294. By Paul Sokolovsky
-
Typo fix in var name.
- 295. By Paul Sokolovsky
-
update_lock_file() no longer needs extra check.
- 296. By Paul Sokolovsky
-
Rename FINISHED -> LAVA_BUNDLE.
- 297. By Paul Sokolovsky
-
Calculate lava_test_shell timeout dynamically based on job.
It's not ok to have the same timeout as gcc (>10hr) for every job, it's
hard to even test stuff which hangs and locks up regularly. - 298. By Paul Sokolovsky
-
Use relative URLs, as this is supposed to be on same server in out setup.
This allows for faithful and comfortable tesing on sandboxes.
Matthew Gretton-Dann (matthew-gretton-dann) wrote : | # |
Approved.
Preview Diff
1 | === modified file 'common.py' |
2 | --- common.py 2012-11-15 01:48:14 +0000 |
3 | +++ common.py 2013-01-18 12:55:25 +0000 |
4 | @@ -17,15 +17,30 @@ |
5 | import time |
6 | import json |
7 | import urlparse |
8 | +import urllib2 |
9 | import ConfigParser |
10 | import base64 |
11 | +import shutil |
12 | +import hashlib |
13 | +import sys |
14 | +import tarfile |
15 | +import logging |
16 | |
17 | import web |
18 | import memcache |
19 | |
20 | +import lava |
21 | + |
22 | + |
23 | +log = logging.getLogger("common") |
24 | + |
25 | +class NoBundleFoundException(Exception): |
26 | + """Raised when no bundle result is found with matching job name.""" |
27 | + pass |
28 | + |
29 | |
30 | class Cache: |
31 | - """In memory cache of file derrived objects.""" |
32 | + """In memory cache of file derived objects.""" |
33 | def __init__(self): |
34 | self.items = {} |
35 | |
36 | @@ -52,7 +67,7 @@ |
37 | |
38 | def shorten(v): |
39 | if '. ' in v: |
40 | - v = v[:v.index('. ')+1] |
41 | + v = v[:v.index('. ') + 1] |
42 | |
43 | if '\n' in v: |
44 | v = v[:v.index('\n')] |
45 | @@ -292,6 +307,7 @@ |
46 | base = special |
47 | abspath = os.path.abspath(os.path.join(base, name)) |
48 | else: |
49 | + # TODO need to check this path. |
50 | base = get_config('general', 'results', '/var/www/ex.seabright.co.nz/') |
51 | abspath = os.path.abspath(os.path.join(base, context, name)) |
52 | |
53 | @@ -310,7 +326,7 @@ |
54 | |
55 | def authenticate(role='admin'): |
56 | if is_development(): |
57 | - return 'michaelh1' |
58 | + return 'devel-user' |
59 | |
60 | oid = web.webopenid.status() |
61 | |
62 | @@ -410,7 +426,8 @@ |
63 | |
64 | def get_config(group, name, fallback=''): |
65 | parser = ConfigParser.SafeConfigParser() |
66 | - parser.read('tcwg-web.ini') |
67 | + dirname = os.path.dirname(__file__) |
68 | + parser.read(dirname + '/tcwg-web.ini') |
69 | |
70 | if parser.has_option(group, name): |
71 | return parser.get(group, name) |
72 | @@ -428,6 +445,7 @@ |
73 | else: |
74 | return template.replace('.SERVER.', '.' + server + '.') |
75 | |
76 | + |
77 | def setup_email(): |
78 | """Copy the email configuration into web.py""" |
79 | for name in 'smtp_server smtp_username smtp_password smtp_starttls smtp_debuglevel'.split(): |
80 | @@ -438,3 +456,290 @@ |
81 | v = base64.b64decode(v) |
82 | |
83 | web.config[name] = v |
84 | + |
85 | + |
86 | +def schedule_job(queue, job, contents=None, use_template_file=False): |
87 | + """Schedule a job. All job scheduling should happen via this |
88 | + function, which encapsulates support for various schdulers in use.""" |
89 | + queue_dir = map_filename('queue/%s/' % queue, 'scheduler') + '/' |
90 | + |
91 | + job_file = os.path.join(queue_dir, job + '.job') |
92 | + if use_template_file and os.path.exists(queue_dir + 'template.txt'): |
93 | + shutil.copyfile(queue_dir + 'template.txt', job_file) |
94 | + else: |
95 | + if contents is None: |
96 | + # Preserve old contents (but make sure file exists) |
97 | + with open(job_file, 'a'): |
98 | + os.utime(job_file, None) |
99 | + else: |
100 | + with open(job_file, 'w') as f: |
101 | + f.write(contents) |
102 | + |
103 | + lava_template = queue_dir + 'lava-job-template.json' |
104 | + if os.path.exists(lava_template): |
105 | + request_env = None |
106 | + if hasattr(web.ctx, 'env'): |
107 | + request_env = web.ctx.env |
108 | + lava_job_id, lava_job_url = lava.submit_job(lava_template, queue, job, request_env) |
109 | + |
110 | + jobfile = '%s%s.job' % (queue_dir, job) |
111 | + lockfile = '%s.lock' % jobfile |
112 | + |
113 | + job_id = create_job_id(jobfile) |
114 | + |
115 | + write_lock_file(lockfile, 'lava', job_id, lava_job_id, lava_job_url) |
116 | + |
117 | + print "Queue %s: scheduled %s for LAVA, job id: %d, url: %s" % \ |
118 | + (queue, job, lava_job_id, lava_job_url) |
119 | + |
120 | + |
121 | +def create_job_id(jobfile): |
122 | + |
123 | + with open(jobfile) as f: |
124 | + details = [x.rstrip() for x in f.readlines()] |
125 | + |
126 | + stat = os.stat(jobfile) |
127 | + sha = hashlib.sha1() |
128 | + sha.update('%s %d %.3f %r' % (jobfile, stat.st_size, |
129 | + stat.st_mtime, details)) |
130 | + digest = int(sha.hexdigest(), 16) |
131 | + job_id = digest % 2 ** 31 |
132 | + |
133 | + return job_id |
134 | + |
135 | + |
136 | +def write_lock_file(lockfile, host, job_id, lava_job_id=None, lava_job_url=None): |
137 | + """Write relevant info into the .lock file.""" |
138 | + |
139 | + with open(lockfile, 'w') as f: |
140 | + f.write('%s\n' % host) |
141 | + f.write('%s\n' % datetime.datetime.utcnow().isoformat()) |
142 | + f.write('%s=%s\n' % ("JOB_ID", job_id)) |
143 | + |
144 | + if lava_job_id: |
145 | + f.write("%s=%d\n" % ("LAVA_JOB_ID", lava_job_id)) |
146 | + if lava_job_url: |
147 | + f.write("%s=%s\n" % ("LAVA_JOB_URL", lava_job_url)) |
148 | + |
149 | + |
150 | +def parse_lock_file(lockfile): |
151 | + """Parse info from the .lock file and return dict.""" |
152 | + |
153 | + result = {} |
154 | + if os.path.exists(lockfile): |
155 | + with open(lockfile, 'r') as f: |
156 | + lines = [x.rstrip() for x in f.readlines()] |
157 | + result["HOST"] = lines[0] |
158 | + result["TIMESTAMP"] = lines[1] |
159 | + for l in lines[2:]: |
160 | + k, v = l.split("=", 1) |
161 | + result[k] = v |
162 | + |
163 | + return result |
164 | + |
165 | + |
166 | +def fetch_lava_log(lava_job_id): |
167 | + """Downloads the lava log file for the specified job id. |
168 | + |
169 | + :param lava_job_id: The id of the LAVA job. |
170 | + :return Tha temporary file name. |
171 | + """ |
172 | + log_file_url = create_job_url(lava_job_id) |
173 | + log_f = urllib2.urlopen(log_file_url) |
174 | + import tempfile |
175 | + handle, name = tempfile.mkstemp() |
176 | + with open(name, 'w+b') as tmp_f: |
177 | + tmp_f.write(log_f.read()) |
178 | + return name |
179 | + |
180 | + |
181 | +def get_bundle_sha1(lava_job_id): |
182 | + """Gets the bundle code for the job id. |
183 | + |
184 | + :param lava_job_id: The id of the LAVA job. |
185 | + :return The bundle code associated with the job. |
186 | + """ |
187 | + bundle_sha1 = None |
188 | + # Get the JSON struct of the job. |
189 | + url = create_job_url(lava_job_id, "json") |
190 | + log.debug("Fetching job info from url: %s", url) |
191 | + if url: |
192 | + try: |
193 | + json_str = json.load(urllib2.urlopen(url)) |
194 | + results = json_str.get("results_link", None) |
195 | + if results: |
196 | + # The last is an empty string, since the URL ends with a /. |
197 | + bundle_sha1 = results.split('/')[-2] |
198 | + except ValueError, ex: |
199 | + print "Error retrieving bundle SHA1 for job %s." % str(lava_job_id) |
200 | + print "Reported error is: %s" + str(ex) |
201 | + return bundle_sha1 |
202 | + |
203 | + |
204 | +def create_job_url(lava_job_id, what='log_file/plain'): |
205 | + """Creates the URL to the log file in LAVA scheduler for specific job.""" |
206 | + url = get_config('lava', 'plain_url', None) |
207 | + if url: |
208 | + url = urlparse.urljoin(url, "scheduler/job/") |
209 | + # We need the trailing slash, or the join doesn't work. |
210 | + url = urlparse.urljoin(url, str(lava_job_id) + "/") |
211 | + url = urlparse.urljoin(url, what) |
212 | + return url |
213 | + |
214 | + |
215 | +def get_result_bundle_logs(job_name, bundle_sha1, lava_job_id): |
216 | + """Gets attachments from LAVA test result bundle. |
217 | + |
218 | + This functions iterates over test runs and downloads all attachments |
219 | + to the correct location where the job logs are supposed to be saved. |
220 | + """ |
221 | + bundle = lava.get_result_bundle(bundle_sha1) |
222 | + bundle = json.loads(bundle['content']) |
223 | + # Find the test ID that matches the job name. |
224 | + bundle_test_run = None |
225 | + for test_run in bundle['test_runs']: |
226 | + if test_run['test_id'] == job_name: |
227 | + bundle_test_run = test_run |
228 | + break |
229 | + |
230 | + if not bundle_test_run: |
231 | + raise NoBundleFoundException("No testcase with the name %s in bundle" % job_name) |
232 | + |
233 | + for test_result in bundle_test_run['test_results']: |
234 | + for attachment in test_result['attachments']: |
235 | + save_lava_log_file(job_name, attachment['pathname'], |
236 | + base64.b64decode(attachment['content']), |
237 | + attachment['mime_type'], |
238 | + lava_job_id) |
239 | + |
240 | + |
241 | +def get_log_dir_name(lava_log): |
242 | + """Returns the dir name found in the LAVA log file. |
243 | + |
244 | + :param lava_log: The name of the saved LAVA log file. |
245 | + :type str |
246 | + """ |
247 | + # We need to match strings in the form of: |
248 | + # + tar -cf [...] |
249 | + matcher = re.compile("\s*\+?\s*tar -cf") |
250 | + path = None |
251 | + if lava_log: |
252 | + lines = None |
253 | + try: |
254 | + # XXX Probably not the most efficient way of parsing the file. |
255 | + # Might be better to read it backwards, since look like the strings |
256 | + # we need are closer to the bottom of the file. |
257 | + with open(lava_log, 'r') as log: |
258 | + lines = log.readlines() |
259 | + for line in lines: |
260 | + if matcher.match(line): |
261 | + split_line = line.strip().split(' ') |
262 | + # Sending the tarball file, the last value is a dot (.) |
263 | + # We need to make sure that we are getting the right |
264 | + # one. |
265 | + match = split_line[-2] |
266 | + if not split_line[-1] == ".": |
267 | + match = split_line[-1] |
268 | + break |
269 | + if match: |
270 | + # We need to treat the name as a path in order to split it |
271 | + # up: check if it has a trailing slash, otherwise add it. |
272 | + dir_matcher = re.compile("/$") |
273 | + if not dir_matcher.match(match): |
274 | + match += "/" |
275 | + path = os.path.dirname(match).split('/')[-1] |
276 | + finally: |
277 | + os.unlink(lava_log) |
278 | + return path |
279 | + |
280 | + |
281 | +def extract_and_remove_tar_file(path): |
282 | + """Extracts the content of a tarball into the file directory, and removes |
283 | + it when done. |
284 | + |
285 | + :param path: The path of the tarball to extract.""" |
286 | + if os.getcwd() != os.path.dirname(path): |
287 | + os.chdir(os.path.dirname(path)) |
288 | + with tarfile.open(path) as tarball: |
289 | + for member in tarball.getmembers(): |
290 | + # Check that it is a file, since we might have directories in it. |
291 | + if member.isfile(): |
292 | + tarball.extract(member) |
293 | + try: |
294 | + # Remove the tarball once done with it. |
295 | + os.unlink(path) |
296 | + except: |
297 | + print "Impossible to remove tarball file '%s'." % path |
298 | + |
299 | + |
300 | +def save_lava_log_file(job_name, filename, content, mime_type, lava_job_id): |
301 | + # Files should be saved under: |
302 | + # /space/build/$job_name/logs/$log_dir/ |
303 | + # TODO Check if we can get the path from the web config. |
304 | + directory = "/space/build/%(job_name)s/logs/%(log_dir)s" |
305 | + log_dir = get_log_dir_name(fetch_lava_log(lava_job_id)) |
306 | + directory %= dict(job_name=job_name, log_dir=log_dir) |
307 | + log.debug("Writing CBuild log file %s to %s", filename, directory) |
308 | + |
309 | + if not os.path.exists(directory): |
310 | + try: |
311 | + os.makedirs(directory) |
312 | + except OSError: |
313 | + # We do not have permissions to write. |
314 | + print "Cannot create directory %s." % directory |
315 | + sys.exit(1) |
316 | + |
317 | + file_path = os.path.join(directory, filename) |
318 | + # Double check that we can write. |
319 | + try: |
320 | + fp = open(file_path, "w+b") |
321 | + except IOError: |
322 | + print "Cannot write file '%s' into %s." % (filename, directory) |
323 | + sys.exit(1) |
324 | + else: |
325 | + with fp: |
326 | + fp.write(content) |
327 | + |
328 | + if tarfile.is_tarfile(file_path): |
329 | + extract_and_remove_tar_file(file_path) |
330 | + |
331 | + |
332 | +def find_lava_jobs(): |
333 | + """Go through the queue directory and recognize all LAVA jobs.""" |
334 | + # This might become obsolete once we switch to LAVA completely. |
335 | + |
336 | + lava_jobs = [] |
337 | + |
338 | + # TODO: this is relative path, so we must take into consideration where |
339 | + # is this function called from. |
340 | + for filename in find_files('hosts.txt', 'queue', 'scheduler'): |
341 | + hosts = [] |
342 | + dirname = os.path.dirname(filename) |
343 | + |
344 | + with open(filename) as f: |
345 | + for line in f: |
346 | + hosts.extend(line.split()) |
347 | + |
348 | + if hosts: |
349 | + for lock_file in find_files('.job.lock', dirname, 'scheduler'): |
350 | + lock_results = parse_lock_file(lock_file) |
351 | + if lock_results["HOST"] == 'lava' and not lock_results.get("LAVA_BUNDLE", None): |
352 | + job = {} |
353 | + job["LAVA_JOB_ID"] = lock_results["LAVA_JOB_ID"] |
354 | + job["FILE_PATH"] = os.path.abspath(lock_file) |
355 | + job["JOB_NAME"] = os.path.basename(lock_file).replace('.job.lock', '') |
356 | + lava_jobs.append(job) |
357 | + |
358 | + return lava_jobs |
359 | + |
360 | + |
361 | +def update_lock_file(path, bundle_id): |
362 | + """Updates the lock file for a job. |
363 | + Writes the bundle id in order to mark the job as already parsed. |
364 | + |
365 | + :param path: The path to the lock file. |
366 | + :bundle_id: The bundle id to write. |
367 | + """ |
368 | + with open(path, 'a') as lock: |
369 | + content = "%s=%s\n" % ("LAVA_BUNDLE", str(bundle_id)) |
370 | + lock.write(content) |
371 | |
372 | === modified file 'index.py' |
373 | --- index.py 2013-01-03 09:56:26 +0000 |
374 | +++ index.py 2013-01-18 12:55:25 +0000 |
375 | @@ -58,7 +58,7 @@ |
376 | web.ctx.user = None |
377 | |
378 | if common.is_development(): |
379 | - web.ctx.user = 'michaelh1' |
380 | + web.ctx.user = 'devel-user' |
381 | |
382 | if web.ctx.oid: |
383 | match = re.match(r'https://launchpad.net/~(.+)', web.ctx.oid) |
384 | |
385 | === added file 'lava.py' |
386 | --- lava.py 1970-01-01 00:00:00 +0000 |
387 | +++ lava.py 2013-01-18 12:55:25 +0000 |
388 | @@ -0,0 +1,79 @@ |
389 | +import json |
390 | +import xmlrpclib |
391 | + |
392 | +import common |
393 | + |
394 | + |
395 | +def get_api(url): |
396 | + user = common.get_config('lava:' + url, 'user') |
397 | + if not user: |
398 | + user = common.get_config('lava', 'user') |
399 | + token = common.get_config('lava:' + url, 'token') |
400 | + if not token: |
401 | + token = common.get_config('lava', 'token') |
402 | + proto, rest = url.split('://') |
403 | + url = "%s://%s:%s@%s" % (proto, user, token, rest) |
404 | + return xmlrpclib.ServerProxy(url) |
405 | + |
406 | + |
407 | +def calc_job_timeout(job_name): |
408 | + if job_name.startswith("gcc"): |
409 | + return 15 * 60 * 60 |
410 | + if job_name.startswith("cortex-str"): |
411 | + return 0.5 * 60 * 60 |
412 | + # Our aim is to avoid lockups, so keep it low for now |
413 | + return 2 * 60 * 60 |
414 | + |
415 | + |
416 | +def submit_job(jobdef_template, queue, job_name, request_env): |
417 | + f = open(jobdef_template) |
418 | + job_def = f.read() |
419 | + f.close() |
420 | + # Allow jobdef to "inherit" lava server from tcwg-web's config |
421 | + # by using %(lava_server)s substituation in jobdef template |
422 | + api_url = common.get_config('lava', 'url') |
423 | + substs = {'queue': queue, 'job': job_name, 'lava_server': api_url, |
424 | + 'timeout': calc_job_timeout(job_name)} |
425 | + # If this is executed from web context, get this server's |
426 | + # address from request. Otherwise, fallback to preconfigured |
427 | + # values (applies when running from command-line for cbuild-tools |
428 | + # for example). |
429 | + if request_env: |
430 | + substs['SERVER_NAME'] = request_env['SERVER_NAME'] |
431 | + substs['SERVER_PORT'] = request_env['SERVER_PORT'] |
432 | + else: |
433 | + substs['SERVER_NAME'] = common.get_config('web', 'server_name') |
434 | + substs['SERVER_PORT'] = common.get_config('web', 'server_port') |
435 | + |
436 | + job_def = job_def % substs |
437 | + |
438 | + |
439 | + submit_url = '' |
440 | + job_dict = json.loads(job_def) |
441 | + submit_dict = filter(lambda a: a['command'] == 'submit_results', |
442 | + job_dict['actions']) |
443 | + if submit_dict: |
444 | + submit_url = submit_dict[0]["parameters"]["server"] |
445 | + |
446 | + # If lava url in config was not specified, let's try to use the same |
447 | + # url as jobdef's submit_results action. |
448 | + if not api_url: |
449 | + api_url = submit_url |
450 | + |
451 | + lava_api = get_api(api_url) |
452 | + job_id = lava_api.scheduler.submit_job(job_def) |
453 | + job_url = '' |
454 | + if submit_url: |
455 | + if submit_url[-1] == '/': |
456 | + submit_url = submit_url[:-1] |
457 | + job_url = submit_url.rsplit('/', 1)[0] + '/scheduler/job/%d' % job_id |
458 | + return job_id, job_url |
459 | + |
460 | +def get_result_bundle(bundle_sha1): |
461 | + """Returns the json format from lava dashboard 'get' call.""" |
462 | + |
463 | + api_url = common.get_config('lava', 'url') |
464 | + lava_api = get_api(api_url) |
465 | + |
466 | + bundle_result = lava_api.dashboard.get(bundle_sha1) |
467 | + return bundle_result |
468 | |
469 | === added file 'lava_polling_cron.py' |
470 | --- lava_polling_cron.py 1970-01-01 00:00:00 +0000 |
471 | +++ lava_polling_cron.py 2013-01-18 12:55:25 +0000 |
472 | @@ -0,0 +1,44 @@ |
473 | +#!/usr/bin/env python |
474 | +# Copyright (C) 2012 Linaro |
475 | + |
476 | +import common |
477 | +import sys |
478 | +import optparse |
479 | +import logging |
480 | + |
481 | + |
482 | +def main(args, options): |
483 | + jobs = common.find_lava_jobs() |
484 | + if not jobs: |
485 | + logging.info("No active LAVA jobs found") |
486 | + return |
487 | + |
488 | + for job in jobs: |
489 | + logging.info("Processing: %r" % job) |
490 | + bundle_sha1 = common.get_bundle_sha1(job.get("LAVA_JOB_ID")) |
491 | + if bundle_sha1: |
492 | + logging.info("Job result bundle: %r", bundle_sha1) |
493 | + # Fetch cbuild logs and put them where cbuild webapp expects |
494 | + try: |
495 | + common.get_result_bundle_logs(job.get("JOB_NAME"), |
496 | + bundle_sha1, |
497 | + job.get("LAVA_JOB_ID")) |
498 | + common.update_lock_file(job.get("FILE_PATH"), bundle_sha1) |
499 | + except: |
500 | + print "Error saving log files for job %s." % job.get("LAVA_JOB_ID") |
501 | + else: |
502 | + # No bundle code means that the job is still running. |
503 | + logging.info("No result bundle available") |
504 | + |
505 | + |
506 | +if __name__ == '__main__': |
507 | + |
508 | + optparser = optparse.OptionParser("%prog <options>") |
509 | + optparser.add_option( |
510 | + "-v", "--verbose", action="store_true") |
511 | + options, args = optparser.parse_args(sys.argv[1:]) |
512 | + if options.verbose: |
513 | + logging.basicConfig(level=logging.DEBUG) |
514 | + else: |
515 | + logging.basicConfig(level=logging.ERROR) |
516 | + main(args, options) |
517 | |
518 | === added file 'schedulejob.py' |
519 | --- schedulejob.py 1970-01-01 00:00:00 +0000 |
520 | +++ schedulejob.py 2013-01-18 12:55:25 +0000 |
521 | @@ -0,0 +1,10 @@ |
522 | +# This is standalone script intended to be called by other scripts/cronjobs |
523 | +# (including from outside components) to schedule a build. |
524 | +# Usage: |
525 | +# python -m schedulejob <queue> <job> |
526 | +import sys |
527 | + |
528 | +import common |
529 | + |
530 | + |
531 | +common.schedule_job(sys.argv[1], sys.argv[2], use_template_file=True, contents='') |
532 | |
533 | === modified file 'scheduler.py' |
534 | --- scheduler.py 2012-12-09 20:18:41 +0000 |
535 | +++ scheduler.py 2013-01-18 12:55:25 +0000 |
536 | @@ -18,10 +18,12 @@ |
537 | import urllib |
538 | import hashlib |
539 | import collections |
540 | +from pipes import quote |
541 | |
542 | import web |
543 | |
544 | import common |
545 | +import lava |
546 | |
547 | hostre = r'([\w\-]+)' |
548 | |
549 | @@ -38,6 +40,8 @@ |
550 | '/job/' + hostre + '/(\w[\w.+\^\-~]+)\.job/release', 'job_release', |
551 | '/job/' + hostre + '/(\w[\w.+\^\-~]+)\.job/bump', 'job_bump', |
552 | '/job/' + hostre + '/(\w[\w.+\^\-~]+)\.job/drop', 'job_drop', |
553 | + # Serve YAML "test definition" for LAVA build |
554 | + '/lava/testdef/([^/]+)/([^/]+)', 'lava_testdef', |
555 | ) |
556 | |
557 | render = web.template.render('templates/', |
558 | @@ -53,7 +57,7 @@ |
559 | |
560 | db = web.database(dbn='sqlite', db=common.map_filename('scheduler.sqlite', 'run')) |
561 | |
562 | -Candidate = collections.namedtuple('Candidate', 'queue name jobfname lockfname jobtime host started locked locktime scaledage') |
563 | +Candidate = collections.namedtuple('Candidate', 'queue name jobfname lockfname jobtime host started locked locktime scaledage lava_url') |
564 | |
565 | def log(host, state, arg, msg=''): |
566 | now = time.time() |
567 | @@ -246,8 +250,11 @@ |
568 | if len(lines) >= 2: |
569 | started = lines[1].strip() |
570 | |
571 | + job_info = common.parse_lock_file(job + '.lock') |
572 | + lava_url = job_info.get("LAVA_JOB_URL") |
573 | + |
574 | if with_locked or locktime < mtime: |
575 | - candidates.append([queue, os.path.basename(job), job, lock, mtime, ahost, started, locktime >= mtime, locktime, 0]) |
576 | + candidates.append([queue, os.path.basename(job), job, lock, mtime, ahost, started, locktime >= mtime, locktime, 0, lava_url]) |
577 | |
578 | if len(candidates) > 1: |
579 | # Normalise the mtimes |
580 | @@ -255,7 +262,7 @@ |
581 | yongest = max(x[4] for x in candidates) |
582 | |
583 | for candidate in candidates: |
584 | - candidate[-1] = (yongest - candidate[4]) / (yongest - oldest + 1) |
585 | + candidate[-2] = (yongest - candidate[4]) / (yongest - oldest + 1) |
586 | |
587 | candidates = [Candidate(*x) for x in candidates] |
588 | # Now have a list of candidates |
589 | @@ -279,19 +286,13 @@ |
590 | with open(jobfile) as f: |
591 | details = [x.rstrip() for x in f.readlines()] |
592 | |
593 | - stat = os.stat(jobfile) |
594 | - sha = hashlib.sha1() |
595 | - sha.update('%s %d %.3f %r' % (jobfile, stat.st_size, stat.st_mtime, details)) |
596 | - digest = int(sha.hexdigest(), 16) |
597 | - job_id = digest % 2**31 |
598 | - |
599 | + job_id = common.create_job_id(jobfile) |
600 | details.append('JOB_ID=%s' % job_id) |
601 | |
602 | - with open(lock, 'w') as f: |
603 | - print >> f, host |
604 | - print >> f, '%s' % datetime.datetime.utcnow().isoformat() |
605 | - print >> f, 'JOB_ID=%s' % job_id |
606 | - return '%s\n\n%s' % (job, '\n'.join(details)) |
607 | + common.write_lock_file(lock, host, job_id) |
608 | + |
609 | + return '%s\n\n%s' % (job, '\n'.join(details)) |
610 | + |
611 | finally: |
612 | fcntl.lockf(lockf.fileno(), fcntl.LOCK_UN) |
613 | finally: |
614 | @@ -365,11 +366,8 @@ |
615 | def GET(self, queue, job): |
616 | common.authenticate() |
617 | |
618 | - filename = common.map_filename('queue/%s/%s.job' % (queue, job), 'scheduler') |
619 | - |
620 | try: |
621 | - with file(filename, 'a'): |
622 | - os.utime(filename, None) |
623 | + common.schedule_job(queue, job) |
624 | finally: |
625 | pass |
626 | |
627 | @@ -460,12 +458,28 @@ |
628 | extras = form.d.extras |
629 | |
630 | for queue in selected: |
631 | - filename = common.map_filename('queue/%s/%s.job' % (queue, job), 'scheduler') |
632 | - |
633 | - with open(filename, 'w') as f: |
634 | - f.write(extras) |
635 | + common.schedule_job(queue, job, contents=extras) |
636 | |
637 | msg = 'Spawned %s into %s' % (job, ', '.join(selected)) |
638 | return web.seeother('/helpers/scheduler?%s' % urllib.urlencode({'msg': msg}), absolute=True) |
639 | |
640 | + |
641 | +class lava_testdef: |
642 | + |
643 | + def GET(self, queue, job): |
644 | + template = common.map_filename('queue/%s/lava-test-shell-template.yaml' % queue, 'scheduler') |
645 | + queue_dir = common.map_filename('queue/%s/' % queue, 'scheduler') + '/' |
646 | + job_file = os.path.join(queue_dir, job + '.job') |
647 | + job_content = "" |
648 | + with open(job_file) as f: |
649 | + job_content = f.read() |
650 | + job_content = quote(job_content) |
651 | + # Yes, CR can be there %) |
652 | + job_content = job_content.replace("\r", "") |
653 | + job_content = job_content.replace("\n", "\\n") |
654 | + |
655 | + with open(template) as f: |
656 | + return f.read() % {'job': job, 'job_content': job_content} |
657 | + |
658 | + |
659 | app = web.application(urls, globals()) |
660 | |
661 | === added file 'tcwg-web.ini.local' |
662 | --- tcwg-web.ini.local 1970-01-01 00:00:00 +0000 |
663 | +++ tcwg-web.ini.local 2013-01-18 12:55:25 +0000 |
664 | @@ -0,0 +1,34 @@ |
665 | +# Sample tcwg-web.ini for local development |
666 | + |
667 | +[general] |
668 | +development=True |
669 | +# Path below matches production install, it's recommended to follow it |
670 | +results=/home/cbuild/var |
671 | + |
672 | +[web] |
673 | +# Name, etc. of the server running web frontend |
674 | +# this is required for LAVA integration |
675 | +server_name=localhost |
676 | +server_port=8080 |
677 | + |
678 | +[lava] |
679 | +user = user_here |
680 | +token = token_here |
681 | +# There're 2 choices: |
682 | +# a) if url is defined, LAVA jobs will be always submitted against this url. |
683 | +# Also, job definition may refer to this url for submitting results to |
684 | +# (by using "%(lava_server)s" substitution in "submit_results" action). |
685 | +# This allows to centrally control location of LAVA instance to work with |
686 | +# (and potentially, exotic scenarios like queuing job against one LAVA server |
687 | +# and let it post results to another server). |
688 | +# b) if url is not defined, a url from jobdef's "submit_results" action |
689 | +# is used to submit job to. This allows to work with different LAVA servers |
690 | +# at the same time (one server per queue). This is useful for development/ |
691 | +# testing for example (local LAVA install vs production). |
692 | +#url = http://validation.linaro.org/lava-server/RPC2/ |
693 | +#plain_url = https://validation.linaro.org/lava-server/ |
694 | + |
695 | +# username/token overrides for particular LAVA server |
696 | +[lava:https://validation.linaro.org/lava-server/RPC2/] |
697 | +user = user_here2 |
698 | +token = token_here2 |
699 | |
700 | === modified file 'templates/buildlog.html' |
701 | --- templates/buildlog.html 2012-11-30 02:31:03 +0000 |
702 | +++ templates/buildlog.html 2013-01-18 12:55:25 +0000 |
703 | @@ -4,7 +4,7 @@ |
704 | $ hosts = db['hosts'] |
705 | $ host_names = sorted(list(hosts), key=lambda x: hosts[x]+x) |
706 | $ width = 100/(len(host_names)+1) |
707 | -$ hroot = 'http://cbuild.validation.linaro.org/build/' |
708 | +$ hroot = '/build/' |
709 | |
710 | $ builds = sorted(db['versions']) |
711 | $ versions = db['versions'] |
712 | |
713 | === modified file 'templates/proposals/index.html' |
714 | --- templates/proposals/index.html 2012-11-30 02:31:03 +0000 |
715 | +++ templates/proposals/index.html 2013-01-18 12:55:25 +0000 |
716 | @@ -5,7 +5,7 @@ |
717 | $ projects = db['lp'].projects.values() |
718 | $ projects = [x for x in projects if x.get('proposals') and any(y.age < age_limit for y in x.proposals)] |
719 | $ projects = sorted(projects, key=lambda x: x.name) |
720 | -$ root = 'http://cbuild.validation.linaro.org/build/' |
721 | +$ root = '/build/' |
722 | $ ssh_root = 'ssh://cbuild@cbuild-master:/home/cbuild/public_html/build/' |
723 | |
724 | $ states = { 'Pending': 'white', 'Finished': 'lightgreen', 'Failed': 'pink', 'Regressed': 'gold' } |
725 | |
726 | === modified file 'templates/recent/index.html' |
727 | --- templates/recent/index.html 2012-11-30 02:31:03 +0000 |
728 | +++ templates/recent/index.html 2013-01-18 12:55:25 +0000 |
729 | @@ -1,7 +1,7 @@ |
730 | $def with (records, dbm) |
731 | $var title = 'Recent' |
732 | |
733 | -$ base = 'http://cbuild.validation.linaro.org/' |
734 | +$ base = '' |
735 | $ spawn = '/helpers/scheduler/spawn' |
736 | $ klasses = { 'benchmarks': 'aliceblue' } |
737 | $ colours = (('gcc-4', '#e0e0ff'),) |
738 | |
739 | === modified file 'templates/scheduler.html' |
740 | --- templates/scheduler.html 2012-12-09 20:17:37 +0000 |
741 | +++ templates/scheduler.html 2013-01-18 12:55:25 +0000 |
742 | @@ -114,7 +114,11 @@ |
743 | bgcolor="#fbfbfb" |
744 | > |
745 | <td>$job.queue</td> |
746 | - <td>$job.name</td> |
747 | + <td> |
748 | + $job.name |
749 | + $if job.lava_url: |
750 | + <a href="$job.lava_url">lava</a> |
751 | + </td> |
752 | <td>$elapsed_str(job.jobtime) ago</td> |
753 | <td> |
754 | $if job.host: |
755 | |
756 | === modified file 'templates/testcompare.html' |
757 | --- templates/testcompare.html 2012-11-30 02:31:03 +0000 |
758 | +++ templates/testcompare.html 2013-01-18 12:55:25 +0000 |
759 | @@ -1,6 +1,6 @@ |
760 | $def with (a, b, base, bases) |
761 | $var title = 'Test Compare' |
762 | -$ builds = 'http://cbuild.validation.linaro.org/build/' |
763 | +$ builds = '/build/' |
764 | |
765 | <h1>Test result comparison</h1> |
766 | Compare against |
767 | |
768 | === modified file 'templates/testlog.html' |
769 | --- templates/testlog.html 2012-11-30 02:31:03 +0000 |
770 | +++ templates/testlog.html 2013-01-18 12:55:25 +0000 |
771 | @@ -3,7 +3,7 @@ |
772 | |
773 | <h1>Test Log</h1> |
774 | |
775 | -$ root = 'http://cbuild.validation.linaro.org/build/' + path |
776 | +$ root = '/build/' + path |
777 | |
778 | Original log: <a href="$root">$root</a>. |
779 | |
780 | |
781 | === modified file 'templates/testtimeline/index.html' |
782 | --- templates/testtimeline/index.html 2012-11-30 02:31:03 +0000 |
783 | +++ templates/testtimeline/index.html 2013-01-18 12:55:25 +0000 |
784 | @@ -8,7 +8,7 @@ |
785 | $for c in groups.values(): |
786 | $ configs |= set(c) |
787 | $ configs = sorted(configs) |
788 | -$ root = 'http://cbuild.validation.linaro.org/build/' |
789 | +$ root = '/build/' |
790 | |
791 | $ configname = None |
792 | $ binname = None |
Paul Sokolovsky <email address hidden> writes:
> You have been requested to review the proposed merge of lp:~linaro-infrastructure/tcwg-web/cbuild-lava into lp:tcwg-web. /code.launchpad .net/~linaro- infrastructure/ tcwg-web/ cbuild- lava/+merge/ 143295 /code.launchpad .net/~linaro- infrastructure/ tcwg-web/ cbuild- lava/+merge/ 143295
>
> For more details, see:
> https:/
>
> Contains:
> - support for new lava-backed build queues.
> - lava link integration support on scheduler page.
> - trivial LAVA API library wrapper.
> - .lock file support for LAVA.
> - LAVA polling support with multiple functions.
>
> --
> https:/
> Your team Linaro Validation Team is requested to review the proposed
> merge of lp:~linaro-infrastructure/tcwg-web/cbuild-lava into
> lp:tcwg-web.
I'm not really capable of reviewing the overall structure. But here's a
few comments anyway.
> === modified file 'common.py'
> --- common.py 2012-11-15 01:48:14 +0000
> +++ common.py 2013-01-15 12:49:22 +0000
> @@ -17,12 +17,35 @@
> import time
> import json
> import urlparse
> +import urllib2
> import ConfigParser
> import base64
> +import shutil
> +import hashlib
> +import sys
>
> import web
> import memcache
>
> +import lava
> +
> +HOST = "host"
> +TIMESTAMP = "timestamp"
> +JOB_ID = "job_id"
> +LAVA_JOB_ID = "lava_job_id"
> +LAVA_JOB_URL = "lava_job_url"
> +
> +LOCK_FILE_KEYS = (
> + (HOST, 'HOST'),
> + (TIMESTAMP, 'TIMESTAMP'),
> + (JOB_ID, 'JOB_ID'),
> + (LAVA_JOB_ID, 'LAVA_JOB_ID'),
> + (LAVA_JOB_URL, 'LAVA_JOB_URL'),
> + )
This doesn't seem to be used anywhere.
> +class NoBundleFoundEx ception( Exception) :
> + """Raised when no bundle result is found with matching job name."""
> + pass
>
> class Cache:
> """In memory cache of file derrived objects."""
Typo: "derrived"
> @@ -292,6 +315,7 @@ abspath( os.path. join(base, name)) 'general' , 'results', '/var/www/ ex.seabright. co.nz/' ) abspath( os.path. join(base, context, name)) role='admin' ): status( ) SafeConfigParse r() read('tcwg- web.ini' ) dirname( __file_ _) has_option( group, name): file=False) : 'queue/ %s/' % queue, 'scheduler') + '/'
> base = special
> abspath = os.path.
> else:
> + # TODO need to check this path.
> base = get_config(
> abspath = os.path.
>
> @@ -310,7 +334,7 @@
>
> def authenticate(
> if is_development():
> - return 'michaelh1'
> + return 'devel-user'
>
> oid = web.webopenid.
>
> @@ -410,7 +434,8 @@
>
> def get_config(group, name, fallback=''):
> parser = ConfigParser.
> - parser.
> + dirname = os.path.
> + parser.read(dirname + '/tcwg-web.ini')
>
> if parser.
> return parser.get(group, name)
> @@ -438,3 +463,244 @@
> v = base64.b64decode(v)
>
> web.config[name] = v
> +
> +def schedule_job(queue, job, contents=None, use_template_
> + """Schedule a job. All job scheduling should happen via this
> + function, which encapsulates support for various schdulers in use."""
> + queue_dir = map_filename(
> +
> + job_file = queue_dir + job + '.job'
Constructing paths with + always make me twitch a bit. What's wrong
with os.path.join?
> +...