Merge lp:~pfalcon/linaro-ci-dashboard/kernel_chain into lp:linaro-ci-dashboard

Proposed by Paul Sokolovsky
Status: Merged
Merged at revision: 68
Proposed branch: lp:~pfalcon/linaro-ci-dashboard/kernel_chain
Merge into: lp:linaro-ci-dashboard
Diff against target: 457 lines (+238/-13)
15 files modified
config_template/jenkins-jobs/AndroidLoop.xml (+1/-1)
config_template/jenkins-jobs/HwpackLoop.xml (+1/-1)
config_template/jenkins-jobs/IntegrationLoop.xml (+6/-1)
config_template/jenkins-jobs/KernelLoop.xml (+1/-1)
dashboard/frontend/hwpack_loop/models/hwpack_loop.py (+1/-1)
dashboard/frontend/kernel_build/models/kernel_loop.py (+20/-0)
dashboard/frontend/kernel_build/tests/__init__.py (+2/-0)
dashboard/frontend/kernel_build/tests/test_kernel_chaining.py (+70/-0)
dashboard/frontend/models/loop.py (+20/-2)
dashboard/frontend/models/loop_build.py (+38/-0)
dashboard/frontend/models/loop_reference.py (+1/-0)
dashboard/jenkinsserver/models/jenkins_server.py (+36/-5)
dashboard/jenkinsserver/tests/test_jenkins_server.py (+35/-0)
dashboard/lib/jenkins_dashboard.py (+2/-1)
dashboard/settings.py (+4/-0)
To merge this branch: bzr merge lp:~pfalcon/linaro-ci-dashboard/kernel_chain
Reviewer Review Type Date Requested Status
Stevan Radaković Approve
Linaro Infrastructure Pending
Review via email: mp+125646@code.launchpad.net

Description of the change

This is preliminary merge proposal for Kernel->Hwpack loop chaining implementation. This so far lacks tests, on which I'm working in the meantime, but I'd like to collect any early feedback. As usual with more or less big changesets, it may be more comfortable to review the changes commit by commit, instead of aggregated diff.

Otherwise, these changes already underwent some refactoring comparing to the initial implementation I coded, and should be pretty architecturally clean IMHO. The main change is to introduce Loop method process_build_results() which is called at the end of build to process build outcome and prepare build results which can be used by downsteam builds, and to add LoopBuild API which process_build_results() can use to query artifacts of a build (because artifacts is the only outcome of a build in Jenkins, so any "build results" would be produced by analyzing artifacts). Some other convenience changes (like providing default implementation of Loop.preconfigure()) were also introduced.

To post a comment you must log in.
69. By Paul Sokolovsky

[merge] Merge from trunk.

70. By Paul Sokolovsky

Typo fix.

Revision history for this message
Stevan Radaković (stevanr) wrote :

Nice work Paul, thanks!!

I have no minor things to note, architecture looks pretty good.
One thing I'd like to see though are tests. There aren't any for one single method we implemented here.

review: Needs Fixing
Revision history for this message
Milo Casagrande (milo) wrote :

Hi Paul!

Some comments follow inline.

On Fri, Sep 21, 2012 at 10:34 AM, Paul Sokolovsky
<email address hidden> wrote:
> https://code.launchpad.net/~pfalcon/linaro-ci-dashboard/kernel_chain/+merge/125646
> Your team Linaro Infrastructure is requested to review the proposed merge of lp:~pfalcon/linaro-ci-dashboard/kernel_chain into lp:linaro-ci-dashboard.
>
> === modified file 'config_template/jenkins-jobs/AndroidLoop.xml'
> --- config_template/jenkins-jobs/AndroidLoop.xml 2012-09-18 15:08:05 +0000
> +++ config_template/jenkins-jobs/AndroidLoop.xml 2012-09-21 08:33:27 +0000
> @@ -178,7 +178,7 @@
> </builders>
> <publishers>
> <hudson.tasks.ArtifactArchiver>
> - <artifacts>build/out/target/*/*/*.img.bz2,build/out/target/*/*/*.tar.bz2,build/out/target/*/*/MD5SUMS,build/out/*.tar.bz2,build/out/*.xml,build/out/*_config,build/out/lava-job-info,build/out/linaro_kernel_build_cmds.sh,build/out/linaro_android_build_cmds.sh,build/out/**/*EULA*</artifacts>
> + <artifacts>build/out/target/*/*/*.img.bz2,build/out/target/*/*/*.tar.bz2,build/out/target/*/*/MD5SUMS,build/out/*.tar.bz2,build/out/*.xml,build/out/*_config,build/out/lava-job-info,build/out/linaro_kernel_build_cmds.sh,build/out/linaro_android_build_cmds.sh,build/out/**/*EULA*,MANIFEST</artifacts>

