diff --git a/.pep8 b/.pep8 new file mode 100644 index 0000000..70d9f64 --- /dev/null +++ b/.pep8 @@ -0,0 +1,2 @@ +[pep8] +ignore = E221,E501 diff --git a/doc/conf.py b/doc/conf.py index 26f65bc..a39a6a4 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -3,7 +3,8 @@ # Python Jenkins documentation build configuration file, created by # sphinx-quickstart on Sat Sep 3 16:24:58 2011. # -# This file is execfile()d with the current directory set to its containing dir. +# This file is execfile()d with the current directory set to its containing +# dir. # # Note that not all possible configuration values are present in this # autogenerated file. @@ -11,9 +12,9 @@ # All configuration values have a default; values that are commented out # serve to show the default. -import sys, os +import os +import sys -import sys, os sys.path.insert(0, os.path.abspath('..')) # If extensions (or modules to document with autodoc) are in another directory, @@ -21,13 +22,13 @@ sys.path.insert(0, os.path.abspath('..')) # documentation root, use os.path.abspath to make it absolute, like shown here. #sys.path.insert(0, os.path.abspath('.')) -# -- General configuration ----------------------------------------------------- +# -- General configuration ---------------------------------------------------- # If your documentation needs a minimal Sphinx version, state it here. #needs_sphinx = '1.0' -# Add any Sphinx extension module names here, as strings. They can be extensions -# coming with Sphinx (named 'sphinx.ext.*') or your custom ones. +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = ['sphinx.ext.autodoc'] # Add any paths that contain templates here, relative to this directory. @@ -69,7 +70,7 @@ release = '0.2' # directories to ignore when looking for source files. exclude_patterns = ['_build'] -# The reST default role (used for this markup: `text`) to use for all documents. +# The reST default role (used for this markup: `text`) to use for all documents #default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. @@ -90,7 +91,7 @@ pygments_style = 'sphinx' #modindex_common_prefix = [] -# -- Options for HTML output --------------------------------------------------- +# -- Options for HTML output -------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. @@ -170,7 +171,7 @@ html_static_path = ['_static'] htmlhelp_basename = 'PythonJenkinsdoc' -# -- Options for LaTeX output -------------------------------------------------- +# -- Options for LaTeX output ------------------------------------------------- # The paper size ('letter' or 'a4'). #latex_paper_size = 'letter' @@ -179,10 +180,10 @@ htmlhelp_basename = 'PythonJenkinsdoc' #latex_font_size = '10pt' # Grouping the document tree into LaTeX files. List of tuples -# (source start file, target name, title, author, documentclass [howto/manual]). +# (source start file, target name, title, author, documentclass [howto/manual]) latex_documents = [ - ('index', 'PythonJenkins.tex', u'Python Jenkins Documentation', - u'Ken Conley, James Page, Tully Foote, Matthew Gertner', 'manual'), + ('index', 'PythonJenkins.tex', u'Python Jenkins Documentation', + u'Ken Conley, James Page, Tully Foote, Matthew Gertner', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of @@ -209,7 +210,7 @@ latex_documents = [ #latex_domain_indices = True -# -- Options for manual page output -------------------------------------------- +# -- Options for manual page output ------------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). diff --git a/jenkins/__init__.py b/jenkins/__init__.py index 5e7a3b1..bcea746 100644 --- a/jenkins/__init__.py +++ b/jenkins/__init__.py @@ -54,41 +54,44 @@ Examples:: j.delete_job('empty_copy') # build a parameterized job - j.build_job('api-test', {'param1': 'test value 1', 'param2': 'test value 2'}) + j.build_job('api-test', { + 'param1': 'test value 1', + 'param2': 'test value 2', + }) ''' -import sys +#import sys import urllib2 import urllib import base64 -import traceback +#import traceback import json import httplib -LAUNCHER_SSH = 'hudson.plugins.sshslaves.SSHLauncher' -LAUNCHER_COMMAND = 'hudson.slaves.CommandLauncher' +LAUNCHER_SSH = 'hudson.plugins.sshslaves.SSHLauncher' +LAUNCHER_COMMAND = 'hudson.slaves.CommandLauncher' LAUNCHER_WINDOWS_SERVICE = 'hudson.os.windows.ManagedWindowsServiceLauncher' -INFO = 'api/json' -JOB_INFO = 'job/%(name)s/api/json?depth=0' -Q_INFO = 'queue/api/json?depth=0' +INFO = 'api/json' +JOB_INFO = 'job/%(name)s/api/json?depth=0' +Q_INFO = 'queue/api/json?depth=0' CANCEL_QUEUE = 'queue/item/%(number)s/cancelQueue' -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' -STOP_BUILD = 'job/%(name)s/%(number)s/stop' +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' +STOP_BUILD = 'job/%(name)s/%(number)s/stop' BUILD_WITH_PARAMS_JOB = 'job/%(name)s/buildWithParameters' -BUILD_INFO = 'job/%(name)s/%(number)d/api/json?depth=0' +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' +NODE_INFO = 'computer/%(name)s/api/json?depth=0' +NODE_TYPE = 'hudson.slaves.DumbSlave$DescriptorImpl' TOGGLE_OFFLINE = 'computer/%(name)s/toggleOffline?offlineMessage=%(msg)s' #for testing only @@ -118,27 +121,31 @@ RECONFIG_XML = ''' false false - - - export FOO=bar - - + + + export FOO=bar + + ''' + 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. + 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): @@ -164,15 +171,17 @@ class Jenkins(object): :returns: dictionary of job information ''' try: - response = self.jenkins_open(urllib2.Request(self.server + JOB_INFO%locals())) + 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) + raise JenkinsException('job[%s] does not exist' % name) except urllib2.HTTPError: - raise JenkinsException('job[%s] does not exist'%name) + raise JenkinsException('job[%s] does not exist' % name) except ValueError: - raise JenkinsException("Could not parse JSON info for job[%s]"%name) + raise JenkinsException( + "Could not parse JSON info for job[%s]" % name) def debug_job_info(self, job_name): ''' @@ -183,17 +192,21 @@ class Jenkins(object): 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. + 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. + # 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)) + 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): @@ -214,15 +227,21 @@ class Jenkins(object): {u'building': False, u'changeSet': {u'items': [{u'date': u'2011-12-19T18:01:52.540557Z', u'msg': u'test', u'revision': 66, u'user': u'unknown', u'paths': [{u'editType': u'edit', u'file': u'/branches/demo/index.html'}]}], u'kind': u'svn', u'revisions': [{u'module': u'http://eaas-svn01.i3.level3.com/eaas', u'revision': 66}]}, u'builtOn': u'', u'description': None, u'artifacts': [{u'relativePath': u'dist/eaas-87-2011-12-19_18-01-57.war', u'displayPath': u'eaas-87-2011-12-19_18-01-57.war', u'fileName': u'eaas-87-2011-12-19_18-01-57.war'}, {u'relativePath': u'dist/eaas-87-2011-12-19_18-01-57.war.zip', u'displayPath': u'eaas-87-2011-12-19_18-01-57.war.zip', u'fileName': u'eaas-87-2011-12-19_18-01-57.war.zip'}], u'timestamp': 1324317717000, u'number': 87, u'actions': [{u'parameters': [{u'name': u'SERVICE_NAME', u'value': u'eaas'}, {u'name': u'PROJECT_NAME', u'value': u'demo'}]}, {u'causes': [{u'userName': u'anonymous', u'shortDescription': u'Started by user anonymous'}]}, {}, {}, {}], u'id': u'2011-12-19_18-01-57', u'keepLog': False, u'url': u'http://eaas-jenkins01.i3.level3.com:9080/job/build_war/87/', u'culprits': [{u'absoluteUrl': u'http://eaas-jenkins01.i3.level3.com:9080/user/unknown', u'fullName': u'unknown'}], u'result': u'SUCCESS', u'duration': 8826, u'fullDisplayName': u'build_war #87'} ''' try: - response = self.jenkins_open(urllib2.Request(self.server + BUILD_INFO%locals())) + 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'%(name, number)) + raise JenkinsException('job[%s] number[%d] does not exist' + % (name, number)) except urllib2.HTTPError: - raise JenkinsException('job[%s] number[%d] does not exist'%(name, number)) + raise JenkinsException('job[%s] number[%d] does not exist' + % (name, number)) except ValueError: - raise JenkinsException('Could not parse JSON info for job[%s] number[%d]'%(name, number)) + raise JenkinsException( + 'Could not parse JSON info for job[%s] number[%d]' + % (name, number) + ) def get_queue_info(self): ''' @@ -233,7 +252,9 @@ class Jenkins(object): >>> 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'] + return json.loads(self.jenkins_open( + urllib2.Request(self.server + Q_INFO) + ))['items'] def cancel_queue(self, number): ''' @@ -259,17 +280,22 @@ class Jenkins(object): >>> 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'} + {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))) + return json.loads(self.jenkins_open( + urllib2.Request(self.server + INFO))) except urllib2.HTTPError: - raise JenkinsException("Error communicating with server[%s]"%self.server) + raise JenkinsException("Error communicating with server[%s]" + % self.server) except httplib.BadStatusLine: - raise JenkinsException("Error communicating with server[%s]"%self.server) + raise JenkinsException("Error communicating with server[%s]" + % self.server) except ValueError: - raise JenkinsException("Could not parse JSON info for server[%s]"%self.server) + raise JenkinsException("Could not parse JSON info for server[%s]" + % self.server) def get_jobs(self): """ @@ -288,20 +314,22 @@ class Jenkins(object): :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(), '')) + self.jenkins_open(urllib2.Request( + self.server + COPY_JOB % locals(), '')) if not self.job_exists(to_name): - raise JenkinsException('create[%s] failed'%(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(), '')) + self.jenkins_open(urllib2.Request( + self.server + DELETE_JOB % locals(), '')) if self.job_exists(name): - raise JenkinsException('delete[%s] failed'%(name)) + raise JenkinsException('delete[%s] failed' % (name)) def enable_job(self, name): ''' @@ -310,7 +338,8 @@ class Jenkins(object): :param name: Name of Jenkins job, ``str`` ''' self.get_job_info(name) - self.jenkins_open(urllib2.Request(self.server + ENABLE_JOB%locals(), '')) + self.jenkins_open(urllib2.Request( + self.server + ENABLE_JOB % locals(), '')) def disable_job(self, name): ''' @@ -319,7 +348,8 @@ class Jenkins(object): :param name: Name of Jenkins job, ``str`` ''' self.get_job_info(name) - self.jenkins_open(urllib2.Request(self.server + DISABLE_JOB%locals(), '')) + self.jenkins_open(urllib2.Request( + self.server + DISABLE_JOB % locals(), '')) def job_exists(self, name): ''' @@ -340,12 +370,13 @@ class Jenkins(object): :param config_xml: config file text, ``str`` ''' if self.job_exists(name): - raise JenkinsException('job[%s] already 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)) + 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)) + raise JenkinsException('create[%s] failed' % (name)) def get_job_config(self, name): ''' @@ -360,20 +391,22 @@ class Jenkins(object): def reconfig_job(self, name, config_xml): ''' - Change configuration of existing Jenkins job. To create a new job, see :meth:`Jenkins.create_job`. + 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() + 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. - + 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 @@ -381,21 +414,24 @@ class Jenkins(object): if parameters: if token: parameters['token'] = token - return self.server + BUILD_WITH_PARAMS_JOB%locals() + '?' + urllib.urlencode(parameters) + return (self.server + BUILD_WITH_PARAMS_JOB % locals() + + '?' + urllib.urlencode(parameters)) elif token: - return self.server + BUILD_JOB%locals() + '?' + urllib.urlencode({'token': token}) + return (self.server + BUILD_JOB % locals() + + '?' + urllib.urlencode({'token': token})) else: - return self.server + BUILD_JOB%locals() + 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))) + raise JenkinsException('no such job[%s]' % (name)) + return self.jenkins_open(urllib2.Request( + self.build_job_url(name, parameters, token))) def stop_build(self, name, number): ''' @@ -414,15 +450,17 @@ class Jenkins(object): :returns: Dictionary of node info, ``dict`` ''' try: - response = self.jenkins_open(urllib2.Request(self.server + NODE_INFO%locals())) + 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) + raise JenkinsException('node[%s] does not exist' % name) except urllib2.HTTPError: - raise JenkinsException('node[%s] does not exist'%name) + raise JenkinsException('node[%s] does not exist' % name) except ValueError: - raise JenkinsException("Could not parse JSON info for node[%s]"%name) + raise JenkinsException("Could not parse JSON info for node[%s]" + % name) def node_exists(self, name): ''' @@ -438,38 +476,40 @@ class Jenkins(object): 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(), '')) + self.jenkins_open(urllib2.Request( + self.server + DELETE_NODE % locals(), '')) if self.node_exists(name): - raise JenkinsException('delete[%s] failed'%(name)) - + raise JenkinsException('delete[%s] failed' % (name)) def disable_node(self, name, msg=''): ''' Disable a node - + :param name: Jenkins node name, ``str`` :param msg: Offline message, ``str`` ''' info = self.get_node_info(name) if info['offline']: return - self.jenkins_open(urllib2.Request(self.server + TOGGLE_OFFLINE%locals())) + self.jenkins_open(urllib2.Request( + self.server + TOGGLE_OFFLINE % locals())) def enable_node(self, name): ''' Enable a node - + :param name: Jenkins node name, ``str`` ''' info = self.get_node_info(name) if not info['offline']: return msg = '' - self.jenkins_open(urllib2.Request(self.server + TOGGLE_OFFLINE%locals())) + self.jenkins_open(urllib2.Request( + self.server + TOGGLE_OFFLINE % locals())) def create_node(self, name, numExecutors=2, nodeDescription=None, remoteFS='/var/lib/jenkins', labels=None, exclusive=False, @@ -485,7 +525,7 @@ class Jenkins(object): :param launcher_params: Additional parameters for the launcher, ``dict`` ''' if self.node_exists(name): - raise JenkinsException('node[%s] already exists'%(name)) + raise JenkinsException('node[%s] already exists' % (name)) mode = 'NORMAL' if exclusive: @@ -494,25 +534,29 @@ class Jenkins(object): launcher_params['stapler-class'] = launcher inner_params = { - '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' : launcher_params + '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': launcher_params } params = { - 'name' : name, - 'type' : NODE_TYPE, - 'json' : json.dumps(inner_params) + 'name': name, + 'type': NODE_TYPE, + 'json': json.dumps(inner_params) } - self.jenkins_open(urllib2.Request(self.server + CREATE_NODE%urllib.urlencode(params))) + self.jenkins_open(urllib2.Request( + self.server + CREATE_NODE % urllib.urlencode(params))) if not self.node_exists(name): - raise JenkinsException('create[%s] failed'%(name)) + raise JenkinsException('create[%s] failed' % (name)) diff --git a/setup.py b/setup.py index 3be64ab..1a779a4 100644 --- a/setup.py +++ b/setup.py @@ -9,4 +9,4 @@ setup(name='python-jenkins', author_email='kwc@willowgarage.com', url='http://launchpad.net/python-jenkins', packages=['jenkins'], - ) + )