Merge lp:~linaro-infrastructure/tcwg-web/cbuild-lava into lp:tcwg-web

Proposed by Stevan Radaković
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
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

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.

To post a comment you must log in.
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.

Revision history for this message
Michael Hudson-Doyle (mwhudson) wrote :
Download full text (24.4 KiB)

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.
>
> For more details, see:
> https://code.launchpad.net/~linaro-infrastructure/tcwg-web/cbuild-lava/+merge/143295
>
> 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://code.launchpad.net/~linaro-infrastructure/tcwg-web/cbuild-lava/+merge/143295
> 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 NoBundleFoundException(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 @@
> base = special
> abspath = os.path.abspath(os.path.join(base, name))
> else:
> + # TODO need to check this path.
> base = get_config('general', 'results', '/var/www/ex.seabright.co.nz/')
> abspath = os.path.abspath(os.path.join(base, context, name))
>
> @@ -310,7 +334,7 @@
>
> def authenticate(role='admin'):
> if is_development():
> - return 'michaelh1'
> + return 'devel-user'
>
> oid = web.webopenid.status()
>
> @@ -410,7 +434,8 @@
>
> def get_config(group, name, fallback=''):
> parser = ConfigParser.SafeConfigParser()
> - parser.read('tcwg-web.ini')
> + dirname = os.path.dirname(__file__)
> + parser.read(dirname + '/tcwg-web.ini')
>
> if parser.has_option(group, name):
> 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_file=False):
> + """Schedule a job. All job scheduling should happen via this
> + function, which encapsulates support for various schdulers in use."""
> + queue_dir = map_filename('queue/%s/' % queue, 'scheduler') + '/'
> +
> + job_file = queue_dir + job + '.job'

Constructing paths with + always make me twitch a bit. What's wrong
with os.path.join?

> +...

Revision history for this message
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_dir_name(lava_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-case-attach [...] results[...]
>> + matcher = re.compile("\s*\+?\s*lava-test-case-attach\s*.*\s*results/")
>> + 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):
>> + match = line.strip().split(' ')[-1]
>> + break
>> + if match:
>> + # We treat it like a path.
>> + path = os.path.dirname(match).split('/')[-1]
>> + 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.

Revision history for this message
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.

Revision history for this message
Matthew Gretton-Dann (matthew-gretton-dann) wrote :

Approved.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
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

Subscribers

People subscribed via source and target branches