diff --git a/README.rst b/README.rst
index 96f5203..e244340 100644
--- a/README.rst
+++ b/README.rst
@@ -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::
diff --git a/doc/source/example.rst b/doc/source/example.rst
index f6e8e05..6088289 100644
--- a/doc/source/example.rst
+++ b/doc/source/example.rst
@@ -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'})
diff --git a/jenkins/__init__.py b/jenkins/__init__.py
index a65d01d..aa5ffce 100644
--- a/jenkins/__init__.py
+++ b/jenkins/__init__.py
@@ -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 = '''
@@ -118,7 +122,7 @@ RECONFIG_XML = '''
false
false
-
+
export FOO=bar
@@ -127,6 +131,28 @@ RECONFIG_XML = '''
'''
+# for testing only
+EMPTY_VIEW_CONFIG_XML = '''
+
+ EMPTY
+ false
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+'''
+
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)
diff --git a/tests/test_jenkins.py b/tests/test_jenkins.py
index b52d12d..b6c6698 100644
--- a/tests/test_jenkins.py
+++ b/tests/test_jenkins.py
@@ -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 = """
+
+ Foo
+
+ """
+ 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 = """
+
+ Foo
+
+ """
+ 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 = """
+
+ Foo
+
+ """
+ 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 = """
+
+ Foo
+
+ """
+ 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)