Browse Source

Migration to using requests

Convert to the requests library to allow for more sophisticated response
handling to be added.

Want to allow for applications to override the response handling in
certain cases where the default is incorrect. Handling of urlopen
responses results in version specific handling to ensure correct
behaviour across multiple versions of python.

Changing to use the requests package, provides a higher level interface
and removes some of the version specific handling for exceptions.

Change-Id: I5369a0d35be4bf8b3b197a51e60aba21b5742cc7
Depends-On: Iabd70aa457ceb4dbc147d7cbaeec913148cb3b56
changes/37/205637/21
Darragh Bailey 6 years ago
parent
commit
34cca0c4d9
  1. 383
      jenkins/__init__.py
  2. 121
      jenkins/urllib_kerb.py
  3. 1
      requirements.txt
  4. 3
      test-requirements.txt
  5. 19
      tests/base.py
  6. 31
      tests/helper.py
  7. 14
      tests/jobs/test_build.py
  8. 4
      tests/jobs/test_config.py
  9. 8
      tests/jobs/test_copy.py
  10. 16
      tests/jobs/test_create.py
  11. 4
      tests/jobs/test_debug.py
  12. 8
      tests/jobs/test_delete.py
  13. 4
      tests/jobs/test_disable.py
  14. 4
      tests/jobs/test_enable.py
  15. 29
      tests/jobs/test_get.py
  16. 47
      tests/jobs/test_info.py
  17. 12
      tests/jobs/test_name.py
  18. 4
      tests/jobs/test_reconfig.py
  19. 8
      tests/jobs/test_rename.py
  20. 2
      tests/jobs/test_set_next_build_number.py
  21. 78
      tests/test_build.py
  22. 40
      tests/test_info.py
  23. 167
      tests/test_jenkins.py
  24. 8
      tests/test_jenkins_sockets.py
  25. 124
      tests/test_kerberos.py
  26. 168
      tests/test_node.py
  27. 43
      tests/test_plugins.py
  28. 24
      tests/test_promotion.py
  29. 6
      tests/test_queue.py
  30. 10
      tests/test_quiet_down.py
  31. 6
      tests/test_script.py
  32. 49
      tests/test_version.py
  33. 24
      tests/test_view.py
  34. 20
      tests/test_whoami.py

383
jenkins/__init__.py

