Merge "Migration to using requests"

This commit is contained in:
Zuul 2018-04-04 20:40:17 +00:00 committed by Gerrit Code Review
commit 7166e97eb2
34 changed files with 648 additions and 833 deletions

View File

@ -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)
@ -848,8 +890,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):
@ -868,8 +911,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):
@ -878,8 +922,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))
@ -889,8 +934,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.
@ -900,8 +946,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.
@ -924,9 +971,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
@ -984,9 +1031,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))
@ -999,7 +1048,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):
@ -1012,8 +1061,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.
@ -1044,8 +1096,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.
@ -1061,8 +1113,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
@ -1104,8 +1158,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.
@ -1164,10 +1219,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:
@ -1182,13 +1238,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]"
@ -1224,8 +1281,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))
@ -1238,8 +1296,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
@ -1250,8 +1309,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,
@ -1296,9 +1356,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')
@ -1308,7 +1368,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.
@ -1317,7 +1377,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.
@ -1328,15 +1392,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))
@ -1358,7 +1422,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'
@ -1374,18 +1438,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']})
@ -1403,8 +1467,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:
@ -1425,7 +1489,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
@ -1450,8 +1514,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))
@ -1465,9 +1529,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):
@ -1479,8 +1545,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.
@ -1488,7 +1557,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):
@ -1504,8 +1573,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:
@ -1549,13 +1618,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 "
@ -1578,8 +1647,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' %
@ -1597,9 +1666,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')
@ -1614,8 +1683,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.
@ -1625,7 +1697,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):
@ -1634,7 +1707,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']:

View File

@ -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)

View File

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

View File

@ -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

View File

@ -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()

View File

@ -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

View File

@ -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)

View File

@ -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)

View File

@ -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(

View File

@ -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),