Creates ability to work with views in jenkins

This patch allows users to create, delete, reconfigure, and list views in
Jenkins. It is very similar to the protocols for working with jobs and has the
same code structure and format.

Change-Id: I79c557520cc9a417399a1a18df0f57da6904ab0e
This commit is contained in:
Brandon Leonard 2015-06-22 13:22:45 -05:00 committed by Antoine Musso
parent ecd79af18b
commit cabb95d873
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.'''
@ -846,3 +872,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)