Allow to wait for jenkins to enter normal operation

During Jenkins deployment via e.g. puppet jenkins might be started but
not yet serving requests. Allow to wait for jenkins to enter normal
operation mode and fail if it doesn't show up after N seconds.

Allow for 0 seconds to just test and return success or failure without
any waiting.

This will allow us to avoid open coding "is jenkins ready?" for
different provisioners like puppet, chef, salt, ansible, ... when this
gets jused by jjb.

Change-Id: I7fd8aed43f571528f27dac681cc1e1f77a0e0ad7
Co-Authored-By: Darragh Bailey <dbailey@hp.com>
This commit is contained in:
Guido Günther 2015-02-11 20:04:15 +01:00 committed by Khai Do
parent 1b08573a94
commit 0f6739103b
3 changed files with 114 additions and 0 deletions

View File

@ -235,3 +235,34 @@ where *prom_name* is the name of the promotion that will get added to the job.
print server.get_promotion_config('prom_name', 'prom_job')
server.delete_promotion('prom_name', 'prom_job')
Example 10: Waiting for Jenkins to be ready
-------------------------------------------
It is possible to ask the API to wait for Jenkins to be ready with a given
timeout. This can be used to aid launching of Jenkins and then waiting for the
REST API to be responsive before continuing with subsequent configuration.
::
# timeout here is the socket connection timeout, for each connection
# attempt it will wait at most 5 seconds before assuming there is
# nothing listening. Useful where firewalls may black hole connections.
server = jenkins.Jenkins('http://localhost:8080', timeout=5)
# wait for at least 30 seconds for Jenkins to be ready
if server.wait_for_normal_op(30):
# actions once running
...
else:
print("Jenkins failed to be ready in sufficient time")
exit 2
Note that the timeout arg to `jenkins.Jenkins()` is the socket connection
timeout. If you set this to be more than the timeout value passed to
`wait_for_normal_op()`, then in cases where the underlying connection is not
rejected (firewall black-hole, or slow connection) then `wait_for_normal_op()`
may wait at least the connection timeout, or a multiple of it where multiple
connection attempts are made. A connection timeout of 5 seconds and a wait
timeout of 8 will result in potentially waiting 10 seconds if both connections
attempts do not get responses.

View File

@ -252,6 +252,7 @@ def auth_headers(username, password):
class Jenkins(object):
_timeout_warning_issued = False
def __init__(self, url, username=None, password=None,
timeout=socket._GLOBAL_DEFAULT_TIMEOUT):
@ -1642,3 +1643,61 @@ class Jenkins(object):
info = self.get_info()
if not info['quietingDown']:
raise JenkinsException('quiet down failed')
def wait_for_normal_op(self, timeout):
'''Wait for jenkins to enter normal operation mode.
:param timeout: number of seconds to wait, ``int``
Note this is not the same as the connection timeout set via
__init__ as that controls the socket timeout. Instead this is
how long to wait until the status returned.
:returns: ``True`` if Jenkins became ready in time, ``False``
otherwise.
Setting timeout to be less than the configured connection timeout
may result in this waiting for at least the connection timeout
length of time before returning. It is recommended that the timeout
here should be at least as long as any set connection timeout.
'''
if timeout < 0:
raise ValueError("Timeout must be >= 0 not %d" % timeout)
if (not self._timeout_warning_issued and
self.timeout != socket._GLOBAL_DEFAULT_TIMEOUT and
timeout < self.timeout):
warnings.warn("Requested timeout to wait for jenkins to resume "
"normal operations is less than configured "
"connection timeout. Unexpected behaviour may "
"occur.")
self._timeout_warning_issued = True
start_time = time.time()
def is_ready():
# only call get_version until it returns without exception
while True:
if self.get_version():
while True:
# json API will only return valid info once Jenkins
# is ready, so just check any known field exists
# when not in normal mode, most requests will
# be ignored or fail
yield 'mode' in self.get_info()
else:
yield False
while True:
try:
if next(is_ready()):
return True
except (KeyError, JenkinsException):
# key missing from JSON, empty response or errors in
# get_info due to incomplete HTTP responses
pass
# check time passed as the communication will also
# take time
if time.time() > start_time + timeout:
break
time.sleep(1)
return False

View File

@ -222,3 +222,27 @@ class JenkinsOpenTest(JenkinsTestBase):
jenkins_mock.call_args[0][0].get_full_url(),
self.make_url('job/TestJob'))
self._check_requests(jenkins_mock.call_args_list)
@patch.object(jenkins.Jenkins, 'jenkins_open',
return_value=json.dumps({'mode': 'NORMAL'}))
@patch.object(jenkins.Jenkins, 'get_version',
return_value="Version42")
def test_wait_for_normal_op(self, version_mock, jenkins_mock):
j = jenkins.Jenkins('http://example.com', 'test', 'test')
self.assertTrue(j.wait_for_normal_op(0))
@patch.object(jenkins.Jenkins, 'jenkins_open',
side_effect=jenkins.EmptyResponseException())
@patch.object(jenkins.Jenkins, 'get_version',
side_effect=jenkins.EmptyResponseException())
def test_wait_for_normal_op__empty_response(self, version_mock, jenkins_mock):
j = jenkins.Jenkins('http://example.com', 'test', 'test')
self.assertFalse(j.wait_for_normal_op(0))
def test_wait_for_normal_op__negative_timeout(self):
j = jenkins.Jenkins('http://example.com', 'test', 'test')
with self.assertRaises(ValueError) as context_manager:
j.wait_for_normal_op(-1)
self.assertEqual(
str(context_manager.exception),
"Timeout must be >= 0 not -1")