Is it possible to be more greedy here? Like "*.img.*", so to avoid
file suffix, or there might be other file types that we do not want?
Minor suggestion though...

> @models.permalink
> def get_detail_url(self):
> return 'KernelLoopDetail', (), {'slug': self.name}
> @@ -135,3 +141,17 @@
> @models.permalink
> def get_update_url(self):
> return 'KernelLoopUpdate', (), {'slug': self.name}
> +
> + def process_build_results(self, loop_build):
> + """This method gets called with list of artifact files from a build.
> + Its responsibility is to categorize artifacts, and prepare result data
> + to be passed to downstream loop in chain. If needed, this method can
> + access content of artifacts (not just list of them).
> + """
> + files = loop_build.list_artifacts()
> + kernel_debs = filter(lambda fname: "linux-image" in fname, files)
> + if len(kernel_debs) == 1:
> + return {"kernel_deb_url": loop_build.get_artifact_url(kernel_debs[0])}
> + else:
> + self.log.warning("No single kernel deb found among: %s", files)
> + return {}

This is more a personal thing than anything else: why not having only
a single exit point? Just using a single return statement.

Overall it looks good and clean, maybe a couple of tests wouldn't hurt.
Thanks!

--
Milo Casagrande
Infrastructure Engineer
Linaro.org <www.linaro.org> │ Open source software for ARM SoCs

71. By Paul Sokolovsky

preconfigure(): Check that argument is not None before iterating over it.

72. By Paul Sokolovsky

Factor out variable with Jenkins URL to ease debugging.

This regulalrly needs to be dumped to find out what's wrong with Jenkins.

73. By Paul Sokolovsky

Add integration test for for artifact querying methods of JenkinsServer.

This required making IntegrationLoop to export artifacts (also exposes
shell code injection with loop build scripts). TODO: Provide special Loop
just for integration testing.

74. By Paul Sokolovsky

Update another reference self.get_build_info() -> self.update_build_info()

75. By Paul Sokolovsky

self.remote_number can be None during tests, so print it as string.

76. By Paul Sokolovsky

Add high-level unit test for kernel => hwpack loop chaining.

77. By Paul Sokolovsky

[merge] Merge from trunk.

Revision history for this message
Paul Sokolovsky (pfalcon) wrote :

Milo:

> Is it possible to be more greedy here? Like "*.img.*", so to avoid
file suffix, or there might be other file types that we do not want?
Minor suggestion though...

Well, if you looked carefully, what I did to that line was adding "MANIFEST" file import. The original line likely was added by yourself while working on Android loop. But as it apparently was taken from a build config as set up be me, here's reply to the very question:

What about "1.img.something.completely.different" file? So yes, it's grounded conservatism imho. Of course, if there're good reasons to change that, we can.

>This is more a personal thing than anything else: why not having only
a single exit point? Just using a single return statement.

Single statement of course would be possible, but that would program transformation skewing the original idea of the code. It's because 2 branches have different semantics: one executes if data is valid and returns expected processed data, while another executes when data invalid and returns "data invalid" token. To make the point more clear, in the next incarnation of this function, return in "data invalid" branch may become raise.

Revision history for this message
Paul Sokolovsky (pfalcon) wrote :

Ok, so I finally get to complete tests for this. 2 tests were added:

