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)