~python-jenkins-developers/python-jenkins/0.2

« back to all changes in this revision

Viewing changes to jenkins.py

  • Committer: James Page
  • Date: 2011-06-03 15:51:39 UTC
  • Revision ID: james.page@canonical.com-20110603155139-lqmohda3rwcjnfwv
Initial version

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
#!/usr/bin/env python
 
2
# Software License Agreement (BSD License)
 
3
#
 
4
# Copyright (c) 2010, Willow Garage, Inc.
 
5
# All rights reserved.
 
6
#
 
7
# Redistribution and use in source and binary forms, with or without
 
8
# modification, are permitted provided that the following conditions
 
9
# are met:
 
10
#
 
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.
 
20
#
 
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.
 
33
#
 
34
# Revision $Id$
 
35
# $Author$
 
36
 
 
37
'''
 
38
Python API for Jenkins
 
39
 
 
40
Examples:
 
41
 
 
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)
 
47
 
 
48
    jenkins.delete_job('empty')
 
49
    jenkins.delete_job('empty_copy')
 
50
 
 
51
    # build a parameterized job
 
52
    jenkins.build_job('api-test', {'param1': 'test value 1', 'param2': 'test value 2'})
 
53
'''
 
54
 
 
55
import sys
 
56
import urllib2
 
57
import urllib
 
58
import base64
 
59
import traceback
 
60
import json
 
61
 
 
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'
 
72
 
 
73
 
 
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'
 
78
 
 
79
 
 
80
#for testing only
 
81
EMPTY_CONFIG_XML = '''<?xml version='1.0' encoding='UTF-8'?>
 
82
<project>
 
83
  <keepDependencies>false</keepDependencies>
 
84
  <properties/>
 
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>
 
91
  <builders/>
 
92
  <publishers/>
 
93
  <buildWrappers/>
 
94
</project>'''
 
95
 
 
96
#for testing only
 
97
RECONFIG_XML = '''<?xml version='1.0' encoding='UTF-8'?>
 
98
<project>
 
99
  <keepDependencies>false</keepDependencies>
 
100
  <properties/>
 
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>
 
107
<builders> 
 
108
    <jenkins.tasks.Shell> 
 
109
      <command>export FOO=bar</command> 
 
110
    </jenkins.tasks.Shell> 
 
111
  </builders> 
 
112
  <publishers/>
 
113
  <buildWrappers/>
 
114
</project>'''
 
115
 
 
116
class JenkinsException(Exception): pass
 
117
 
 
118
def auth_headers(username, password):
 
119
    '''
 
120
    Simple implementation of HTTP Basic Authentication. Returns the 'Authentication' header value.
 
121
    '''
 
122
    return 'Basic ' + base64.encodestring('%s:%s' % (username, password))[:-1]
 
123
 
 
124
class Jenkins(object):
 
125
    
 
126
    def __init__(self, url, username=None, password=None):
 
127
        '''
 
128
        Create handle to Jenkins instance.
 
129
 
 
130
        @param url: URL of Jenkins server
 
131
        @type  url: str
 
132
        '''
 
133
        if url[-1] == '/':
 
134
            self.server = url
 
135
        else:
 
136
            self.server = url + '/'
 
137
        if username is not None and password is not None:            
 
138
            self.auth = auth_headers(username, password)
 
139
        else:
 
140
            self.auth = None
 
141
        
 
142
    def get_job_info(self, name):
 
143
        try:
 
144
            return eval(urllib2.urlopen(self.server + JOB_INFO%locals()).read())
 
145
        except:
 
146
            raise JenkinsException('job[%s] does not exist'%name)
 
147
        
 
148
    def debug_job_info(self, job_name):
 
149
        '''
 
150
        Print out job info in more readable format
 
151
        '''
 
152
        for k, v in self.get_job_info(job_name).iteritems():
 
153
            print k, v
 
154
 
 
155
    def jenkins_open(self, req):
 
156
        '''
 
157
        Utility routine for opening an HTTP request to a Jenkins server. 
 
158
        '''
 
159
        try:
 
160
            if self.auth:
 
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
 
168
    
 
169
    def get_queue_info(self):
 
170
        '''
 
171
        @return: list of job dictionaries
 
172
        '''
 
173
        return eval(urllib2.urlopen(self.server + Q_INFO).read())['items']
 
174
 
 
175
    def copy_job(self, from_name, to_name):
 
176
        '''
 
177
        Copy a Jenkins job
 
178
 
 
179
        @param from_name: Name of Jenkins job to copy from
 
180
        @type  from_name: str
 
181
        @param to_name: Name of Jenkins job to copy to
 
182
        @type  to_name: str
 
183
        '''
 
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))
 
188
 
 
189
    def delete_job(self, name):
 
190
        '''
 
191
        Delete Jenkins job permanently.
 
192
        
 
193
        @param name: Name of Jenkins job
 
194
        @type  name: str
 
195
        '''
 
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))
 
200
    
 
201
    def enable_job(self, name):
 
202
        '''
 
203
        Enable Jenkins job.
 
204
 
 
205
        @param name: Name of Jenkins job
 
206
        @type  name: str
 
207
        '''
 
