python-jenkins-developers team mailing list archive
-
python-jenkins-developers team
-
Mailing list archive
-
Message #00032
[Merge] lp:~kenrumer/python-jenkins/build_info into lp:python-jenkins
Ken Rumer has proposed merging lp:~kenrumer/python-jenkins/build_info into lp:python-jenkins.
Requested reviews:
Python Jenkins Developers (python-jenkins-developers)
For more details, see:
https://code.launchpad.net/~kenrumer/python-jenkins/build_info/+merge/94631
Gets build information from Jenkins. Can use to get the status of a build. Loop until the build is complete, return the artifact URL(s).
Example usage:
def start( **kwargs ):
"""
Start a build on the continuous integration server
Arguments:
service_name:
Name of the service/application
project_name:
Name of the project/software branch
vcs_server_type:
type of version control server "svn" or "git"
vcs_host:
host name of the vcs server
vcs_port:
port number for the vcs server
vcs_username:
username to authenticate vcs server
vcs_password:
password to authenticate vcs server
ci_host:
host name of the continuous integreation server
ci_port:
port number of the continuous integreation server
ci_username:
username to authenticate continuous integreation server
ci_password:
password to authenticate continuous integreation server
Outputs:
version:
the id defined for this build job. probably the timestamp of the build
build_number:
the build revision
timestamp:
the datestamp of the build
duration:
length of time the job took
result:
result of the build job
artifacts:
filename:
the filename the continuous integration server built
url:
retrieval location on the continuous integration server
"""
import jenkins
import time
j = jenkins.Jenkins('http://'+kwargs['ci_host']+':'+kwargs['ci_port'], kwargs['ci_username'], kwargs['ci_password'])
next_build_number = j.get_job_info('build_'+kwargs['vcs_server_type'])['nextBuildNumber']
params = {
'service_name': kwargs['service_name'],
'project_name': kwargs['project_name'],
'vcs_server_type': kwargs['vcs_server_type'],
'vcs_host': kwargs['vcs_host'],
'vcs_port': kwargs['vcs_port'],
'vcs_username': kwargs['vcs_username'],
'vcs_password': kwargs['vcs_password']
}
output = j.build_job('build_'+kwargs['vcs_server_type'], params)
last_completed_build_number = 0
last_successful_build_number = 0
while ( 1 ):
if (j.get_job_info('build_'+kwargs['vcs_server_type'])['lastCompletedBuild'] == None):
time.sleep(2)
continue
last_completed_build_number = j.get_job_info('build_'+kwargs['vcs_server_type'])['lastCompletedBuild']['number']
if (last_completed_build_number < next_build_number):
time.sleep(2)
continue
build_info = j.get_build_info('build_'+kwargs['vcs_server_type'], next_build_number)
break
artifacts = []
for artifact in build_info['artifacts']:
artifacts.append({ 'filename': artifact['fileName'], 'url': build_info['url'].encode('utf8')+'artifact/'+artifact['relativePath'].encode("utf8") })
return {'version': build_info['id'].encode('utf8'), 'build_number': build_info['number'], 'timestamp': build_info['timestamp'], 'duration': build_info['duration'], 'result': build_info['result'].encode("utf8"), 'artifacts': artifacts}
--
https://code.launchpad.net/~kenrumer/python-jenkins/build_info/+merge/94631
Your team Python Jenkins Developers is requested to review the proposed merge of lp:~kenrumer/python-jenkins/build_info into lp:python-jenkins.
=== modified file 'jenkins/__init__.py'
--- jenkins/__init__.py 2011-09-04 00:24:56 +0000
+++ jenkins/__init__.py 2012-02-24 23:20:22 +0000
@@ -1,429 +1,442 @@
-#!/usr/bin/env python
-# Software License Agreement (BSD License)
-#
-# Copyright (c) 2010, Willow Garage, Inc.
-# All rights reserved.
-#
-# Redistribution and use in source and binary forms, with or without
-# modification, are permitted provided that the following conditions
-# are met:
-#
-# * Redistributions of source code must retain the above copyright
-# notice, this list of conditions and the following disclaimer.
-# * Redistributions in binary form must reproduce the above
-# copyright notice, this list of conditions and the following
-# disclaimer in the documentation and/or other materials provided
-# with the distribution.
-# * Neither the name of Willow Garage, Inc. nor the names of its
-# contributors may be used to endorse or promote products derived
-# from this software without specific prior written permission.
-#
-# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
-# 'AS IS' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
-# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
-# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
-# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
-# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
-# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
-# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
-# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
-# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
-# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
-# POSSIBILITY OF SUCH DAMAGE.
-#
-# Authors:
-# Ken Conley <kwc@xxxxxxxxxxxxxxxx>
-# James Page <james.page@xxxxxxxxxxxxx>
-# Tully Foote <tfoote@xxxxxxxxxxxxxxxx>
-# Matthew Gertner <matthew.gertner@xxxxxxxxx>
-
-'''
-Python API for Jenkins
-
-Examples::
-
- j = jenkins.Jenkins('http://your_url_here', 'username', 'password')
- j.get_jobs()
- j.create_job('empty', jenkins.EMPTY_CONFIG_XML)
- j.disable_job('empty')
- j.copy_job('empty', 'empty_copy')
- j.enable_job('empty_copy')
- j.reconfig_job('empty_copy', jenkins.RECONFIG_XML)
-
- j.delete_job('empty')
- j.delete_job('empty_copy')
-
- # build a parameterized job
- j.build_job('api-test', {'param1': 'test value 1', 'param2': 'test value 2'})
-'''
-
-import sys
-import urllib2
-import urllib
-import base64
-import traceback
-import json
-import httplib
-
-INFO = 'api/json'
-JOB_INFO = 'job/%(name)s/api/json?depth=0'
-Q_INFO = 'queue/api/json?depth=0'
-CREATE_JOB = 'createItem?name=%(name)s' #also post config.xml
-CONFIG_JOB = 'job/%(name)s/config.xml'
-DELETE_JOB = 'job/%(name)s/doDelete'
-ENABLE_JOB = 'job/%(name)s/enable'
-DISABLE_JOB = 'job/%(name)s/disable'
-COPY_JOB = 'createItem?name=%(to_name)s&mode=copy&from=%(from_name)s'
-BUILD_JOB = 'job/%(name)s/build'
-BUILD_WITH_PARAMS_JOB = 'job/%(name)s/buildWithParameters'
-
-
-CREATE_NODE = 'computer/doCreateItem?%s'
-DELETE_NODE = 'computer/%(name)s/doDelete'
-NODE_INFO = 'computer/%(name)s/api/json?depth=0'
-NODE_TYPE = 'hudson.slaves.DumbSlave$DescriptorImpl'
-
-
-#for testing only
-EMPTY_CONFIG_XML = '''<?xml version='1.0' encoding='UTF-8'?>
-<project>
- <keepDependencies>false</keepDependencies>
- <properties/>
- <scm class='jenkins.scm.NullSCM'/>
- <canRoam>true</canRoam>
- <disabled>false</disabled>
- <blockBuildWhenUpstreamBuilding>false</blockBuildWhenUpstreamBuilding>
- <triggers class='vector'/>
- <concurrentBuild>false</concurrentBuild>
- <builders/>
- <publishers/>
- <buildWrappers/>
-</project>'''
-
-#for testing only
-RECONFIG_XML = '''<?xml version='1.0' encoding='UTF-8'?>
-<project>
- <keepDependencies>false</keepDependencies>
- <properties/>
- <scm class='jenkins.scm.NullSCM'/>
- <canRoam>true</canRoam>
- <disabled>false</disabled>
- <blockBuildWhenUpstreamBuilding>false</blockBuildWhenUpstreamBuilding>
- <triggers class='vector'/>
- <concurrentBuild>false</concurrentBuild>
-<builders>
- <jenkins.tasks.Shell>
- <command>export FOO=bar</command>
- </jenkins.tasks.Shell>
- </builders>
- <publishers/>
- <buildWrappers/>
-</project>'''
-
-class JenkinsException(Exception):
- '''
- General exception type for jenkins-API-related failures.
- '''
- pass
-
-def auth_headers(username, password):
- '''
- Simple implementation of HTTP Basic Authentication. Returns the 'Authentication' header value.
- '''
- return 'Basic ' + base64.encodestring('%s:%s' % (username, password))[:-1]
-
-class Jenkins(object):
-
- def __init__(self, url, username=None, password=None):
- '''
- Create handle to Jenkins instance.
-
- :param url: URL of Jenkins server, ``str``
- '''
- if url[-1] == '/':
- self.server = url
- else:
- self.server = url + '/'
- if username is not None and password is not None:
- self.auth = auth_headers(username, password)
- else:
- self.auth = None
-
- def get_job_info(self, name):
- '''
- Get job information dictionary.
-
- :param name: Job name, ``str``
- :returns: dictionary of job information
- '''
- try:
- response = self.jenkins_open(urllib2.Request(self.server + JOB_INFO%locals()))
- if response:
- return json.loads(response)
- else:
- raise JenkinsException('job[%s] does not exist'%name)
- except urllib2.HTTPError:
- raise JenkinsException('job[%s] does not exist'%name)
- except ValueError:
- raise JenkinsException("Could not parse JSON info for job[%s]"%name)
-
- def debug_job_info(self, job_name):
- '''
- Print out job info in more readable format
- '''
- for k, v in self.get_job_info(job_name).iteritems():
- print k, v
-
- def jenkins_open(self, req):
- '''
- Utility routine for opening an HTTP request to a Jenkins server. This should only be used
- to extends the :class:`Jenkins` API.
- '''
- try:
- if self.auth:
- req.add_header('Authorization', self.auth)
- return urllib2.urlopen(req).read()
- except urllib2.HTTPError, e:
- # Jenkins's funky authentication means its nigh impossible to distinguish errors.
- if e.code in [401, 403, 500]:
- raise JenkinsException('Error in request. Possibly authentication failed [%s]'%(e.code))
- # right now I'm getting 302 infinites on a successful delete
-
- def get_queue_info(self):
- '''
- :returns: list of job dictionaries, ``[dict]``
-
- Example::
- >>> queue_info = j.get_queue_info()
- >>> print(queue_info[0])
- {u'task': {u'url': u'http://your_url/job/my_job/', u'color': u'aborted_anime', u'name': u'my_job'}, u'stuck': False, u'actions': [{u'causes': [{u'shortDescription': u'Started by timer'}]}], u'buildable': False, u'params': u'', u'buildableStartMilliseconds': 1315087293316, u'why': u'Build #2,532 is already in progress (ETA:10 min)', u'blocked': True}
- '''
- return json.loads(self.jenkins_open(urllib2.Request(self.server + Q_INFO)))['items']
-
- def get_info(self):
- """
- Get information on this Master. This information
- includes job list and view information.
-
- :returns: dictionary of information about Master, ``dict``
-
- Example::
-
- >>> info = j.get_info()
- >>> jobs = info['jobs']
- >>> print(jobs[0])
- {u'url': u'http://your_url_here/job/my_job/', u'color': u'blue', u'name': u'my_job'}
-
- """
- try:
- return json.loads(self.jenkins_open(urllib2.Request(self.server + INFO)))
- except urllib2.HTTPError:
- raise JenkinsException("Error communicating with server[%s]"%self.server)
- except httplib.BadStatusLine:
- raise JenkinsException("Error communicating with server[%s]"%self.server)
- except ValueError:
- raise JenkinsException("Could not parse JSON info for server[%s]"%self.server)
-
- def get_jobs(self):
- """
- Get list of jobs running. Each job is a dictionary with
- 'name', 'url', and 'color' keys.
-
- :returns: list of jobs, ``[ { str: str} ]``
- """
- return self.get_info()['jobs']
-
- def copy_job(self, from_name, to_name):
- '''
- Copy a Jenkins job
-
- :param from_name: Name of Jenkins job to copy from, ``str``
- :param to_name: Name of Jenkins job to copy to, ``str``
- '''
- self.get_job_info(from_name)
- self.jenkins_open(urllib2.Request(self.server + COPY_JOB%locals(), ''))
- if not self.job_exists(to_name):
- raise JenkinsException('create[%s] failed'%(to_name))
-
- def delete_job(self, name):
- '''
- Delete Jenkins job permanently.
-
- :param name: Name of Jenkins job, ``str``
- '''
- self.get_job_info(name)
- self.jenkins_open(urllib2.Request(self.server + DELETE_JOB%locals(), ''))
- if self.job_exists(name):
- raise JenkinsException('delete[%s] failed'%(name))
-
- def enable_job(self, name):
- '''
- Enable Jenkins job.
-
- :param name: Name of Jenkins job, ``str``
- '''
- self.get_job_info(name)
- self.jenkins_open(urllib2.Request(self.server + ENABLE_JOB%locals(), ''))
-
- def disable_job(self, name):
- '''
- Disable Jenkins job. To re-enable, call :meth:`Jenkins.enable_job`.
-
- :param name: Name of Jenkins job, ``str``
- '''
- self.get_job_info(name)
- self.jenkins_open(urllib2.Request(self.server + DISABLE_JOB%locals(), ''))
-
- def job_exists(self, name):
- '''
- :param name: Name of Jenkins job, ``str``
- :returns: ``True`` if Jenkins job exists
- '''
- try:
- self.get_job_info(name)
- return True
- except JenkinsException:
- return False
-
- def create_job(self, name, config_xml):
- '''
- Create a new Jenkins job
-
- :param name: Name of Jenkins job, ``str``
- :param config_xml: config file text, ``str``
- '''
- if self.job_exists(name):
- raise JenkinsException('job[%s] already exists'%(name))
-
- headers = {'Content-Type': 'text/xml'}
- self.jenkins_open(urllib2.Request(self.server + CREATE_JOB%locals(), config_xml, headers))
- if not self.job_exists(name):
- raise JenkinsException('create[%s] failed'%(name))
-
- def get_job_config(self, name):
- '''
- Get configuration of existing Jenkins job.
-
- :param name: Name of Jenkins job, ``str``
- :returns: job configuration (XML format)
- '''
- get_config_url = self.server + CONFIG_JOB%locals()
- return self.jenkins_open(urllib2.Request(get_config_url))
-
- def reconfig_job(self, name, config_xml):
- '''
- Change configuration of existing Jenkins job. To create a new job, see :meth:`Jenkins.create_job`.
-
- :param name: Name of Jenkins job, ``str``
- :param config_xml: New XML configuration, ``str``
- '''
- self.get_job_info(name)
- headers = {'Content-Type': 'text/xml'}
- reconfig_url = self.server + CONFIG_JOB%locals()
- self.jenkins_open(urllib2.Request(reconfig_url, config_xml, headers))
-
- def build_job_url(self, name, parameters=None, token=None):
- '''
- Get URL to trigger build job. Authenticated setups may require configuring a token on the server side.
-
- :param parameters: parameters for job, or None., ``dict``
- :param token: (optional) token for building job, ``str``
- :returns: URL for building job
- '''
- if parameters:
- if token:
- parameters['token'] = token
- return self.server + BUILD_WITH_PARAMS_JOB%locals() + '?' + urllib.urlencode(parameters)
- elif token:
- return self.server + BUILD_JOB%locals() + '?' + urllib.urlencode({'token': token})
- else:
- return self.server + BUILD_JOB%locals()
-
- def build_job(self, name, parameters=None, token=None):
- '''
- Trigger build job.
-
- :param parameters: parameters for job, or ``None``, ``dict``
- '''
- if not self.job_exists(name):
- raise JenkinsException('no such job[%s]'%(name))
- return self.jenkins_open(urllib2.Request(self.build_job_url(name, parameters, token)))
-
- def get_node_info(self, name):
- '''
- Get node information dictionary
-
- :param name: Node name, ``str``
- :returns: Dictionary of node info, ``dict``
- '''
- try:
- response = self.jenkins_open(urllib2.Request(self.server + NODE_INFO%locals()))
- if response:
- return json.loads(response)
- else:
- raise JenkinsException('node[%s] does not exist'%name)
- except urllib2.HTTPError:
- raise JenkinsException('node[%s] does not exist'%name)
- except ValueError:
- raise JenkinsException("Could not parse JSON info for node[%s]"%name)
-
- def node_exists(self, name):
- '''
- :param name: Name of Jenkins node, ``str``
- :returns: ``True`` if Jenkins node exists
- '''
- try:
- self.get_node_info(name)
- return True
- except JenkinsException:
- return False
-
- def delete_node(self, name):
- '''
- Delete Jenkins node permanently.
-
- :param name: Name of Jenkins node, ``str``
- '''
- self.get_node_info(name)
- self.jenkins_open(urllib2.Request(self.server + DELETE_NODE%locals(), ''))
- if self.node_exists(name):
- raise JenkinsException('delete[%s] failed'%(name))
-
-
- def create_node(self, name, numExecutors=2, nodeDescription=None,
- remoteFS='/var/lib/jenkins', labels=None, exclusive=False):
- '''
- :param name: name of node to create, ``str``
- :param numExecutors: number of executors for node, ``int``
- :param nodeDescription: Description of node, ``str``
- :param remoteFS: Remote filesystem location to use, ``str``
- :param labels: Labels to associate with node, ``str``
- :param exclusive: Use this node for tied jobs only, ``bool``
- '''
- if self.node_exists(name):
- raise JenkinsException('node[%s] already exists'%(name))
-
- mode = 'NORMAL'
- if exclusive:
- mode = 'EXCLUSIVE'
-
- params = {
- 'name' : name,
- 'type' : NODE_TYPE,
- 'json' : json.dumps ({
- 'name' : name,
- 'nodeDescription' : nodeDescription,
- 'numExecutors' : numExecutors,
- 'remoteFS' : remoteFS,
- 'labelString' : labels,
- 'mode' : mode,
- 'type' : NODE_TYPE,
- 'retentionStrategy' : { 'stapler-class' : 'hudson.slaves.RetentionStrategy$Always' },
- 'nodeProperties' : { 'stapler-class-bag' : 'true' },
- 'launcher' : { 'stapler-class' : 'hudson.slaves.JNLPLauncher' }
- })
- }
-
- self.jenkins_open(urllib2.Request(self.server + CREATE_NODE%urllib.urlencode(params)))
- if not self.node_exists(name):
- raise JenkinsException('create[%s] failed'%(name))
+#!/usr/bin/env python
+# Software License Agreement (BSD License)
+#
+# Copyright (c) 2010, Willow Garage, Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following
+# disclaimer in the documentation and/or other materials provided
+# with the distribution.
+# * Neither the name of Willow Garage, Inc. nor the names of its
+# contributors may be used to endorse or promote products derived
+# from this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# 'AS IS' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+#
+# Authors:
+# Ken Conley <kwc@xxxxxxxxxxxxxxxx>
+# James Page <james.page@xxxxxxxxxxxxx>
+# Tully Foote <tfoote@xxxxxxxxxxxxxxxx>
+# Matthew Gertner <matthew.gertner@xxxxxxxxx>
+
+'''
+Python API for Jenkins
+
+Examples::
+
+ j = jenkins.Jenkins('http://your_url_here', 'username', 'password')
+ j.get_jobs()
+ j.create_job('empty', jenkins.EMPTY_CONFIG_XML)
+ j.disable_job('empty')
+ j.copy_job('empty', 'empty_copy')
+ j.enable_job('empty_copy')
+ j.reconfig_job('empty_copy', jenkins.RECONFIG_XML)
+
+ j.delete_job('empty')
+ j.delete_job('empty_copy')
+
+ # build a parameterized job
+ j.build_job('api-test', {'param1': 'test value 1', 'param2': 'test value 2'})
+'''
+
+import sys
+import urllib2
+import urllib
+import base64
+import traceback
+import json
+import httplib
+
+INFO = 'api/json'
+JOB_INFO = 'job/%(name)s/api/json?depth=0'
+Q_INFO = 'queue/api/json?depth=0'
+CREATE_JOB = 'createItem?name=%(name)s' #also post config.xml
+CONFIG_JOB = 'job/%(name)s/config.xml'
+DELETE_JOB = 'job/%(name)s/doDelete'
+ENABLE_JOB = 'job/%(name)s/enable'
+DISABLE_JOB = 'job/%(name)s/disable'
+COPY_JOB = 'createItem?name=%(to_name)s&mode=copy&from=%(from_name)s'
+BUILD_JOB = 'job/%(name)s/build'
+BUILD_WITH_PARAMS_JOB = 'job/%(name)s/buildWithParameters'
+BUILD_INFO = 'job/%(name)s/%(number)d/api/json?depth=0'
+
+
+CREATE_NODE = 'computer/doCreateItem?%s'
+DELETE_NODE = 'computer/%(name)s/doDelete'
+NODE_INFO = 'computer/%(name)s/api/json?depth=0'
+NODE_TYPE = 'hudson.slaves.DumbSlave$DescriptorImpl'
+
+
+#for testing only
+EMPTY_CONFIG_XML = '''<?xml version='1.0' encoding='UTF-8'?>
+<project>
+ <keepDependencies>false</keepDependencies>
+ <properties/>
+ <scm class='jenkins.scm.NullSCM'/>
+ <canRoam>true</canRoam>
+ <disabled>false</disabled>
+ <blockBuildWhenUpstreamBuilding>false</blockBuildWhenUpstreamBuilding>
+ <triggers class='vector'/>
+ <concurrentBuild>false</concurrentBuild>
+ <builders/>
+ <publishers/>
+ <buildWrappers/>
+</project>'''
+
+#for testing only
+RECONFIG_XML = '''<?xml version='1.0' encoding='UTF-8'?>
+<project>
+ <keepDependencies>false</keepDependencies>
+ <properties/>
+ <scm class='jenkins.scm.NullSCM'/>
+ <canRoam>true</canRoam>
+ <disabled>false</disabled>
+ <blockBuildWhenUpstreamBuilding>false</blockBuildWhenUpstreamBuilding>
+ <triggers class='vector'/>
+ <concurrentBuild>false</concurrentBuild>
+<builders>
+ <jenkins.tasks.Shell>
+ <command>export FOO=bar</command>
+ </jenkins.tasks.Shell>
+ </builders>
+ <publishers/>
+ <buildWrappers/>
+</project>'''
+
+class JenkinsException(Exception):
+ '''
+ General exception type for jenkins-API-related failures.
+ '''
+ pass
+
+def auth_headers(username, password):
+ '''
+ Simple implementation of HTTP Basic Authentication. Returns the 'Authentication' header value.
+ '''
+ return 'Basic ' + base64.encodestring('%s:%s' % (username, password))[:-1]
+
+class Jenkins(object):
+
+ def __init__(self, url, username=None, password=None):
+ '''
+ Create handle to Jenkins instance.
+
+ :param url: URL of Jenkins server, ``str``
+ '''
+ if url[-1] == '/':
+ self.server = url
+ else:
+ self.server = url + '/'
+ if username is not None and password is not None:
+ self.auth = auth_headers(username, password)
+ else:
+ self.auth = None
+
+ def get_job_info(self, name):
+ '''
+ Get job information dictionary.
+
+ :param name: Job name, ``str``
+ :returns: dictionary of job information
+ '''
+ try:
+ response = self.jenkins_open(urllib2.Request(self.server + JOB_INFO%locals()))
+ if response:
+ return json.loads(response)
+ else:
+ raise JenkinsException('job[%s] does not exist'%name)
+ except urllib2.HTTPError:
+ raise JenkinsException('job[%s] does not exist'%name)
+ except ValueError:
+ raise JenkinsException("Could not parse JSON info for job[%s]"%name)
+
+ def debug_job_info(self, job_name):
+ '''
+ Print out job info in more readable format
+ '''
+ for k, v in self.get_job_info(job_name).iteritems():
+ print k, v
+
+ def jenkins_open(self, req):
+ '''
+ Utility routine for opening an HTTP request to a Jenkins server. This should only be used
+ to extends the :class:`Jenkins` API.
+ '''
+ try:
+ if self.auth:
+ req.add_header('Authorization', self.auth)
+ return urllib2.urlopen(req).read()
+ except urllib2.HTTPError, e:
+ # Jenkins's funky authentication means its nigh impossible to distinguish errors.
+ if e.code in [401, 403, 500]:
+ raise JenkinsException('Error in request. Possibly authentication failed [%s]'%(e.code))
+ # right now I'm getting 302 infinites on a successful delete
+
+ def get_build_info(self, name, number):
+ try:
+ response = self.jenkins_open(urllib2.Request(self.server + BUILD_INFO%locals()))
+ if response:
+ return json.loads(response)
+ else:
+ raise JenkinsException('job[!s] number[!d] does not exist'.format(name, number))
+ except urllib2.HTTPError:
+ raise JenkinsException('job[!s] number[!d] does not exist'.format(name, number))
+ except ValueError:
+ raise JenkinsException("Could not parse JSON info for job[!s] number[!d]".format(name, number))
+
+ def get_queue_info(self):
+ '''
+ :returns: list of job dictionaries, ``[dict]``
+
+ Example::
+ >>> queue_info = j.get_queue_info()
+ >>> print(queue_info[0])
+ {u'task': {u'url': u'http://your_url/job/my_job/', u'color': u'aborted_anime', u'name': u'my_job'}, u'stuck': False, u'actions': [{u'causes': [{u'shortDescription': u'Started by timer'}]}], u'buildable': False, u'params': u'', u'buildableStartMilliseconds': 1315087293316, u'why': u'Build #2,532 is already in progress (ETA:10 min)', u'blocked': True}
+ '''
+ return json.loads(self.jenkins_open(urllib2.Request(self.server + Q_INFO)))['items']
+
+ def get_info(self):
+ """
+ Get information on this Master. This information
+ includes job list and view information.
+
+ :returns: dictionary of information about Master, ``dict``
+
+ Example::
+
+ >>> info = j.get_info()
+ >>> jobs = info['jobs']
+ >>> print(jobs[0])
+ {u'url': u'http://your_url_here/job/my_job/', u'color': u'blue', u'name': u'my_job'}
+
+ """
+ try:
+ return json.loads(self.jenkins_open(urllib2.Request(self.server + INFO)))
+ except urllib2.HTTPError:
+ raise JenkinsException("Error communicating with server[%s]"%self.server)
+ except httplib.BadStatusLine:
+ raise JenkinsException("Error communicating with server[%s]"%self.server)
+ except ValueError:
+ raise JenkinsException("Could not parse JSON info for server[%s]"%self.server)
+
+ def get_jobs(self):
+ """
+ Get list of jobs running. Each job is a dictionary with
+ 'name', 'url', and 'color' keys.
+
+ :returns: list of jobs, ``[ { str: str} ]``
+ """
+ return self.get_info()['jobs']
+
+ def copy_job(self, from_name, to_name):
+ '''
+ Copy a Jenkins job
+
+ :param from_name: Name of Jenkins job to copy from, ``str``
+ :param to_name: Name of Jenkins job to copy to, ``str``
+ '''
+ self.get_job_info(from_name)
+ self.jenkins_open(urllib2.Request(self.server + COPY_JOB%locals(), ''))
+ if not self.job_exists(to_name):
+ raise JenkinsException('create[%s] failed'%(to_name))
+
+ def delete_job(self, name):
+ '''
+ Delete Jenkins job permanently.
+
+ :param name: Name of Jenkins job, ``str``
+ '''
+ self.get_job_info(name)
+ self.jenkins_open(urllib2.Request(self.server + DELETE_JOB%locals(), ''))
+ if self.job_exists(name):
+ raise JenkinsException('delete[%s] failed'%(name))
+
+ def enable_job(self, name):
+ '''
+ Enable Jenkins job.
+
+ :param name: Name of Jenkins job, ``str``
+ '''
+ self.get_job_info(name)
+ self.jenkins_open(urllib2.Request(self.server + ENABLE_JOB%locals(), ''))
+
+ def disable_job(self, name):
+ '''
+ Disable Jenkins job. To re-enable, call :meth:`Jenkins.enable_job`.
+
+ :param name: Name of Jenkins job, ``str``
+ '''
+ self.get_job_info(name)
+ self.jenkins_open(urllib2.Request(self.server + DISABLE_JOB%locals(), ''))
+
+ def job_exists(self, name):
+ '''
+ :param name: Name of Jenkins job, ``str``
+ :returns: ``True`` if Jenkins job exists
+ '''
+ try:
+ self.get_job_info(name)
+ return True
+ except JenkinsException:
+ return False
+
+ def create_job(self, name, config_xml):
+ '''
+ Create a new Jenkins job
+
+ :param name: Name of Jenkins job, ``str``
+ :param config_xml: config file text, ``str``
+ '''
+ if self.job_exists(name):
+ raise JenkinsException('job[%s] already exists'%(name))
+
+ headers = {'Content-Type': 'text/xml'}
+ self.jenkins_open(urllib2.Request(self.server + CREATE_JOB%locals(), config_xml, headers))
+ if not self.job_exists(name):
+ raise JenkinsException('create[%s] failed'%(name))
+
+ def get_job_config(self, name):
+ '''
+ Get configuration of existing Jenkins job.
+
+ :param name: Name of Jenkins job, ``str``
+ :returns: job configuration (XML format)
+ '''
+ get_config_url = self.server + CONFIG_JOB%locals()
+ return self.jenkins_open(urllib2.Request(get_config_url))
+
+ def reconfig_job(self, name, config_xml):
+ '''
+ Change configuration of existing Jenkins job. To create a new job, see :meth:`Jenkins.create_job`.
+
+ :param name: Name of Jenkins job, ``str``
+ :param config_xml: New XML configuration, ``str``
+ '''
+ self.get_job_info(name)
+ headers = {'Content-Type': 'text/xml'}
+ reconfig_url = self.server + CONFIG_JOB%locals()
+ self.jenkins_open(urllib2.Request(reconfig_url, config_xml, headers))
+
+ def build_job_url(self, name, parameters=None, token=None):
+ '''
+ Get URL to trigger build job. Authenticated setups may require configuring a token on the server side.
+
+ :param parameters: parameters for job, or None., ``dict``
+ :param token: (optional) token for building job, ``str``
+ :returns: URL for building job
+ '''
+ if parameters:
+ if token:
+ parameters['token'] = token
+ return self.server + BUILD_WITH_PARAMS_JOB%locals() + '?' + urllib.urlencode(parameters)
+ elif token:
+ return self.server + BUILD_JOB%locals() + '?' + urllib.urlencode({'token': token})
+ else:
+ return self.server + BUILD_JOB%locals()
+
+ def build_job(self, name, parameters=None, token=None):
+ '''
+ Trigger build job.
+
+ :param parameters: parameters for job, or ``None``, ``dict``
+ '''
+ if not self.job_exists(name):
+ raise JenkinsException('no such job[%s]'%(name))
+ return self.jenkins_open(urllib2.Request(self.build_job_url(name, parameters, token)))
+
+ def get_node_info(self, name):
+ '''
+ Get node information dictionary
+
+ :param name: Node name, ``str``
+ :returns: Dictionary of node info, ``dict``
+ '''
+ try:
+ response = self.jenkins_open(urllib2.Request(self.server + NODE_INFO%locals()))
+ if response:
+ return json.loads(response)
+ else:
+ raise JenkinsException('node[%s] does not exist'%name)
+ except urllib2.HTTPError:
+ raise JenkinsException('node[%s] does not exist'%name)
+ except ValueError:
+ raise JenkinsException("Could not parse JSON info for node[%s]"%name)
+
+ def node_exists(self, name):
+ '''
+ :param name: Name of Jenkins node, ``str``
+ :returns: ``True`` if Jenkins node exists
+ '''
+ try:
+ self.get_node_info(name)
+ return True
+ except JenkinsException:
+ return False
+
+ def delete_node(self, name):
+ '''
+ Delete Jenkins node permanently.
+
+ :param name: Name of Jenkins node, ``str``
+ '''
+ self.get_node_info(name)
+ self.jenkins_open(urllib2.Request(self.server + DELETE_NODE%locals(), ''))
+ if self.node_exists(name):
+ raise JenkinsException('delete[%s] failed'%(name))
+
+
+ def create_node(self, name, numExecutors=2, nodeDescription=None,
+ remoteFS='/var/lib/jenkins', labels=None, exclusive=False):
+ '''
+ :param name: name of node to create, ``str``
+ :param numExecutors: number of executors for node, ``int``
+ :param nodeDescription: Description of node, ``str``
+ :param remoteFS: Remote filesystem location to use, ``str``
+ :param labels: Labels to associate with node, ``str``
+ :param exclusive: Use this node for tied jobs only, ``bool``
+ '''
+ if self.node_exists(name):
+ raise JenkinsException('node[%s] already exists'%(name))
+
+ mode = 'NORMAL'
+ if exclusive:
+ mode = 'EXCLUSIVE'
+
+ params = {
+ 'name' : name,
+ 'type' : NODE_TYPE,
+ 'json' : json.dumps ({
+ 'name' : name,
+ 'nodeDescription' : nodeDescription,
+ 'numExecutors' : numExecutors,
+ 'remoteFS' : remoteFS,
+ 'labelString' : labels,
+ 'mode' : mode,
+ 'type' : NODE_TYPE,
+ 'retentionStrategy' : { 'stapler-class' : 'hudson.slaves.RetentionStrategy$Always' },
+ 'nodeProperties' : { 'stapler-class-bag' : 'true' },
+ 'launcher' : { 'stapler-class' : 'hudson.slaves.JNLPLauncher' }
+ })
+ }
+
+ self.jenkins_open(urllib2.Request(self.server + CREATE_NODE%urllib.urlencode(params)))
+ if not self.node_exists(name):
+ raise JenkinsException('create[%s] failed'%(name))