Add a default http timeout for connections to jenkins

Without a timeout a script can hang forever when attempting
to connect to jenkins. This change sets a default timeout
of 2 minutes.  Selection of default value is pretty arbitrary
on this change.

Closes-Bug: #1273329
Change-Id: If84778231b88d78a02a89a56f38f95d6deada80a
This commit is contained in:
Khai Do
2014-12-04 00:16:09 -08:00
parent b0302b0d0f
commit e197dd5454
2 changed files with 34 additions and 4 deletions

View File

@@ -51,6 +51,7 @@ import json
import six import six
from six.moves.http_client import BadStatusLine from six.moves.http_client import BadStatusLine
from six.moves.urllib.error import HTTPError from six.moves.urllib.error import HTTPError
from six.moves.urllib.error import URLError
from six.moves.urllib.parse import quote, urlencode from six.moves.urllib.parse import quote, urlencode
from six.moves.urllib.request import Request, urlopen from six.moves.urllib.request import Request, urlopen
@@ -59,6 +60,7 @@ LAUNCHER_COMMAND = 'hudson.slaves.CommandLauncher'
LAUNCHER_JNLP = 'hudson.slaves.JNLPLauncher' LAUNCHER_JNLP = 'hudson.slaves.JNLPLauncher'
LAUNCHER_WINDOWS_SERVICE = 'hudson.os.windows.ManagedWindowsServiceLauncher' LAUNCHER_WINDOWS_SERVICE = 'hudson.os.windows.ManagedWindowsServiceLauncher'
DEFAULT_CONN_TIMEOUT = 120
INFO = 'api/json' INFO = 'api/json'
PLUGIN_INFO = 'pluginManager/api/json?depth=%(depth)s' PLUGIN_INFO = 'pluginManager/api/json?depth=%(depth)s'
CRUMB_URL = 'crumbIssuer/api/json' CRUMB_URL = 'crumbIssuer/api/json'
@@ -146,7 +148,7 @@ def auth_headers(username, password):
class Jenkins(object): class Jenkins(object):
def __init__(self, url, username=None, password=None): def __init__(self, url, username=None, password=None, timeout=DEFAULT_CONN_TIMEOUT):
'''Create handle to Jenkins instance. '''Create handle to Jenkins instance.
All methods will raise :class:`JenkinsException` on failure. All methods will raise :class:`JenkinsException` on failure.
@@ -154,6 +156,7 @@ class Jenkins(object):
:param username: Server username, ``str`` :param username: Server username, ``str``
:param password: Server password, ``str`` :param password: Server password, ``str``
:param url: URL of Jenkins server, ``str`` :param url: URL of Jenkins server, ``str``
:param timeout: Server connection timeout (in seconds), ``int``
''' '''
if url[-1] == '/': if url[-1] == '/':
self.server = url self.server = url
@@ -164,6 +167,7 @@ class Jenkins(object):
else: else:
self.auth = None self.auth = None
self.crumb = None self.crumb = None
self.timeout = timeout
def _get_encoded_params(self, params): def _get_encoded_params(self, params):
for k, v in params.items(): for k, v in params.items():
@@ -244,7 +248,8 @@ class Jenkins(object):
req.add_header('Authorization', self.auth) req.add_header('Authorization', self.auth)
if add_crumb: if add_crumb:
self.maybe_add_crumb(req) self.maybe_add_crumb(req)
return urlopen(req).read() response = urlopen(req, timeout=self.timeout).read()
return response
except HTTPError as e: except HTTPError as e:
# Jenkins's funky authentication means its nigh impossible to # Jenkins's funky authentication means its nigh impossible to
# distinguish errors. # distinguish errors.
@@ -260,7 +265,8 @@ class Jenkins(object):
) )
elif e.code == 404: elif e.code == 404:
raise NotFoundException('Requested item could not be found') raise NotFoundException('Requested item could not be found')
# right now I'm getting 302 infinites on a successful delete except URLError as e:
raise JenkinsException('Error in request: %s' % (e.reason))
def get_build_info(self, name, number, depth=0): def get_build_info(self, name, number, depth=0):
'''Get build information dictionary. '''Get build information dictionary.
@@ -372,7 +378,7 @@ class Jenkins(object):
try: try:
request = Request(self.server) request = Request(self.server)
request.add_header('X-Jenkins', '0.0') request.add_header('X-Jenkins', '0.0')
response = urlopen(request) response = urlopen(request, timeout=self.timeout)
return response.info().getheader('X-Jenkins') return response.info().getheader('X-Jenkins')
except HTTPError: except HTTPError:
raise JenkinsException("Error communicating with server[%s]" raise JenkinsException("Error communicating with server[%s]"

View File

@@ -77,6 +77,14 @@ class JenkinsTest(unittest.TestCase):
self.assertEqual(j.auth.decode(), 'Basic %s' % ( self.assertEqual(j.auth.decode(), 'Basic %s' % (
long_str_b64 + 'Om' + long_str_b64[2:] + 'YQ==')) long_str_b64 + 'Om' + long_str_b64[2:] + 'YQ=='))
def test_constructor_default_timeout(self):
j = jenkins.Jenkins('http://example.com')
self.assertEqual(j.timeout, 120)
def test_constructor_custom_timeout(self):
j = jenkins.Jenkins('http://example.com', timeout=300)
self.assertEqual(j.timeout, 300)
@patch.object(jenkins.Jenkins, 'jenkins_open') @patch.object(jenkins.Jenkins, 'jenkins_open')
def test_get_job_config_encodes_job_name(self, jenkins_mock): def test_get_job_config_encodes_job_name(self, jenkins_mock):
j = jenkins.Jenkins('http://example.com/', 'test', 'test') j = jenkins.Jenkins('http://example.com/', 'test', 'test')
@@ -182,6 +190,22 @@ class JenkinsTest(unittest.TestCase):
jenkins_mock.call_args[0][0].get_full_url(), jenkins_mock.call_args[0][0].get_full_url(),
'http://example.com/job/TestJob') 'http://example.com/job/TestJob')
@patch('jenkins.urlopen')
def test_jenkins_open__timeout(self, jenkins_mock):
jenkins_mock.side_effect = jenkins.URLError(
reason="timed out")
j = jenkins.Jenkins('http://example.com/', 'test', 'test', timeout=1)
request = jenkins.Request('http://example.com/job/TestJob')
with self.assertRaises(jenkins.JenkinsException) as context_manager:
j.jenkins_open(request, add_crumb=False)
self.assertEqual(
str(context_manager.exception),
'Error in request: timed out')
self.assertEqual(
jenkins_mock.call_args[0][0].get_full_url(),
'http://example.com/job/TestJob')
@patch.object(jenkins.Jenkins, 'jenkins_open') @patch.object(jenkins.Jenkins, 'jenkins_open')
def test_assert_job_exists__job_missing(self, jenkins_mock): def test_assert_job_exists__job_missing(self, jenkins_mock):
jenkins_mock.side_effect = jenkins.NotFoundException() jenkins_mock.side_effect = jenkins.NotFoundException()