Merge "Creates ability to work with views in jenkins"

This commit is contained in:
Jenkins 2015-07-06 16:33:01 +00:00 committed by Gerrit Code Review
commit 60ba8a2156
4 changed files with 350 additions and 1 deletions

View File

@ -20,6 +20,7 @@ the things you can use it for:
* Create nodes
* Enable/Disable nodes
* Get information on nodes
* Create/delete/reconfig views
* and many more..
To install::

View File

@ -15,6 +15,11 @@ Example usage::
j.delete_job('empty')
j.delete_job('empty_copy')
j.get_views()
j.create_view('EMPTY', jenkins.EMPTY_VIEW_CONFIG_XML)
j.view_exists('EMPTY')
j.delete_view('EMPTY')
# build a parameterized job
# requires setting up api-test job to accept 'param1' & 'param2'
j.build_job('api-test', {'param1': 'test value 1', 'param2': 'test value 2'})

View File

@ -90,6 +90,10 @@ NODE_INFO = 'computer/%(name)s/api/json?depth=%(depth)s'
NODE_TYPE = 'hudson.slaves.DumbSlave$DescriptorImpl'
TOGGLE_OFFLINE = 'computer/%(name)s/toggleOffline?offlineMessage=%(msg)s'
CONFIG_NODE = 'computer/%(name)s/config.xml'
VIEW_NAME = 'view/%(name)s/api/json?tree=name'
CREATE_VIEW = 'createView?name=%(name)s'
CONFIG_VIEW = 'view/%(name)s/config.xml'
DELETE_VIEW = 'view/%(name)s/doDelete'
# for testing only
EMPTY_CONFIG_XML = '''<?xml version='1.0' encoding='UTF-8'?>
@ -118,7 +122,7 @@ RECONFIG_XML = '''<?xml version='1.0' encoding='UTF-8'?>
<blockBuildWhenUpstreamBuilding>false</blockBuildWhenUpstreamBuilding>
<triggers class='vector'/>
<concurrentBuild>false</concurrentBuild>
<builders>
<builders>
<jenkins.tasks.Shell>
<command>export FOO=bar</command>
</jenkins.tasks.Shell>
@ -127,6 +131,28 @@ RECONFIG_XML = '''<?xml version='1.0' encoding='UTF-8'?>
<buildWrappers/>
</project>'''
# for testing only
EMPTY_VIEW_CONFIG_XML = '''<?xml version="1.0" encoding="UTF-8"?>
<hudson.model.ListView>
<name>EMPTY</name>
<filterExecutors>false</filterExecutors>
<filterQueue>false</filterQueue>
<properties class="hudson.model.View$PropertyList"/>
<jobNames>
<comparator class="hudson.util.CaseInsensitiveComparator"/>
</jobNames>
<jobFilters/>
<columns>
<hudson.views.StatusColumn/>
<hudson.views.WeatherColumn/>
<hudson.views.JobColumn/>
<hudson.views.LastSuccessColumn/>
<hudson.views.LastFailureColumn/>
<hudson.views.LastDurationColumn/>
<hudson.views.BuildButtonColumn/>
</columns>
</hudson.model.ListView>'''
class JenkinsException(Exception):
'''General exception type for jenkins-API-related failures.'''
@ -848,3 +874,103 @@ class Jenkins(object):
except HTTPError:
raise JenkinsException('job[%s] number[%d] does not exist'
% (name, number))
def get_view_name(self, name):
'''Return the name of a view using the API.
That is roughly an identity method which can be used to quickly verify
a view exists or is accessible without causing too much stress on the
server side.
:param name: View name, ``str``
:returns: Name of view or None
'''
try:
response = self.jenkins_open(
Request(self.server + VIEW_NAME %
self._get_encoded_params(locals())))
except NotFoundException:
return None
else:
actual = json.loads(response)['name']
if actual != name:
raise JenkinsException(
'Jenkins returned an unexpected view name %s '
'(expected: %s)' % (actual, name))
return actual
def assert_view_exists(self, name,
exception_message='view[%s] does not exist'):
'''Raise an exception if a view does not exist
:param name: Name of Jenkins view, ``str``
:param exception_message: Message to use for the exception. Formatted
with ``name``
:throws: :class:`JenkinsException` whenever the view does not exist
'''
if not self.view_exists(name):
raise JenkinsException(exception_message % name)
def view_exists(self, name):
'''Check whether a view exists
:param name: Name of Jenkins view, ``str``
:returns: ``True`` if Jenkins view exists
'''
if self.get_view_name(name) == name:
return True
def get_views(self):
"""Get list of views running.
Each view is a dictionary with 'name' and 'url' keys.
:returns: list of views, ``[ { str: str} ]``
"""
return self.get_info()['views']
def delete_view(self, name):
'''Delete Jenkins view permanently.
:param name: Name of Jenkins view, ``str``
'''
self.jenkins_open(Request(
self.server + DELETE_VIEW % self._get_encoded_params(locals()),
b''))
if self.view_exists(name):
raise JenkinsException('delete[%s] failed' % (name))
def create_view(self, name, config_xml):
'''Create a new Jenkins view
:param name: Name of Jenkins view, ``str``
:param config_xml: config file text, ``str``
'''
if self.view_exists(name):
raise JenkinsException('view[%s] already exists' % (name))
self.jenkins_open(Request(
self.server + CREATE_VIEW % self._get_encoded_params(locals()),
config_xml.encode('utf-8'), DEFAULT_HEADERS))
self.assert_view_exists(name, 'create[%s] failed')
def reconfig_view(self, name, config_xml):
'''Change configuration of existing Jenkins view.
To create a new view, see :meth:`Jenkins.create_view`.
:param name: Name of Jenkins view, ``str``
:param config_xml: New XML configuration, ``str``
'''
reconfig_url = self.server + CONFIG_VIEW % self._get_encoded_params(locals())
self.jenkins_open(Request(reconfig_url, config_xml.encode('utf-8'),
DEFAULT_HEADERS))
def get_view_config(self, name):
'''Get configuration of existing Jenkins view.
:param name: Name of Jenkins view, ``str``
:returns: view configuration (XML format)
'''
request = Request(self.server + CONFIG_VIEW % self._get_encoded_params(locals()))
return self.jenkins_open(request)