1. Integrational test for new JenkinsServer methods to query artifacts.
2. Unit tests which mocks JenkinsServer methods and assures proper data flow between upstream and downstream tests in chain.

Revision history for this message
Stevan Radaković (stevanr) wrote :

Looks good to me Paul, thanks!
Approve +1

review: Approve
Revision history for this message
Milo Casagrande (milo) wrote :

Hi Paul,

thanks for fixing this, and also for the previous comments.
It looks good to me, I only have some errors when running the tests, but overall all the tests pass (total number is 96). Is that normal?

This is the kind of Python errors I see:
https://pastebin.linaro.org/991/

Revision history for this message
Stevan Radaković (stevanr) wrote :

Strange I don't get such errors. Let's check tomorrow with Paul.

Revision history for this message
Paul Sokolovsky (pfalcon) wrote :

Milo:

Here's my story: I didn't do work on ci-dashboard for a while, so when I restarted with work on these tested, I was greeted with the need to upgrade jenkins.war. I did, and then on first "python dashboard/manage.py test" run I got errors similar to those you quoted, except that in my case there were more errors "JenkinsException: create[testjob_integration] failed" and no "JenkinsException: build[2] does not exist for job test". I almost prepared to scratch my head and blame spurious jenkins upgrades that we have so far, but then I decided just to rerun the testsuite. Those errors were gone. There was another one though:

====
FAIL: test_login_teams (django_openid_auth.tests.test_views.RelyingPartyTests)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/usr/lib/pymodules/python2.7/django_openid_auth/tests/test_views.py", line 359, in test_login_teams
    self.assertTrue(group in user.groups.all())
AssertionError: False is not true
====

But that's known by all of us (?) heizenbug, except that it has much lower oscillation frequency (i.e. I have it stable, including with the trunk now even with several reruns, tomorrow it may be gone, day after appear again).

