Merge "Creates ability to work with views in jenkins"
This commit is contained in:
commit
60ba8a2156
@ -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::
|
||||
|
@ -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'})
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
Loading…
Reference in New Issue
Block a user