View File

@ -1642,3 +1642,220 @@ class JenkinsTest(unittest.TestCase):
jenkins_mock.call_args[0][0].get_full_url(),
u'http://example.com/queue/api/json?depth=0')
self._check_requests(jenkins_mock.call_args_list)
@patch.object(jenkins.Jenkins, 'jenkins_open')
def test_get_view_name(self, jenkins_mock):
view_name_to_return = {u'name': 'Test View'}
jenkins_mock.return_value = json.dumps(view_name_to_return)
j = jenkins.Jenkins('http://example.com/', 'test', 'test')
view_name = j.get_view_name(u'Test View')
self.assertEqual(view_name, 'Test View')
self.assertEqual(
jenkins_mock.call_args[0][0].get_full_url(),
u'http://example.com/view/Test%20View/api/json?tree=name')
self._check_requests(jenkins_mock.call_args_list)
@patch.object(jenkins.Jenkins, 'jenkins_open')
def test_get_view_name__None(self, jenkins_mock):
jenkins_mock.side_effect = jenkins.NotFoundException()
j = jenkins.Jenkins('http://example.com/', 'test', 'test')
view_name = j.get_view_name(u'TestView')
self.assertEqual(view_name, None)
self.assertEqual(
jenkins_mock.call_args[0][0].get_full_url(),
u'http://example.com/view/TestView/api/json?tree=name')
self._check_requests(jenkins_mock.call_args_list)
@patch.object(jenkins.Jenkins, 'jenkins_open')
def test_get_view_name__unexpected_view_name(self, jenkins_mock):
view_name_to_return = {u'name': 'not the right name'}
jenkins_mock.return_value = json.dumps(view_name_to_return)
j = jenkins.Jenkins('http://example.com/', 'test', 'test')
with self.assertRaises(jenkins.JenkinsException) as context_manager:
j.get_view_name(u'TestView')
self.assertEqual(
jenkins_mock.call_args_list[0][0][0].get_full_url(),
'http://example.com/view/TestView/api/json?tree=name')
self.assertEqual(
str(context_manager.exception),
'Jenkins returned an unexpected view name {0} '
'(expected: {1})'.format(view_name_to_return['name'], 'TestView'))
self._check_requests(jenkins_mock.call_args_list)
@patch.object(jenkins.Jenkins, 'jenkins_open')
def test_assert_view_exists__view_missing(self, jenkins_mock):
jenkins_mock.side_effect = jenkins.NotFoundException()
j = jenkins.Jenkins('http://example.com/', 'test', 'test')
with self.assertRaises(jenkins.JenkinsException) as context_manager:
j.assert_view_exists('NonExistent')
self.assertEqual(
str(context_manager.exception),
'view[NonExistent] does not exist')
self._check_requests(jenkins_mock.call_args_list)
@patch.object(jenkins.Jenkins, 'jenkins_open')
def test_assert_view_exists__view_exists(self, jenkins_mock):
jenkins_mock.side_effect = [
json.dumps({'name': 'ExistingView'}),
]
j = jenkins.Jenkins('http://example.com/', 'test', 'test')
j.assert_view_exists('ExistingView')
self._check_requests(jenkins_mock.call_args_list)
@patch.object(jenkins.Jenkins, 'jenkins_open')
def test_get_views(self, jenkins_mock):
views = {
u'url': u'http://your_url_here/view/my_view/',
u'name': u'my_view',
}
view_info_to_return = {u'views': views}
jenkins_mock.return_value = json.dumps(view_info_to_return)
j = jenkins.Jenkins('http://example.com/', 'test', 'test')
view_info = j.get_views()
self.assertEqual(view_info, views)
self.assertEqual(
jenkins_mock.call_args[0][0].get_full_url(),
u'http://example.com/api/json')
self._check_requests(jenkins_mock.call_args_list)
@patch.object(jenkins.Jenkins, 'jenkins_open')
def test_delete_view(self, jenkins_mock):
jenkins_mock.side_effect = [
None,
jenkins.NotFoundException(),
]
j = jenkins.Jenkins('http://example.com/', 'test', 'test')
j.delete_view(u'Test View')
self.assertEqual(
jenkins_mock.call_args_list[0][0][0].get_full_url(),
'http://example.com/view/Test%20View/doDelete')
self._check_requests(jenkins_mock.call_args_list)
@patch.object(jenkins.Jenkins, 'jenkins_open')
def test_delete_view__delete_failed(self, jenkins_mock):
jenkins_mock.side_effect = [
json.dumps({'name': 'TestView'}),
json.dumps({'name': 'TestView'}),
json.dumps({'name': 'TestView'}),
]
j = jenkins.Jenkins('http://example.com/', 'test', 'test')
with self.assertRaises(jenkins.JenkinsException) as context_manager:
j.delete_view(u'TestView')
self.assertEqual(
jenkins_mock.call_args_list[0][0][0].get_full_url(),
'http://example.com/view/TestView/doDelete')
self.assertEqual(
str(context_manager.exception),
'delete[TestView] failed')
self._check_requests(jenkins_mock.call_args_list)
@patch.object(jenkins.Jenkins, 'jenkins_open')
def test_create_view(self, jenkins_mock):
config_xml = """
<listView>
<description>Foo</description>
<jobNames />
</listView>"""
jenkins_mock.side_effect = [
jenkins.NotFoundException(),
None,
json.dumps({'name': 'Test View'}),
]
j = jenkins.Jenkins('http://example.com/', 'test', 'test')
j.create_view(u'Test View', config_xml)
self.assertEqual(
jenkins_mock.call_args_list[1][0][0].get_full_url(),
'http://example.com/createView?name=Test%20View')
self._check_requests(jenkins_mock.call_args_list)
@patch.object(jenkins.Jenkins, 'jenkins_open')
def test_create_view__already_exists(self, jenkins_mock):
config_xml = """
<listView>
<description>Foo</description>
<jobNames />
</listView>"""
jenkins_mock.side_effect = [
json.dumps({'name': 'TestView'}),
None,
]
j = jenkins.Jenkins('http://example.com/', 'test', 'test')
with self.assertRaises(jenkins.JenkinsException) as context_manager:
j.create_view(u'TestView', config_xml)
self.assertEqual(
jenkins_mock.call_args_list[0][0][0].get_full_url(),
'http://example.com/view/TestView/api/json?tree=name')
self.assertEqual(
str(context_manager.exception),
'view[TestView] already exists')
self._check_requests(jenkins_mock.call_args_list)
@patch.object(jenkins.Jenkins, 'jenkins_open')
def test_create_view__create_failed(self, jenkins_mock):
config_xml = """
<listView>
<description>Foo</description>
<jobNames />
</listView>"""
jenkins_mock.side_effect = [
jenkins.NotFoundException(),
None,
jenkins.NotFoundException(),
]
j = jenkins.Jenkins('http://example.com/', 'test', 'test')
with self.assertRaises(jenkins.JenkinsException) as context_manager:
j.create_view(u'TestView', config_xml)
self.assertEqual(
jenkins_mock.call_args_list[0][0][0].get_full_url(),
'http://example.com/view/TestView/api/json?tree=name')
self.assertEqual(
jenkins_mock.call_args_list[1][0][0].get_full_url(),
'http://example.com/createView?name=TestView')
self.assertEqual(
str(context_manager.exception),
'create[TestView] failed')
self._check_requests(jenkins_mock.call_args_list)
@patch.object(jenkins.Jenkins, 'jenkins_open')
def test_reconfig_view(self, jenkins_mock):
config_xml = """
<listView>
<description>Foo</description>
<jobNames />
</listView>"""
jenkins_mock.side_effect = [
json.dumps({'name': 'Test View'}),
None,
]
j = jenkins.Jenkins('http://example.com/', 'test', 'test')
j.reconfig_view(u'Test View', config_xml)
self.assertEqual(jenkins_mock.call_args[0][0].get_full_url(),
u'http://example.com/view/Test%20View/config.xml')
self._check_requests(jenkins_mock.call_args_list)
@patch.object(jenkins.Jenkins, 'jenkins_open')
def test_get_view_config_encodes_view_name(self, jenkins_mock):
j = jenkins.Jenkins('http://example.com/', 'test', 'test')
j.get_view_config(u'Test View')
self.assertEqual(
jenkins_mock.call_args[0][0].get_full_url(),
u'http://example.com/view/Test%20View/config.xml')
self._check_requests(jenkins_mock.call_args_list)