So, answering your question: it's "normal" (in quotes), in the sense that it's matter of fact that it does happen for more than one developer. It's not normal in other senses of this word, and we should take measures to investigate and fix those non-deterministic failures. First measure would be to use well-known jenkins version, instead of non-deterministic one - I proposed that some time ago, but didn't make that change at that time, so now it seems time has come.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'config_template/jenkins-jobs/AndroidLoop.xml'
2--- config_template/jenkins-jobs/AndroidLoop.xml 2012-09-18 15:08:05 +0000
3+++ config_template/jenkins-jobs/AndroidLoop.xml 2012-10-04 15:43:21 +0000
4@@ -178,7 +178,7 @@
5 </builders>
6 <publishers>
7 <hudson.tasks.ArtifactArchiver>
8- <artifacts>build/out/target/*/*/*.img.bz2,build/out/target/*/*/*.tar.bz2,build/out/target/*/*/MD5SUMS,build/out/*.tar.bz2,build/out/*.xml,build/out/*_config,build/out/lava-job-info,build/out/linaro_kernel_build_cmds.sh,build/out/linaro_android_build_cmds.sh,build/out/**/*EULA*</artifacts>
9+ <artifacts>build/out/target/*/*/*.img.bz2,build/out/target/*/*/*.tar.bz2,build/out/target/*/*/MD5SUMS,build/out/*.tar.bz2,build/out/*.xml,build/out/*_config,build/out/lava-job-info,build/out/linaro_kernel_build_cmds.sh,build/out/linaro_android_build_cmds.sh,build/out/**/*EULA*,MANIFEST</artifacts>
10 <latestOnly>false</latestOnly>
11 </hudson.tasks.ArtifactArchiver>
12 <hudson.plugins.logparser.LogParserPublisher>
13
14=== modified file 'config_template/jenkins-jobs/HwpackLoop.xml'
15--- config_template/jenkins-jobs/HwpackLoop.xml 2012-09-18 14:44:17 +0000
16+++ config_template/jenkins-jobs/HwpackLoop.xml 2012-10-04 15:43:21 +0000
17@@ -19,7 +19,7 @@
18 </builders>
19 <publishers>
20 <hudson.tasks.ArtifactArchiver>
21- <artifacts>*.tar.gz</artifacts>
22+ <artifacts>*.tar.gz,MANIFEST</artifacts>
23 <latestOnly>false</latestOnly>
24 </hudson.tasks.ArtifactArchiver>
25 </publishers>
26
27=== modified file 'config_template/jenkins-jobs/IntegrationLoop.xml'
28--- config_template/jenkins-jobs/IntegrationLoop.xml 2012-09-17 10:27:00 +0000
29+++ config_template/jenkins-jobs/IntegrationLoop.xml 2012-10-04 15:43:21 +0000
30@@ -17,6 +17,11 @@
31 <command>{build_script}</command>
32 </hudson.tasks.Shell>
33 </builders>
34- <publishers/>
35+ <publishers>
36+ <hudson.tasks.ArtifactArchiver>
37+ <artifacts>*.result,MANIFEST</artifacts>
38+ <latestOnly>false</latestOnly>
39+ </hudson.tasks.ArtifactArchiver>
40+ </publishers>
41 <buildWrappers/>
42 </project>
43
44=== modified file 'config_template/jenkins-jobs/KernelLoop.xml'
45--- config_template/jenkins-jobs/KernelLoop.xml 2012-09-18 13:29:37 +0000
46+++ config_template/jenkins-jobs/KernelLoop.xml 2012-10-04 15:43:21 +0000
47@@ -19,7 +19,7 @@
48 </builders>
49 <publishers>
50 <hudson.tasks.ArtifactArchiver>
51- <artifacts>*.deb</artifacts>
52+ <artifacts>*.deb,MANIFEST</artifacts>
53 <latestOnly>false</latestOnly>
54 </hudson.tasks.ArtifactArchiver>
55 </publishers>
56
57=== modified file 'dashboard/frontend/hwpack_loop/models/hwpack_loop.py'
58--- dashboard/frontend/hwpack_loop/models/hwpack_loop.py 2012-09-17 13:00:42 +0000
59+++ dashboard/frontend/hwpack_loop/models/hwpack_loop.py 2012-10-04 15:43:21 +0000
60@@ -32,7 +32,7 @@
61
62 # Version of hwpack tarball to build (passed directly to
63 # linaro-hwpack-create)
64- hwpack_build_version = models.CharField(max_length=15)
65+ hwpack_build_version = models.CharField(max_length=15, default="1")
66
67 # URL of kernel deb package to use for hwpack creation
68 kernel_deb_url = models.CharField(max_length=255, default="", blank=True)
69
70=== modified file 'dashboard/frontend/kernel_build/models/kernel_loop.py'
71--- dashboard/frontend/kernel_build/models/kernel_loop.py 2012-09-12 12:48:46 +0000
72+++ dashboard/frontend/kernel_build/models/kernel_loop.py 2012-10-04 15:43:21 +0000
73@@ -19,6 +19,7 @@
74 from django.db import models
75 from frontend.models.loop import Loop
76 from jenkinsserver.models.jenkins_server import JenkinsServer
77+from frontend.models.loop_reference import HWPACK_LOOP
78
79
80 LLT_URL = (r'git://git.linaro.org/kernel/linux-linaro-tracking.git',
81@@ -124,6 +125,11 @@
82 # TODO: Log error.
83 pass
84
85+ @staticmethod
86+ def can_chain_into():
87+ chain = [HWPACK_LOOP]
88+ return chain
89+
90 @models.permalink
91 def get_detail_url(self):
92 return 'KernelLoopDetail', (), {'slug': self.name}
93@@ -135,3 +141,17 @@
94 @models.permalink
95 def get_update_url(self):
96 return 'KernelLoopUpdate', (), {'slug': self.name}
97+
98+ def process_build_results(self, loop_build):
99+ """This method gets called with list of artifact files from a build.
100+ Its responsibility is to categorize artifacts, and prepare result data
101+ to be passed to downstream loop in chain. If needed, this method can
102+ access content of artifacts (not just list of them).
103+ """
104+ files = loop_build.list_artifacts()
105+ kernel_debs = filter(lambda fname: "linux-image" in fname, files)
106+ if len(kernel_debs) == 1:
107+ return {"kernel_deb_url": loop_build.get_artifact_url(kernel_debs[0])}
108+ else:
109+ self.log.warning("No single kernel deb found among: %s", files)
110+ return {}
111
112=== modified file 'dashboard/frontend/kernel_build/tests/__init__.py'
113--- dashboard/frontend/kernel_build/tests/__init__.py 2012-08-23 11:22:21 +0000
114+++ dashboard/frontend/kernel_build/tests/__init__.py 2012-10-04 15:43:21 +0000
115@@ -1,9 +1,11 @@
116 from dashboard.frontend.kernel_build.tests.test_kernel_build_views import *
117 from dashboard.frontend.kernel_build.tests.test_kernel_build_clientresponse \
118 import *
119+from dashboard.frontend.kernel_build.tests.test_kernel_chaining import *
120
121
122 __test__ = {
123 'KernelBuildViewsTest': KernelBuildViewsTest,
124 'KernelBuildClientResponseTests': KernelBuildClientResponseTests,
125+ 'KernelHwpackChainingTest': KernelHwpackChainingTest,
126 }
127
128=== added file 'dashboard/frontend/kernel_build/tests/test_kernel_chaining.py'
129--- dashboard/frontend/kernel_build/tests/test_kernel_chaining.py 1970-01-01 00:00:00 +0000
130+++ dashboard/frontend/kernel_build/tests/test_kernel_chaining.py 2012-10-04 15:43:21 +0000
131@@ -0,0 +1,70 @@
132+# Copyright (C) 2012 Linaro
133+#
134+# This file is part of linaro-ci-dashboard.
135+#
136+# linaro-ci-dashboard is free software: you can redistribute it and/or modify
137+# it under the terms of the GNU Affero General Public License as published by
138+# the Free Software Foundation, either version 3 of the License, or
139+# (at your option) any later version.
140+#
141+# linaro-ci-dashboard is distributed in the hope that it will be useful,
142+# but WITHOUT ANY WARRANTY; without even the implied warranty of
143+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
144+# GNU Affero General Public License for more details.
145+#
146+# You should have received a copy of the GNU Affero General Public License
147+# along with linaro-ci-dashboard. If not, see <http://www.gnu.org/licenses/>.
148+
149+from django.test import TestCase
150+from mock import patch
151+
152+from jenkinsserver.models.jenkins_server import JenkinsServer
153+from frontend.models.loop_build import LoopBuild
154+from frontend.kernel_build.models.kernel_loop import KernelLoop
155+from frontend.hwpack_loop.models.hwpack_loop import HwpackLoop
156+
157+
158+class KernelHwpackChainingTest(TestCase):
159+
160+ def setUp(self):
161+ self.server = JenkinsServer.objects.get(id=1)
162+
163+ @patch('jenkinsserver.models.jenkins_server.JenkinsServer.create_or_update_loop')
164+ @patch('jenkinsserver.models.jenkins_server.JenkinsServer.get_build_artifact_content')
165+ @patch('lib.jenkins_dashboard.JenkinsDashboard.get_build_info')
166+ def test_chaining(self, get_build_info_m, get_build_artifact_content_m, mck1):
167+ kernel_package = "linux-image.deb"
168+ get_build_info_m.return_value = {"result": LoopBuild.SUCCESS}
169+ get_build_artifact_content_m.return_value = kernel_package + "\n"
170+
171+ hwpack_loop = HwpackLoop()
172+ hwpack_loop.name = "hwpack_test"
173+ hwpack_loop.server = self.server
174+ hwpack_loop.save()
175+ self.assertFalse(hwpack_loop.kernel_deb_url)
176+
177+ loop = KernelLoop()
178+ loop.name = "kernel_test"
179+ loop.server = self.server
180+ loop.next_loop = hwpack_loop
181+ loop.save()
182+ build = loop.schedule_build()
183+
184+ self.server.sync_unfinished_builds()
185+
186+ # Reload object from DB in Django-speak
187+ build = LoopBuild.objects.get(id=build.id)
188+
189+ # Make sure kernel build status/results were updated
190+ self.assertEquals(build.status, LoopBuild.SUCCESS)
191+ self.assertTrue(kernel_package in build.result_xml)
192+
193+ # Make sure results from kernel loop were propagated to hwpack
194+ hwpack_loop = HwpackLoop.objects.get(id=hwpack_loop.id)
195+ self.assertTrue(kernel_package in hwpack_loop.kernel_deb_url)
196+
197+ # Make sure that hwpack build was scheduled
198+ self.assertEquals(len(LoopBuild.objects.all()), 2)
199+ hwpack_build = LoopBuild.objects.get(loop=hwpack_loop)
200+ self.assertTrue(hwpack_build)
201+ self.assertEquals(hwpack_build.status, LoopBuild.SCHEDULED)
202
203=== modified file 'dashboard/frontend/models/loop.py'
204--- dashboard/frontend/models/loop.py 2012-09-20 13:51:27 +0000
205+++ dashboard/frontend/models/loop.py 2012-10-04 15:43:21 +0000
206@@ -259,8 +259,26 @@
207
208 def preconfigure(self, configuration=None):
209 """Makes a build parameter dict from build result configuration.
210-
211- This method should be overridden by subclasses.
212+ Default implementation just assigns loop's attributes from dictionary
213+ keys. This method may be overridden by subclasses.
214+
215+ :return Extra Jenkins build scheduling parameters. TODO: breaks separation
216+ """
217+ if configuration:
218+ for key, value in configuration.iteritems():
219+ if hasattr(self, key):
220+ setattr(self, key, value)
221+ else:
222+ self.log.warning("Input build parameters for %s contain "\
223+ "unknown key/value pair %s=%s", self, key, value)
224+ self.save()
225+ return {}
226+
227+ def process_build_results(self, loop_build):
228+ """This method gets called with list of artifact files from a build.
229+ Its responsibility is to categorize artifacts, and prepare result data
230+ to be passed to downstream loop in chain. If needed, this method can
231+ access content of artifacts (not just list of them).
232 """
233 return {}
234
235
236=== modified file 'dashboard/frontend/models/loop_build.py'
237--- dashboard/frontend/models/loop_build.py 2012-09-19 10:50:22 +0000
238+++ dashboard/frontend/models/loop_build.py 2012-10-04 15:43:21 +0000
239@@ -84,3 +84,41 @@
240 def build_finished(self):
241 """Method to be called after the build is executed."""
242 self.loop.continue_down_the_chain(self.get_build_result())
243+
244+ def list_artifacts(self):
245+ """
246+ List artifacts as produced by the build.
247+
248+ :return List containing full paths of artifacts files
249+ """
250+ return self.loop.server.list_build_artifacts(self.loop.name,
251+ self.remote_number)
252+
253+ def get_artifact_url(self, artifact_name):
254+ """
255+ Get a URL from which given artifact can be downloaded (for example,
256+ for passing to a build script).
257+
258+ :param artifact_name Full artifact path, as returned by
259+ list_artifacts()
260+ :return A URL
261+ """
262+ return self.loop.server.get_build_artifact_url(self.loop.name,
263+ self.remote_number, artifact_name)
264+
265+ def get_artifact_content(self, artifact_name):
266+ """
267+ Get contents of an artifact. This function loads entire artifact
268+ into memory, so should not be used with big artifacts.
269+
270+ :param artifact_name Full artifact path, as returned by
271+ list_artifacts()
272+ :return Raw binary data of artifact
273+ """
274+ return self.loop.server.get_build_artifact_url(self.loop.name,
275+ self.remote_number, artifact_name)
276+
277+ def __repr__(self):
278+ return "%s(id=%s, loop.name=%s, remote_numer=%s)" % \
279+ (self.__class__.__name__, self.id, self.loop.name,
280+ self.remote_number)
281
282=== modified file 'dashboard/frontend/models/loop_reference.py'
283--- dashboard/frontend/models/loop_reference.py 2012-08-31 11:53:01 +0000
284+++ dashboard/frontend/models/loop_reference.py 2012-10-04 15:43:21 +0000
285@@ -22,3 +22,4 @@
286 KERNEL_LOOP = ('KernelLoop')
287 INTEGRATION_LOOP = ('IntegrationLoop')
288 ANDROID_TEXTFIELD_LOOP = ('AndroidTextFieldLoop')
289+HWPACK_LOOP = ('HwpackLoop')
290
291=== modified file 'dashboard/jenkinsserver/models/jenkins_server.py'
292--- dashboard/jenkinsserver/models/jenkins_server.py 2012-09-18 16:01:01 +0000
293+++ dashboard/jenkinsserver/models/jenkins_server.py 2012-10-04 15:43:21 +0000
294@@ -16,11 +16,14 @@
295 # You should have received a copy of the GNU Affero General Public License
296 # along with linaro-ci-dashboard. If not, see <http://www.gnu.org/licenses/>.
297
298+import urllib2
299+
300 from django.db import models
301 from django.conf import settings
302 from lib.jenkins_dashboard import JenkinsDashboard
303 from lib.logger import Logger
304 from lib.template import TextTemplate, XMLTemplate
305+from dashboard.lib.xml_to_dict import DictToXml
306
307
308 class JenkinsServer(models.Model):
309@@ -71,9 +74,9 @@
310 # update the build info in CI.
311 if build.remote_number and \
312 jenkins_build["number"] == build.remote_number:
313- self.get_build_info(build)
314+ self.update_build_info(build)
315
316- def get_build_info(self, build):
317+ def update_build_info(self, build):
318 jenkins_build = self.jenkins.get_build_info(build.loop.name,
319 build.remote_number)
320 # Duration in jenkins is presented in milliseconds
321@@ -82,18 +85,22 @@
322
323 if jenkins_build.get("result", None):
324 build.status = jenkins_build.get("result", None).lower()
325- # TODO: Add build result xml, but from where?
326+ # Let loop process the build outcome and extract anything relevant
327+ # to store as build results.
328+ loop = build.loop.get_child_object()
329+ values = loop.process_build_results(build)
330+ build.result_xml = DictToXml(values).dict_to_tree()
331 build.save()
332
333 def sync_unfinished_builds(self):
334 from frontend.models.loop_build import LoopBuild
335 builds = LoopBuild.objects.filter(
336- status__in=LoopBuild.NON_FINISHED_STATUSES)
337+ status__in=LoopBuild.NON_FINISHED_STATUSES).order_by('id')
338 for build in builds:
339 # We don't wont the sync proces to fail if there is no particular
340 # loop present in jenkins. Log the error and continue.
341 try:
342- self.get_build_info(build)
343+ self.update_build_info(build)
344 except:
345 self.log.exception("build[%s] does not exist for job %s" % (build.remote_number, build.loop.name))
346 pass
347@@ -148,3 +155,27 @@
348 self.jenkins.build_job(jobname, parameters)
349 job_info = self.jenkins.get_job_info(jobname)
350 return job_info["nextBuildNumber"]
351+
352+ def get_build_artifact_url(self, jobname, build_no, artifact_name):
353+ url = settings.ARTIFACT_TEMPLATE_URL % locals()
354+ return url
355+
356+ def get_build_artifact_content(self, jobname, build_no, artifact_name):
357+ "Get contents of a build artifact."
358+ url = self.get_build_artifact_url(jobname, build_no, artifact_name)
359+ response = urllib2.urlopen(url)
360+ contents = response.read()
361+ response.close()
362+ return contents
363+
364+ def list_build_artifacts(self, jobname, build_no):
365+ try:
366+ manifest = self.get_build_artifact_content(jobname, build_no, "MANIFEST")
367+ except urllib2.HTTPError, e:
368+ if e.code == 404:
369+ self.log.warn("MANIFEST artifact not found for %s/%s, assumed empty", jobname, build_no)
370+ manifest = ""
371+ else:
372+ raise
373+ files = [f for f in manifest.split("\n") if f]
374+ return files
375
376=== modified file 'dashboard/jenkinsserver/tests/test_jenkins_server.py'
377--- dashboard/jenkinsserver/tests/test_jenkins_server.py 2012-09-18 16:01:01 +0000
378+++ dashboard/jenkinsserver/tests/test_jenkins_server.py 2012-10-04 15:43:21 +0000
379@@ -15,9 +15,12 @@
380 #
381 # You should have received a copy of the GNU Affero General Public License
382 # along with linaro-ci-dashboard. If not, see <http://www.gnu.org/licenses/>.
383+import time
384+
385 from django.test import TestCase
386 from jenkinsserver.models.jenkins_server import JenkinsServer
387 import jenkins
388+from frontend.models.loop_build import LoopBuild
389 from frontend.integration_loop.models.integration_loop import IntegrationLoop
390
391
392@@ -118,3 +121,35 @@
393 self.assertIsNotNone(build.duration)
394 self.assertIsNot(build.duration, 0.0)
395 self.server.delete_job(self.test_job_name)
396+
397+ def test_build_artifacts(self):
398+ integration_loop = IntegrationLoop()
399+ integration_loop.name = self.test_job_name
400+ integration_loop.server = self.server
401+ integration_loop.branch = 'none:none || mkdir -p '
402+ integration_loop.precommand = 'cd ..; echo "foobar" >file.result'
403+ integration_loop.command = 'ls -1 *.result >MANIFEST'
404+ integration_loop.save()
405+ build = integration_loop.schedule_build()
406+ for i in range(10):
407+ self.server.sync_unfinished_builds()
408+ # Reload object from DB in Django-speak
409+ build = LoopBuild.objects.get(id=build.id)
410+ if build.status not in (LoopBuild.SCHEDULED, LoopBuild.RUNNING):
411+ break
412+ time.sleep(2)
413+ self.assertEquals(build.status, LoopBuild.SUCCESS)
414+ # We could test LoopBuild methods here, that would give us even more
415+ # coverage, but - we're testing JenkinsServer here, so let's test it.
416+ artifacts = self.server.list_build_artifacts(integration_loop.name,
417+ build.remote_number)
418+ self.assertEquals(artifacts, ["file.result"])
419+
420+ url = self.server.get_build_artifact_url(integration_loop.name,
421+ build.remote_number, artifacts[0])
422+ self.assertTrue(url.startswith("http://"))
423+ self.assertTrue(artifacts[0] in url)
424+
425+ data = self.server.get_build_artifact_content(integration_loop.name,
426+ build.remote_number, artifacts[0])
427+ self.assertEquals(data, "foobar\n")
428
429=== modified file 'dashboard/lib/jenkins_dashboard.py'
430--- dashboard/lib/jenkins_dashboard.py 2012-09-18 15:06:02 +0000
431+++ dashboard/lib/jenkins_dashboard.py 2012-10-04 15:43:21 +0000
432@@ -30,8 +30,9 @@
433 def get_build_info(self, name, build_number):
434
435 try:
436+ url = self.server + self.BUILD_INFO % locals()
437 response = self.jenkins_open(
438- urllib2.Request(self.server + self.BUILD_INFO % locals()))
439+ urllib2.Request(url))
440 if response:
441 return json.loads(response)
442 else:
443
444=== modified file 'dashboard/settings.py'
445--- dashboard/settings.py 2012-09-25 08:07:10 +0000
446+++ dashboard/settings.py 2012-10-04 15:43:21 +0000
447@@ -227,6 +227,10 @@
448 JENKINS_LOG_NAME = 'jenkins.log'
449 JENKINS_DB_ID = 1
450
451+# URL from which build artifacts can be downloaded
452+ARTIFACT_TEMPLATE_URL = "http://localhost:" + JENKINS_PORT + \
453+ "/jenkins/job/%(jobname)s/%(build_no)s/artifact/%(artifact_name)s"
454+
455 # LAVA specific values
456 LAVA_URL = 'validation.linaro.org/lava-server/RPC2/'
457 LAVA_ACCEPTING_ADDRESS = '0.0.0.0'

Subscribers

People subscribed via source and target branches