2
# Software License Agreement (BSD License)
4
# Copyright (c) 2010, Willow Garage, Inc.
7
# Redistribution and use in source and binary forms, with or without
8
# modification, are permitted provided that the following conditions
11
# * Redistributions of source code must retain the above copyright
12
# notice, this list of conditions and the following disclaimer.
13
# * Redistributions in binary form must reproduce the above
14
# copyright notice, this list of conditions and the following
15
# disclaimer in the documentation and/or other materials provided
16
# with the distribution.
17
# * Neither the name of Willow Garage, Inc. nor the names of its
18
# contributors may be used to endorse or promote products derived
19
# from this software without specific prior written permission.
21
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
22
# 'AS IS' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
23
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
24
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
25
# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
26
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
27
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
28
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
29
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
30
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
31
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
32
# POSSIBILITY OF SUCH DAMAGE.
38
Python API for Jenkins
42
jenkins.create_job('empty', EMPTY_CONFIG_XML)
43
jenkins.disable_job('empty')
44
jenkins.copy_job('empty', 'empty_copy')
45
jenkins.enable_job('empty_copy')
46
jenkins.reconfig_job('empty_copy', RECONFIG_XML)
48
jenkins.delete_job('empty')
49
jenkins.delete_job('empty_copy')
51
# build a parameterized job
52
jenkins.build_job('api-test', {'param1': 'test value 1', 'param2': 'test value 2'})
62
JOB_INFO = 'job/%(name)s/api/python?depth=0'
63
Q_INFO = 'queue/api/python?depth=0'
64
CREATE_JOB = 'createItem?name=%(name)s' #also post config.xml
65
RECONFIG_JOB = 'job/%(name)s/config.xml'
66
DELETE_JOB = 'job/%(name)s/doDelete'
67
ENABLE_JOB = 'job/%(name)s/enable'
68
DISABLE_JOB = 'job/%(name)s/disable'
69
COPY_JOB = 'createItem?name=%(to_name)s&mode=copy&from=%(from_name)s'
70
BUILD_JOB = 'job/%(name)s/build'
71
BUILD_WITH_PARAMS_JOB = 'job/%(name)s/buildWithParameters'
74
CREATE_NODE = 'computer/doCreateItem?%s'
75
DELETE_NODE = 'computer/%(name)s/doDelete'
76
NODE_INFO = 'computer/%(name)s/api/python?depth=0'
77
NODE_TYPE = 'hudson.slaves.DumbSlave$DescriptorImpl'
81
EMPTY_CONFIG_XML = '''<?xml version='1.0' encoding='UTF-8'?>
83
<keepDependencies>false</keepDependencies>
85
<scm class='jenkins.scm.NullSCM'/>
86
<canRoam>true</canRoam>
87
<disabled>false</disabled>
88
<blockBuildWhenUpstreamBuilding>false</blockBuildWhenUpstreamBuilding>
89
<triggers class='vector'/>
90
<concurrentBuild>false</concurrentBuild>
97
RECONFIG_XML = '''<?xml version='1.0' encoding='UTF-8'?>
99
<keepDependencies>false</keepDependencies>
101
<scm class='jenkins.scm.NullSCM'/>
102
<canRoam>true</canRoam>
103
<disabled>false</disabled>
104
<blockBuildWhenUpstreamBuilding>false</blockBuildWhenUpstreamBuilding>
105
<triggers class='vector'/>
106
<concurrentBuild>false</concurrentBuild>
108
<jenkins.tasks.Shell>
109
<command>export FOO=bar</command>
110
</jenkins.tasks.Shell>
116
class JenkinsException(Exception): pass
118
def auth_headers(username, password):
120
Simple implementation of HTTP Basic Authentication. Returns the 'Authentication' header value.
122
return 'Basic ' + base64.encodestring('%s:%s' % (username, password))[:-1]
124
class Jenkins(object):
126
def __init__(self, url, username=None, password=None):
128
Create handle to Jenkins instance.
130
@param url: URL of Jenkins server
136
self.server = url + '/'
137
if username is not None and password is not None:
138
self.auth = auth_headers(username, password)
142
def get_job_info(self, name):
144
return eval(urllib2.urlopen(self.server + JOB_INFO%locals()).read())
146
raise JenkinsException('job[%s] does not exist'%name)
148
def debug_job_info(self, job_name):
150
Print out job info in more readable format
152
for k, v in self.get_job_info(job_name).iteritems():
155
def jenkins_open(self, req):
157
Utility routine for opening an HTTP request to a Jenkins server.
161
req.add_header('Authorization', self.auth)
162
return urllib2.urlopen(req).read()
163
except urllib2.HTTPError, e:
164
# Jenkins's funky authentication means its nigh impossible to distinguish errors.
165
if e.code in [401, 403, 500]:
166
raise JenkinsException('Error in request. Possibly authentication failed [%s]'%(e.code))
167
# right now I'm getting 302 infinites on a successful delete
169
def get_queue_info(self):
171
@return: list of job dictionaries
173
return eval(urllib2.urlopen(self.server + Q_INFO).read())['items']
175
def copy_job(self, from_name, to_name):
179
@param from_name: Name of Jenkins job to copy from
181
@param to_name: Name of Jenkins job to copy to
184
self.get_job_info(from_name)
185
self.jenkins_open(urllib2.Request(self.server + COPY_JOB%locals(), ''))
186
if not self.job_exists(to_name):
187
raise JenkinsException('create[%s] failed'%(to_name))
189
def delete_job(self, name):
191
Delete Jenkins job permanently.
193
@param name: Name of Jenkins job
196
self.get_job_info(name)
197
self.jenkins_open(urllib2.Request(self.server + DELETE_JOB%locals(), ''))
198
if self.job_exists(name):
199
raise JenkinsException('delete[%s] failed'%(name))
201
def enable_job(self, name):
205
@param name: Name of Jenkins job
208
self.get_job_info(name)
209
self.jenkins_open(urllib2.Request(self.server + ENABLE_JOB%locals(), ''))
211
def disable_job(self, name):
213
Disable Jenkins job. To re-enable, call enable_job().
215
@param name: Name of Jenkins job
218
self.get_job_info(name)
219
self.jenkins_open(urllib2.Request(self.server + DISABLE_JOB%locals(), ''))
221
def job_exists(self, name):
223
@param name: Name of Jenkins job
225
@return: True if Jenkins job exists
228
self.get_job_info(name)
233
def create_job(self, name, config_xml):
235
Create a new Jenkins job
237
@param name: Name of Jenkins job
239
@param config_xml: config file text
240
@type config_xml: str
242
if self.job_exists(name):
243
raise JenkinsException('job[%s] already exists'%(name))
245
headers = {'Content-Type': 'text/xml'}
246
self.jenkins_open(urllib2.Request(self.server + CREATE_JOB%locals(), config_xml, headers))
247
if not self.job_exists(name):
248
raise JenkinsException('create[%s] failed'%(name))
250
def reconfig_job(self, name, config_xml):
252
Change configuration of existing Jenkins job.
254
@param name: Name of Jenkins job
256
@param config_xml: New XML configuration
257
@type config_xml: str
259
self.get_job_info(name)
260
headers = {'Content-Type': 'text/xml'}
261
reconfig_url = self.server + RECONFIG_JOB%locals()
262
self.jenkins_open(urllib2.Request(reconfig_url, config_xml, headers))
264
def build_job_url(self, name, parameters=None, token=None):
266
@param parameters: parameters for job, or None.
267
@type parameters: dict
271
parameters['token'] = token
272
return self.server + BUILD_WITH_PARAMS_JOB%locals() + '?' + urllib.urlencode(parameters)
274
return self.server + BUILD_JOB%locals() + '?' + urllib.urlencode({'token': token})
276
return self.server + BUILD_JOB%locals()
278
def build_job(self, name, parameters=None, token=None):
280
@param parameters: parameters for job, or None.
281
@type parameters: dict
283
if not self.job_exists(name):
284
raise JenkinsException('no such job[%s]'%(name))
285
return self.jenkins_open(urllib2.Request(self.build_job_url(name, parameters, token), ''))
287
def get_node_info(self, name):
289
return eval(urllib2.urlopen(self.server + NODE_INFO%locals()).read())
291
raise JenkinsException('node[%s] does not exist'%name)
293
def node_exists(self, name):
295
@param name: Name of Jenkins node
297
@return: True if Jenkins node exists
300
self.get_node_info(name)
305
def delete_node(self, name):
307
Delete Jenkins node permanently.
309
@param name: Name of Jenkins node
312
self.get_node_info(name)
313
self.jenkins_open(urllib2.Request(self.server + DELETE_NODE%locals(), ''))
314
if self.node_exists(name):
315
raise JenkinsException('delete[%s] failed'%(name))
318
def create_node(self, name, numExecutors=2, nodeDescription=None,
319
remoteFS='/var/lib/jenkins', labels=None):
321
@param name: name of node to create
323
@param numExecutors: number of executors for node
324
@type numExecutors: int
325
@param nodeDescription: Description of node
327
@param remoteFS: Remote filesystem location to use
329
@param labels: Labels to associate with node
332
if self.node_exists(name):
333
raise JenkinsException('node[%s] already exists'%(name))
338
'json' : json.dumps ({
340
'nodeDescription' : nodeDescription,
341
'numExecutors' : numExecutors,
342
'remoteFS' : remoteFS,
343
'labelString' : labels,
344
'mode' : 'EXCLUSIVE',
346
'retentionStrategy' : { 'stapler-class' : 'hudson.slaves.RetentionStrategy$Always' },
347
'nodeProperties' : { 'stapler-class-bag' : 'true' },
348
'launcher' : { 'stapler-class' : 'hudson.slaves.JNLPLauncher' }
352
self.jenkins_open(urllib2.Request(self.server + CREATE_NODE%urllib.urlencode(params)))
353
if not self.node_exists(name):
354
raise JenkinsException('create[%s] failed'%(name))