@ -46,7 +46,6 @@
See examples at :doc:`examples`
'''
import base64
import json
import re
import socket
@ -55,24 +54,18 @@ import time
import warnings
import multi_key_dict
import six
import requests
import requests.exceptions as req_exc
from six.moves.http_client import BadStatusLine
from six.moves.urllib.error import HTTPError
from six.moves.urllib.error import URLError
from six.moves.urllib.parse import quote, urlencode, urljoin, urlparse
from six.moves.urllib.request import Request, install_opener, build_opener, urlopen
from jenkins import plugins
try:
import kerberos
assert kerberos # pyflakes
from jenkins import urllib_kerb
opener = build_opener()
opener.add_handler(urllib_kerb.HTTPNegotiateHandler())
install_opener(opener)
import requests_kerberos
except ImportError:
pass
requests_kerberos = None
if sys.version_info < (2, 7, 0):
@ -240,17 +233,6 @@ class TimeoutException(JenkinsException):
'''A special exception to call out in the case of a socket timeout.'''
def auth_headers(username, password):
'''Simple implementation of HTTP Basic Authentication.
Returns the 'Authentication' header value.
'''
auth = '%s:%s' % (username, password)
if isinstance(auth, six.text_type):
auth = auth.encode('utf-8')
return b'Basic ' + base64.b64encode(auth)
class Jenkins(object):
_timeout_warning_issued = False
@ -269,12 +251,25 @@ class Jenkins(object):
self.server = url
else:
self.server = url + '/'
self._auths = [('anonymous', None)]
self._auth_resolved = False
if username is not None and password is not None:
self.auth = auth_headers(username, password)
else:
self.auth = None
self._auths[0] = (
'basic',
requests.auth.HTTPBasicAuth(
username.encode('utf-8'), password.encode('utf-8'))
)
if requests_kerberos is not None:
self._auths.append(
('kerberos', requests_kerberos.HTTPKerberosAuth())
)
self.auth = None
self.crumb = None
self.timeout = timeout
self._session = requests.Session()
def _get_encoded_params(self, params):
for k, v in params.items():
@ -296,14 +291,48 @@ class Jenkins(object):
# We don't know yet whether we need a crumb
if self.crumb is None:
try:
response = self.jenkins_open(Request(
self._build_url(CRUMB_URL)), add_crumb=False)
response = self.jenkins_open(requests.Request(
'GET', self._build_url(CRUMB_URL)), add_crumb=False)
except (NotFoundException, EmptyResponseException):
self.crumb = False
else:
self.crumb = json.loads(response)
if self.crumb:
req.add_header(self.crumb['crumbRequestField'], self.crumb['crumb'])
req.headers[self.crumb['crumbRequestField']] = self.crumb['crumb']
def _maybe_add_auth(self):
if self._auth_resolved:
return
if len(self._auths) == 1:
# If we only have one auth mechanism specified, just require it
self._session.auth = self._auths[0][1]
else:
# Attempt the list of auth mechanisms and keep the first that works
# otherwise default to the first one in the list (last popped).
# This is a hack to allow the transparent use of kerberos to work
# in future, we should require explicit request to use kerberos
failures = []
for name, auth in reversed(self._auths):
try:
self.jenkins_open(
requests.Request('GET', self._build_url(INFO),
auth=auth),
add_crumb=False, resolve_auth=False)
self._session.auth = auth
break
except Exception as exc:
# assume authentication failure
failures.append("auth(%s) %s" % (name, exc))
continue
else:
raise JenkinsException(
'Unable to authenticate with any scheme:\n%s'
% '\n'.join(failures))
self._auth_resolved = True
self.auth = self._session.auth
def _add_missing_builds(self, data):
"""Query Jenkins to get all builds of a job.
@ -327,8 +356,9 @@ class Jenkins(object):
if all_builds_loaded:
return data
folder_url, short_name = self._get_job_folder(data["name"])
response = self.jenkins_open(Request(self._build_url(ALL_BUILDS,
locals())))
response = self.jenkins_open(requests.Request(
'GET', self._build_url(ALL_BUILDS, locals())
))
if response:
data["builds"] = json.loads(response)["allBuilds"]
else:
@ -352,8 +382,8 @@ class Jenkins(object):
'''
folder_url, short_name = self._get_job_folder(name)
try:
response = self.jenkins_open(Request(
self._build_url(JOB_INFO, locals())
response = self.jenkins_open(requests.Request(
'GET', self._build_url(JOB_INFO, locals())
))
if response:
if fetch_all_builds:
@ -362,7 +392,7 @@ class Jenkins(object):
return json.loads(response)
else:
raise JenkinsException('job[%s] does not exist' % name)
except HTTPError:
except (req_exc.HTTPError, NotFoundException):
raise JenkinsException('job[%s] does not exist' % name)
except ValueError:
raise JenkinsException(
@ -397,8 +427,8 @@ class Jenkins(object):
'''
folder_url, short_name = self._get_job_folder(name)
try:
response = self.jenkins_open(Request(
self._build_url(JOB_NAME, locals())
response = self.jenkins_open(requests.Request(
'GET', self._build_url(JOB_NAME, locals())
))
except NotFoundException:
return None
@ -415,40 +445,57 @@ class Jenkins(object):
for k, v in self.get_job_info(job_name).items():
print(k, v)
def jenkins_open(self, req, add_crumb=True):
def _response_handler(self, response):
'''Handle response objects'''
# raise exceptions if occurred
response.raise_for_status()
headers = response.headers
if (headers.get('content-length') is None and
headers.get('transfer-encoding') is None):
# response body should only exist if one of these is provided
raise EmptyResponseException(
"Error communicating with server[%s]: "
"empty response" % self.server)
# Reponse objects will automatically return unicode encoded
# when accessing .text property
return response
def _request(self, req):
r = self._session.prepare_request(req)
return self._session.send(r, timeout=self.timeout)
def jenkins_open(self, req, add_crumb=True, resolve_auth=True):
'''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)
if resolve_auth:
self._maybe_add_auth()
if add_crumb:
self.maybe_add_crumb(req)
response = urlopen(req, timeout=self.timeout).read()
if response is None:
raise EmptyResponseException(
"Error communicating with server[%s]: "
"empty response" % self.server)
return response.decode('utf-8')
except HTTPError as e:
return self._response_handler(
self._request(req)).text
except req_exc.HTTPError as e:
# Jenkins's funky authentication means its nigh impossible to
# distinguish errors.
if e.code in [401, 403, 500]:
# six.moves.urllib.error.HTTPError provides a 'reason'
# attribute for all python version except for ver 2.6
# Falling back to HTTPError.msg since it contains the
# same info as reason
if e.response.status_code in [401, 403, 500]:
raise JenkinsException(
'Error in request. ' +
'Possibly authentication failed [%s]: %s' % (
e.code, e.msg)
e.response.status_code, e.response.reason)
)
elif e.code == 404:
elif e.response.status_code == 404:
raise NotFoundException('Requested item could not be found')
else:
raise
except socket.timeout as e:
except req_exc.Timeout as e:
raise TimeoutException('Error in request: %s' % (e))
except URLError as e:
# python 2.6 compatibility to ensure same exception raised
@ -476,15 +523,15 @@ class Jenkins(object):
'''
folder_url, short_name = self._get_job_folder(name)
try:
response = self.jenkins_open(Request(
self._build_url(BUILD_INFO, locals())
response = self.jenkins_open(requests.Request(
'GET', self._build_url(BUILD_INFO, locals())
))
if response:
return json.loads(response)
else:
raise JenkinsException('job[%s] number[%d] does not exist'
% (name, number))
except HTTPError:
except (req_exc.HTTPError, NotFoundException):
raise JenkinsException('job[%s] number[%d] does not exist'
% (name, number))
except ValueError:
@ -502,7 +549,7 @@ class Jenkins(object):
{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(
Request(self._build_url(Q_INFO))
requests.Request('GET', self._build_url(Q_INFO))
))['items']
def cancel_queue(self, id):
@ -514,8 +561,9 @@ class Jenkins(object):
# https://issues.jenkins-ci.org/browse/JENKINS-21311
try:
self.jenkins_open(
Request(self._build_url(CANCEL_QUEUE, locals()), b'',
headers={'Referer': self.server}))
requests.Request(
'POST', self._build_url(CANCEL_QUEUE, locals()),
headers={'Referer': self.server}))
except NotFoundException:
# Exception is expected; cancel_queue() is a best-effort
# mechanism, so ignore it
@ -546,9 +594,9 @@ class Jenkins(object):
url += query
try:
return json.loads(self.jenkins_open(
Request(self._build_url(url))
requests.Request('GET', self._build_url(INFO))
))
except (HTTPError, BadStatusLine):
except (req_exc.HTTPError, BadStatusLine):
raise BadHTTPException("Error communicating with server[%s]"
% self.server)
except ValueError:
@ -570,7 +618,9 @@ class Jenkins(object):
"""
try:
response = self.jenkins_open(Request(self._build_url(WHOAMI_URL)))
response = self.jenkins_open(requests.Request(
'GET', self._build_url(WHOAMI_URL)
))
if response is None:
raise EmptyResponseException(
"Error communicating with server[%s]: "
@ -578,7 +628,7 @@ class Jenkins(object):
return json.loads(response)
except (HTTPError, BadStatusLine):
except (req_exc.HTTPError, BadStatusLine):
raise BadHTTPException("Error communicating with server[%s]"
% self.server)
@ -595,21 +645,13 @@ class Jenkins(object):
"""
try:
request = Request(self._build_url(''))
request.add_header('X-Jenkins', '0.0')
response = urlopen(request, timeout=self.timeout)
if response is None:
raise EmptyResponseException(
"Error communicating with server[%s]: "
"empty response" % self.server)
request = requests.Request('GET', self._build_url(''))
request.headers['X-Jenkins'] = '0.0'
response = self._response_handler(self._request(request))
if six.PY2:
return response.info().getheader('X-Jenkins')
return response.headers['X-Jenkins']
if six.PY3:
return response.getheader('X-Jenkins')
except (HTTPError, BadStatusLine):
except (req_exc.HTTPError, BadStatusLine):
raise BadHTTPException("Error communicating with server[%s]"
% self.server)
@ -711,8 +753,8 @@ class Jenkins(object):
try:
plugins_info_json = json.loads(self.jenkins_open(
Request(self._build_url(PLUGIN_INFO, locals()))))
except (HTTPError, BadStatusLine):
requests.Request('GET', self._build_url(PLUGIN_INFO, locals()))))
except (req_exc.HTTPError, BadStatusLine):
raise BadHTTPException("Error communicating with server[%s]"
% self.server)
except ValueError:
@ -758,7 +800,7 @@ class Jenkins(object):
"""
if view_name:
return self._get_view_jobs(view_name=view_name)
return self._get_view_jobs(name=view_name)
else:
return self.get_all_jobs(folder_depth=folder_depth)
@ -849,8 +891,9 @@ class Jenkins(object):
raise JenkinsException('copy[%s to %s] failed, source and destination '
'folder must be the same' % (from_name, to_name))
self.jenkins_open(Request(
self._build_url(COPY_JOB, locals()), b''))
self.jenkins_open(requests.Request(
'POST', self._build_url(COPY_JOB, locals())
))
self.assert_job_exists(to_name, 'create[%s] failed')
def rename_job(self, from_name, to_name):
@ -869,8 +912,9 @@ class Jenkins(object):
if from_folder_url != to_folder_url:
raise JenkinsException('rename[%s to %s] failed, source and destination folder '
'must be the same' % (from_name, to_name))
self.jenkins_open(Request(
self._build_url(RENAME_JOB, locals()), b''))
self.jenkins_open(requests.Request(
'POST', self._build_url(RENAME_JOB, locals())
))
self.assert_job_exists(to_name, 'rename[%s] failed')
def delete_job(self, name):
@ -879,8 +923,9 @@ class Jenkins(object):
:param name: Name of Jenkins job, ``str``
'''
folder_url, short_name = self._get_job_folder(name)
self.jenkins_open(Request(
self._build_url(DELETE_JOB, locals()), b''))
self.jenkins_open(requests.Request(
'POST', self._build_url(DELETE_JOB, locals())
))
if self.job_exists(name):
raise JenkinsException('delete[%s] failed' % (name))
@ -890,8 +935,9 @@ class Jenkins(object):
:param name: Name of Jenkins job, ``str``
'''
folder_url, short_name = self._get_job_folder(name)
self.jenkins_open(Request(
self._build_url(ENABLE_JOB, locals()), b''))
self.jenkins_open(requests.Request(
'POST', self._build_url(ENABLE_JOB, locals())
))
def disable_job(self, name):
'''Disable Jenkins job.
@ -901,8 +947,9 @@ class Jenkins(object):
:param name: Name of Jenkins job, ``str``
'''
folder_url, short_name = self._get_job_folder(name)
self.jenkins_open(Request(
self._build_url(DISABLE_JOB, locals()), b''))
self.jenkins_open(requests.Request(
'POST', self._build_url(DISABLE_JOB, locals())
))
def set_next_build_number(self, name, number):
'''Set a job's next build number.
@ -925,9 +972,9 @@ class Jenkins(object):
>>> server.set_next_build_number('job_name', next_bn + 50)
'''
folder_url, short_name = self._get_job_folder(name)
self.jenkins_open(
Request(self._build_url(SET_JOB_BUILD_NUMBER, locals()),
("nextBuildNumber=%d" % number).encode('utf-8')))
self.jenkins_open(requests.Request(
'POST', self._build_url(SET_JOB_BUILD_NUMBER, locals()),
data=("nextBuildNumber=%d" % number).encode('utf-8')))
def job_exists(self, name):
'''Check whether a job exists
@ -985,9 +1032,11 @@ class Jenkins(object):
raise JenkinsException('job[%s] already exists' % (name))
try:
self.jenkins_open(Request(
self._build_url(CREATE_JOB, locals()),
config_xml.encode('utf-8'), DEFAULT_HEADERS))
self.jenkins_open(requests.Request(
'POST', self._build_url(CREATE_JOB, locals()),
data=config_xml.encode('utf-8'),
headers=DEFAULT_HEADERS
))
except NotFoundException:
raise JenkinsException('Cannot create job[%s] because folder '
'for the job does not exist' % (name))
@ -1000,7 +1049,7 @@ class Jenkins(object):
:returns: job configuration (XML format)
'''
folder_url, short_name = self._get_job_folder(name)
request = Request(self._build_url(CONFIG_JOB, locals()))
request = requests.Request('GET', self._build_url(CONFIG_JOB, locals()))
return self.jenkins_open(request)
def reconfig_job(self, name, config_xml):
@ -1013,8 +1062,11 @@ class Jenkins(object):
'''
folder_url, short_name = self._get_job_folder(name)
reconfig_url = self._build_url(CONFIG_JOB, locals())
self.jenkins_open(Request(reconfig_url, config_xml.encode('utf-8'),
DEFAULT_HEADERS))
self.jenkins_open(requests.Request(
'POST', reconfig_url,
data=config_xml.encode('utf-8'),
headers=DEFAULT_HEADERS
))
def build_job_url(self, name, parameters=None, token=None):
'''Get URL to trigger build job.
@ -1045,8 +1097,8 @@ class Jenkins(object):
:param parameters: parameters for job, or ``None``, ``dict``
:param token: Jenkins API token
'''
return self.jenkins_open(Request(
self.build_job_url(name, parameters, token), b''))
return self.jenkins_open(requests.Request(
'POST', self.build_job_url(name, parameters, token)))
def run_script(self, script):
'''Execute a groovy script on the jenkins master.
@ -1062,8 +1114,10 @@ class Jenkins(object):
Plugin:mailer, Plugin:jquery, Plugin:antisamy-markup-formatter,
Plugin:maven-plugin, Plugin:pam-auth]'
'''
return self.jenkins_open(Request(self._build_url(SCRIPT_TEXT),
"script=".encode('utf-8') + quote(script).encode('utf-8')))
return self.jenkins_open(
requests.Request(
'POST', self._build_url(SCRIPT_TEXT),
data="script=".encode('utf-8') + quote(script).encode('utf-8')))
def install_plugin(self, name, include_dependencies=True):
'''Install a plugin and its dependencies from the Jenkins public
@ -1105,8 +1159,9 @@ class Jenkins(object):
:param number: Jenkins build number for the job, ``int``
'''
folder_url, short_name = self._get_job_folder(name)
self.jenkins_open(Request(
self._build_url(STOP_BUILD, locals()), b''))
self.jenkins_open(requests.Request(
'POST', self._build_url(STOP_BUILD, locals())
))
def get_running_builds(self):
'''Return list of running builds.
@ -1165,10 +1220,11 @@ class Jenkins(object):
:returns: List of nodes, ``[ { str: str, str: bool} ]``
'''
try:
nodes_data = json.loads(self.jenkins_open(Request(self._build_url(NODE_LIST))))
nodes_data = json.loads(self.jenkins_open(
requests.Request('GET', self._build_url(NODE_LIST))))
return [{'name': c["displayName"], 'offline': c["offline"]}
for c in nodes_data["computer"]]
except (HTTPError, BadStatusLine):
except (req_exc.HTTPError, BadStatusLine):
raise BadHTTPException("Error communicating with server[%s]"
% self.server)
except ValueError:
@ -1183,13 +1239,14 @@ class Jenkins(object):
:returns: Dictionary of node info, ``dict``
'''
try:
response = self.jenkins_open(Request(
self._build_url(NODE_INFO, locals())))
response = self.jenkins_open(requests.Request(
'GET', self._build_url(NODE_INFO, locals())
))
if response:
return json.loads(response)
else:
raise JenkinsException('node[%s] does not exist' % name)
except HTTPError:
except (req_exc.HTTPError, NotFoundException):
raise JenkinsException('node[%s] does not exist' % name)
except ValueError:
raise JenkinsException("Could not parse JSON info for node[%s]"
@ -1225,8 +1282,9 @@ class Jenkins(object):
:param name: Name of Jenkins node, ``str``
'''
self.get_node_info(name)
self.jenkins_open(Request(
self._build_url(DELETE_NODE, locals()), b''))
self.jenkins_open(requests.Request(
'POST', self._build_url(DELETE_NODE, locals())
))
if self.node_exists(name):
raise JenkinsException('delete[%s] failed' % (name))
@ -1239,8 +1297,9 @@ class Jenkins(object):
info = self.get_node_info(name)
if info['offline']:
return
self.jenkins_open(Request(
self._build_url(TOGGLE_OFFLINE, locals()), b''))
self.jenkins_open(requests.Request(
'POST', self._build_url(TOGGLE_OFFLINE, locals())
))
def enable_node(self, name):
'''Enable a node
@ -1251,8 +1310,9 @@ class Jenkins(object):
if not info['offline']:
return
msg = ''
self.jenkins_open(Request(
self._build_url(TOGGLE_OFFLINE, locals()), b''))
self.jenkins_open(requests.Request(
'POST', self._build_url(TOGGLE_OFFLINE, locals())
))
def create_node(self, name, numExecutors=2, nodeDescription=None,
remoteFS='/var/lib/jenkins', labels=None, exclusive=False,
@ -1297,9 +1357,9 @@ class Jenkins(object):
'json': json.dumps(inner_params)
}
self.jenkins_open(Request(
self._build_url(CREATE_NODE, locals()),
urlencode(params).encode('utf-8')))
self.jenkins_open(requests.Request(
'POST', self._build_url(CREATE_NODE, locals()), data=params)
)
self.assert_node_exists(name, 'create[%s] failed')
@ -1309,7 +1369,7 @@ class Jenkins(object):
:param name: Jenkins node name, ``str``
'''
get_config_url = self._build_url(CONFIG_NODE, locals())
return self.jenkins_open(Request(get_config_url))
return self.jenkins_open(requests.Request('GET', get_config_url))
def reconfig_node(self, name, config_xml):
'''Change the configuration for an existing node.
@ -1318,7 +1378,11 @@ class Jenkins(object):
:param config_xml: New XML configuration, ``str``
'''
reconfig_url = self._build_url(CONFIG_NODE, locals())
self.jenkins_open(Request(reconfig_url, config_xml.encode('utf-8'), DEFAULT_HEADERS))
self.jenkins_open(requests.Request(
'POST', reconfig_url,
data=config_xml.encode('utf-8'),
headers=DEFAULT_HEADERS
))
def get_build_console_output(self, name, number):
'''Get build console text.
@ -1329,15 +1393,15 @@ class Jenkins(object):
'''
folder_url, short_name = self._get_job_folder(name)
try:
response = self.jenkins_open(Request(
self._build_url(BUILD_CONSOLE_OUTPUT, locals())
response = self.jenkins_open(requests.Request(
'GET', self._build_url(BUILD_CONSOLE_OUTPUT, locals())
))
if response:
return response
else:
raise JenkinsException('job[%s] number[%d] does not exist'
% (name, number))
except HTTPError:
except (req_exc.HTTPError, NotFoundException):
raise JenkinsException('job[%s] number[%d] does not exist'
% (name, number))
@ -1359,7 +1423,7 @@ class Jenkins(object):
return folder_url, short_name
def _get_view_jobs(self, view_name):
def _get_view_jobs(self, name):
'''Get list of jobs on the view specified.
Each job is a dictionary with 'name', 'url', 'color' and 'fullname'
@ -1375,18 +1439,18 @@ class Jenkins(object):
'''
try:
response = self.jenkins_open(Request(
self._build_url(VIEW_JOBS, {u'name': view_name})
response = self.jenkins_open(requests.Request(
'GET', self._build_url(VIEW_JOBS, locals())
))
if response:
jobs = json.loads(response)['jobs']
else:
raise JenkinsException('view[%s] does not exist' % view_name)
except HTTPError:
raise JenkinsException('view[%s] does not exist' % view_name)
raise JenkinsException('view[%s] does not exist' % name)
except NotFoundException:
raise JenkinsException('view[%s] does not exist' % name)
except ValueError:
raise JenkinsException(
'Could not parse JSON info for view[%s]' % view_name)
'Could not parse JSON info for view[%s]' % name)
for job_dict in jobs:
job_dict.update({u'fullname': job_dict[u'name']})
@ -1404,8 +1468,8 @@ class Jenkins(object):
:returns: Name of view or None
'''
try:
response = self.jenkins_open(Request(
self._build_url(VIEW_NAME, locals())))
response = self.jenkins_open(requests.Request(
'GET', self._build_url(VIEW_NAME, locals())))
except NotFoundException:
return None
else:
@ -1426,7 +1490,7 @@ class Jenkins(object):
:throws: :class:`JenkinsException` whenever the view does not exist
'''
if not self.view_exists(name):
raise JenkinsException(exception_message % name)
raise NotFoundException(exception_message % name)
def view_exists(self, name):
'''Check whether a view exists
@ -1451,8 +1515,8 @@ class Jenkins(object):
:param name: Name of Jenkins view, ``str``
'''
self.jenkins_open(Request(
self._build_url(DELETE_VIEW, locals()), b''
self.jenkins_open(requests.Request(
'POST', self._build_url(DELETE_VIEW, locals())
))
if self.view_exists(name):
raise JenkinsException('delete[%s] failed' % (name))
@ -1466,9 +1530,11 @@ class Jenkins(object):
if self.view_exists(name):
raise JenkinsException('view[%s] already exists' % (name))
self.jenkins_open(Request(
self._build_url(CREATE_VIEW, locals()),
config_xml.encode('utf-8'), DEFAULT_HEADERS))
self.jenkins_open(requests.Request(
'POST', self._build_url(CREATE_VIEW, locals()),
data=config_xml.encode('utf-8'),
headers=DEFAULT_HEADERS
))
self.assert_view_exists(name, 'create[%s] failed')
def reconfig_view(self, name, config_xml):
@ -1480,8 +1546,11 @@ class Jenkins(object):
:param config_xml: New XML configuration, ``str``
'''
reconfig_url = self._build_url(CONFIG_VIEW, locals())
self.jenkins_open(Request(reconfig_url, config_xml.encode('utf-8'),
DEFAULT_HEADERS))
self.jenkins_open(requests.Request(
'POST', reconfig_url,
data=config_xml.encode('utf-8'),
headers=DEFAULT_HEADERS
))
def get_view_config(self, name):
'''Get configuration of existing Jenkins view.
@ -1489,7 +1558,7 @@ class Jenkins(object):
:param name: Name of Jenkins view, ``str``
:returns: view configuration (XML format)
'''
request = Request(self._build_url(CONFIG_VIEW, locals()))
request = requests.Request('GET', self._build_url(CONFIG_VIEW, locals()))
return self.jenkins_open(request)
def get_promotion_name(self, name, job_name):
@ -1505,8 +1574,8 @@ class Jenkins(object):
'''
folder_url, short_name = self._get_job_folder(job_name)
try:
response = self.jenkins_open(Request(
self._build_url(PROMOTION_NAME, locals())))
response = self.jenkins_open(requests.Request(
'GET', self._build_url(PROMOTION_NAME, locals())))
except NotFoundException:
return None
else:
@ -1550,13 +1619,13 @@ class Jenkins(object):
'''
folder_url, short_name = self._get_job_folder(job_name)
try:
response = self.jenkins_open(Request(
self._build_url(PROMOTION_INFO, locals())))
response = self.jenkins_open(requests.Request(
'GET', self._build_url(PROMOTION_INFO, locals())))
if response:
return json.loads(response)
else:
raise JenkinsException('job[%s] does not exist' % job_name)
except HTTPError:
except req_exc.HTTPError:
raise JenkinsException('job[%s] does not exist' % job_name)
except ValueError:
raise JenkinsException("Could not parse JSON info for "
@ -1579,8 +1648,8 @@ class Jenkins(object):
:param name: Name of Jenkins promotion, ``str``
'''
folder_url, short_name = self._get_job_folder(job_name)
self.jenkins_open(Request(
self._build_url(DELETE_PROMOTION, locals()), b''
self.jenkins_open(requests.Request(
'POST', self._build_url(DELETE_PROMOTION, locals())
))
if self.promotion_exists(name, job_name):
raise JenkinsException('delete[%s] from job[%s] failed' %
@ -1598,9 +1667,9 @@ class Jenkins(object):
% (name, job_name))
folder_url, short_name = self._get_job_folder(job_name)
self.jenkins_open(Request(
self._build_url(CREATE_PROMOTION, locals()),
config_xml.encode('utf-8'), DEFAULT_HEADERS))
self.jenkins_open(requests.Request(
'POST', self._build_url(CREATE_PROMOTION, locals()),
data=config_xml.encode('utf-8'), headers=DEFAULT_HEADERS))
self.assert_promotion_exists(name, job_name, 'create[%s] at '
'job[%s] failed')
@ -1615,8 +1684,11 @@ class Jenkins(object):
'''
folder_url, short_name = self._get_job_folder(job_name)
reconfig_url = self._build_url(CONFIG_PROMOTION, locals())
self.jenkins_open(Request(reconfig_url, config_xml.encode('utf-8'),
DEFAULT_HEADERS))
self.jenkins_open(requests.Request(
'POST', reconfig_url,
data=config_xml.encode('utf-8'),
headers=DEFAULT_HEADERS
))
def get_promotion_config(self, name, job_name):
'''Get configuration of existing Jenkins promotion.
@ -1626,7 +1698,8 @@ class Jenkins(object):
:returns: promotion configuration (XML format)
'''
folder_url, short_name = self._get_job_folder(job_name)
request = Request(self._build_url(CONFIG_PROMOTION, locals()))
request = requests.Request(
'GET', self._build_url(CONFIG_PROMOTION, locals()))
return self.jenkins_open(request)
def quiet_down(self):
@ -1635,7 +1708,7 @@ class Jenkins(object):
No new builds will be started allowing running builds to complete
prior to shutdown of the server.
'''
request = Request(self._build_url(QUIET_DOWN))
request = requests.Request('POST', self._build_url(QUIET_DOWN))
self.jenkins_open(request)
info = self.get_info()
if not info['quietingDown']:

121
jenkins/urllib_kerb.py

@ -1,121 +0,0 @@
# Copyright (C) 2015 OpenStack Foundation
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import logging
import re
import kerberos
from six.moves.urllib import error, request
logger = logging.getLogger(__name__)
class HTTPNegotiateHandler(request.BaseHandler):
handler_order = 490 # before Digest auth
def __init__(self, max_tries=5):
self.krb_context = None
self.tries = 0
self.max_tries = max_tries
self.re_extract_auth = re.compile('.*?Negotiate\s*([^,]*)', re.I)
def http_error_401(self, req, fp, code, msg, headers):
logger.debug("INSIDE http_error_401")
try:
try:
krb_req = self._extract_krb_value(headers)
except ValueError:
# Negotiate header not found or a similar error
# we can't handle this, let the next handler have a go
return None
if not krb_req:
# First reply from server (no neg value)
self.tries = 0
krb_req = ""
else:
if self.tries > self.max_tries:
raise error.HTTPError(
req.get_full_url(), 401, "Negotiate auth failed",
headers, None)
self.tries += 1
try:
krb_resp = self._krb_response(req.host, krb_req)
req.add_unredirected_header('Authorization',
"Negotiate %s" % krb_resp)
resp = self.parent.open(req, timeout=req.timeout)
self._authenticate_server(resp.headers)
return resp
except kerberos.GSSError as err:
try:
msg = err.args[1][0]
except Exception:
msg = "Negotiate auth failed"
logger.debug(msg)
return None # let the next handler (if any) have a go
finally:
if self.krb_context is not None:
kerberos.authGSSClientClean(self.krb_context)
self.krb_context = None
def _krb_response(self, host, krb_val):
logger.debug("INSIDE _krb_response")
_dummy, self.krb_context = kerberos.authGSSClientInit("HTTP@%s" % host)
kerberos.authGSSClientStep(self.krb_context, krb_val)
response = kerberos.authGSSClientResponse(self.krb_context)
logger.debug("kerb auth successful")
return response
def _authenticate_server(self, headers):
logger.debug("INSIDE _authenticate_server")
try:
val = self._extract_krb_value(headers)
except ValueError:
logger.critical("Server authentication failed."
"Auth value couldn't be extracted from headers.")
return None
if not val:
logger.critical("Server authentication failed."
"Empty 'Negotiate' value.")
return None
kerberos.authGSSClientStep(self.krb_context, val)
def _extract_krb_value(self, headers):
logger.debug("INSIDE _extract_krb_value")
header = headers.get('www-authenticate', None)
if header is None:
msg = "www-authenticate header not found"
logger.debug(msg)
raise ValueError(msg)
if "negotiate" in header.lower():
matches = self.re_extract_auth.search(header)
if matches:
return matches.group(1)
else:
return ""
else:
msg = "Negotiate not in www-authenticate header (%s)" % header
logger.debug(msg)
raise ValueError(msg)

1
requirements.txt

@ -1,3 +1,4 @@
six>=1.3.0
pbr>=0.8.2
multi_key_dict
requests

3
test-requirements.txt

@ -1,9 +1,10 @@
coverage>=3.6
hacking>=0.5.6,<0.11
kerberos>=1.2.4
mock<1.1
unittest2
python-subunit
requests-mock>=1.4.0
requests-kerberos
sphinx>=1.2,<1.3.0
testrepository
testscenarios

19
tests/base.py

@ -1,6 +1,5 @@
import sys
from six.moves.urllib.request import build_opener
from testscenarios import TestWithScenarios
import jenkins
@ -25,7 +24,8 @@ class JenkinsTestBase(TestWithScenarios, unittest.TestCase):
def setUp(self):
super(JenkinsTestBase, self).setUp()
self.opener = build_opener()
# TODO(darragh) would be useful if this could be mocked
jenkins.requests_kerberos = None
self.j = jenkins.Jenkins(self.base_url, 'test', 'test')
@ -35,17 +35,4 @@ class JenkinsTestBase(TestWithScenarios, unittest.TestCase):
def _check_requests(self, requests):
for req in requests:
self._check_request(req[0][0])
def _check_request(self, request):
# taken from opener.open() in request
# attribute request.type is only set automatically for python 3
# requests, must use request.get_type() for python 2.7
protocol = request.type or request.get_type()
# check that building the request doesn't throw any exception
meth_name = protocol + "_request"
for processor in self.opener.process_request.get(protocol, []):
meth = getattr(processor, meth_name)
request = meth(request)
req[0][0].prepare()

31
tests/helper.py

@ -1,8 +1,11 @@
import functools
import json
from multiprocessing import Process
from multiprocessing import Queue
import traceback
from mock import Mock
import requests
from six.moves import socketserver
@ -73,3 +76,31 @@ class NullServer(socketserver.TCPServer):
socketserver.TCPServer.__init__(
self, server_address, socketserver.BaseRequestHandler,
*args, **kwargs)
def build_response_mock(status_code, json_body=None, headers=None, **kwargs):
real_response = requests.Response()
real_response.status_code = status_code
text = None
if json_body is not None:
text = json.dumps(json_body)
if headers is not {}:
real_response.headers['content-length'] = len(text)
if headers is not None:
for k, v in headers.items():
real_response.headers[k] = v
for k, v in kwargs.items():
setattr(real_response, k, v)
response = Mock(wraps=real_response, autospec=True)
if text:
response.text = text
# for some reason, wraps cannot handle attributes which are dicts
# and accessed by key
response.headers = real_response.headers
return response

14
tests/jobs/test_build.py

@ -14,7 +14,7 @@ class JenkinsBuildJobTest(JenkinsJobsTestBase):
build_info = self.j.build_job(u'Test Job')
self.assertEqual(jenkins_mock.call_args[0][0].get_full_url(),
self.assertEqual(jenkins_mock.call_args[0][0].url,
self.make_url('job/Test%20Job/build'))
self.assertEqual(build_info, {'foo': 'bar'})
self._check_requests(jenkins_mock.call_args_list)
@ -27,7 +27,7 @@ class JenkinsBuildJobTest(JenkinsJobsTestBase):
build_info = self.j.build_job(u'a Folder/Test Job')
self.assertEqual(jenkins_mock.call_args[0][0].get_full_url(),
self.assertEqual(jenkins_mock.call_args[0][0].url,
self.make_url('job/a%20Folder/job/Test%20Job/build'))
self.assertEqual(build_info, {'foo': 'bar'})
self._check_requests(jenkins_mock.call_args_list)
@ -40,7 +40,7 @@ class JenkinsBuildJobTest(JenkinsJobsTestBase):
build_info = self.j.build_job(u'TestJob', token='some_token')
self.assertEqual(jenkins_mock.call_args[0][0].get_full_url(),
self.assertEqual(jenkins_mock.call_args[0][0].url,
self.make_url('job/TestJob/build?token=some_token'))
self.assertEqual(build_info, {'foo': 'bar'})
self._check_requests(jenkins_mock.call_args_list)
@ -53,7 +53,7 @@ class JenkinsBuildJobTest(JenkinsJobsTestBase):
build_info = self.j.build_job(u'a Folder/TestJob', token='some_token')
self.assertEqual(jenkins_mock.call_args[0][0].get_full_url(),
self.assertEqual(jenkins_mock.call_args[0][0].url,
self.make_url('job/a%20Folder/job/TestJob/build?token=some_token'))
self.assertEqual(build_info, {'foo': 'bar'})
self._check_requests(jenkins_mock.call_args_list)
@ -69,8 +69,8 @@ class JenkinsBuildJobTest(JenkinsJobsTestBase):
parameters={'when': 'now', 'why': 'because I felt like it'},
token='some_token')
self.assertTrue('token=some_token' in jenkins_mock.call_args[0][0].get_full_url())
self.assertTrue('when=now' in jenkins_mock.call_args[0][0].get_full_url())
self.assertTrue('why=because+I+felt+like+it' in jenkins_mock.call_args[0][0].get_full_url())
self.assertTrue('token=some_token' in jenkins_mock.call_args[0][0].url)
self.assertTrue('when=now' in jenkins_mock.call_args[0][0].url)
self.assertTrue('why=because+I+felt+like+it' in jenkins_mock.call_args[0][0].url)
self.assertEqual(build_info, {'foo': 'bar'})
self._check_requests(jenkins_mock.call_args_list)

4
tests/jobs/test_config.py

@ -11,7 +11,7 @@ class JenkinsGetJobConfigTest(JenkinsJobsTestBase):
self.j.get_job_config(u'Test Job')
self.assertEqual(
jenkins_mock.call_args[0][0].get_full_url(),
jenkins_mock.call_args[0][0].url,
self.make_url('job/Test%20Job/config.xml'))
self._check_requests(jenkins_mock.call_args_list)
@ -20,6 +20,6 @@ class JenkinsGetJobConfigTest(JenkinsJobsTestBase):
self.j.get_job_config(u'a folder/Test Job')
self.assertEqual(
jenkins_mock.call_args[0][0].get_full_url(),
jenkins_mock.call_args[0][0].url,
self.make_url('job/a%20folder/job/Test%20Job/config.xml'))
self._check_requests(jenkins_mock.call_args_list)

8
tests/jobs/test_copy.py

@ -18,7 +18,7 @@ class JenkinsCopyJobTest(JenkinsJobsTestBase):
self.j.copy_job(u'Test Job', u'Test Job_2')
self.assertEqual(
jenkins_mock.call_args_list[0][0][0].get_full_url(),
jenkins_mock.call_args_list[0][0][0].url,
self.make_url('createItem?name=Test%20Job_2&mode=copy&from=Test%20Job'))
self.assertTrue(self.j.job_exists('Test Job_2'))
self._check_requests(jenkins_mock.call_args_list)
@ -34,7 +34,7 @@ class JenkinsCopyJobTest(JenkinsJobsTestBase):
self.j.copy_job(u'a Folder/Test Job', u'a Folder/Test Job_2')
self.assertEqual(
jenkins_mock.call_args_list[0][0][0].get_full_url(),
jenkins_mock.call_args_list[0][0][0].url,
self.make_url('job/a%20Folder/createItem?name=Test%20Job_2'
'&mode=copy&from=Test%20Job'))
self.assertTrue(self.j.job_exists('a Folder/Test Job_2'))
@ -50,7 +50,7 @@ class JenkinsCopyJobTest(JenkinsJobsTestBase):
with self.assertRaises(jenkins.JenkinsException) as context_manager:
self.j.copy_job(u'TestJob', u'TestJob_2')
self.assertEqual(
jenkins_mock.call_args_list[0][0][0].get_full_url(),
jenkins_mock.call_args_list[0][0][0].url,
self.make_url('createItem?name=TestJob_2&mode=copy&from=TestJob'))
self.assertEqual(
str(context_manager.exception),
@ -67,7 +67,7 @@ class JenkinsCopyJobTest(JenkinsJobsTestBase):
with self.assertRaises(jenkins.JenkinsException) as context_manager:
self.j.copy_job(u'a Folder/TestJob', u'a Folder/TestJob_2')
self.assertEqual(
jenkins_mock.call_args_list[0][0][0].get_full_url(),
jenkins_mock.call_args_list[0][0][0].url,
self.make_url('job/a%20Folder/createItem?name=TestJob_2&mode=copy'
'&from=TestJob'))
self.assertEqual(

16
tests/jobs/test_create.py

@ -18,7 +18,7 @@ class JenkinsCreateJobTest(JenkinsJobsTestBase):
self.j.create_job(u'Test Job', self.config_xml)
self.assertEqual(
jenkins_mock.call_args_list[1][0][0].get_full_url(),
jenkins_mock.call_args_list[1][0][0].url,
self.make_url('createItem?name=Test%20Job'))
self._check_requests(jenkins_mock.call_args_list)
@ -33,7 +33,7 @@ class JenkinsCreateJobTest(JenkinsJobsTestBase):
self.j.create_job(u'a Folder/Test Job', self.config_xml)
self.assertEqual(
jenkins_mock.call_args_list[1][0][0].get_full_url(),
jenkins_mock.call_args_list[1][0][0].url,
self.make_url('job/a%20Folder/createItem?name=Test%20Job'))
self._check_requests(jenkins_mock.call_args_list)
@ -47,7 +47,7 @@ class JenkinsCreateJobTest(JenkinsJobsTestBase):
with self.assertRaises(jenkins.JenkinsException) as context_manager:
self.j.create_job(u'TestJob', self.config_xml)
self.assertEqual(
jenkins_mock.call_args_list[0][0][0].get_full_url(),
jenkins_mock.call_args_list[0][0][0].url,
self.make_url('job/TestJob/api/json?tree=name'))
self.assertEqual(
str(context_manager.exception),
@ -64,7 +64,7 @@ class JenkinsCreateJobTest(JenkinsJobsTestBase):
with self.assertRaises(jenkins.JenkinsException) as context_manager:
self.j.create_job(u'a Folder/TestJob', self.config_xml)
self.assertEqual(
jenkins_mock.call_args_list[0][0][0].get_full_url(),
jenkins_mock.call_args_list[0][0][0].url,
self.make_url('job/a%20Folder/job/TestJob/api/json?tree=name'))
self.assertEqual(
str(context_manager.exception),
@ -82,10 +82,10 @@ class JenkinsCreateJobTest(JenkinsJobsTestBase):
with self.assertRaises(jenkins.JenkinsException) as context_manager:
self.j.create_job(u'TestJob', self.config_xml)
self.assertEqual(
jenkins_mock.call_args_list[0][0][0].get_full_url(),
jenkins_mock.call_args_list[0][0][0].url,
self.make_url('job/TestJob/api/json?tree=name'))
self.assertEqual(
jenkins_mock.call_args_list[1][0][0].get_full_url(),
jenkins_mock.call_args_list[1][0][0].url,
self.make_url('createItem?name=TestJob'))
self.assertEqual(
str(context_manager.exception),
@ -103,10 +103,10 @@ class JenkinsCreateJobTest(JenkinsJobsTestBase):
with self.assertRaises(jenkins.JenkinsException) as context_manager:
self.j.create_job(u'a Folder/TestJob', self.config_xml)
self.assertEqual(
jenkins_mock.call_args_list[0][0][0].get_full_url(),
jenkins_mock.call_args_list[0][0][0].url,
self.make_url('job/a%20Folder/job/TestJob/api/json?tree=name'))
self.assertEqual(
jenkins_mock.call_args_list[1][0][0].get_full_url(),
jenkins_mock.call_args_list[1][0][0].url,
self.make_url('job/a%20Folder/createItem?name=TestJob'))
self.assertEqual(
str(context_manager.exception),

4
tests/jobs/test_debug.py

@ -20,7 +20,7 @@ class JenkinsDebugJobInfoTest(JenkinsJobsTestBase):
self.j.debug_job_info(u'Test Job')
self.assertEqual(
jenkins_mock.call_args[0][0].get_full_url(),
jenkins_mock.call_args[0][0].url,
self.make_url('job/Test%20Job/api/json?depth=0'))
self._check_requests(jenkins_mock.call_args_list)
@ -37,6 +37,6 @@ class JenkinsDebugJobInfoTest(JenkinsJobsTestBase):
self.j.debug_job_info(u'a Folder/Test Job')
self.assertEqual(
jenkins_mock.call_args[0][0].get_full_url(),
jenkins_mock.call_args[0][0].url,
self.make_url('job/a%20Folder/job/Test%20Job/api/json?depth=0'))
self._check_requests(jenkins_mock.call_args_list)

8
tests/jobs/test_delete.py

@ -17,7 +17,7 @@ class JenkinsDeleteJobTest(JenkinsJobsTestBase):
self.j.delete_job(u'Test Job')
self.assertEqual(
jenkins_mock.call_args_list[0][0][0].get_full_url(),
jenkins_mock.call_args_list[0][0][0].url,
self.make_url('job/Test%20Job/doDelete'))
self._check_requests(jenkins_mock.call_args_list)
@ -31,7 +31,7 @@ class JenkinsDeleteJobTest(JenkinsJobsTestBase):
self.j.delete_job(u'a Folder/Test Job')
self.assertEqual(
jenkins_mock.call_args_list[0][0][0].get_full_url(),
jenkins_mock.call_args_list[0][0][0].url,
self.make_url('job/a%20Folder/job/Test%20Job/doDelete'))
self._check_requests(jenkins_mock.call_args_list)
@ -46,7 +46,7 @@ class JenkinsDeleteJobTest(JenkinsJobsTestBase):
with self.assertRaises(jenkins.JenkinsException) as context_manager:
self.j.delete_job(u'TestJob')
self.assertEqual(
jenkins_mock.call_args_list[0][0][0].get_full_url(),
jenkins_mock.call_args_list[0][0][0].url,
self.make_url('job/TestJob/doDelete'))
self.assertEqual(
str(context_manager.exception),
@ -64,7 +64,7 @@ class JenkinsDeleteJobTest(JenkinsJobsTestBase):
with self.assertRaises(jenkins.JenkinsException) as context_manager:
self.j.delete_job(u'a Folder/TestJob')
self.assertEqual(
jenkins_mock.call_args_list[0][0][0].get_full_url(),
jenkins_mock.call_args_list[0][0][0].url,
self.make_url('job/a%20Folder/job/TestJob/doDelete'))
self.assertEqual(
str(context_manager.exception),

4
tests/jobs/test_disable.py

@ -17,7 +17,7 @@ class JenkinsDisableJobTest(JenkinsJobsTestBase):
self.j.disable_job(u'Test Job')
self.assertEqual(
jenkins_mock.call_args_list[0][0][0].get_full_url(),
jenkins_mock.call_args_list[0][0][0].url,
self.make_url('job/Test%20Job/disable'))
self.assertTrue(self.j.job_exists('Test Job'))
self._check_requests(jenkins_mock.call_args_list)