From cabb95d873c488b8a2d0a30926484ae7c4c9b4db Mon Sep 17 00:00:00 2001 From: Brandon Leonard Date: Mon, 22 Jun 2015 13:22:45 -0500 Subject: [PATCH] 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 --- README.rst | 1 + doc/source/example.rst | 5 + jenkins/__init__.py | 128 +++++++++++++++++++++++- tests/test_jenkins.py | 217 +++++++++++++++++++++++++++++++++++++++++ 4 files changed, 350 insertions(+), 1 deletion(-) 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 6a989fd..8eb2538 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.''' @@ -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) 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)