Merge lp:~didrocks/tarmac/jenkins-plugins into lp:tarmac

Proposed by Didier Roche on 2011-11-23
Status: Superseded
Proposed branch: lp:~didrocks/tarmac/jenkins-plugins
Merge into: lp:tarmac
Prerequisite: lp:~didrocks/tarmac/addmergeignore
Diff against target: 319 lines (+310/-0)
2 files modified
tarmac/plugins/jenkins.py (+191/-0)
tarmac/plugins/tests/test_jenkins.py (+119/-0)
To merge this branch: bzr merge lp:~didrocks/tarmac/jenkins-plugins
Reviewer Review Type Date Requested Status
Paul Hummer 2011-11-23 Pending
Review via email: mp+83163@code.launchpad.net

Description of the Change

Add a jenkins plugin to be able to run a job and accept/reject a merge from this job result.

To post a comment you must log in.
lp:~didrocks/tarmac/jenkins-plugins updated on 2011-11-23
400. By Didier Roche on 2011-11-23

add a jenkins plugin, to be able to connect tarmac to a jenkins job for a project

Unmerged revisions

400. By Didier Roche on 2011-11-23

add a jenkins plugin, to be able to connect tarmac to a jenkins job for a project

399. By Didier Roche on 2011-11-23

add the new ignore exception

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== added file 'tarmac/plugins/jenkins.py'
2--- tarmac/plugins/jenkins.py 1970-01-01 00:00:00 +0000
3+++ tarmac/plugins/jenkins.py 2011-11-23 13:31:50 +0000
4@@ -0,0 +1,191 @@
5+# This file is part of Tarmac.
6+#
7+# Copyright 2011 Canonical, Ltd.
8+#
9+# Tarmac is free software: you can redistribute it and/or modify
10+# it under the terms of the GNU General Public License version 3 as
11+# published by the Free Software Foundation.
12+#
13+# Tarmac is distributed in the hope that it will be useful,
14+# but WITHOUT ANY WARRANTY; without even the implied warranty of
15+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16+# GNU General Public License for more details.
17+#
18+# You should have received a copy of the GNU General Public License
19+# along with Tarmac. If not, see <http://www.gnu.org/licenses/>.
20+"""Tarmac plug-in for integrating with Jenkins."""
21+
22+from tarmac.exceptions import TarmacMergeError, TarmacMergeIgnore
23+from tarmac.hooks import tarmac_hooks
24+from tarmac.plugins import TarmacPlugin
25+
26+import os
27+import urllib2
28+import stat
29+from time import sleep
30+
31+WAIT_BEFORE_NETWORK_RETRIAL = 1
32+NUMBER_OF_RETRY = 3
33+
34+class JenkinsReject(TarmacMergeError):
35+ """Error for when a jenkins rejects the branch."""
36+
37+class JenkinsTimeout(TarmacMergeIgnore):
38+ """Error for when a jenkins is not reachable."""
39+
40+class JenkinsCheck(TarmacPlugin):
41+ """Tarmac plug-in for integrating with jenkins.
42+
43+ This plug-in run a jenkins job on the target branch, and then wait
44+ on the result (this can take some time). When the job ended, it checks
45+ its status and will accordingly will cause the branch merge to fail or
46+ not.
47+ Set jenkins_job_url to your configuration for the current target to use
48+ it.
49+ """
50+
51+ def run(self, command, target, source, proposal):
52+ """Start and wait the jenkins job to end."""
53+
54+ try:
55+ # do not use jenkins if no jenkins_job_url
56+ target.config.jenkins_job_url
57+ except AttributeError:
58+ return
59+ try:
60+ jenkins_job = JenkinsJob(target.config, target, self.logger)
61+ self.jenkins_job = jenkins_job
62+ except AttributeError:
63+ message = (u'You set a jenkins_job_url configuration, but didn\'t setup other'
64+ u'mandatory settings for jenkins plugin to run. Those are:\n'
65+ u'jenkins_job_url, packaging_branch, jenkins_token')
66+ raise JenkinsTimeout(message)
67+
68+ # start server job
69+ (status, response) = jenkins_job.start()
70+ if not status:
71+ message = (u'The Jenkins server %(jenkins_url)s is not reachable'
72+ u'(message)s.' %
73+ {'jenkins_url': jenkins_job.url, 'message': response})
74+ raise JenkinsTimeout(message)
75+
76+ # check it is still running
77+ sleep(10)
78+ while(jenkins_job.is_running() and jenkins_job.timeout < NUMBER_OF_RETRY):
79+ self.logger.debug('Jenkins job %(jenkins_job_url)s/%(job_number)s/console running.' % {
80+ 'jenkins_job_url': jenkins_job.url,
81+ 'job_number': jenkins_job.number})
82+ sleep(3)
83+ running = False
84+
85+ if jenkins_job.timeout == NUMBER_OF_RETRY:
86+ message = (u'The Jenkins server %(jenkins_url)s is not reachable'
87+ u'for retrieving run status.' % {'jenkins_url': jenkins_job.url})
88+ raise JenkinsTimeout(message)
89+
90+ # get latest result
91+ (status, response) = jenkins_job.last_job_passed()
92+ if not status:
93+ message = u'The Jenkins job failed.'
94+ comment = (u'The Jenkins job %(jenkins_job_url)s/%(job_number)s/console reported an error '
95+ u'when processing this %(source)s branch.\nNot merging it.' % {
96+ 'source': proposal.source_branch.display_name,
97+ 'jenkins_job_url': jenkins_job.public_url,
98+ 'job_number': jenkins_job.number})
99+ raise JenkinsReject(message, comment)
100+
101+class JenkinsJob(object):
102+ """Jenkins Job Server communication object binding"""
103+
104+ def __init__(self, config, target, logger):
105+ super(JenkinsJob, self).__init__()
106+ self.logger = logger
107+ self.target = target
108+ self.tree_path = target.tree.abspath('')
109+ self.number = None
110+ # let other like jenkins see the tree!
111+ os.chmod(self.tree_path, stat.S_IRWXU + stat.S_IROTH + stat.S_IXOTH)
112+ self.timeout=0
113+ self.url = config.jenkins_job_url
114+ self.packaging_branch = config.packaging_branch
115+ try:
116+ self.public_url = config.jenkins_public_url
117+ except AttributeError:
118+ self.public_url = self.url
119+ self.token = config.jenkins_token
120+
121+ def _get_json_from_service(self, data):
122+ true = True
123+ false = False
124+ null = None
125+ return eval(data)
126+
127+ def start(self):
128+ '''start a jenkins job'''
129+ revno = self.target.bzr_branch.revno()
130+ req = urllib2.Request("%(url)s/buildWithParameters?token=%(token)s&dir=%(dir)s&branch=%(packaging_branch)s&trunkrev=%(revno)s"
131+ % {'url': self.url, 'token': self.token, 'dir': self.tree_path, 'revno': revno,
132+ 'packaging_branch': self.packaging_branch})
133+ trial_inc = 0
134+ while(trial_inc < NUMBER_OF_RETRY):
135+ try:
136+ response = urllib2.urlopen(req)
137+ error_msg = None
138+ break
139+ except urllib2.URLError, e:
140+ error_msg = e
141+ trial_inc += 1
142+ sleep(WAIT_BEFORE_NETWORK_RETRIAL)
143+ if error_msg:
144+ message = (u'The Jenkins server %(jenkins_url)s is not reachable'
145+ u'(message)s.' % {'jenkins_url': self.url, 'message': error_msg})
146+ raise JenkinsTimeout(message)
147+ data = response.read()
148+ #if data == 'OK':
149+ return (True, 'Job started')
150+ #return (False, data)
151+
152+ def is_running(self):
153+ '''check if the current job is still running'''
154+ req = urllib2.Request("%(url)s/lastBuild/api/json" % {'url': self.url})
155+ try:
156+ response = urllib2.urlopen(req)
157+ except urllib2.URLError, e:
158+ self.logger.debug('Can\'t get an answer for %(url)s, incrementing the '
159+ 'timeout counter.')
160+ self.timeout += 1
161+ return True # Assuming still busy
162+ self.timeout=0 # got an answer, reset the timeout
163+ data = response.read()
164+ if not self.number:
165+ self.number = self._get_json_from_service(data)['number']
166+ if self.number != self._get_json_from_service(data)['number']:
167+ return False
168+ return self._get_json_from_service(data)['building']
169+
170+
171+ def last_job_passed(self):
172+ '''check if the last jenkins job passed, return as well the response if not'''
173+ req = urllib2.Request("%(url)s/lastBuild/api/json" % {'url': self.url})
174+ trial_inc = 0
175+ while(trial_inc < NUMBER_OF_RETRY):
176+ try:
177+ response = urllib2.urlopen(req)
178+ error_msg = None
179+ break
180+ except urllib2.URLError, e:
181+ error_msg = e
182+ trial_inc += 1
183+ sleep(WAIT_BEFORE_NETWORK_RETRIAL)
184+ if error_msg:
185+ message = (u'The Jenkins server %(jenkins_url)s is not reachable'
186+ u'(message)s.' % {'jenkins_url': self.url, 'message': error_msg})
187+ raise JenkinsTimeout(message)
188+ data = response.read()
189+ if self._get_json_from_service(data)['result'] == 'SUCCESS':
190+ return (True, 'Everything is ok')
191+ return (False, data)
192+
193+tarmac_hooks['tarmac_pre_commit'].hook(
194+ JenkinsCheck(), 'Jenkins integration check plug-in')
195+
196
197=== added file 'tarmac/plugins/tests/test_jenkins.py'
198--- tarmac/plugins/tests/test_jenkins.py 1970-01-01 00:00:00 +0000
199+++ tarmac/plugins/tests/test_jenkins.py 2011-11-23 13:31:50 +0000
200@@ -0,0 +1,119 @@
201+# Copyright 2011 Canonical, Ltd.
202+#
203+# This file is part of Tarmac.
204+#
205+# Tarmac is free software: you can redistribute it and/or modify
206+# it under the terms of the GNU General Public License version 3 as
207+# published by the Free Software Foundation.
208+#
209+# Tarmac is distributed in the hope that it will be useful,
210+# but WITHOUT ANY WARRANTY; without even the implied warranty of
211+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
212+# GNU General Public License for more details.
213+#
214+# You should have received a copy of the GNU General Public License
215+# along with Tarmac. If not, see <http://www.gnu.org/licenses/>.
216+"""Tests for the Jenkins Integration plug-in."""
217+
218+from tarmac.plugins.jenkins import JenkinsCheck, JenkinsTimeout, JenkinsReject
219+from tarmac.tests import TarmacTestCase
220+from tarmac.tests.mock import Thing
221+
222+import os
223+import shutil
224+import tempfile
225+
226+class JenkinsCheckTests(TarmacTestCase):
227+ """Test the jenkins integration."""
228+
229+ def setUp(self):
230+ super(JenkinsCheckTests, self).setUp()
231+ self.jenkins_dir = tempfile.mkdtemp()
232+ self.jenkins_token = "footoken"
233+ config=Thing(jenkins_job_url="file://%s" % self.jenkins_dir,
234+ packaging_branch='lp:~foo/test/ubuntu',
235+ jenkins_token=self.jenkins_token,
236+ jenkins_public_url="fooo")
237+ self.target = Thing(config=config, tree=Thing(abspath=os.path.abspath),
238+ bzr_branch=Thing(revno=lambda: 10))
239+ self.proposal = Thing(
240+ source_branch = Thing(display_name='lp:foo/bar'))
241+ self.command = Thing()
242+ self.plugin = JenkinsCheck()
243+
244+ def tearDown(self):
245+ super(JenkinsCheckTests, self).tearDown()
246+ shutil.rmtree(self.jenkins_dir)
247+
248+ def generate_jenkins_mock_server(self, run_status=None, building=None, last_job_status=None):
249+ if run_status:
250+ run_test_file = ('%(url)s/buildWithParameters?token=%(token)s&dir=%(dir)s&branch=%(branch)s&trunkrev=%(revno)s'
251+ % {'url': self.jenkins_dir, 'token': self.jenkins_token, 'dir': self.target.tree.abspath(''),
252+ 'revno': self.target.bzr_branch.revno(), 'branch': self.target.config.packaging_branch})
253+ os.makedirs(os.path.dirname(run_test_file))
254+ with open(run_test_file, 'w') as f:
255+ f.write("")
256+ if building or last_job_status:
257+ os.makedirs(self.jenkins_dir + '/lastBuild/api/')
258+ with open(self.jenkins_dir + '/lastBuild/api/json', 'w') as f:
259+ f.write('{"actions":[{"parameters":[{"name":"RELEASE","value":"oneiric"},{"name":"VARIANT","value":"alternate"},{"name":"ARCH","value":"i386"},{"name":"PRESEED","value":"oem"},{"name":"FLAVOR","value":"ubuntu"}]},{"causes":[]},{}],"artifacts":[],"building":%(building)s,"description":null,"duration":2701176,"fullDisplayName":"oneiric-alternate-i386_oem #65","id":"2011-11-07_07-57-49","keepLog":false,"number":65,"result":"%(last_job_status)s","timestamp":1320652669000,"url":"http://jenkins.qa.ubuntu.com/job/oneiric-alternate-i386_oem/65/","builtOn":"aldebaran","changeSet":{"items":[],"kind":null},"culprits":[]}' % {'building': building, 'last_job_status': last_job_status})
260+
261+ def test_fail_no_server(self):
262+ """Test that the merge actually fail when there is no server."""
263+ # no file
264+ self.assertRaises(JenkinsTimeout, self.plugin.run, command=self.command,
265+ target=self.target, source=None,
266+ proposal=self.proposal)
267+
268+ def test_fail_running_doesnt_answer(self):
269+ """Test that the merge actually fail when is_running() doesn't answer
270+ and don't stall."""
271+ # no "running" file
272+ self.generate_jenkins_mock_server(run_status='OK')
273+ self.assertRaises(JenkinsTimeout, self.plugin.run, command=self.command,
274+ target=self.target, source=None,
275+ proposal=self.proposal)
276+
277+ def test_pass(self):
278+ """Test that nothing is raised if all finished as excpected."""
279+ self.generate_jenkins_mock_server(run_status='OK', building = 'false',
280+ last_job_status='SUCCESS')
281+ self.plugin.run(command=self.command,
282+ target=self.target, source=None,
283+ proposal=self.proposal)
284+
285+ def test_jenkins_not_passed(self):
286+ """Test that the merge actually fail if jenkins last job failed"""
287+ self.generate_jenkins_mock_server(run_status='OK', building = 'false',
288+ last_job_status='FAILURE')
289+ self.assertRaises(JenkinsReject, self.plugin.run, command=self.command,
290+ target=self.target, source=None,
291+ proposal=self.proposal)
292+
293+ def test_no_jenkins_parameter(self):
294+ """Test that nothing happens if there is no jenkins parameter"""
295+ self.target = Thing(config=Thing())
296+ self.plugin.run(command=self.command, target=self.target, source=None,
297+ proposal=self.proposal)
298+
299+ def test_jenkins_fail_if_only_jenkins_url(self):
300+ """Test that jenkins fails if there is only the jenkins url but no other mandatory parameters"""
301+ self.target = Thing(config=Thing(jenkins_job_url="file://%s" % self.jenkins_dir))
302+ self.assertRaises(JenkinsTimeout, self.plugin.run, command=self.command,
303+ target=self.target, source=None,
304+ proposal=self.proposal)
305+
306+ def test_no_jenkins_public_url(self):
307+ """Test that if no public url is given to jenkins, the jenkins job is used"""
308+ config = Thing(jenkins_job_url="file://%s" % self.jenkins_dir,
309+ packaging_branch='lp:~foo/test/ubuntu',
310+ jenkins_token=self.jenkins_token)
311+ self.target = Thing(config=config, tree=Thing(abspath=os.path.abspath),
312+ bzr_branch=Thing(revno=lambda: 10))
313+ self.generate_jenkins_mock_server(run_status='OK', building = 'false',
314+ last_job_status='SUCCESS')
315+ self.plugin.run(command=self.command,
316+ target=self.target, source=None,
317+ proposal=self.proposal)
318+ self.assertEqual(self.plugin.jenkins_job.url, self.plugin.jenkins_job.public_url)
319+

Subscribers

People subscribed via source and target branches