208
        self.get_job_info(name)
 
209
        self.jenkins_open(urllib2.Request(self.server + ENABLE_JOB%locals(), ''))
 
210
 
 
211
    def disable_job(self, name):
 
212
        '''
 
213
        Disable Jenkins job. To re-enable, call enable_job().
 
214
 
 
215
        @param name: Name of Jenkins job
 
216
        @type  name: str
 
217
        '''
 
218
        self.get_job_info(name)
 
219
        self.jenkins_open(urllib2.Request(self.server + DISABLE_JOB%locals(), ''))
 
220
 
 
221
    def job_exists(self, name):
 
222
        '''
 
223
        @param name: Name of Jenkins job
 
224
        @type  name: str
 
225
        @return: True if Jenkins job exists
 
226
        '''
 
227
        try:
 
228
            self.get_job_info(name)
 
229
            return True
 
230
        except:
 
231
            return False
 
232
 
 
233
    def create_job(self, name, config_xml):
 
234
        '''
 
235
        Create a new Jenkins job
 
236
 
 
237
        @param name: Name of Jenkins job
 
238
        @type  name: str
 
239
        @param config_xml: config file text
 
240
        @type  config_xml: str
 
241
        '''
 
242
        if self.job_exists(name):
 
243
            raise JenkinsException('job[%s] already exists'%(name))
 
244
 
 
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))
 
249
    
 
250
    def reconfig_job(self, name, config_xml):
 
251
        '''
 
252
        Change configuration of existing Jenkins job.
 
253
 
 
254
        @param name: Name of Jenkins job
 
255
        @type  name: str
 
256
        @param config_xml: New XML configuration
 
257
        @type  config_xml: str
 
258
        '''
 
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))
 
263
 
 
264
    def build_job_url(self, name, parameters=None, token=None):
 
265
        '''
 
266
        @param parameters: parameters for job, or None.
 
267
        @type  parameters: dict
 
268
        '''
 
269
        if parameters:
 
270
            if token:
 
271
                parameters['token'] = token
 
272
            return self.server + BUILD_WITH_PARAMS_JOB%locals() + '?' + urllib.urlencode(parameters)
 
273
        elif token:
 
274
            return self.server + BUILD_JOB%locals() + '?' + urllib.urlencode({'token': token})
 
275
        else:
 
276
            return self.server + BUILD_JOB%locals()
 
277
 
 
278
    def build_job(self, name, parameters=None, token=None):
 
279
        '''
 
280
        @param parameters: parameters for job, or None.
 
281
        @type  parameters: dict
 
282
        '''
 
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), ''))        
 
286
  
 
287
    def get_node_info(self, name):
 
288
        try:
 
289
            return eval(urllib2.urlopen(self.server + NODE_INFO%locals()).read())
 
290
        except:
 
291
            raise JenkinsException('node[%s] does not exist'%name)
 
292
 
 
293
    def node_exists(self, name):
 
294
        '''
 
295
        @param name: Name of Jenkins node 
 
296
        @type  name: str
 
297
        @return: True if Jenkins node exists
 
298
        '''
 
299
        try:
 
300
            self.get_node_info(name)
 
301
            return True
 
302
        except:
 
303
            return False
 
304
            
 
305
    def delete_node(self, name):
 
306
        '''
 
307
        Delete Jenkins node permanently.
 
308
        
 
309
        @param name: Name of Jenkins node
 
310
        @type  name: str
 
311
        '''
 
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))
 
316
    
 
317
    
 
318
    def create_node(self, name, numExecutors=2, nodeDescription=None,
 
319
                    remoteFS='/var/lib/jenkins', labels=None):
 
320
        '''
 
321
        @param name: name of node to create
 
322
        @type  name: str
 
323
        @param numExecutors: number of executors for node
 
324
        @type  numExecutors: int
 
325
        @param nodeDescription: Description of node
 
326
        @type  name: str
 
327
        @param remoteFS: Remote filesystem location to use
 
328
        @type  name: str
 
329
        @param labels: Labels to associate with node
 
330
        @type  name: str        
 
331
        '''
 
332
        if self.node_exists(name):
 
333
            raise JenkinsException('node[%s] already exists'%(name))
 
334
        
 
335
        params = {
 
336
            'name' : name,
 
337
            'type' : NODE_TYPE,
 
338
            'json' : json.dumps ({
 
339
                'name'            : name,
 
340
                'nodeDescription' : nodeDescription,
 
341
                'numExecutors'    : numExecutors,
 
342
                'remoteFS'        : remoteFS,
 
343
                'labelString'     : labels,
 
344
                'mode'            : 'EXCLUSIVE',
 
345
                'type'            : NODE_TYPE,
 
346
                'retentionStrategy' : { 'stapler-class'  : 'hudson.slaves.RetentionStrategy$Always' },
 
347
                'nodeProperties'    : { 'stapler-class-bag' : 'true' },
 
348
                'launcher'          : { 'stapler-class' : 'hudson.slaves.JNLPLauncher' }
 
349
            })
 
350
        }
 
351
        
 
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))
 
355