From 8c5148af422d16b053aa04ea23aeff54b9e72c34 Mon Sep 17 00:00:00 2001 From: Rupesh Tare Date: Tue, 6 Jan 2015 19:40:30 +0530 Subject: [PATCH 01/67] writing unit test cases for cassandra module --- tests/unit/modules/cassandra_test.py | 113 +++++++++++++++++++++++++++ 1 file changed, 113 insertions(+) create mode 100644 tests/unit/modules/cassandra_test.py diff --git a/tests/unit/modules/cassandra_test.py b/tests/unit/modules/cassandra_test.py new file mode 100644 index 0000000000..7ad548dfe4 --- /dev/null +++ b/tests/unit/modules/cassandra_test.py @@ -0,0 +1,113 @@ +# -*- coding: utf-8 -*- +''' + :codeauthor: :email:`Rupesh Tare ` +''' + +# Import Salt Testing Libs +from salttesting import TestCase, skipIf +from salttesting.mock import ( + MagicMock, + patch, + NO_MOCK, + NO_MOCK_REASON +) + +# Import Salt Libs +from salt.modules import cassandra +HAS_PYCASSA = False +try: + from pycassa.system_manager import SystemManager + HAS_PYCASSA = True +except ImportError: + pass + +# Globals +# from pycassa.system_manager.SystemManager import list_keyspaces +cassandra.__grains__ = {} +cassandra.__salt__ = {} +cassandra.__context__ = {} +cassandra.__opts__ = {} + + +@skipIf(NO_MOCK, NO_MOCK_REASON) +class CassandraTestCase(TestCase): + ''' + Test cases for salt.modules.cassandra + ''' + def test_compactionstats(self): + ''' + Test for Return compactionstats info + ''' + mock = MagicMock(return_value='A') + with patch.object(cassandra, '_nodetool', mock): + self.assertEqual(cassandra.compactionstats(), 'A') + + def test_version(self): + ''' + Test for Return the cassandra version + ''' + mock = MagicMock(return_value='A') + with patch.object(cassandra, '_nodetool', mock): + self.assertEqual(cassandra.version(), 'A') + + def test_netstats(self): + ''' + Test for Return netstats info + ''' + mock = MagicMock(return_value='A') + with patch.object(cassandra, '_nodetool', mock): + self.assertEqual(cassandra.netstats(), 'A') + + def test_tpstats(self): + ''' + Test for Return tpstats info + ''' + mock = MagicMock(return_value='A') + with patch.object(cassandra, '_nodetool', mock): + self.assertEqual(cassandra.tpstats(), 'A') + + def test_info(self): + ''' + Test for Return cassandra node info + ''' + mock = MagicMock(return_value='A') + with patch.object(cassandra, '_nodetool', mock): + self.assertEqual(cassandra.info(), 'A') + + def test_ring(self): + ''' + Test for Return ring info + ''' + mock = MagicMock(return_value='A') + with patch.object(cassandra, '_nodetool', mock): + self.assertEqual(cassandra.ring(), 'A') + + def test_keyspaces(self): + ''' + Test for Return existing keyspaces + ''' + mock = MagicMock(side_effect=['thrift_port', 'host']) + with patch.dict(cassandra.__salt__, {'config.option': mock}): + mock = MagicMock() + with patch.object(cassandra, 'SystemManager', mock): + mock = MagicMock(return_value=['A']) + with patch.object(cassandra, 'list_keyspaces', mock): + self.assertEqual(cassandra.keyspaces(), '') + + def test_column_families(self): + ''' + Test for Return existing column families for all keyspaces + ''' + pass + + def test_column_family_definition(self): + ''' + Test for Return a dictionary of column family definitions for the given + keyspace/column_family + ''' + pass + + +if __name__ == '__main__': + from integration import run_tests + run_tests(CassandraTestCase, needs_daemon=False) From 09b6088b624515ebee41be6344ef085b93f41c8c Mon Sep 17 00:00:00 2001 From: Rupesh Tare Date: Wed, 7 Jan 2015 17:21:17 +0530 Subject: [PATCH 02/67] Implemented unit test cases for module environ --- tests/unit/modules/environ_test.py | 112 +++++++++++++++++++++++++++++ 1 file changed, 112 insertions(+) create mode 100644 tests/unit/modules/environ_test.py diff --git a/tests/unit/modules/environ_test.py b/tests/unit/modules/environ_test.py new file mode 100644 index 0000000000..2bf78e4038 --- /dev/null +++ b/tests/unit/modules/environ_test.py @@ -0,0 +1,112 @@ +# -*- coding: utf-8 -*- +''' + :codeauthor: :email:`Rupesh Tare ` +''' + +# Import Salt Testing Libs +from salttesting import TestCase, skipIf +from salttesting.mock import ( + MagicMock, + patch, + NO_MOCK, + NO_MOCK_REASON +) + +# Import Salt Libs +from salt.modules import environ +from salt.exceptions import CommandExecutionError +import os + + +# Globals +environ.__grains__ = {} +environ.__salt__ = {} +environ.__context__ = {} + + +@skipIf(NO_MOCK, NO_MOCK_REASON) +class EnvironTestCase(TestCase): + ''' + Test cases for salt.modules.environ + ''' + def test_setval(self): + ''' + Test for set a single salt process environment variable. Returns True + on success. + ''' + mock = MagicMock(return_value=None) + with patch.object(os.environ, 'pop', mock): + self.assertEqual(environ.setval('key', False, True), None) + + mock = MagicMock(side_effect=Exception()) + with patch.object(os.environ, 'pop', mock): + self.assertFalse(environ.setval('key', False, True)) + + self.assertEqual(environ.setval('key', False), '') + + self.assertFalse(environ.setval('key', True)) + + def test_setenv(self): + ''' + Set multiple salt process environment variables from a dict. + Returns a dict. + ''' + self.assertFalse(environ.setenv('environ')) + + self.assertFalse(environ.setenv({'A': True}, + False, + True, + False)) + + mock = MagicMock(return_value={}) + with patch.dict(os.environ, mock): + mock = MagicMock(return_value=None) + with patch.object(environ, 'setval', mock): + self.assertEqual(environ.setenv({}, + False, + True, + False)['QT_QPA_PLATFORMTHEME'], + None) + + def test_get(self): + ''' + Get a single salt process environment variable. + ''' + self.assertFalse(environ.get(True)) + + self.assertEqual(environ.get('key'), '') + + def test_has_value(self): + ''' + Determine whether the key exists in the current salt process + environment dictionary. Optionally compare the current value + of the environment against the supplied value string. + ''' + self.assertFalse(environ.has_value(True)) + + self.assertTrue(environ.has_value('QT_QPA_PLATFORMTHEME', + 'appmenu-qt5')) + + self.assertFalse(environ.has_value('QT_QPA_PLATFORMTHEME', 'value')) + + self.assertFalse(environ.has_value('key', 'value')) + + self.assertFalse(environ.has_value('key')) + + def test_item(self): + ''' + Get one or more salt process environment variables. + Returns a dict. + ''' + self.assertEqual(environ.item(None), {}) + + def test_items(self): + ''' + Return a dict of the entire environment set for the salt process + ''' + self.assertTrue(environ.items()) + + +if __name__ == '__main__': + from integration import run_tests + run_tests(EnvironTestCase, needs_daemon=False) From 7a83039a5a14cc265b0fb4913d6dbc5312dd58fa Mon Sep 17 00:00:00 2001 From: Rupesh Tare Date: Wed, 7 Jan 2015 17:42:56 +0530 Subject: [PATCH 03/67] removed unused import --- tests/unit/modules/environ_test.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/unit/modules/environ_test.py b/tests/unit/modules/environ_test.py index 2bf78e4038..5f26074fa6 100644 --- a/tests/unit/modules/environ_test.py +++ b/tests/unit/modules/environ_test.py @@ -14,7 +14,6 @@ from salttesting.mock import ( # Import Salt Libs from salt.modules import environ -from salt.exceptions import CommandExecutionError import os From 73ccbf6b37ec3ba59a04ba41b442535f1adc1f8d Mon Sep 17 00:00:00 2001 From: Rupesh Tare Date: Wed, 7 Jan 2015 18:46:47 +0530 Subject: [PATCH 04/67] Implemented unit test cases fordebian_service module --- tests/unit/modules/debian_service_test.py | 176 ++++++++++++++++++++++ 1 file changed, 176 insertions(+) create mode 100644 tests/unit/modules/debian_service_test.py diff --git a/tests/unit/modules/debian_service_test.py b/tests/unit/modules/debian_service_test.py new file mode 100644 index 0000000000..811f50aa2f --- /dev/null +++ b/tests/unit/modules/debian_service_test.py @@ -0,0 +1,176 @@ +# -*- coding: utf-8 -*- +''' + :codeauthor: :email:`Rupesh Tare ` +''' + +# Import Salt Testing Libs +from salttesting import TestCase, skipIf +from salttesting.mock import ( + MagicMock, + patch, + NO_MOCK, + NO_MOCK_REASON +) + +# Import Salt Libs +from salt.modules import debian_service + + +# Globals +debian_service.__grains__ = {} +debian_service.__salt__ = {} +debian_service.__context__ = {} +debian_service.__opts__ = {} + + +@skipIf(NO_MOCK, NO_MOCK_REASON) +class DebianServicesTestCase(TestCase): + ''' + Test cases for salt.modules.debian_service + ''' + def test_get_enabled(self): + ''' + Test for Return a list of service that are enabled on boot + ''' + mock = MagicMock(return_value=1) + with patch.object(debian_service, '_get_runlevel', mock): + self.assertEqual(debian_service.get_enabled()[0], 'apparmor') + + def test_get_disabled(self): + ''' + Test for Return a set of services that are installed but disabled + ''' + mock = MagicMock(return_value=['A']) + with patch.object(debian_service, 'get_all', mock): + mock = MagicMock(return_value=['B']) + with patch.object(debian_service, 'get_enabled', mock): + self.assertEqual(debian_service.get_disabled(), ['A']) + + def test_available(self): + ''' + Test for Returns ``True`` if the specified service is + available, otherwise returns + ``False``. + ''' + mock = MagicMock(return_value=['A']) + with patch.object(debian_service, 'get_all', mock): + self.assertFalse(debian_service.available('name')) + + def test_missing(self): + ''' + Test for The inverse of service.available. + ''' + mock = MagicMock(return_value=['A']) + with patch.object(debian_service, 'get_all', mock): + self.assertTrue(debian_service.missing('name')) + + def test_getall(self): + ''' + Test for Return all available boot services + ''' + mock = MagicMock(return_value=('A')) + with patch.object(debian_service, 'get_enabled', mock): + self.assertEqual(debian_service.get_all()[0], 'A') + + def test_start(self): + ''' + Test for Start the specified service + ''' + mock = MagicMock(return_value=True) + with patch.object(debian_service, '_service_cmd', mock): + with patch.dict(debian_service.__salt__, {'cmd.retcode': mock}): + self.assertFalse(debian_service.start('name')) + + def test_stop(self): + ''' + Test for Stop the specified service + ''' + mock = MagicMock(return_value=True) + with patch.object(debian_service, '_service_cmd', mock): + with patch.dict(debian_service.__salt__, {'cmd.retcode': mock}): + self.assertFalse(debian_service.stop('name')) + + def test_restart(self): + ''' + Test for Restart the named service + ''' + mock = MagicMock(return_value=True) + with patch.object(debian_service, '_service_cmd', mock): + with patch.dict(debian_service.__salt__, {'cmd.retcode': mock}): + self.assertFalse(debian_service.restart('name')) + + def test_reload_(self): + ''' + Test for Reload the named service + ''' + mock = MagicMock(return_value=True) + with patch.object(debian_service, '_service_cmd', mock): + with patch.dict(debian_service.__salt__, {'cmd.retcode': mock}): + self.assertFalse(debian_service.reload_('name')) + + def test_force_reload(self): + ''' + Test for Force-reload the named service + ''' + mock = MagicMock(return_value=True) + with patch.object(debian_service, '_service_cmd', mock): + with patch.dict(debian_service.__salt__, {'cmd.retcode': mock}): + self.assertFalse(debian_service.force_reload('name')) + + def test_status(self): + ''' + Test for Return the status for a service + ''' + mock = MagicMock(return_value=True) + with patch.dict(debian_service.__salt__, {'status.pid': mock}): + self.assertTrue(debian_service.status('name', 1)) + + mock = MagicMock(return_value='A') + with patch.object(debian_service, '_service_cmd', mock): + mock = MagicMock(return_value=True) + with patch.dict(debian_service.__salt__, {'cmd.retcode': mock}): + self.assertFalse(debian_service.status('name')) + + def test_enable(self): + ''' + Test for Enable the named service to start at boot + ''' + mock = MagicMock(return_value='5') + with patch.object(debian_service, '_osrel', mock): + mock = MagicMock(return_value='') + with patch.object(debian_service, '_cmd_quote', mock): + mock = MagicMock(return_value=True) + with patch.dict(debian_service.__salt__, + {'cmd.retcode': mock}): + self.assertFalse(debian_service.enable('name')) + + def test_disable(self): + ''' + Test for Disable the named service to start at boot + ''' + mock = MagicMock(return_value='5') + with patch.object(debian_service, '_osrel', mock): + mock = MagicMock(return_value=True) + with patch.dict(debian_service.__salt__, {'cmd.retcode': mock}): + self.assertFalse(debian_service.disable('name')) + + def test_enabled(self): + ''' + Test for Return True if the named service is enabled, false otherwise + ''' + mock = MagicMock(return_value=['A']) + with patch.object(debian_service, 'get_enabled', mock): + self.assertFalse(debian_service.enabled('name')) + + def test_disabled(self): + ''' + Test for Return True if the named service is enabled, false otherwise + ''' + mock = MagicMock(return_value=['A']) + with patch.object(debian_service, 'get_enabled', mock): + self.assertFalse(debian_service.disabled('name')) + + +if __name__ == '__main__': + from integration import run_tests + run_tests(DebianServicesTestCase, needs_daemon=False) From 0de948f03b07f0a4e9e388359bffaf59cbe73cff Mon Sep 17 00:00:00 2001 From: Rupesh Tare Date: Thu, 8 Jan 2015 18:57:58 +0530 Subject: [PATCH 05/67] Implemented unit test cases for cassandra module --- tests/unit/modules/cassandra_test.py | 52 +++++++++++++++++++++++----- 1 file changed, 44 insertions(+), 8 deletions(-) diff --git a/tests/unit/modules/cassandra_test.py b/tests/unit/modules/cassandra_test.py index 7ad548dfe4..0e32918aa1 100644 --- a/tests/unit/modules/cassandra_test.py +++ b/tests/unit/modules/cassandra_test.py @@ -14,6 +14,7 @@ from salttesting.mock import ( # Import Salt Libs from salt.modules import cassandra + HAS_PYCASSA = False try: from pycassa.system_manager import SystemManager @@ -86,26 +87,61 @@ class CassandraTestCase(TestCase): ''' Test for Return existing keyspaces ''' - mock = MagicMock(side_effect=['thrift_port', 'host']) + mock = MagicMock(side_effect=['8000', 'localhost']) with patch.dict(cassandra.__salt__, {'config.option': mock}): - mock = MagicMock() - with patch.object(cassandra, 'SystemManager', mock): - mock = MagicMock(return_value=['A']) - with patch.object(cassandra, 'list_keyspaces', mock): - self.assertEqual(cassandra.keyspaces(), '') + with patch.object(SystemManager, + 'list_keyspaces') as mock_method: + mock_method.return_value = ['A'] + self.assertEqual(cassandra.keyspaces(), ['A']) def test_column_families(self): ''' Test for Return existing column families for all keyspaces ''' - pass + mock = MagicMock(side_effect=['8000', 'localhost']) + with patch.dict(cassandra.__salt__, {'config.option': mock}): + with patch.object(SystemManager, + 'list_keyspaces') as mock_method: + mock_method.return_value = ['A'] + self.assertEqual(cassandra.column_families('B'), None) + + mock = MagicMock(side_effect=['8000', 'localhost']) + with patch.dict(cassandra.__salt__, {'config.option': mock}): + with patch.object(SystemManager, + 'list_keyspaces') as mock_method: + mock_method.return_value = ['A'] + with patch.object(SystemManager, + 'get_keyspace_column_families') as mock_method: + mock_method.return_value = {'B': 1, 'C': 2} + self.assertEqual(cassandra.column_families('A'), + ['C', 'B']) + + mock = MagicMock(side_effect=['8000', 'localhost']) + with patch.dict(cassandra.__salt__, {'config.option': mock}): + with patch.object(SystemManager, + 'list_keyspaces') as mock_method: + mock_method.return_value = ['A'] + with patch.object(SystemManager, + 'get_keyspace_column_families') as mock_method: + mock_method.return_value = {'B': 1, 'C': 2} + self.assertEqual(cassandra.column_families(), {'A': + ['C', 'B']}) def test_column_family_definition(self): ''' Test for Return a dictionary of column family definitions for the given keyspace/column_family ''' - pass + mock = MagicMock(side_effect=['8000', 'localhost']) + with patch.dict(cassandra.__salt__, {'config.option': mock}): + with patch.object(SystemManager, + 'list_keyspaces') as mock_method: + mock_method.return_value = ['A'] + with patch.object(SystemManager, + 'get_keyspace_column_families') as mock_method: + mock_method.return_value = {'B': 1, 'C': 2} + self.assertEqual(cassandra.column_family_definition('A', 'B'), + None) if __name__ == '__main__': From e5bdbf592c58b9842fc5275ff9cc96f38490bff4 Mon Sep 17 00:00:00 2001 From: Rupesh Tare Date: Fri, 9 Jan 2015 17:06:59 +0530 Subject: [PATCH 06/67] Implemented unit test cases for dnsmasq module --- tests/unit/modules/dnsmasq_test.py | 86 ++++++++++++++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 tests/unit/modules/dnsmasq_test.py diff --git a/tests/unit/modules/dnsmasq_test.py b/tests/unit/modules/dnsmasq_test.py new file mode 100644 index 0000000000..d8d59a73e4 --- /dev/null +++ b/tests/unit/modules/dnsmasq_test.py @@ -0,0 +1,86 @@ +# -*- coding: utf-8 -*- +''' + :codeauthor: :email:`Rupesh Tare ` +''' + +# Import Salt Testing Libs +from salttesting import TestCase, skipIf +from salttesting.mock import ( + mock_open, + MagicMock, + patch, + NO_MOCK, + NO_MOCK_REASON +) + +# Import Salt Libs +from salt.modules import dnsmasq + +# Import python libs +import os + +# Globals +dnsmasq.__salt__ = {} + + +@skipIf(NO_MOCK, NO_MOCK_REASON) +class DnsmasqTestCase(TestCase): + ''' + TestCase for the salt.modules.at module + ''' + def test_version(self): + ''' + test to show installed version of dnsmasq. + ''' + mock = MagicMock(return_value='A B C') + with patch.dict(dnsmasq.__salt__, {'cmd.run': mock}): + self.assertEqual(dnsmasq.version(), "C") + + def test_fullversion(self): + ''' + Test to Show installed version of dnsmasq and compile options. + ''' + mock = MagicMock(return_value='A B C\nD E F G H I') + with patch.dict(dnsmasq.__salt__, {'cmd.run': mock}): + self.assertDictEqual(dnsmasq.fullversion(), + {'version': 'C', + 'compile options': ['G', 'H', 'I']}) + + def test_set_config(self): + ''' + test to show installed version of dnsmasq. + ''' + mock = MagicMock(return_value={'conf-dir': 'A'}) + with patch.object(dnsmasq, 'get_config', mock): + mock = MagicMock(return_value=['.', '~', 'bak', '#']) + with patch.object(os, 'listdir', mock): + self.assertDictEqual(dnsmasq.set_config(), {}) + + def test_get_config(self): + ''' + test to dumps all options from the config file. + ''' + mock = MagicMock(return_value={'conf-dir': 'A'}) + with patch.object(dnsmasq, 'get_config', mock): + mock = MagicMock(return_value=['.', '~', 'bak', '#']) + with patch.object(os, 'listdir', mock): + self.assertDictEqual(dnsmasq.get_config(), {'conf-dir': 'A'}) + + def test_parse_dnamasq(self): + ''' + test for generic function for parsing dnsmasq files including includes. + ''' + text_file_data = '\n'.join(["line here", "second line", "A=B", "#"]) + with patch('salt.utils.fopen', + mock_open(read_data=text_file_data), + create=True) as m: + m.return_value.__iter__.return_value = text_file_data.splitlines() + self.assertDictEqual(dnsmasq._parse_dnamasq('filename'), + {'A': 'B', + 'unparsed': ['a line here', + 'the second line']}) + + +if __name__ == '__main__': + from integration import run_tests + run_tests(DnsmasqTestCase, needs_daemon=False) From ff221219f2a6c4a979d717977a8262fa121eab00 Mon Sep 17 00:00:00 2001 From: Erik Johnson Date: Fri, 9 Jan 2015 14:57:28 -0600 Subject: [PATCH 07/67] Remove trailing underscore from cmd_{un,}zip_ This is unnecessary and means that one needs to use the trailing underscore if they want to directly run those functions. --- salt/modules/archive.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/salt/modules/archive.py b/salt/modules/archive.py index 5feedb8dd6..ae4f4e8b89 100644 --- a/salt/modules/archive.py +++ b/salt/modules/archive.py @@ -169,8 +169,8 @@ def gunzip(gzipfile, template=None, runas=None): @decorators.which('zip') -def cmd_zip_(zip_file, sources, template=None, - cwd=None, recurse=False, runas=None): +def cmd_zip(zip_file, sources, template=None, + cwd=None, recurse=False, runas=None): ''' Uses the zip command to create zip files @@ -225,8 +225,8 @@ def cmd_zip_(zip_file, sources, template=None, python_shell=False).splitlines() -@decorators.depends('zipfile', fallback_function=cmd_zip_) -def zip_(archive, sources, template=None, runas=None): +@decorators.depends('zipfile', fallback_function=cmd_zip) +def zip_(archive, sources, template=None): ''' Uses the zipfile module to create zip files @@ -271,7 +271,8 @@ def zip_(archive, sources, template=None, runas=None): @decorators.which('unzip') -def cmd_unzip_(zip_file, dest, excludes=None, template=None, options=None, runas=None): +def cmd_unzip(zip_file, dest, excludes=None, + template=None, options=None, runas=None): ''' Uses the unzip command to unpack zip files @@ -321,8 +322,8 @@ def cmd_unzip_(zip_file, dest, excludes=None, template=None, options=None, runas python_shell=False).splitlines() -@decorators.depends('zipfile', fallback_function=cmd_unzip_) -def unzip(archive, dest, excludes=None, template=None, options=None, runas=None): +@decorators.depends('zipfile', fallback_function=cmd_unzip) +def unzip(archive, dest, excludes=None, template=None, options=None): ''' Uses the zipfile module to unpack zip files From f8f28580df477ec353e5faf7b1da2404c3691a7f Mon Sep 17 00:00:00 2001 From: Rupesh Tare Date: Tue, 13 Jan 2015 19:00:17 +0530 Subject: [PATCH 08/67] test ddns module --- tests/unit/modules/ddns_test.py | 111 ++++++++++++++++++++++++++++++++ 1 file changed, 111 insertions(+) create mode 100644 tests/unit/modules/ddns_test.py diff --git a/tests/unit/modules/ddns_test.py b/tests/unit/modules/ddns_test.py new file mode 100644 index 0000000000..6bb86ff374 --- /dev/null +++ b/tests/unit/modules/ddns_test.py @@ -0,0 +1,111 @@ +# -*- coding: utf-8 -*- +''' + :codeauthor: :email:`Rupesh Tare ` +''' + +# Import Salt Testing Libs +from salttesting import TestCase, skipIf +from salttesting.mock import ( + mock_open, + MagicMock, + patch, + NO_MOCK, + NO_MOCK_REASON +) + +# Import Salt Libs +from salt.modules import ddns + +try: + import dns.query + import dns.update + import dns.tsigkeyring + dns_support = True +except ImportError as e: + dns_support = False + +# Import salt libs +import salt.utils +import json +# Globals +ddns.__grains__ = {} +ddns.__salt__ = {} + + +@skipIf(NO_MOCK, NO_MOCK_REASON) +class DDNSTestCase(TestCase): + ''' + TestCase for the salt.modules.ddns module + ''' + @patch('salt.modules.ddns.update') + def test_add_host(self, ddns_update): + ''' + Test cases for Add, replace, or update the A + and PTR (reverse) records for a host. + ''' + ddns_update.return_value = False + self.assertFalse(ddns.add_host(zone='A', + name='B', + ttl=1, + ip='172.27.0.0')) + + ddns_update.return_value = True + self.assertTrue(ddns.add_host(zone='A', + name='B', + ttl=1, + ip='172.27.0.0')) + + @patch('salt.modules.ddns.delete') + def test_delete_host(self, ddns_delete): + ''' + Tests for delete the forward and reverse records for a host. + ''' + ddns_delete.return_value = False + with patch.object(dns.query, 'udp') as mock: + mock.answer = [{'address': 'localhost'}] + self.assertFalse(ddns.delete_host(zone='A', name='B')) + + def test_update(self): + ''' + Test to add, replace, or update a DNS record. + ''' + file_data = json.dumps({'A': 'B'}) + with patch('dns.message.make_query', return_value=True): + with patch('dns.rdatatype.from_text', return_value=True): + with patch('dns.rdata.from_text', return_value=True): + mock = MagicMock(return_value=True) + with patch.dict(ddns.__salt__, {'config.option': mock}): + mock = MagicMock(return_value=True) + with patch.dict(ddns.__salt__, {'file.file_exists': mock}): + with patch('salt.utils.fopen', + mock_open(read_data=file_data), + create=True): + with patch.object(dns.tsigkeyring, 'from_text', return_value=True): + with patch.object(dns.query, 'udp') as mock: + mock.answer = [{'address': 'localhost'}] + self.assertFalse(ddns.update(zone='A', + name='B', + ttl=1, + rdtype='C', + data='D')) + + def test_delete(self): + ''' + Test to delete a DNS record. + ''' + file_data = json.dumps({'A': 'B'}) + with patch.object(dns.query, 'udp') as mock: + mock.answer = [{'address': 'localhost'}] + mock = MagicMock(return_value=True) + with patch.dict(ddns.__salt__, {'config.option': mock}): + mock = MagicMock(return_value=True) + with patch.dict(ddns.__salt__, {'file.file_exists': mock}): + with patch('salt.utils.fopen', + mock_open(read_data=file_data), + create=True): + with patch.object(dns.tsigkeyring, 'from_text', return_value=True): + self.assertFalse(ddns.delete(zone='A', name='B')) + +if __name__ == '__main__': + from integration import run_tests + run_tests(DDNSTestCase, needs_daemon=False) \ No newline at end of file From 721fffa02ce26d8d0af0202c3888e150f127f833 Mon Sep 17 00:00:00 2001 From: Rupesh Tare Date: Tue, 13 Jan 2015 19:54:49 +0530 Subject: [PATCH 09/67] fix pylint errors --- tests/unit/modules/ddns_test.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/unit/modules/ddns_test.py b/tests/unit/modules/ddns_test.py index 6bb86ff374..f27ce87569 100644 --- a/tests/unit/modules/ddns_test.py +++ b/tests/unit/modules/ddns_test.py @@ -18,14 +18,11 @@ from salt.modules import ddns try: import dns.query - import dns.update import dns.tsigkeyring dns_support = True except ImportError as e: dns_support = False -# Import salt libs -import salt.utils import json # Globals ddns.__grains__ = {} @@ -76,11 +73,13 @@ class DDNSTestCase(TestCase): mock = MagicMock(return_value=True) with patch.dict(ddns.__salt__, {'config.option': mock}): mock = MagicMock(return_value=True) - with patch.dict(ddns.__salt__, {'file.file_exists': mock}): + with patch.dict(ddns.__salt__, + {'file.file_exists': mock}): with patch('salt.utils.fopen', mock_open(read_data=file_data), create=True): - with patch.object(dns.tsigkeyring, 'from_text', return_value=True): + with patch.object(dns.tsigkeyring, 'from_text', + return_value=True): with patch.object(dns.query, 'udp') as mock: mock.answer = [{'address': 'localhost'}] self.assertFalse(ddns.update(zone='A', @@ -103,9 +102,10 @@ class DDNSTestCase(TestCase): with patch('salt.utils.fopen', mock_open(read_data=file_data), create=True): - with patch.object(dns.tsigkeyring, 'from_text', return_value=True): + with patch.object(dns.tsigkeyring, 'from_text', + return_value=True): self.assertFalse(ddns.delete(zone='A', name='B')) if __name__ == '__main__': from integration import run_tests - run_tests(DDNSTestCase, needs_daemon=False) \ No newline at end of file + run_tests(DDNSTestCase, needs_daemon=False) From 95c82b10ce8777bbc6bcc6c2903a12b8807aa9c7 Mon Sep 17 00:00:00 2001 From: Colton Myers Date: Tue, 13 Jan 2015 11:52:05 -0700 Subject: [PATCH 10/67] Force contents to string under Falsey conditions too Ref #19669 --- salt/states/file.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/salt/states/file.py b/salt/states/file.py index 9b918d4440..6711bee4a3 100644 --- a/salt/states/file.py +++ b/salt/states/file.py @@ -1203,7 +1203,7 @@ def managed(name, run. ''' # contents must be a string - if contents: + if contents is not None: contents = str(contents) # Make sure that leading zeros stripped by YAML loader are added back From b39dbe9686756c0af87277d02b539f8dfae8cc73 Mon Sep 17 00:00:00 2001 From: Mike Place Date: Fri, 9 Jan 2015 16:58:21 -0700 Subject: [PATCH 11/67] Give event-based mines time to propogate --- salt/modules/mine.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/salt/modules/mine.py b/salt/modules/mine.py index f587884cb0..db61268a0a 100644 --- a/salt/modules/mine.py +++ b/salt/modules/mine.py @@ -7,6 +7,7 @@ from __future__ import absolute_import # Import python libs import copy import logging +import time # Import salt libs import salt.crypt @@ -41,7 +42,11 @@ def _mine_send(load, opts): load['tok'] = _auth().gen_token('salt') eventer = salt.utils.event.MinionEvent(opts) - return eventer.fire_event(load, '_minion_mine') + event_ret = eventer.fire_event(load, '_minion_mine') + # We need to pause here to allow for the decoupled nature of + # events time to allow the mine to propogate + time.sleep(2.0) + return event_ret def _mine_get(load, opts): From f04a81989b70d0e465b5e8b1ca954fa7759421dd Mon Sep 17 00:00:00 2001 From: Mike Place Date: Tue, 13 Jan 2015 12:24:45 -0700 Subject: [PATCH 12/67] Return unique list of minions to be batched. Refs #19535 --- salt/cli/batch.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/salt/cli/batch.py b/salt/cli/batch.py index 8f3a64e6cf..f1b5ddc80d 100644 --- a/salt/cli/batch.py +++ b/salt/cli/batch.py @@ -50,7 +50,8 @@ class Batch(object): if not self.quiet: print_cli('{0} Detected for this batch run'.format(minion)) fret.append(minion) - return sorted(fret) + # Returns + return sorted(frozenset(fret)) def get_bnum(self): ''' From 503dfea26b19d44a677448aad7376fa2b19e5333 Mon Sep 17 00:00:00 2001 From: Mike Place Date: Tue, 13 Jan 2015 13:09:23 -0700 Subject: [PATCH 13/67] Warn about unsupported vt in cmd mod Closes #19364 --- salt/modules/cmdmod.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/salt/modules/cmdmod.py b/salt/modules/cmdmod.py index 3295821ae9..9a79ea884d 100644 --- a/salt/modules/cmdmod.py +++ b/salt/modules/cmdmod.py @@ -214,6 +214,8 @@ def _run(cmd, if not os.path.isfile(shell) or not os.access(shell, os.X_OK): msg = 'The shell {0} is not available'.format(shell) raise CommandExecutionError(msg) + if salt.utils.is_windows() and use_vt: # Memozation so not much overhead + raise CommandExecutionError('VT not available on windows') if shell.lower().strip() == 'powershell': # If we were called by script(), then fakeout the Windows From 2717c1bd1c4bc3776aac968d7121256cc5ff7b6c Mon Sep 17 00:00:00 2001 From: Justin Findlay Date: Tue, 13 Jan 2015 13:09:49 -0700 Subject: [PATCH 14/67] fix freebsd commands --- salt/modules/freebsdkmod.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/salt/modules/freebsdkmod.py b/salt/modules/freebsdkmod.py index 30103e2fcd..1c10ffb5ac 100644 --- a/salt/modules/freebsdkmod.py +++ b/salt/modules/freebsdkmod.py @@ -63,7 +63,8 @@ def _set_persistent_module(mod): return set() mods = _get_persistent_modules() mods.add(mod) - __salt__['cmd.run_all']("sysrc kld_list='{0}'".format(' '.join(mods))) + __salt__['cmd.run_all']("sysrc kld_list='{0}'".format(' '.join(mods)), + python_shell=False) return set([mod]) @@ -75,7 +76,8 @@ def _remove_persistent_module(mod): return set() mods = _get_persistent_modules() mods.remove(mod) - __salt__['cmd.run_all']("sysrc kld_list='{0}'".format(' '.join(mods))) + __salt__['cmd.run_all']("sysrc kld_list='{0}'".format(' '.join(mods)), + python_shell=False) return set([mod]) @@ -178,7 +180,8 @@ def load(mod, persist=False): salt '*' kmod.load bhyve ''' pre_mods = lsmod() - response = __salt__['cmd.run_all']('kldload {0}'.format(mod)) + response = __salt__['cmd.run_all']('kldload {0}'.format(mod), + python_shell=False) if response['retcode'] == 0: post_mods = lsmod() mods = _new_mods(pre_mods, post_mods) @@ -221,7 +224,8 @@ def remove(mod, persist=False): salt '*' kmod.remove vmm ''' pre_mods = lsmod() - __salt__['cmd.run_all']('kldunload {0}'.format(mod)) + __salt__['cmd.run_all']('kldunload {0}'.format(mod), + python_shell=False) post_mods = lsmod() mods = _rm_mods(pre_mods, post_mods) persist_mods = set() From c172470c30802816fdcd4ba63e174d06042b2131 Mon Sep 17 00:00:00 2001 From: Mike Place Date: Thu, 8 Jan 2015 10:04:19 -0700 Subject: [PATCH 15/67] Try giving some rest tornado requests a little more time --- .../netapi/rest_tornado/test_app.py | 45 +++++++++++++++++-- 1 file changed, 42 insertions(+), 3 deletions(-) diff --git a/tests/integration/netapi/rest_tornado/test_app.py b/tests/integration/netapi/rest_tornado/test_app.py index 005afbc10d..7d6bd2988b 100644 --- a/tests/integration/netapi/rest_tornado/test_app.py +++ b/tests/integration/netapi/rest_tornado/test_app.py @@ -43,7 +43,10 @@ class TestSaltAPIHandler(SaltnadoTestCase): ''' Test the root path which returns the list of clients we support ''' - response = self.fetch('/') + response = self.fetch('/', + connect_timeout=10, + request_timeout=10, + ) self.assertEqual(response.code, 200) response_obj = json.loads(response.body) self.assertEqual(response_obj['clients'], @@ -67,7 +70,9 @@ class TestSaltAPIHandler(SaltnadoTestCase): method='POST', body=json.dumps(low), headers={'Content-Type': self.content_type_map['json']}, - follow_redirects=False + follow_redirects=False, + connect_timeout=10, + request_timeout=10, ) self.assertEqual(response.code, 302) self.assertEqual(response.headers['Location'], '/login') @@ -86,6 +91,8 @@ class TestSaltAPIHandler(SaltnadoTestCase): body=json.dumps(low), headers={'Content-Type': self.content_type_map['json'], saltnado.AUTH_TOKEN_HEADER: self.token['token']}, + connect_timeout=10, + request_timeout=10, ) response_obj = json.loads(response.body) self.assertEqual(response_obj['return'], [{'minion': True, 'sub_minion': True}]) @@ -103,6 +110,8 @@ class TestSaltAPIHandler(SaltnadoTestCase): body=json.dumps(low), headers={'Content-Type': self.content_type_map['json'], saltnado.AUTH_TOKEN_HEADER: self.token['token']}, + connect_timeout=10, + request_timeout=10, ) response_obj = json.loads(response.body) self.assertEqual(response_obj['return'], ["No minions matched the target. No command was sent, no jid was assigned."]) @@ -121,6 +130,8 @@ class TestSaltAPIHandler(SaltnadoTestCase): body=json.dumps(low), headers={'Content-Type': self.content_type_map['json'], saltnado.AUTH_TOKEN_HEADER: self.token['token']}, + connect_timeout=10, + request_timeout=10, ) response_obj = json.loads(response.body) self.assertEqual(response_obj['return'], [{'minion': True, 'sub_minion': True}]) @@ -140,6 +151,8 @@ class TestSaltAPIHandler(SaltnadoTestCase): body=json.dumps(low), headers={'Content-Type': self.content_type_map['json'], saltnado.AUTH_TOKEN_HEADER: self.token['token']}, + connect_timeout=10, + request_timeout=10, ) response_obj = json.loads(response.body) self.assertEqual(response_obj['return'], [{'minion': True, 'sub_minion': True}]) @@ -157,6 +170,8 @@ class TestSaltAPIHandler(SaltnadoTestCase): body=json.dumps(low), headers={'Content-Type': self.content_type_map['json'], saltnado.AUTH_TOKEN_HEADER: self.token['token']}, + connect_timeout=10, + request_timeout=10, ) response_obj = json.loads(response.body) self.assertEqual(response_obj['return'], [{}]) @@ -172,6 +187,8 @@ class TestSaltAPIHandler(SaltnadoTestCase): body=json.dumps(low), headers={'Content-Type': self.content_type_map['json'], saltnado.AUTH_TOKEN_HEADER: self.token['token']}, + connect_timeout=10, + request_timeout=10, ) response_obj = json.loads(response.body) # TODO: verify pub function? Maybe look at how we test the publisher @@ -193,6 +210,8 @@ class TestSaltAPIHandler(SaltnadoTestCase): body=json.dumps(low), headers={'Content-Type': self.content_type_map['json'], saltnado.AUTH_TOKEN_HEADER: self.token['token']}, + connect_timeout=10, + request_timeout=10, ) response_obj = json.loads(response.body) self.assertEqual(len(response_obj['return']), 2) @@ -222,6 +241,8 @@ class TestSaltAPIHandler(SaltnadoTestCase): body=json.dumps(low), headers={'Content-Type': self.content_type_map['json'], saltnado.AUTH_TOKEN_HEADER: self.token['token']}, + connect_timeout=10, + request_timeout=10, ) response_obj = json.loads(response.body) self.assertEqual(len(response_obj['return']), 3) # make sure we got 3 responses @@ -255,6 +276,8 @@ class TestSaltAPIHandler(SaltnadoTestCase): body=json.dumps(low), headers={'Content-Type': self.content_type_map['json'], saltnado.AUTH_TOKEN_HEADER: self.token['token']}, + connect_timeout=10, + request_timeout=10, ) response_obj = json.loads(response.body) self.assertEqual(response_obj['return'], [['minion', 'sub_minion']]) @@ -279,6 +302,8 @@ class TestMinionSaltAPIHandler(SaltnadoTestCase): method='GET', headers={saltnado.AUTH_TOKEN_HEADER: self.token['token']}, follow_redirects=False, + connect_timeout=10, + request_timeout=10, ) response_obj = json.loads(response.body) self.assertEqual(len(response_obj['return']), 1) @@ -293,6 +318,8 @@ class TestMinionSaltAPIHandler(SaltnadoTestCase): method='GET', headers={saltnado.AUTH_TOKEN_HEADER: self.token['token']}, follow_redirects=False, + connect_timeout=10, + request_timeout=10, ) response_obj = json.loads(response.body) self.assertEqual(len(response_obj['return']), 1) @@ -309,6 +336,8 @@ class TestMinionSaltAPIHandler(SaltnadoTestCase): body=json.dumps(low), headers={'Content-Type': self.content_type_map['json'], saltnado.AUTH_TOKEN_HEADER: self.token['token']}, + connect_timeout=10, + request_timeout=10, ) response_obj = json.loads(response.body) # TODO: verify pub function? Maybe look at how we test the publisher @@ -327,6 +356,8 @@ class TestMinionSaltAPIHandler(SaltnadoTestCase): body=json.dumps(low), headers={'Content-Type': self.content_type_map['json'], saltnado.AUTH_TOKEN_HEADER: self.token['token']}, + connect_timeout=10, + request_timeout=10, ) response_obj = json.loads(response.body) # TODO: verify pub function? Maybe look at how we test the publisher @@ -349,6 +380,8 @@ class TestMinionSaltAPIHandler(SaltnadoTestCase): body=json.dumps(low), headers={'Content-Type': self.content_type_map['json'], saltnado.AUTH_TOKEN_HEADER: self.token['token']}, + connect_timeout=10, + request_timeout=10, ) self.assertEqual(response.code, 400) @@ -431,6 +464,8 @@ class TestRunSaltAPIHandler(SaltnadoTestCase): body=json.dumps(low), headers={'Content-Type': self.content_type_map['json'], saltnado.AUTH_TOKEN_HEADER: self.token['token']}, + connect_timeout=10, + request_timeout=10, ) response_obj = json.loads(response.body) self.assertEqual(response_obj['return'], [{'minion': True, 'sub_minion': True}]) @@ -456,7 +491,9 @@ class TestEventsSaltAPIHandler(SaltnadoTestCase): self.events_to_fire = 5 response = self.fetch('/events', headers={saltnado.AUTH_TOKEN_HEADER: self.token['token']}, - streaming_callback=self.on_event + streaming_callback=self.on_event, + connect_timeout=10, + request_timeout=10, ) def _stop(self): @@ -516,6 +553,8 @@ class TestWebhookSaltAPIHandler(SaltnadoTestCase): method='POST', body='foo=bar', headers={saltnado.AUTH_TOKEN_HEADER: self.token['token']}, + connect_timeout=10, + request_timeout=10, ) response_obj = json.loads(response.body) self.assertTrue(response_obj['success']) From 374ab040114b500120d712898d05b8c043fc186b Mon Sep 17 00:00:00 2001 From: Flavian Date: Fri, 9 Jan 2015 19:50:03 +0100 Subject: [PATCH 16/67] Backport #19580 to 2014.7 Conflicts: salt/cloud/clouds/msazure.py --- salt/cloud/clouds/msazure.py | 48 +++++++++++++++++++++++++----------- 1 file changed, 34 insertions(+), 14 deletions(-) diff --git a/salt/cloud/clouds/msazure.py b/salt/cloud/clouds/msazure.py index 23afe8f132..b4f1ed5492 100644 --- a/salt/cloud/clouds/msazure.py +++ b/salt/cloud/clouds/msazure.py @@ -25,22 +25,27 @@ http://www.windowsazure.com/en-us/develop/python/how-to-guides/service-managemen # pylint: disable=E0102 # Import python libs -import time +from __future__ import absolute_import + import copy -import pprint import logging +import pprint +import time -# Import salt cloud libs import salt.config as config -import salt.utils.cloud from salt.exceptions import SaltCloudSystemExit +import salt.utils.cloud + +# Import python libs +# Import salt cloud libs # Import azure libs HAS_LIBS = False try: import azure import azure.servicemanagement - from azure import WindowsAzureConflictError + from azure import (WindowsAzureConflictError, + WindowsAzureMissingResourceError) HAS_LIBS = True except ImportError: pass @@ -435,10 +440,13 @@ def create(vm_): 'location': vm_['location'], } + ssh_port = config.get_cloud_config_value('port', vm_, __opts__, + default='22', search_global=True) + ssh_endpoint = azure.servicemanagement.ConfigurationSetInputEndpoint( name='SSH', protocol='TCP', - port='22', + port=ssh_port, local_port='22', ) @@ -461,7 +469,7 @@ def create(vm_): vm_kwargs = { 'service_name': service_name, - 'deployment_name': vm_['name'], + 'deployment_name': service_name, 'deployment_slot': vm_['slot'], 'label': label, 'role_name': vm_['name'], @@ -519,12 +527,18 @@ def create(vm_): return False try: conn.create_virtual_machine_deployment(**vm_kwargs) + except WindowsAzureConflictError: + log.debug("Conflict error. The deployment may already exist, trying add_role") + # Deleting two useless keywords + del vm_kwargs["deployment_slot"] + del vm_kwargs["label"] + conn.add_role(**vm_kwargs) except Exception as exc: error = 'The hosted service name is invalid.' if error in str(exc): log.error( 'Error creating {0} on Azure.\n\n' - 'The hosted service name is invalid. The name can contain ' + 'The VM name is invalid. The name can contain ' 'only letters, numbers, and hyphens. The name must start with ' 'a letter and must end with a letter or a number.'.format( vm_['name'] @@ -534,8 +548,12 @@ def create(vm_): ) else: log.error( - 'Error creating {0} on Azure\n\n' - 'The following exception was thrown when trying to ' + 'Error creating {0} on Azure.\n\n' + 'The Virtual Machine could not be created. If you ' + 'are using an already existing Cloud Service, ' + 'make sure you set up the `port` variable corresponding ' + 'to the SSH port exists and that the port number is not ' + 'already in use.\nThe following exception was thrown when trying to ' 'run the initial deployment: \n{1}'.format( vm_['name'], str(exc) ), @@ -549,11 +567,12 @@ def create(vm_): Wait for the IP address to become available ''' try: - data = show_instance(vm_['name'], call='action') - except Exception: + conn.get_role(service_name, service_name, vm_["name"]) + data = show_instance(service_name, call='action') + if 'url' in data and data['url'] != str(''): + return data['url'] + except WindowsAzureMissingResourceError: pass - if 'url' in data and data['url'] != str(''): - return data['url'] time.sleep(1) return False @@ -582,6 +601,7 @@ def create(vm_): deploy_kwargs = { 'opts': __opts__, 'host': hostname, + 'port': ssh_port, 'username': ssh_username, 'password': ssh_password, 'script': deploy_script, From ecb15c01bd98492c137531e5e17de57dd432e733 Mon Sep 17 00:00:00 2001 From: Erik Johnson Date: Mon, 12 Jan 2015 20:25:30 -0600 Subject: [PATCH 17/67] archive.{,un}zip feature parity with legacy funcs This begins the process of making the new archive.zip and archive.unzip functions work like the legacy versions (now named archive.cmd_zip, archive.cmd_unzip). --- salt/modules/archive.py | 86 +++++++++++++++++++++++++++++++---------- 1 file changed, 65 insertions(+), 21 deletions(-) diff --git a/salt/modules/archive.py b/salt/modules/archive.py index ae4f4e8b89..ace2a076a6 100644 --- a/salt/modules/archive.py +++ b/salt/modules/archive.py @@ -169,9 +169,10 @@ def gunzip(gzipfile, template=None, runas=None): @decorators.which('zip') -def cmd_zip(zip_file, sources, template=None, - cwd=None, recurse=False, runas=None): +def cmd_zip(zip_file, sources, template=None, cwd=None, runas=None): ''' + .. versionadded:: 2015.2.0 + Uses the zip command to create zip files zip_file @@ -213,9 +214,7 @@ def cmd_zip(zip_file, sources, template=None, ''' if isinstance(sources, string_types): sources = [s.strip() for s in sources.split(',')] - cmd = ['zip'] - if recurse: - cmd.append('-r') + cmd = ['zip', '-r'] cmd.append('{0}'.format(zip_file)) cmd.extend(sources) return __salt__['cmd.run'](cmd, @@ -226,9 +225,20 @@ def cmd_zip(zip_file, sources, template=None, @decorators.depends('zipfile', fallback_function=cmd_zip) -def zip_(archive, sources, template=None): +def zip_(zip_file, sources, template=None, cwd=None, runas=None): ''' - Uses the zipfile module to create zip files + Uses the ``zipfile`` python module to create zip files + + .. versionchanged:: 2015.2.0 + This function was rewritten to use Python's native zip file support. + The old functionality has been preserved in the new function + :mod:`archive.zip ` will be used + instead. CLI Example: @@ -244,28 +254,62 @@ def zip_(archive, sources, template=None): .. code-block:: bash salt '*' archive.zip template=jinja /tmp/zipfile.zip /tmp/sourcefile1,/tmp/{{grains.id}}.txt - ''' - (archive, sources) = _render_filenames(archive, sources, None, template) + if runas: + return cmd_zip(zip_file, sources, template, cwd, runas) + zip_file, sources = _render_filenames(zip_file, sources, None, template) if isinstance(sources, string_types): - sources = [s.strip() for s in sources.split(',')] + sources = [x.strip() for x in sources.split(',')] + elif isinstance(sources, (float, integer_types)): + sources = [str(sources)] + + if not cwd: + for src in sources: + if not os.path.isabs(src): + raise SaltInvocationError( + 'Relative paths require the \'cwd\' parameter' + ) + else: + def _bad_cwd(): + raise SaltInvocationError('cwd must be absolute') + try: + if not os.path.isabs(cwd): + _bad_cwd() + except AttributeError: + _bad_cwd() archived_files = [] - with zipfile.ZipFile(archive, "w", zipfile.ZIP_DEFLATED) as zf: + with zipfile.ZipFile(zip_file, "w", zipfile.ZIP_DEFLATED) as zfile: for src in sources: + if cwd: + src = os.path.join(cwd, src) if os.path.exists(src): - if os.path.isdir(src): - rel_root = os.path.abspath(os.path.join(src, os.pardir)) - for dir_name, sub_dirs, files in os.walk(src): - for filename in files: - abs_name = os.path.abspath(os.path.join(dir_name, filename)) - arc_name = os.path.join(os.path.relpath(dir_name, rel_root), filename) - archived_files.append(arc_name) - zf.write(abs_name, arc_name) + if os.path.isabs(src): + rel_root = '/' else: - archived_files.append(src) - zf.write(src) + rel_root = cwd if cwd is not None else '/' + if os.path.isdir(src): + for dir_name, sub_dirs, files in os.walk(src): + if cwd and dir_name.startswith(cwd): + arc_dir = os.path.relpath(dir_name, cwd) + else: + arc_dir = os.path.relpath(dir_name, rel_root) + if arc_dir: + archived_files.append(arc_dir + '/') + zfile.write(dir_name, arc_dir) + for filename in files: + abs_name = os.path.join(dir_name, filename) + arc_name = os.path.join(arc_dir, filename) + archived_files.append(arc_name) + zfile.write(abs_name, arc_name) + else: + if cwd and src.startswith(cwd): + arc_name = os.path.relpath(src, cwd) + else: + arc_name = os.path.relpath(src, rel_root) + archived_files.append(arc_name) + zfile.write(src, arc_name) return archived_files From fc809280a8e68834bda0c12eab0bb0394eaf42f9 Mon Sep 17 00:00:00 2001 From: Erik Johnson Date: Tue, 13 Jan 2015 15:49:34 -0600 Subject: [PATCH 18/67] Finish archive.{,un}zip feature parity with legacy funcs Added support for runas to these functions and made some docstring fixes. --- salt/modules/archive.py | 323 ++++++++++++++++++++++++++++------------ 1 file changed, 230 insertions(+), 93 deletions(-) diff --git a/salt/modules/archive.py b/salt/modules/archive.py index ace2a076a6..8c38377175 100644 --- a/salt/modules/archive.py +++ b/salt/modules/archive.py @@ -10,7 +10,7 @@ import os # Import salt libs from salt.exceptions import SaltInvocationError, CommandExecutionError -from salt.ext.six import string_types +from salt.ext.six import string_types, integer_types from salt.utils import \ which as _which, which_bin as _which_bin, is_windows as _is_windows import salt.utils.decorators as decorators @@ -64,7 +64,7 @@ def tar(options, tarfile, sources=None, dest=None, cwd=None, template=None, runa sources Comma delimited list of files to **pack** into the tarfile. Can also be - passed as a python list. + passed as a Python list. dest The destination directory into which to **unpack** the tarfile @@ -172,15 +172,20 @@ def gunzip(gzipfile, template=None, runas=None): def cmd_zip(zip_file, sources, template=None, cwd=None, runas=None): ''' .. versionadded:: 2015.2.0 + In versions 2014.7.x and earlier, this function was known as + ``archive.zip``. - Uses the zip command to create zip files + Uses the ``zip`` command to create zip files. This command is part of the + `Info-ZIP`_ suite of tools, and is typically packaged as simply ``zip``. + + .. _`Info-ZIP`: http://www.info-zip.org/ zip_file Path of zip file to be created sources Comma-separated list of sources to include in the zip file. Sources can - also be passed in a python list. + also be passed in a Python list. template : None Can be set to 'jinja' or another supported template engine to render @@ -191,26 +196,31 @@ def cmd_zip(zip_file, sources, template=None, cwd=None, runas=None): salt '*' archive.zip template=jinja /tmp/zipfile.zip /tmp/sourcefile1,/tmp/{{grains.id}}.txt cwd : None - Run the zip command from the specified directory. Use this argument - along with relative file paths to create zip files which do not - contain the leading directories. If not specified, this will default - to the home directory of the user under which the salt minion process - is running. + Use this argument along with relative paths in ``sources`` to create + zip files which do not contain the leading directories. If not + specified, the zip file will be created as if the cwd was ``/``, and + creating a zip file of ``/foo/bar/baz.txt`` will contain the parent + directories ``foo`` and ``bar``. To create a zip file containing just + ``baz.txt``, the following command would be used: + + .. code-block:: bash + + salt '*' archive.zip /tmp/baz.zip baz.txt cwd=/foo/bar .. versionadded:: 2014.7.1 - recurse : False - Recursively include contents of sources which are directories. Combine - this with the ``cwd`` argument and use relative paths for the sources - to create a zip file which does not contain the leading directories. + runas : None + Create the zip file as the specified user. Defaults to the user under + which the minion is running. + + .. versionadded:: 2015.2.0 - .. versionadded:: 2014.7.1 CLI Example: .. code-block:: bash - salt '*' archive.zip /tmp/zipfile.zip /tmp/sourcefile1,/tmp/sourcefile2 + salt '*' archive.cmd_zip /tmp/zipfile.zip /tmp/sourcefile1,/tmp/sourcefile2 ''' if isinstance(sources, string_types): sources = [s.strip() for s in sources.split(',')] @@ -227,36 +237,62 @@ def cmd_zip(zip_file, sources, template=None, cwd=None, runas=None): @decorators.depends('zipfile', fallback_function=cmd_zip) def zip_(zip_file, sources, template=None, cwd=None, runas=None): ''' - Uses the ``zipfile`` python module to create zip files + Uses the ``zipfile`` Python module to create zip files .. versionchanged:: 2015.2.0 This function was rewritten to use Python's native zip file support. The old functionality has been preserved in the new function - :mod:`archive.zip `. For versions + 2014.7.x and earlier, see the :mod:`archive.cmd_zip + ` documentation. - .. note:: + zip_file + Path of zip file to be created + + sources + Comma-separated list of sources to include in the zip file. Sources can + also be passed in a Python list. + + template : None + Can be set to 'jinja' or another supported template engine to render + the command arguments before execution: + + .. code-block:: bash + + salt '*' archive.zip template=jinja /tmp/zipfile.zip /tmp/sourcefile1,/tmp/{{grains.id}}.txt + + cwd : None + Use this argument along with relative paths in ``sources`` to create + zip files which do not contain the leading directories. If not + specified, the zip file will be created as if the cwd was ``/``, and + creating a zip file of ``/foo/bar/baz.txt`` will contain the parent + directories ``foo`` and ``bar``. To create a zip file containing just + ``baz.txt``, the following command would be used: + + .. code-block:: bash + + salt '*' archive.zip /tmp/baz.zip baz.txt cwd=/foo/bar + + runas : None + Create the zip file as the specified user. Defaults to the user under + which the minion is running. - If creating a zip file using the ``runas`` parameter, - :mod:`archive.cmd_zip ` will be used - instead. CLI Example: .. code-block:: bash salt '*' archive.zip /tmp/zipfile.zip /tmp/sourcefile1,/tmp/sourcefile2 - - The template arg can be set to 'jinja' or another supported template - engine to render the command arguments before execution. - - For example: - - .. code-block:: bash - - salt '*' archive.zip template=jinja /tmp/zipfile.zip /tmp/sourcefile1,/tmp/{{grains.id}}.txt ''' if runas: - return cmd_zip(zip_file, sources, template, cwd, runas) + euid = os.geteuid() + egid = os.getegid() + uinfo = __salt__['user.info'](runas) + if not uinfo: + raise SaltInvocationError( + 'User \'{0}\' does not exist'.format(runas) + ) + zip_file, sources = _render_filenames(zip_file, sources, None, template) if isinstance(sources, string_types): @@ -279,37 +315,58 @@ def zip_(zip_file, sources, template=None, cwd=None, runas=None): except AttributeError: _bad_cwd() - archived_files = [] - with zipfile.ZipFile(zip_file, "w", zipfile.ZIP_DEFLATED) as zfile: - for src in sources: - if cwd: - src = os.path.join(cwd, src) - if os.path.exists(src): - if os.path.isabs(src): - rel_root = '/' - else: - rel_root = cwd if cwd is not None else '/' - if os.path.isdir(src): - for dir_name, sub_dirs, files in os.walk(src): - if cwd and dir_name.startswith(cwd): - arc_dir = os.path.relpath(dir_name, cwd) - else: - arc_dir = os.path.relpath(dir_name, rel_root) - if arc_dir: - archived_files.append(arc_dir + '/') - zfile.write(dir_name, arc_dir) - for filename in files: - abs_name = os.path.join(dir_name, filename) - arc_name = os.path.join(arc_dir, filename) - archived_files.append(arc_name) - zfile.write(abs_name, arc_name) - else: - if cwd and src.startswith(cwd): - arc_name = os.path.relpath(src, cwd) + if runas and (euid != uinfo['uid'] or guid != uinfo['gid']): + # Change the egid first, as changing it after the euid will likely fail + # if the runas user is non-privileged. + os.setegid(uinfo['gid']) + os.seteuid(uinfo['uid']) + + try: + exc = None + archived_files = [] + with zipfile.ZipFile(zip_file, "w", zipfile.ZIP_DEFLATED) as zfile: + for src in sources: + if cwd: + src = os.path.join(cwd, src) + if os.path.exists(src): + if os.path.isabs(src): + rel_root = '/' else: - arc_name = os.path.relpath(src, rel_root) - archived_files.append(arc_name) - zfile.write(src, arc_name) + rel_root = cwd if cwd is not None else '/' + if os.path.isdir(src): + for dir_name, sub_dirs, files in os.walk(src): + if cwd and dir_name.startswith(cwd): + arc_dir = os.path.relpath(dir_name, cwd) + else: + arc_dir = os.path.relpath(dir_name, rel_root) + if arc_dir: + archived_files.append(arc_dir + '/') + zfile.write(dir_name, arc_dir) + for filename in files: + abs_name = os.path.join(dir_name, filename) + arc_name = os.path.join(arc_dir, filename) + archived_files.append(arc_name) + zfile.write(abs_name, arc_name) + else: + if cwd and src.startswith(cwd): + arc_name = os.path.relpath(src, cwd) + else: + arc_name = os.path.relpath(src, rel_root) + archived_files.append(arc_name) + zfile.write(src, arc_name) + except Exception as exc: + pass + finally: + # Restore the euid/egid + if runas: + os.seteuid(euid) + os.setegid(egid) + if exc is not None: + # Wait to raise the exception until euid/egid are restored to avoid + # permission errors in writing to minion log. + raise CommandExecutionError( + 'Exception encountered creating zipfile: {0}'.format(exc) + ) return archived_files @@ -318,7 +375,14 @@ def zip_(zip_file, sources, template=None, cwd=None, runas=None): def cmd_unzip(zip_file, dest, excludes=None, template=None, options=None, runas=None): ''' - Uses the unzip command to unpack zip files + .. versionadded:: 2015.2.0 + In versions 2014.7.x and earlier, this function was known as + ``archive.unzip``. + + Uses the ``unzip`` command to unpack zip files. This command is part of the + `Info-ZIP`_ suite of tools, and is typically packaged as simply ``unzip``. + + .. _`Info-ZIP`: http://www.info-zip.org/ zip_file Path of zip file to be unpacked @@ -326,8 +390,9 @@ def cmd_unzip(zip_file, dest, excludes=None, dest The destination directory into which the file should be unpacked - options : None - Options to pass to the ``unzip`` binary + excludes : None + Comma-separated list of files not to unpack. Can also be passed in a + Python list. template : None Can be set to 'jinja' or another supported template engine to render @@ -337,6 +402,15 @@ def cmd_unzip(zip_file, dest, excludes=None, salt '*' archive.unzip template=jinja /tmp/zipfile.zip /tmp/{{grains.id}}/ excludes=file_1,file_2 + options : None + Additional command-line options to pass to the ``unzip`` binary. + + runas : None + Unpack the zip file as the specified user. Defaults to the user under + which the minion is running. + + .. versionadded:: 2015.2.0 + CLI Example: .. code-block:: bash @@ -344,7 +418,9 @@ def cmd_unzip(zip_file, dest, excludes=None, salt '*' archive.unzip /tmp/zipfile.zip /home/strongbad/ excludes=file_1,file_2 ''' if isinstance(excludes, string_types): - excludes = [entry.strip() for entry in excludes.split(',')] + excludes = [x.strip() for x in excludes.split(',')] + elif isinstance(excludes, (float, integer_types)): + excludes = [str(excludes)] cmd = ['unzip'] if options: @@ -363,47 +439,108 @@ def cmd_unzip(zip_file, dest, excludes=None, cmd.extend(excludes) return __salt__['cmd.run'](cmd, template=template, + runas=runas, python_shell=False).splitlines() @decorators.depends('zipfile', fallback_function=cmd_unzip) -def unzip(archive, dest, excludes=None, template=None, options=None): +def unzip(zip_file, dest, excludes=None, template=None, runas=None): ''' - Uses the zipfile module to unpack zip files + Uses the ``zipfile`` Python module to unpack zip files - options: - Options to pass to the ``unzip`` binary. + .. versionchanged:: 2015.2.0 + This function was rewritten to use Python's native zip file support. + The old functionality has been preserved in the new function + :mod:`archive.cmd_unzip `. For versions + 2014.7.x and earlier, see the :mod:`archive.cmd_zip + ` documentation. + + zip_file + Path of zip file to be unpacked + + dest + The destination directory into which the file should be unpacked + + excludes : None + Comma-separated list of files not to unpack. Can also be passed in a + Python list. + + template : None + Can be set to 'jinja' or another supported template engine to render + the command arguments before execution: + + .. code-block:: bash + + salt '*' archive.unzip template=jinja /tmp/zipfile.zip /tmp/{{grains.id}}/ excludes=file_1,file_2 + + runas : None + Unpack the zip file as the specified user. Defaults to the user under + which the minion is running. CLI Example: .. code-block:: bash salt '*' archive.unzip /tmp/zipfile.zip /home/strongbad/ excludes=file_1,file_2 - - The template arg can be set to 'jinja' or another supported template - engine to render the command arguments before execution. - - For example: - - .. code-block:: bash - - salt '*' archive.unzip template=jinja /tmp/zipfile.zip /tmp/{{grains.id}}/ excludes=file_1,file_2 - ''' - (archive, dest) = _render_filenames(archive, dest, None, template) - with zipfile.ZipFile(archive) as zf: - files = zf.namelist() - if excludes is None: - zf.extractall(dest) - return files + if runas: + euid = os.geteuid() + egid = os.getegid() + uinfo = __salt__['user.info'](runas) + if not uinfo: + raise SaltInvocationError( + 'User \'{0}\' does not exist'.format(runas) + ) - if not isinstance(excludes, list): - excludes = excludes.split(",") - cleaned_files = [x for x in files if x not in excludes] - for f in cleaned_files: - if f not in excludes: - zf.extract(f, dest) - return cleaned_files + zip_file, dest = _render_filenames(zip_file, dest, None, template) + + if not os.path.isdir(dest): + raise SaltInvocationError( + 'Destination directory {0} does not exist'.format(dest) + ) + + if runas and (euid != uinfo['uid'] or guid != uinfo['gid']): + # Change the egid first, as changing it after the euid will likely fail + # if the runas user is non-privileged. + os.setegid(uinfo['gid']) + os.seteuid(uinfo['uid']) + + try: + exc = None + # Define cleaned_files here so that an exception will not prevent this + # variable from being defined and cause a NameError in the return + # statement at the end of the function. + cleaned_files = [] + with zipfile.ZipFile(zip_file) as zfile: + files = zfile.namelist() + if excludes is None: + zfile.extractall(dest) + return files + + if isinstance(excludes, string_types): + excludes = [x.strip() for x in excludes.split(',')] + elif isinstance(excludes, (float, integer_types)): + excludes = [str(excludes)] + + cleaned_files.extend([x for x in files if x not in excludes]) + for target in cleaned_files: + if target not in excludes: + zfile.extract(target, dest) + except Exception as exc: + pass + finally: + # Restore the euid/egid + if runas: + os.seteuid(euid) + os.setegid(egid) + if exc is not None: + # Wait to raise the exception until euid/egid are restored to avoid + # permission errors in writing to minion log. + raise CommandExecutionError( + 'Exception encountered unpacking zipfile: {0}'.format(exc) + ) + + return cleaned_files @decorators.which('rar') @@ -418,7 +555,7 @@ def rar(rarfile, sources, template=None, cwd=None, runas=None): sources Comma-separated list of sources to include in the rar file. Sources can - also be passed in a python list. + also be passed in a Python list. cwd : None Run the rar command from the specified directory. Use this argument From 43ab12fd0ecd510cc30354351633d93883693af6 Mon Sep 17 00:00:00 2001 From: rallytime Date: Tue, 13 Jan 2015 15:25:23 -0700 Subject: [PATCH 19/67] Whitespace fix --- salt/cloud/clouds/msazure.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/salt/cloud/clouds/msazure.py b/salt/cloud/clouds/msazure.py index b4f1ed5492..7d6868896a 100644 --- a/salt/cloud/clouds/msazure.py +++ b/salt/cloud/clouds/msazure.py @@ -529,8 +529,8 @@ def create(vm_): conn.create_virtual_machine_deployment(**vm_kwargs) except WindowsAzureConflictError: log.debug("Conflict error. The deployment may already exist, trying add_role") - # Deleting two useless keywords - del vm_kwargs["deployment_slot"] + # Deleting two useless keywords + del vm_kwargs["deployment_slot"] del vm_kwargs["label"] conn.add_role(**vm_kwargs) except Exception as exc: From 5dbfd0241fc31b7158022040bd522d9d29941363 Mon Sep 17 00:00:00 2001 From: Kevin Bowling Date: Tue, 13 Jan 2015 16:25:18 -0700 Subject: [PATCH 20/67] Switch freebsdkmod to use loader.conf --- salt/modules/freebsdkmod.py | 44 +++++++++++++++++++++++++------------ 1 file changed, 30 insertions(+), 14 deletions(-) diff --git a/salt/modules/freebsdkmod.py b/salt/modules/freebsdkmod.py index 1c10ffb5ac..411ec77ac4 100644 --- a/salt/modules/freebsdkmod.py +++ b/salt/modules/freebsdkmod.py @@ -5,11 +5,21 @@ Module to manage FreeBSD kernel modules # Import python libs import os +import re + +# Import salt libs +import salt.utils # Define the module's virtual name __virtualname__ = 'kmod' +_LOAD_MODULE = '{0}_load="YES"' +_LOADER_CONF = '/boot/loader.conf' +_MODULE_RE = '^{0}_load="YES"' +_MODULES_RE = '^(\w+)_load="YES"' + + def __virtual__(): ''' Only runs on FreeBSD systems @@ -45,39 +55,45 @@ def _rm_mods(pre_mods, post_mods): return pre - post +def _get_module_name(line): + match = re.search(_MODULES_RE, line) + if match: + return match.group(1) + return None + + def _get_persistent_modules(): + ''' + Returns a list of modules in loader.conf that load on boot. + ''' mods = set() - response = __salt__['cmd.run_all']('sysrc -niq kld_list') - if response['retcode'] == 0: - for mod in response['stdout'].split(): - mods.add(mod) + with salt.utils.fopen(_LOADER_CONF, 'r') as loader_conf: + for line in loader_conf: + line = line.strip() + mod_name = _get_module_name(line) + if mod_name: + mods.add(mod_name) return mods def _set_persistent_module(mod): ''' - Add a module to sysrc to make it persistent. + Add a module to loader.conf to make it persistent. ''' if not mod or mod in mod_list(True) or mod not in \ available(): return set() - mods = _get_persistent_modules() - mods.add(mod) - __salt__['cmd.run_all']("sysrc kld_list='{0}'".format(' '.join(mods)), - python_shell=False) + __salt__['file.append'](_LOADER_CONF, _LOAD_MODULE.format(mod)) return set([mod]) def _remove_persistent_module(mod): ''' - Remove module from sysrc. + Remove module from loader.conf. ''' if not mod or mod not in mod_list(True): return set() - mods = _get_persistent_modules() - mods.remove(mod) - __salt__['cmd.run_all']("sysrc kld_list='{0}'".format(' '.join(mods)), - python_shell=False) + __salt__['file.sed'](_LOADER_CONF, _MODULE_RE.format(mod), '') return set([mod]) From 16b30f34bb89d4b38847802fd9776c137ff2fc81 Mon Sep 17 00:00:00 2001 From: Stuart Jansen Date: Tue, 13 Jan 2015 19:07:14 -0700 Subject: [PATCH 21/67] Enable salt-cloud bootstrap with ssh gateway --- salt/cloud/clouds/ec2.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/salt/cloud/clouds/ec2.py b/salt/cloud/clouds/ec2.py index 347a58a345..2da1851d32 100644 --- a/salt/cloud/clouds/ec2.py +++ b/salt/cloud/clouds/ec2.py @@ -1754,7 +1754,7 @@ def wait_for_instance( data = {} ssh_gateway_config = vm_.get( - 'ssh_gateway_config', get_ssh_gateway_config(vm_) + 'gateway', get_ssh_gateway_config(vm_) ) salt.utils.cloud.fire_event( @@ -1903,8 +1903,7 @@ def create(vm_=None, call=None): # Get SSH Gateway config early to verify the private_key, # if used, exists or not. We don't want to deploy an instance # and not be able to access it via the gateway. - ssh_gateway_config = get_ssh_gateway_config(vm_) - vm_['ssh_gateway_config'] = ssh_gateway_config + vm_['gateway'] = get_ssh_gateway_config(vm_) location = get_location(vm_) vm_['location'] = location From 5d76de8dd9976ebc47df50fcc4674f03b85d5676 Mon Sep 17 00:00:00 2001 From: Erik Johnson Date: Tue, 13 Jan 2015 21:01:34 -0600 Subject: [PATCH 22/67] Add salt.utils.relpath to work around Python bug 5117 --- salt/utils/__init__.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/salt/utils/__init__.py b/salt/utils/__init__.py index 2c9b23ab91..90c6fd4ae5 100644 --- a/salt/utils/__init__.py +++ b/salt/utils/__init__.py @@ -2286,3 +2286,32 @@ def sdecode(string_): except UnicodeDecodeError: continue return string_ + + +def relpath(path, start='.'): + ''' + Work around Python bug #5117, which is not (and will not be) patched in + Python 2.6 (http://bugs.python.org/issue5117) + ''' + if sys.version_info < (2, 7) and 'posix' in sys.builtin_module_names: + # The below code block is based on posixpath.relpath from Python 2.7, + # which has the fix for this bug. + if not path: + raise ValueError('no path specified') + + start_list = [ + x for x in os.path.abspath(start).split(os.path.sep) if x + ] + path_list = [ + x for x in os.path.abspath(path).split(os.path.sep) if x + ] + + # work out how much of the filepath is shared by start and path. + i = len(os.path.commonprefix([start_list, path_list])) + + rel_list = [os.path.pardir] * (len(start_list)-i) + path_list[i:] + if not rel_list: + return os.path.curdir + return os.path.join(*rel_list) + + return os.path.relpath(path, start=start) From 62b7e124151b11bf1fce9ea21e9774bb8d7792d7 Mon Sep 17 00:00:00 2001 From: Erik Johnson Date: Tue, 13 Jan 2015 21:06:13 -0600 Subject: [PATCH 23/67] os.path.relpath -> salt.utils.relpath Also clean up some imports --- salt/modules/archive.py | 52 ++++++++++++++++++----------------------- 1 file changed, 23 insertions(+), 29 deletions(-) diff --git a/salt/modules/archive.py b/salt/modules/archive.py index 8c38377175..86ca69d780 100644 --- a/salt/modules/archive.py +++ b/salt/modules/archive.py @@ -7,13 +7,9 @@ A module to wrap (non-Windows) archive calls from __future__ import absolute_import import os - # Import salt libs from salt.exceptions import SaltInvocationError, CommandExecutionError from salt.ext.six import string_types, integer_types -from salt.utils import \ - which as _which, which_bin as _which_bin, is_windows as _is_windows -import salt.utils.decorators as decorators import salt.utils # TODO: Check that the passed arguments are correct @@ -33,17 +29,18 @@ except ImportError: def __virtual__(): - if _is_windows(): + if salt.utils.is_windows(): return HAS_ZIPFILE commands = ('tar', 'gzip', 'gunzip', 'zip', 'unzip', 'rar', 'unrar') # If none of the above commands are in $PATH this module is a no-go - if not any(_which(cmd) for cmd in commands): + if not any(salt.utils.which(cmd) for cmd in commands): return False return True -@decorators.which('tar') -def tar(options, tarfile, sources=None, dest=None, cwd=None, template=None, runas=None): +@salt.utils.decorators.which('tar') +def tar(options, tarfile, sources=None, dest=None, + cwd=None, template=None, runas=None): ''' .. note:: @@ -114,7 +111,7 @@ def tar(options, tarfile, sources=None, dest=None, cwd=None, template=None, runa python_shell=False).splitlines() -@decorators.which('gzip') +@salt.utils.decorators.which('gzip') def gzip(sourcefile, template=None, runas=None): ''' Uses the gzip command to create gzip files @@ -141,7 +138,7 @@ def gzip(sourcefile, template=None, runas=None): python_shell=False).splitlines() -@decorators.which('gunzip') +@salt.utils.decorators.which('gunzip') def gunzip(gzipfile, template=None, runas=None): ''' Uses the gunzip command to unpack gzip files @@ -168,7 +165,7 @@ def gunzip(gzipfile, template=None, runas=None): python_shell=False).splitlines() -@decorators.which('zip') +@salt.utils.decorators.which('zip') def cmd_zip(zip_file, sources, template=None, cwd=None, runas=None): ''' .. versionadded:: 2015.2.0 @@ -234,7 +231,7 @@ def cmd_zip(zip_file, sources, template=None, cwd=None, runas=None): python_shell=False).splitlines() -@decorators.depends('zipfile', fallback_function=cmd_zip) +@salt.utils.decorators.depends('zipfile', fallback_function=cmd_zip) def zip_(zip_file, sources, template=None, cwd=None, runas=None): ''' Uses the ``zipfile`` Python module to create zip files @@ -316,7 +313,7 @@ def zip_(zip_file, sources, template=None, cwd=None, runas=None): _bad_cwd() if runas and (euid != uinfo['uid'] or guid != uinfo['gid']): - # Change the egid first, as changing it after the euid will likely fail + # Change the egid first, as changing it after the euid will fail # if the runas user is non-privileged. os.setegid(uinfo['gid']) os.seteuid(uinfo['uid']) @@ -324,7 +321,7 @@ def zip_(zip_file, sources, template=None, cwd=None, runas=None): try: exc = None archived_files = [] - with zipfile.ZipFile(zip_file, "w", zipfile.ZIP_DEFLATED) as zfile: + with zipfile.ZipFile(zip_file, 'w', zipfile.ZIP_DEFLATED) as zfile: for src in sources: if cwd: src = os.path.join(cwd, src) @@ -336,9 +333,10 @@ def zip_(zip_file, sources, template=None, cwd=None, runas=None): if os.path.isdir(src): for dir_name, sub_dirs, files in os.walk(src): if cwd and dir_name.startswith(cwd): - arc_dir = os.path.relpath(dir_name, cwd) + arc_dir = salt.utils.relpath(dir_name, cwd) else: - arc_dir = os.path.relpath(dir_name, rel_root) + arc_dir = salt.utils.relpath(dir_name, + rel_root) if arc_dir: archived_files.append(arc_dir + '/') zfile.write(dir_name, arc_dir) @@ -349,9 +347,9 @@ def zip_(zip_file, sources, template=None, cwd=None, runas=None): zfile.write(abs_name, arc_name) else: if cwd and src.startswith(cwd): - arc_name = os.path.relpath(src, cwd) + arc_name = salt.utils.relpath(src, cwd) else: - arc_name = os.path.relpath(src, rel_root) + arc_name = salt.utils.relpath(src, rel_root) archived_files.append(arc_name) zfile.write(src, arc_name) except Exception as exc: @@ -371,7 +369,7 @@ def zip_(zip_file, sources, template=None, cwd=None, runas=None): return archived_files -@decorators.which('unzip') +@salt.utils.decorators.which('unzip') def cmd_unzip(zip_file, dest, excludes=None, template=None, options=None, runas=None): ''' @@ -443,7 +441,7 @@ def cmd_unzip(zip_file, dest, excludes=None, python_shell=False).splitlines() -@decorators.depends('zipfile', fallback_function=cmd_unzip) +@salt.utils.decorators.depends('zipfile', fallback_function=cmd_unzip) def unzip(zip_file, dest, excludes=None, template=None, runas=None): ''' Uses the ``zipfile`` Python module to unpack zip files @@ -494,13 +492,8 @@ def unzip(zip_file, dest, excludes=None, template=None, runas=None): zip_file, dest = _render_filenames(zip_file, dest, None, template) - if not os.path.isdir(dest): - raise SaltInvocationError( - 'Destination directory {0} does not exist'.format(dest) - ) - if runas and (euid != uinfo['uid'] or guid != uinfo['gid']): - # Change the egid first, as changing it after the euid will likely fail + # Change the egid first, as changing it after the euid will fail # if the runas user is non-privileged. os.setegid(uinfo['gid']) os.seteuid(uinfo['uid']) @@ -543,7 +536,7 @@ def unzip(zip_file, dest, excludes=None, template=None, runas=None): return cleaned_files -@decorators.which('rar') +@salt.utils.decorators.which('rar') def rar(rarfile, sources, template=None, cwd=None, runas=None): ''' Uses `rar for Linux`_ to create rar files @@ -591,7 +584,7 @@ def rar(rarfile, sources, template=None, cwd=None, runas=None): python_shell=False).splitlines() -@decorators.which_bin(('unrar', 'rar')) +@salt.utils.decorators.which_bin(('unrar', 'rar')) def unrar(rarfile, dest, excludes=None, template=None, runas=None): ''' Uses `rar for Linux`_ to unpack rar files @@ -622,7 +615,8 @@ def unrar(rarfile, dest, excludes=None, template=None, runas=None): if isinstance(excludes, string_types): excludes = [entry.strip() for entry in excludes.split(',')] - cmd = [_which_bin(('unrar', 'rar')), 'x', '-idp', '{0}'.format(rarfile)] + cmd = [salt.utils.which_bin(('unrar', 'rar')), + 'x', '-idp', '{0}'.format(rarfile)] if excludes is not None: for exclude in excludes: cmd.extend(['-x', '{0}'.format(exclude)]) From 904f9b6c90068dd42737f810223d9d325b71ca85 Mon Sep 17 00:00:00 2001 From: Erik Johnson Date: Tue, 13 Jan 2015 21:07:23 -0600 Subject: [PATCH 24/67] Fix archive tests This fixes references to the badly-named legacy zip and unzip functions, as well as skipping the unrar test when neither unrar and rar are installed. --- tests/unit/modules/archive_test.py | 34 ++++++++++++++++-------------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/tests/unit/modules/archive_test.py b/tests/unit/modules/archive_test.py index f2ca32e9e3..9a7e0de45a 100644 --- a/tests/unit/modules/archive_test.py +++ b/tests/unit/modules/archive_test.py @@ -16,6 +16,7 @@ ensure_in_syspath('../../') # Import salt libs from salt.modules import archive from salt.exceptions import CommandNotFoundError +from salt.utils import which_bin class ZipFileMock(MagicMock): @@ -129,28 +130,28 @@ class ArchiveTestCase(TestCase): def test_cmd_zip(self): mock = MagicMock(return_value='salt') with patch.dict(archive.__salt__, {'cmd.run': mock}): - ret = archive.cmd_zip_( + ret = archive.cmd_zip( '/tmp/salt.{{grains.id}}.zip', '/tmp/tmpePe8yO,/tmp/tmpLeSw1A', template='jinja' ) self.assertEqual(['salt'], ret) mock.assert_called_once_with( - ['zip', '/tmp/salt.{{grains.id}}.zip', + ['zip', '-r', '/tmp/salt.{{grains.id}}.zip', '/tmp/tmpePe8yO', '/tmp/tmpLeSw1A'], runas=None, python_shell=False, template='jinja', cwd=None ) mock = MagicMock(return_value='salt') with patch.dict(archive.__salt__, {'cmd.run': mock}): - ret = archive.cmd_zip_( + ret = archive.cmd_zip( '/tmp/salt.{{grains.id}}.zip', ['/tmp/tmpePe8yO', '/tmp/tmpLeSw1A'], template='jinja' ) self.assertEqual(['salt'], ret) mock.assert_called_once_with( - ['zip', '/tmp/salt.{{grains.id}}.zip', + ['zip', '-r', '/tmp/salt.{{grains.id}}.zip', '/tmp/tmpePe8yO', '/tmp/tmpLeSw1A'], runas=None, python_shell=False, template='jinja', cwd=None ) @@ -164,7 +165,7 @@ class ArchiveTestCase(TestCase): '/tmp/tmpePe8yO,/tmp/tmpLeSw1A', template='jinja' ) - self.assertEqual(['/tmp/tmpePe8yO', '/tmp/tmpLeSw1A'], ret) + self.assertEqual(['tmp/tmpePe8yO', 'tmp/tmpLeSw1A'], ret) @patch('salt.utils.which', lambda exe: None) def test_zip_raises_exception_if_not_found(self): @@ -172,7 +173,7 @@ class ArchiveTestCase(TestCase): with patch.dict(archive.__salt__, {'cmd.run': mock}): self.assertRaises( CommandNotFoundError, - archive.cmd_zip_, + archive.cmd_zip, '/tmp/salt.{{grains.id}}.zip', '/tmp/tmpePe8yO,/tmp/tmpLeSw1A', template='jinja', @@ -183,22 +184,22 @@ class ArchiveTestCase(TestCase): def test_cmd_unzip(self): mock = MagicMock(return_value='salt') with patch.dict(archive.__salt__, {'cmd.run': mock}): - ret = archive.cmd_unzip_( + ret = archive.cmd_unzip( '/tmp/salt.{{grains.id}}.zip', '/tmp/dest', excludes='/tmp/tmpePe8yO,/tmp/tmpLeSw1A', - runas=None, template='jinja' + template='jinja' ) self.assertEqual(['salt'], ret) mock.assert_called_once_with( ['unzip', '/tmp/salt.{{grains.id}}.zip', '-d', '/tmp/dest', '-x', '/tmp/tmpePe8yO', '/tmp/tmpLeSw1A'], - python_shell=False, template='jinja' + runas=None, python_shell=False, template='jinja' ) mock = MagicMock(return_value='salt') with patch.dict(archive.__salt__, {'cmd.run': mock}): - ret = archive.cmd_unzip_( + ret = archive.cmd_unzip( '/tmp/salt.{{grains.id}}.zip', '/tmp/dest', excludes=['/tmp/tmpePe8yO', '/tmp/tmpLeSw1A'], @@ -208,12 +209,12 @@ class ArchiveTestCase(TestCase): mock.assert_called_once_with( ['unzip', '/tmp/salt.{{grains.id}}.zip', '-d', '/tmp/dest', '-x', '/tmp/tmpePe8yO', '/tmp/tmpLeSw1A'], - python_shell=False, template='jinja' + runas=None, python_shell=False, template='jinja' ) mock = MagicMock(return_value='salt') with patch.dict(archive.__salt__, {'cmd.run': mock}): - ret = archive.cmd_unzip_( + ret = archive.cmd_unzip( '/tmp/salt.{{grains.id}}.zip', '/tmp/dest', excludes='/tmp/tmpePe8yO,/tmp/tmpLeSw1A', @@ -224,12 +225,12 @@ class ArchiveTestCase(TestCase): mock.assert_called_once_with( ['unzip', '-fo', '/tmp/salt.{{grains.id}}.zip', '-d', '/tmp/dest', '-x', '/tmp/tmpePe8yO', '/tmp/tmpLeSw1A'], - python_shell=False, template='jinja' + runas=None, python_shell=False, template='jinja' ) mock = MagicMock(return_value='salt') with patch.dict(archive.__salt__, {'cmd.run': mock}): - ret = archive.cmd_unzip_( + ret = archive.cmd_unzip( '/tmp/salt.{{grains.id}}.zip', '/tmp/dest', excludes=['/tmp/tmpePe8yO', '/tmp/tmpLeSw1A'], @@ -240,7 +241,7 @@ class ArchiveTestCase(TestCase): mock.assert_called_once_with( ['unzip', '-fo', '/tmp/salt.{{grains.id}}.zip', '-d', '/tmp/dest', '-x', '/tmp/tmpePe8yO', '/tmp/tmpLeSw1A'], - python_shell=False, template='jinja' + runas=None, python_shell=False, template='jinja' ) def test_unzip(self): @@ -260,7 +261,7 @@ class ArchiveTestCase(TestCase): with patch.dict(archive.__salt__, {'cmd.run': mock}): self.assertRaises( CommandNotFoundError, - archive.cmd_unzip_, + archive.cmd_unzip, '/tmp/salt.{{grains.id}}.zip', '/tmp/dest', excludes='/tmp/tmpePe8yO,/tmp/tmpLeSw1A', @@ -308,6 +309,7 @@ class ArchiveTestCase(TestCase): ) self.assertFalse(mock.called) + @skipIf(which_bin(('unrar', 'rar')) is None, 'unrar not installed') @patch('salt.utils.which', lambda exe: exe) @patch('salt.utils.which_bin', lambda exe: exe) def test_unrar(self): From fa3c3afaaacdb35da9b3eeebd9f0e448add478ad Mon Sep 17 00:00:00 2001 From: Erik Johnson Date: Tue, 13 Jan 2015 21:34:21 -0600 Subject: [PATCH 25/67] Docstring corrections --- salt/modules/archive.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/salt/modules/archive.py b/salt/modules/archive.py index 86ca69d780..0addd4e71e 100644 --- a/salt/modules/archive.py +++ b/salt/modules/archive.py @@ -190,7 +190,7 @@ def cmd_zip(zip_file, sources, template=None, cwd=None, runas=None): .. code-block:: bash - salt '*' archive.zip template=jinja /tmp/zipfile.zip /tmp/sourcefile1,/tmp/{{grains.id}}.txt + salt '*' archive.cmd_zip template=jinja /tmp/zipfile.zip /tmp/sourcefile1,/tmp/{{grains.id}}.txt cwd : None Use this argument along with relative paths in ``sources`` to create @@ -202,7 +202,7 @@ def cmd_zip(zip_file, sources, template=None, cwd=None, runas=None): .. code-block:: bash - salt '*' archive.zip /tmp/baz.zip baz.txt cwd=/foo/bar + salt '*' archive.cmd_zip /tmp/baz.zip baz.txt cwd=/foo/bar .. versionadded:: 2014.7.1 @@ -398,7 +398,7 @@ def cmd_unzip(zip_file, dest, excludes=None, .. code-block:: bash - salt '*' archive.unzip template=jinja /tmp/zipfile.zip /tmp/{{grains.id}}/ excludes=file_1,file_2 + salt '*' archive.cmd_unzip template=jinja /tmp/zipfile.zip /tmp/{{grains.id}}/ excludes=file_1,file_2 options : None Additional command-line options to pass to the ``unzip`` binary. @@ -413,7 +413,7 @@ def cmd_unzip(zip_file, dest, excludes=None, .. code-block:: bash - salt '*' archive.unzip /tmp/zipfile.zip /home/strongbad/ excludes=file_1,file_2 + salt '*' archive.cmd_unzip /tmp/zipfile.zip /home/strongbad/ excludes=file_1,file_2 ''' if isinstance(excludes, string_types): excludes = [x.strip() for x in excludes.split(',')] From 4f74473db41160de5abc419b26b8ff6dcd880bf1 Mon Sep 17 00:00:00 2001 From: Erik Johnson Date: Tue, 13 Jan 2015 22:00:52 -0600 Subject: [PATCH 26/67] Remove 'recurse' argument from archive.zip I added this a few days ago, and have decided that it is better not to have this argument, that recursing should be the default behavior. --- salt/modules/archive.py | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/salt/modules/archive.py b/salt/modules/archive.py index eca0e690d8..3e46b0a9d8 100644 --- a/salt/modules/archive.py +++ b/salt/modules/archive.py @@ -154,7 +154,7 @@ def gunzip(gzipfile, template=None): @decorators.which('zip') -def zip_(zipfile, sources, template=None, cwd=None, recurse=False): +def zip_(zipfile, sources, template=None, cwd=None): ''' Uses the zip command to create zip files @@ -182,13 +182,6 @@ def zip_(zipfile, sources, template=None, cwd=None, recurse=False): .. versionadded:: 2014.7.1 - recurse : False - Recursively include contents of sources which are directories. Combine - this with the ``cwd`` argument and use relative paths for the sources - to create a zip file which does not contain the leading directories. - - .. versionadded:: 2014.7.1 - CLI Example: .. code-block:: bash @@ -197,9 +190,7 @@ def zip_(zipfile, sources, template=None, cwd=None, recurse=False): ''' if isinstance(sources, string_types): sources = [s.strip() for s in sources.split(',')] - cmd = ['zip'] - if recurse: - cmd.append('-r') + cmd = ['zip', '-r'] cmd.append('{0}'.format(zipfile)) cmd.extend(sources) return __salt__['cmd.run'](cmd, From 9e9c0b182ec9e9b4fd62b512e5697e06feee86a6 Mon Sep 17 00:00:00 2001 From: Erik Johnson Date: Tue, 13 Jan 2015 22:17:54 -0600 Subject: [PATCH 27/67] Improve docstrings --- salt/modules/archive.py | 37 +++++++++++++++++++++++++++---------- 1 file changed, 27 insertions(+), 10 deletions(-) diff --git a/salt/modules/archive.py b/salt/modules/archive.py index 3e46b0a9d8..c50e26c507 100644 --- a/salt/modules/archive.py +++ b/salt/modules/archive.py @@ -156,14 +156,17 @@ def gunzip(gzipfile, template=None): @decorators.which('zip') def zip_(zipfile, sources, template=None, cwd=None): ''' - Uses the zip command to create zip files + Uses the ``zip`` command to create zip files. This command is part of the + `Info-ZIP`_ suite of tools, and is typically packaged as simply ``zip``. + + .. _`Info-ZIP`: http://www.info-zip.org/ zipfile Path of zip file to be created sources Comma-separated list of sources to include in the zip file. Sources can - also be passed in a python list. + also be passed in a Python list. template : None Can be set to 'jinja' or another supported template engine to render @@ -174,14 +177,20 @@ def zip_(zipfile, sources, template=None, cwd=None): salt '*' archive.zip template=jinja /tmp/zipfile.zip /tmp/sourcefile1,/tmp/{{grains.id}}.txt cwd : None - Run the zip command from the specified directory. Use this argument - along with relative file paths to create zip files which do not - contain the leading directories. If not specified, this will default - to the home directory of the user under which the salt minion process - is running. + Use this argument along with relative paths in ``sources`` to create + zip files which do not contain the leading directories. If not + specified, the zip file will be created as if the cwd was ``/``, and + creating a zip file of ``/foo/bar/baz.txt`` will contain the parent + directories ``foo`` and ``bar``. To create a zip file containing just + ``baz.txt``, the following command would be used: + + .. code-block:: bash + + salt '*' archive.zip /tmp/baz.zip baz.txt cwd=/foo/bar .. versionadded:: 2014.7.1 + CLI Example: .. code-block:: bash @@ -202,7 +211,10 @@ def zip_(zipfile, sources, template=None, cwd=None): @decorators.which('unzip') def unzip(zipfile, dest, excludes=None, template=None, options=None): ''' - Uses the unzip command to unpack zip files + Uses the ``unzip`` command to unpack zip files. This command is part of the + `Info-ZIP`_ suite of tools, and is typically packaged as simply ``unzip``. + + .. _`Info-ZIP`: http://www.info-zip.org/ zipfile Path of zip file to be unpacked @@ -210,8 +222,9 @@ def unzip(zipfile, dest, excludes=None, template=None, options=None): dest The destination directory into which the file should be unpacked - options : None - Options to pass to the ``unzip`` binary + excludes : None + Comma-separated list of files not to unpack. Can also be passed in a + Python list. template : None Can be set to 'jinja' or another supported template engine to render @@ -221,6 +234,10 @@ def unzip(zipfile, dest, excludes=None, template=None, options=None): salt '*' archive.unzip template=jinja /tmp/zipfile.zip /tmp/{{grains.id}}/ excludes=file_1,file_2 + options : None + Additional command-line options to pass to the ``unzip`` binary. + + CLI Example: .. code-block:: bash From 24752ffe93866e0729fa9380431ebfca1212e5d3 Mon Sep 17 00:00:00 2001 From: Erik Johnson Date: Tue, 13 Jan 2015 22:19:14 -0600 Subject: [PATCH 28/67] Fix archive tests --- tests/unit/modules/archive_test.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/unit/modules/archive_test.py b/tests/unit/modules/archive_test.py index 841d9a1c95..45061706cc 100644 --- a/tests/unit/modules/archive_test.py +++ b/tests/unit/modules/archive_test.py @@ -121,7 +121,7 @@ class ArchiveTestCase(TestCase): ) self.assertEqual(['salt'], ret) mock.assert_called_once_with( - ['zip', '/tmp/salt.{{grains.id}}.zip', + ['zip', '-r', '/tmp/salt.{{grains.id}}.zip', '/tmp/tmpePe8yO', '/tmp/tmpLeSw1A'], python_shell=False, template='jinja', cwd=None ) @@ -135,7 +135,7 @@ class ArchiveTestCase(TestCase): ) self.assertEqual(['salt'], ret) mock.assert_called_once_with( - ['zip', '/tmp/salt.{{grains.id}}.zip', + ['zip', '-r', '/tmp/salt.{{grains.id}}.zip', '/tmp/tmpePe8yO', '/tmp/tmpLeSw1A'], python_shell=False, template='jinja', cwd=None ) From 4a1a5123f27c1130a3eae529f813711a47475d8b Mon Sep 17 00:00:00 2001 From: rallytime Date: Tue, 13 Jan 2015 21:39:27 -0700 Subject: [PATCH 29/67] Remove old --out options from salt-cloud docs Fixes #19453 --- doc/ref/cli/salt-cloud.rst | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/doc/ref/cli/salt-cloud.rst b/doc/ref/cli/salt-cloud.rst index ed9589a888..c57cc45972 100644 --- a/doc/ref/cli/salt-cloud.rst +++ b/doc/ref/cli/salt-cloud.rst @@ -118,17 +118,12 @@ Options form, this is suitable for re-reading the output into an executing python script with eval. -.. option:: --text-out +.. option:: --out=OUTPUT, --output=OUTPUT - Print the output from the salt command in the same form the shell would. - -.. option:: --yaml-out - - Print the output from the salt command in yaml. - -.. option:: --json-out - - Print the output from the salt command in json. + Print the output from the salt-cloud command using the specified outputter. The + builtins are 'raw', 'compact', 'no_return', 'grains', 'overstatestage', 'pprint', + 'json', 'nested', 'yaml', 'highstate', 'quiet', 'key', 'txt', 'newline_values_only', + 'virt_query'. .. option:: --no-color From 7ae691d0492929e06e4cf4a2a39b886e9ad95e73 Mon Sep 17 00:00:00 2001 From: rallytime Date: Wed, 14 Jan 2015 08:32:28 -0700 Subject: [PATCH 30/67] Whitespace fix --- salt/modules/mine.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/salt/modules/mine.py b/salt/modules/mine.py index db61268a0a..bf68bfa6c3 100644 --- a/salt/modules/mine.py +++ b/salt/modules/mine.py @@ -44,7 +44,7 @@ def _mine_send(load, opts): eventer = salt.utils.event.MinionEvent(opts) event_ret = eventer.fire_event(load, '_minion_mine') # We need to pause here to allow for the decoupled nature of - # events time to allow the mine to propogate + # events time to allow the mine to propagate time.sleep(2.0) return event_ret From fa5dd4127dafe755580d4cc8c2b383df271cd98f Mon Sep 17 00:00:00 2001 From: Colton Myers Date: Wed, 14 Jan 2015 13:42:38 -0700 Subject: [PATCH 31/67] Fix FunctionWrapper to allow for jinja salt.cmd.run() syntax Fixes #19681 --- salt/client/ssh/wrapper/__init__.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/salt/client/ssh/wrapper/__init__.py b/salt/client/ssh/wrapper/__init__.py index 66fefd7162..50b04f9dd2 100644 --- a/salt/client/ssh/wrapper/__init__.py +++ b/salt/client/ssh/wrapper/__init__.py @@ -7,6 +7,7 @@ as ZeroMQ salt, but via ssh. ''' # Import python libs import json +import copy # Import salt libs import salt.loader @@ -27,8 +28,10 @@ class FunctionWrapper(object): wfuncs=None, mods=None, fsclient=None, + cmd_prefix=None, **kwargs): super(FunctionWrapper, self).__init__() + self.cmd_prefix = cmd_prefix self.wfuncs = wfuncs if isinstance(wfuncs, dict) else {} self.opts = opts self.mods = mods if isinstance(mods, dict) else {} @@ -41,6 +44,29 @@ class FunctionWrapper(object): ''' Return the function call to simulate the salt local lookup system ''' + if '.' not in cmd and not self.cmd_prefix: + # Form of salt.cmd.run in Jinja -- it's expecting a subdictionary + # containing only 'cmd' module calls, in that case. Create a new + # FunctionWrapper which contains the prefix 'cmd' (again, for the + # salt.cmd.run example) + kwargs = copy.deepcopy(self.kwargs) + id_ = kwargs.pop('id_') + host = kwargs.pop('host') + return FunctionWrapper(self.opts, + id_, + host, + wfuncs=self.wfuncs, + mods=self.mods, + fsclient=self.fsclient, + cmd_prefix=cmd, + **kwargs) + + if self.cmd_prefix: + # We're in an inner FunctionWrapper as created by the code block + # above. Reconstruct the original cmd in the form 'cmd.run' and + # then evaluate as normal + cmd = '{0}.{1}'.format(self.cmd_prefix, cmd) + if cmd in self.wfuncs: return self.wfuncs[cmd] From 3b29fa0d0517f316ea00d0eb8053f383338e809c Mon Sep 17 00:00:00 2001 From: Colton Myers Date: Wed, 14 Jan 2015 14:21:17 -0700 Subject: [PATCH 32/67] Remove msgpack from thin generation for salt-ssh --- salt/utils/thin.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/salt/utils/thin.py b/salt/utils/thin.py index d49a6d87a9..180b81116a 100644 --- a/salt/utils/thin.py +++ b/salt/utils/thin.py @@ -14,11 +14,6 @@ import tempfile import jinja2 import yaml import requests -try: - import msgpack - HAS_MSGPACK = True -except ImportError: - HAS_MSGPACK = False try: import certifi HAS_CERTIFI = True @@ -128,9 +123,6 @@ def gen_thin(cachedir, extra_mods='', overwrite=False, so_mods=''): os.path.dirname(yaml.__file__), os.path.dirname(requests.__file__) ] - if HAS_MSGPACK: - tops.append(os.path.dirname(msgpack.__file__)) - if HAS_URLLIB3: tops.append(os.path.dirname(urllib3.__file__)) From 2e364ac2495b62336c22691e4da2e0a612012216 Mon Sep 17 00:00:00 2001 From: Colton Myers Date: Wed, 14 Jan 2015 15:17:21 -0700 Subject: [PATCH 33/67] Add more release notes for 2014.7.1 --- doc/topics/releases/2014.7.1.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/doc/topics/releases/2014.7.1.rst b/doc/topics/releases/2014.7.1.rst index a5ec7417bd..0949aee7c8 100644 --- a/doc/topics/releases/2014.7.1.rst +++ b/doc/topics/releases/2014.7.1.rst @@ -17,3 +17,7 @@ Version 2014.7.1 is a bugfix release for :doc:`2014.7.0 pillar globbing is still disabled for those modes, for security reasons. (:issue:`17194`) - Fix for ``tty: True`` in salt-ssh (:issue:`16847`) +- Fix for supervisord states when supervisor not installed to system python + (:issue:`18044`) +- Fix for logging when ``log_level='quiet'`` for :mod:`cmd.run + ` (:issue:`19479`) From 228ada2890dc49df1713717b6e2f135ccf80ca04 Mon Sep 17 00:00:00 2001 From: Colton Myers Date: Wed, 14 Jan 2015 15:17:33 -0700 Subject: [PATCH 34/67] Add release notes for 2014.7.2 --- doc/topics/releases/2014.7.2.rst | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 doc/topics/releases/2014.7.2.rst diff --git a/doc/topics/releases/2014.7.2.rst b/doc/topics/releases/2014.7.2.rst new file mode 100644 index 0000000000..979cfae2e0 --- /dev/null +++ b/doc/topics/releases/2014.7.2.rst @@ -0,0 +1,10 @@ +=========================== +Salt 2014.7.1 Release Notes +=========================== + +:release: TBA + +Version 2014.7.2 is a bugfix release for :doc:`2014.7.0 +`. The changes include: + +- Fix erroneous warnings for systemd service enabled check (:issue:`19606`) From b59ac43c5795a6045af883886b88f95bb51f9c08 Mon Sep 17 00:00:00 2001 From: Joseph Hall Date: Wed, 14 Jan 2015 22:56:27 +0000 Subject: [PATCH 35/67] Default http.query() to urllib2, but still allow requests --- salt/utils/http.py | 164 +++++++++++++++++++++++++++++++++++++-------- 1 file changed, 136 insertions(+), 28 deletions(-) diff --git a/salt/utils/http.py b/salt/utils/http.py index 295c45f977..cd9ea1990e 100644 --- a/salt/utils/http.py +++ b/salt/utils/http.py @@ -13,6 +13,22 @@ import logging import salt.ext.six.moves.http_cookiejar # pylint: disable=E0611 from salt._compat import ElementTree as ET +import ssl +from ssl import CertificateError +try: + from ssl import match_hostname + HAS_MATCHHOSTNAME = True +except ImportError: + try: + from backports.ssl_match_hostname import match_hostname + HAS_MATCHHOSTNAME = True + except ImportError: + HAS_MATCHHOSTNAME = False +import socket +import urllib +import urllib2 +import httplib + # Import salt libs import salt.utils import salt.utils.xmlutil as xml @@ -22,14 +38,17 @@ from salt.template import compile_template from salt import syspaths # Import 3rd party libs -import requests +try: + import requests + HAS_REQUESTS = True +except ImportError: + HAS_REQUESTS = False import msgpack log = logging.getLogger(__name__) JARFILE = os.path.join(syspaths.CACHE_DIR, 'cookies.txt') SESSIONJARFILE = os.path.join(syspaths.CACHE_DIR, 'cookies.session.p') - def query(url, method='GET', params=None, @@ -58,15 +77,31 @@ def query(url, test=False, test_url=None, node='minion', + port=80, opts=None, + requests_lib=None, + ca_bundle=None, **kwargs): ''' Query a resource, and decode the return data ''' ret = {} - requests_log = logging.getLogger('requests') - requests_log.setLevel(logging.WARNING) + if requests_lib is None: + requests_lib = opts.get('requests_lib', False) + + if requests_lib is True: + if HAS_REQUESTS is False: + ret['error'] = ('http.query has been set to use requests, but the ' + 'requests library does not seem to be installed') + log.error(ret['error']) + return ret + else: + requests_log = logging.getLogger('requests') + requests_log.setLevel(logging.WARNING) + + if ca_bundle is None: + ca_bundle = get_ca_bundle(opts) if opts is None: if node == 'master': @@ -122,20 +157,24 @@ def query(url, else: auth = None - sess = requests.Session() - sess.auth = auth - sess.headers.update(header_dict) - log.trace('Request Headers: {0}'.format(sess.headers)) + if requests_lib is True: + sess = requests.Session() + sess.auth = auth + sess.headers.update(header_dict) + log.trace('Request Headers: {0}'.format(sess.headers)) + sess_cookies = sess.cookies + else: + sess_cookies = None if cookies is not None: if cookie_format == 'mozilla': - sess.cookies = salt.ext.six.moves.http_cookiejar.MozillaCookieJar(cookie_jar) + sess_cookies = salt.ext.six.moves.http_cookiejar.MozillaCookieJar(cookie_jar) else: - sess.cookies = salt.ext.six.moves.http_cookiejar.LWPCookieJar(cookie_jar) + sess_cookies = salt.ext.six.moves.http_cookiejar.LWPCookieJar(cookie_jar) if not os.path.isfile(cookie_jar): - sess.cookies.save() + sess_cookies.save() else: - sess.cookies.load() + sess_cookies.load() if test is True: if test_url is None: @@ -144,36 +183,85 @@ def query(url, url = test_url ret['test'] = True - result = sess.request( - method, url, params=params, data=data - ) - log.debug('Response Status Code: {0}'.format(result.status_code)) - log.trace('Response Headers: {0}'.format(result.headers)) - log.trace('Response Text: {0}'.format(result.text)) - log.trace('Response Cookies: {0}'.format(result.cookies.get_dict())) + if requests_lib is True: + result = sess.request( + method, url, params=params, data=data + ) + result_status_code = result.status_code + result_headers = result.headers + result_text = result.text + result_cookies = result.cookies + else: + request = urllib2.Request(url) + + if url.startswith('https') or port == 443: + if not HAS_MATCHHOSTNAME: + log.warn(('match_hostname() not available, SSL hostname ' + 'checking not available. THIS CONNECTION MAY NOT BE SECURE!')) + else: + hostname = request.get_host() + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.connect((hostname, 443)) + sockwrap = ssl.wrap_socket( + sock, + ca_certs=ca_bundle, + cert_reqs=ssl.CERT_REQUIRED + ) + try: + match_hostname(sockwrap.getpeercert(), 'github.com') + except CertificateError as exc: + ret['error'] = ( + 'The certificate was invalid. ' + 'Error returned was: {0}'.format( + pprint.pformat(exc) + ) + ) + return ret + + opener = urllib2.build_opener( + urllib2.HTTPHandler, + urllib2.HTTPCookieProcessor(sess_cookies) + ) + for header in header_dict: + reques.add_header(header, header_dict[header]) + request.get_method = lambda: method + result = opener.open(request) + + result_status_code = result.code + result_headers = result.headers.headers + result_text = result.read() + + log.debug('Response Status Code: {0}'.format(result_status_code)) + log.trace('Response Headers: {0}'.format(result_headers)) + log.trace('Response Cookies: {0}'.format(sess_cookies)) + try: + log.trace('Response Text: {0}'.format(result_text)) + except UnicodeEncodeError as exc: + log.trace(('Cannot Trace Log Response Text: {0}. This may be due to ' + 'incompatibilities between requests and logging.').format(exc)) if cookies is not None: - sess.cookies.save() + sess_cookies.save() if persist_session is True: # TODO: See persist_session above - if 'set-cookie' in result.headers: + if 'set-cookie' in result_headers: with salt.utils.fopen(session_cookie_jar, 'w') as fh_: - session_cookies = result.headers.get('set-cookie', None) + session_cookies = result_headers.get('set-cookie', None) if session_cookies is not None: msgpack.dump({'Cookie': session_cookies}, fh_) else: msgpack.dump('', fh_) if status is True: - ret['status'] = result.status_code + ret['status'] = result_status_code if headers is True: - ret['headers'] = result.headers + ret['headers'] = result_headers if decode is True: if decode_type == 'auto': - content_type = result.headers.get( + content_type = result_headers.get( 'content-type', 'application/json' ) if 'xml' in content_type: @@ -195,21 +283,41 @@ def query(url, return ret if decode_type == 'json': - ret['dict'] = json.loads(result.text) + ret['dict'] = json.loads(result_text) elif decode_type == 'xml': ret['dict'] = [] - items = ET.fromstring(result.text) + items = ET.fromstring(result_text) for item in items: ret['dict'].append(xml.to_dict(item)) else: text = True if text is True: - ret['text'] = result.text + ret['text'] = result_text return ret +def get_ca_bundle(opts): + ''' + Return the location of the ca bundle file + + Possible locations of this file (alphabetical order): + + /etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem + /etc/pki/tls/certs/ca-bundle.crt + /etc/pki/tls/certs/ca-bundle.trust.crt + /etc/ssl/certs/ca-bundle.crt + /etc/ssl/certs/ca-certificates.crt + /var/lib/ca-certificates/ca-bundle.pem + + The following article discusses this file: + + http://tinyurl.com/k7rx42a + ''' + return opts.get('ca_bundle', '/etc/ssl/certs/ca-certificates.crt') + + def _render(template, render, renderer, template_dict, opts): ''' Render a template From 7f0e4a3c5b8616c9cab1e83444b54f4e777aba3c Mon Sep 17 00:00:00 2001 From: Joseph Hall Date: Thu, 15 Jan 2015 01:18:07 +0000 Subject: [PATCH 36/67] Iterate through a list of known paths for ca_bundle --- salt/utils/http.py | 33 ++++++++++++++++++++------------- 1 file changed, 20 insertions(+), 13 deletions(-) diff --git a/salt/utils/http.py b/salt/utils/http.py index cd9ea1990e..7e06a0fb00 100644 --- a/salt/utils/http.py +++ b/salt/utils/http.py @@ -300,22 +300,29 @@ def query(url, def get_ca_bundle(opts): ''' - Return the location of the ca bundle file - - Possible locations of this file (alphabetical order): - - /etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem - /etc/pki/tls/certs/ca-bundle.crt - /etc/pki/tls/certs/ca-bundle.trust.crt - /etc/ssl/certs/ca-bundle.crt - /etc/ssl/certs/ca-certificates.crt - /var/lib/ca-certificates/ca-bundle.pem - - The following article discusses this file: + Return the location of the ca bundle file. See the following article: http://tinyurl.com/k7rx42a ''' - return opts.get('ca_bundle', '/etc/ssl/certs/ca-certificates.crt') + if hasattr(get_ca_bundle, '__return_value__'): + return get_ca_bundle.__return_value__ + + opts_bundle = opts.get('ca_bundle', None) + if opts_bundle is not None and os.path.exists(opts_bundle): + return opts_bundle + + for path in ( + '/etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem', + '/etc/pki/tls/certs/ca-bundle.crt', + '/etc/pki/tls/certs/ca-bundle.trust.crt', + '/etc/ssl/certs/ca-bundle.crt', + '/etc/ssl/certs/ca-certificates.crt', + '/var/lib/ca-certificates/ca-bundle.pem', + ): + if os.path.exists(path): + return path + + return None def _render(template, render, renderer, template_dict, opts): From 435afb6e26d3d1d39930cf240c6b9bedb409ba77 Mon Sep 17 00:00:00 2001 From: Pedro Algarvio Date: Thu, 15 Jan 2015 01:22:35 +0000 Subject: [PATCH 37/67] No logging should be done in `__virtual__` --- salt/modules/boto_vpc.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/salt/modules/boto_vpc.py b/salt/modules/boto_vpc.py index ac77bd7eb5..0c12e4149c 100644 --- a/salt/modules/boto_vpc.py +++ b/salt/modules/boto_vpc.py @@ -67,14 +67,10 @@ def __virtual__(): # which was added in boto 2.8.0 # https://github.com/boto/boto/commit/33ac26b416fbb48a60602542b4ce15dcc7029f12 if not HAS_BOTO: - log.debug('The boto_vpc module requires boto {0} to be installed.'.format(required_boto_version)) return False elif _LooseVersion(boto.__version__) < _LooseVersion(required_boto_version): - log.debug('The boto_vpc module requires boto {0} to be installed. Current boto version: {1}'.format( - required_boto_version, boto.__version__)) return False else: - log.debug('Installed boto version: {0}'.format(boto.__version__)) return True From 034bbe45b0f7ecd5147fac1e9714e8530afdce39 Mon Sep 17 00:00:00 2001 From: Joseph Hall Date: Thu, 15 Jan 2015 01:47:00 +0000 Subject: [PATCH 38/67] Linting --- salt/utils/http.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/salt/utils/http.py b/salt/utils/http.py index 7e06a0fb00..9cebd0d155 100644 --- a/salt/utils/http.py +++ b/salt/utils/http.py @@ -14,20 +14,19 @@ import salt.ext.six.moves.http_cookiejar # pylint: disable=E0611 from salt._compat import ElementTree as ET import ssl -from ssl import CertificateError try: + from ssl import CertificateError from ssl import match_hostname HAS_MATCHHOSTNAME = True except ImportError: try: + from backports.ssl_match_hostname import CertificateError from backports.ssl_match_hostname import match_hostname HAS_MATCHHOSTNAME = True except ImportError: HAS_MATCHHOSTNAME = False import socket -import urllib import urllib2 -import httplib # Import salt libs import salt.utils @@ -49,6 +48,7 @@ log = logging.getLogger(__name__) JARFILE = os.path.join(syspaths.CACHE_DIR, 'cookies.txt') SESSIONJARFILE = os.path.join(syspaths.CACHE_DIR, 'cookies.session.p') + def query(url, method='GET', params=None, @@ -91,7 +91,7 @@ def query(url, requests_lib = opts.get('requests_lib', False) if requests_lib is True: - if HAS_REQUESTS is False: + if HAS_REQUESTS is False: ret['error'] = ('http.query has been set to use requests, but the ' 'requests library does not seem to be installed') log.error(ret['error']) @@ -223,7 +223,7 @@ def query(url, urllib2.HTTPCookieProcessor(sess_cookies) ) for header in header_dict: - reques.add_header(header, header_dict[header]) + request.add_header(header, header_dict[header]) request.get_method = lambda: method result = opener.open(request) From 6cd5baaf620a1111d9782a48cb443af8afa7a087 Mon Sep 17 00:00:00 2001 From: Joseph Hall Date: Thu, 15 Jan 2015 02:08:46 +0000 Subject: [PATCH 39/67] More linting, and fixing hardcoded host --- salt/utils/http.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/salt/utils/http.py b/salt/utils/http.py index 9cebd0d155..cb27a5fa92 100644 --- a/salt/utils/http.py +++ b/salt/utils/http.py @@ -15,8 +15,8 @@ from salt._compat import ElementTree as ET import ssl try: - from ssl import CertificateError - from ssl import match_hostname + from ssl import CertificateError # pylint: disable=E0611 + from ssl import match_hostname # pylint: disable=E0611 HAS_MATCHHOSTNAME = True except ImportError: try: @@ -208,7 +208,7 @@ def query(url, cert_reqs=ssl.CERT_REQUIRED ) try: - match_hostname(sockwrap.getpeercert(), 'github.com') + match_hostname(sockwrap.getpeercert(), hostname) except CertificateError as exc: ret['error'] = ( 'The certificate was invalid. ' From 2e72f5ff3891e191426a45e317a5dfdd27a41684 Mon Sep 17 00:00:00 2001 From: Thomas Jackson Date: Wed, 14 Jan 2015 21:22:40 -0800 Subject: [PATCH 40/67] Remove *insane* 7200s timeout (2h). If the pillar isn't back in the regular 1m its probably busted, if not we can set this timeout to something else, but 2h seems excessive. I've found a few minions stuck in this code block, since the minion's main process can get stuck here waiting for 2 hours. --- salt/pillar/__init__.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/salt/pillar/__init__.py b/salt/pillar/__init__.py index 3081fcfa68..4e7e4b7612 100644 --- a/salt/pillar/__init__.py +++ b/salt/pillar/__init__.py @@ -89,8 +89,6 @@ class RemotePillar(object): load['ext'] = self.ext ret_pillar = self.channel.crypted_transfer_decode_dictentry(load, dictkey='pillar', - tries=3, - timeout=7200, ) if not isinstance(ret_pillar, dict): From 4d5cc90c6ae6d234cd72aa36081509060e7a8198 Mon Sep 17 00:00:00 2001 From: rallytime Date: Wed, 14 Jan 2015 23:08:00 -0700 Subject: [PATCH 41/67] Remove sshpass checks from utils/cloud.py and other references Fixes #19219 --- doc/topics/development/conventions/packaging.rst | 2 -- salt/runners/manage.py | 4 ++-- salt/utils/cloud.py | 7 ------- 3 files changed, 2 insertions(+), 11 deletions(-) diff --git a/doc/topics/development/conventions/packaging.rst b/doc/topics/development/conventions/packaging.rst index c93d7b9dba..dadb3418e1 100644 --- a/doc/topics/development/conventions/packaging.rst +++ b/doc/topics/development/conventions/packaging.rst @@ -195,7 +195,6 @@ Depends ~~~~~~~ - `Salt Common` -- `sshpass` - `Python MessagePack` (Messagepack C lib, or msgpack-pure) Salt Cloud @@ -220,7 +219,6 @@ Depends ~~~~~~~ - `Salt Common` -- `sshpass` - `apache libcloud` >= 0.14.0 Salt Doc diff --git a/salt/runners/manage.py b/salt/runners/manage.py index 06c5e2a8d4..c053fc319d 100644 --- a/salt/runners/manage.py +++ b/salt/runners/manage.py @@ -291,8 +291,8 @@ def bootstrap(version="develop", script = 'https://bootstrap.saltstack.com' for host in hosts.split(","): # Could potentially lean on salt-ssh utils to make - # deployment easier on existing hosts (i.e. use sshpass, - # or expect, pass better options to ssh etc) + # deployment easier on existing hosts such as passing + # better options to ssh subprocess.call(["ssh", "root@" if root_user else "" + host, "python -c 'import urllib; " diff --git a/salt/utils/cloud.py b/salt/utils/cloud.py index f5a60b0366..99a20a0d07 100644 --- a/salt/utils/cloud.py +++ b/salt/utils/cloud.py @@ -286,13 +286,6 @@ def bootstrap(vm_, opts): ) ) - if key_filename is None and salt.utils.which('sshpass') is None: - raise SaltCloudSystemExit( - 'Cannot deploy salt in a VM if the \'ssh_keyfile\' setting ' - 'is not set and \'sshpass\' binary is not present on the ' - 'system for the password.' - ) - if key_filename is None and ('password' not in vm_ or not vm_['password']): raise SaltCloudSystemExit( 'Cannot deploy salt in a VM if the \'ssh_keyfile\' setting ' From c76326076f63680fec1786395b796be291ddcc4f Mon Sep 17 00:00:00 2001 From: rallytime Date: Wed, 14 Jan 2015 23:11:29 -0700 Subject: [PATCH 42/67] Remove sshpass checks from gogrid --- salt/cloud/clouds/gogrid.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/salt/cloud/clouds/gogrid.py b/salt/cloud/clouds/gogrid.py index 97d5c7bdad..60c957ed78 100644 --- a/salt/cloud/clouds/gogrid.py +++ b/salt/cloud/clouds/gogrid.py @@ -100,13 +100,6 @@ def create(vm_): ''' Create a single VM from a data dict ''' - deploy = config.get_cloud_config_value('deploy', vm_, __opts__) - if deploy is True and salt.utils.which('sshpass') is None: - raise SaltCloudSystemExit( - 'Cannot deploy salt in a VM if the \'sshpass\' binary is not ' - 'present on the system.' - ) - salt.utils.cloud.fire_event( 'event', 'starting create', @@ -244,7 +237,6 @@ def create(vm_): transport=__opts__['transport'] ) - deployed = False if win_installer: deployed = salt.utils.cloud.deploy_windows(**deploy_kwargs) else: From 65ce51611b25b6a716827730ccc6753ee1991add Mon Sep 17 00:00:00 2001 From: rallytime Date: Wed, 14 Jan 2015 23:13:28 -0700 Subject: [PATCH 43/67] Remove sshpass checks from saltify --- salt/cloud/clouds/saltify.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/salt/cloud/clouds/saltify.py b/salt/cloud/clouds/saltify.py index 65cb47f755..8193a344c7 100644 --- a/salt/cloud/clouds/saltify.py +++ b/salt/cloud/clouds/saltify.py @@ -79,11 +79,10 @@ def create(vm_): ) ) - if key_filename is None and salt.utils.which('sshpass') is None: + if key_filename is None and ('password' not in vm_ or not vm_['password']): raise SaltCloudSystemExit( - 'Cannot deploy salt in a VM if the \'ssh_keyfile\' setting ' - 'is not set and \'sshpass\' binary is not present on the ' - 'system for the password.' + 'Cannot deploy salt in a VM if either the \'ssh_keyfile\' or the ' + '\'ssh_password\' is not set.' ) ret = {} @@ -185,7 +184,6 @@ def create(vm_): transport=__opts__['transport'] ) - deployed = False if win_installer: deployed = salt.utils.cloud.deploy_windows(**deploy_kwargs) else: From f748ac56f436fa17f2f0b28aa634405b2e2112a0 Mon Sep 17 00:00:00 2001 From: rallytime Date: Wed, 14 Jan 2015 23:19:06 -0700 Subject: [PATCH 44/67] Remove sshpass check in joyent driver --- salt/cloud/clouds/joyent.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/salt/cloud/clouds/joyent.py b/salt/cloud/clouds/joyent.py index a6bd12bbbd..4d8cb21427 100644 --- a/salt/cloud/clouds/joyent.py +++ b/salt/cloud/clouds/joyent.py @@ -162,12 +162,10 @@ def create(vm_): key_filename = config.get_cloud_config_value( 'private_key', vm_, __opts__, search_global=False, default=None ) - if deploy is True and key_filename is None and \ - salt.utils.which('sshpass') is None: + if deploy is True and key_filename is None: raise SaltCloudSystemExit( 'Cannot deploy salt in a VM if the \'private_key\' setting ' - 'is not set and \'sshpass\' binary is not present on the ' - 'system for the password.' + 'is not set.' ) salt.utils.cloud.fire_event( From e9d32c562b7018cc81eae135f8cb612d741108b9 Mon Sep 17 00:00:00 2001 From: rallytime Date: Wed, 14 Jan 2015 23:21:38 -0700 Subject: [PATCH 45/67] Remove sshpass checks in rackspace driver --- salt/cloud/clouds/rackspace.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/salt/cloud/clouds/rackspace.py b/salt/cloud/clouds/rackspace.py index 0204545f7f..410b9dbcfe 100644 --- a/salt/cloud/clouds/rackspace.py +++ b/salt/cloud/clouds/rackspace.py @@ -196,12 +196,6 @@ def create(vm_): Create a single VM from a data dict ''' deploy = config.get_cloud_config_value('deploy', vm_, __opts__) - if deploy is True and salt.utils.which('sshpass') is None: - raise SaltCloudSystemExit( - 'Cannot deploy salt in a VM if the \'sshpass\' binary is not ' - 'present on the system.' - ) - salt.utils.cloud.fire_event( 'event', 'starting create', @@ -429,7 +423,6 @@ def create(vm_): transport=__opts__['transport'] ) - deployed = False if win_installer: deployed = salt.utils.cloud.deploy_windows(**deploy_kwargs) else: From 2b44f612c09bc8276fd02abb39d54316531e9438 Mon Sep 17 00:00:00 2001 From: rallytime Date: Wed, 14 Jan 2015 23:25:02 -0700 Subject: [PATCH 46/67] Remove sshpass check in nova driver --- salt/cloud/clouds/nova.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/salt/cloud/clouds/nova.py b/salt/cloud/clouds/nova.py index eef4ba2ac6..6a61cf4e73 100644 --- a/salt/cloud/clouds/nova.py +++ b/salt/cloud/clouds/nova.py @@ -536,11 +536,10 @@ def create(vm_): ) if deploy is True and key_filename is None and \ - salt.utils.which('sshpass') is None: + ('password' not in vm_ or not vm_['password']): raise SaltCloudSystemExit( - 'Cannot deploy salt in a VM if the \'ssh_key_file\' setting ' - 'is not set and \'sshpass\' binary is not present on the ' - 'system for the password.' + 'Cannot deploy salt in a VM if either the \'ssh_key_file\' or ' + 'or the \'ssh_password\' setting is not set.' ) vm_['key_filename'] = key_filename From 6602e8ef13ea4b9b67bc4b6747fdb905bc58e079 Mon Sep 17 00:00:00 2001 From: rallytime Date: Wed, 14 Jan 2015 23:26:58 -0700 Subject: [PATCH 47/67] Remove sshpass checks from parallels --- salt/cloud/clouds/parallels.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/salt/cloud/clouds/parallels.py b/salt/cloud/clouds/parallels.py index 02696fa3d4..4ffd441f8f 100644 --- a/salt/cloud/clouds/parallels.py +++ b/salt/cloud/clouds/parallels.py @@ -258,13 +258,6 @@ def create(vm_): ''' Create a single VM from a data dict ''' - deploy = config.get_cloud_config_value('deploy', vm_, __opts__) - if deploy is True and salt.utils.which('sshpass') is None: - raise SaltCloudSystemExit( - 'Cannot deploy salt in a VM if the \'sshpass\' binary is not ' - 'present on the system.' - ) - salt.utils.cloud.fire_event( 'event', 'starting create', From bb132207c0a85a6486205b7dc4003fb96ce00469 Mon Sep 17 00:00:00 2001 From: rallytime Date: Wed, 14 Jan 2015 23:30:18 -0700 Subject: [PATCH 48/67] Remove sshpass check from proxmox --- salt/cloud/clouds/proxmox.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/salt/cloud/clouds/proxmox.py b/salt/cloud/clouds/proxmox.py index b1ead3c222..978d7ce618 100644 --- a/salt/cloud/clouds/proxmox.py +++ b/salt/cloud/clouds/proxmox.py @@ -477,12 +477,6 @@ def create(vm_): salt-cloud -p proxmox-ubuntu vmhostname ''' ret = {} - deploy = config.get_cloud_config_value('deploy', vm_, __opts__) - if deploy is True and salt.utils.which('sshpass') is None: - raise SaltCloudSystemExit( - 'Cannot deploy salt in a VM if the \'sshpass\' binary is not ' - 'present on the system.' - ) salt.utils.cloud.fire_event( 'event', @@ -647,7 +641,6 @@ def create(vm_): transport=__opts__['transport'] ) - deployed = False if win_installer: deployed = salt.utils.cloud.deploy_windows(**deploy_kwargs) else: From 2581adb1b3b40fbbb98b20643c3446417285d087 Mon Sep 17 00:00:00 2001 From: rallytime Date: Wed, 14 Jan 2015 23:33:21 -0700 Subject: [PATCH 49/67] Remove the sshpass checks in openstack --- salt/cloud/clouds/openstack.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/salt/cloud/clouds/openstack.py b/salt/cloud/clouds/openstack.py index f913738f99..2baa36815f 100644 --- a/salt/cloud/clouds/openstack.py +++ b/salt/cloud/clouds/openstack.py @@ -591,11 +591,10 @@ def create(vm_): ) if deploy is True and key_filename is None and \ - salt.utils.which('sshpass') is None: + ('password' not in vm_ or not vm_['password']): raise SaltCloudSystemExit( 'Cannot deploy salt in a VM if the \'ssh_key_file\' setting ' - 'is not set and \'sshpass\' binary is not present on the ' - 'system for the password.' + 'or the \'ssh_password\' setting is not set.' ) vm_['key_filename'] = key_filename From dfe38a210319658d9d89940cbd8fa584b2953b0b Mon Sep 17 00:00:00 2001 From: rallytime Date: Wed, 14 Jan 2015 23:39:01 -0700 Subject: [PATCH 50/67] Fix openstack driver --- salt/cloud/clouds/openstack.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/salt/cloud/clouds/openstack.py b/salt/cloud/clouds/openstack.py index 2baa36815f..ecac85d457 100644 --- a/salt/cloud/clouds/openstack.py +++ b/salt/cloud/clouds/openstack.py @@ -590,13 +590,6 @@ def create(vm_): ) ) - if deploy is True and key_filename is None and \ - ('password' not in vm_ or not vm_['password']): - raise SaltCloudSystemExit( - 'Cannot deploy salt in a VM if the \'ssh_key_file\' setting ' - 'or the \'ssh_password\' setting is not set.' - ) - vm_['key_filename'] = key_filename salt.utils.cloud.fire_event( From 34390b7ad7a869f6a403d99807f6976e2777bffa Mon Sep 17 00:00:00 2001 From: rallytime Date: Wed, 14 Jan 2015 23:44:56 -0700 Subject: [PATCH 51/67] Remove keyfile check --- salt/cloud/clouds/joyent.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/salt/cloud/clouds/joyent.py b/salt/cloud/clouds/joyent.py index 4d8cb21427..480e3a76fe 100644 --- a/salt/cloud/clouds/joyent.py +++ b/salt/cloud/clouds/joyent.py @@ -158,15 +158,9 @@ def create(vm_): salt-cloud -p profile_name vm_name ''' - deploy = config.get_cloud_config_value('deploy', vm_, __opts__) key_filename = config.get_cloud_config_value( 'private_key', vm_, __opts__, search_global=False, default=None ) - if deploy is True and key_filename is None: - raise SaltCloudSystemExit( - 'Cannot deploy salt in a VM if the \'private_key\' setting ' - 'is not set.' - ) salt.utils.cloud.fire_event( 'event', From a6d4b0cbb7e0477c7e448022b8c8b685d99fade0 Mon Sep 17 00:00:00 2001 From: rallytime Date: Wed, 14 Jan 2015 23:51:02 -0700 Subject: [PATCH 52/67] Fix nova sshpass check --- salt/cloud/clouds/nova.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/salt/cloud/clouds/nova.py b/salt/cloud/clouds/nova.py index 6a61cf4e73..16fdfa86bd 100644 --- a/salt/cloud/clouds/nova.py +++ b/salt/cloud/clouds/nova.py @@ -535,13 +535,6 @@ def create(vm_): ) ) - if deploy is True and key_filename is None and \ - ('password' not in vm_ or not vm_['password']): - raise SaltCloudSystemExit( - 'Cannot deploy salt in a VM if either the \'ssh_key_file\' or ' - 'or the \'ssh_password\' setting is not set.' - ) - vm_['key_filename'] = key_filename salt.utils.cloud.fire_event( From a3b472ddb0ce22ca738dc3200e0bc1a7d21a7c84 Mon Sep 17 00:00:00 2001 From: rallytime Date: Thu, 15 Jan 2015 08:52:20 -0700 Subject: [PATCH 53/67] Fix saltify driver check --- salt/cloud/clouds/saltify.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/salt/cloud/clouds/saltify.py b/salt/cloud/clouds/saltify.py index 8193a344c7..1afa968002 100644 --- a/salt/cloud/clouds/saltify.py +++ b/salt/cloud/clouds/saltify.py @@ -69,9 +69,11 @@ def create(vm_): 'No Deploy': '\'deploy\' is not enabled. Not deploying.' } } + key_filename = config.get_cloud_config_value( 'key_filename', vm_, __opts__, search_global=False, default=None ) + if key_filename is not None and not os.path.isfile(key_filename): raise SaltCloudConfigError( 'The defined ssh_keyfile {0!r} does not exist'.format( @@ -79,12 +81,6 @@ def create(vm_): ) ) - if key_filename is None and ('password' not in vm_ or not vm_['password']): - raise SaltCloudSystemExit( - 'Cannot deploy salt in a VM if either the \'ssh_keyfile\' or the ' - '\'ssh_password\' is not set.' - ) - ret = {} log.info('Provisioning existing machine {0}'.format(vm_['name'])) From fc062d5881771b9882230ec7fc4291372d90cc58 Mon Sep 17 00:00:00 2001 From: Joseph Hall Date: Thu, 15 Jan 2015 09:46:06 -0700 Subject: [PATCH 54/67] Add show_service() function to msazure --- salt/cloud/clouds/msazure.py | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/salt/cloud/clouds/msazure.py b/salt/cloud/clouds/msazure.py index ba0012e5b4..1135d88c04 100644 --- a/salt/cloud/clouds/msazure.py +++ b/salt/cloud/clouds/msazure.py @@ -414,6 +414,37 @@ def show_instance(name, call=None): return nodes[name] +def show_service(kwargs=None, conn=None, call=None): + ''' + Show the details from the provider concerning an instance + ''' + if call != 'function': + raise SaltCloudSystemExit( + 'The show_service function must be called with -f or --function.' + ) + + if not conn: + conn = get_conn() + + services = conn.list_hosted_services() + for service in services: + if kwargs['service_name'] != service.service_name: + continue + props = service.hosted_service_properties + ret = { + 'affinity_group': props.affinity_group, + 'date_created': props.date_created, + 'date_last_modified': props.date_last_modified, + 'description': props.description, + 'extended_properties': props.extended_properties, + 'label': props.label, + 'location': props.location, + 'status': props.status, + } + return ret + return None + + def create(vm_): ''' Create a single VM from a data dict From 6216d17e5acde645a11ea6f394e8c3d13ad09dc4 Mon Sep 17 00:00:00 2001 From: Erik Johnson Date: Thu, 15 Jan 2015 16:59:02 -0600 Subject: [PATCH 55/67] Fix incorrect variable name --- salt/modules/archive.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/salt/modules/archive.py b/salt/modules/archive.py index 9a97cb4b7f..f61a7a5df3 100644 --- a/salt/modules/archive.py +++ b/salt/modules/archive.py @@ -312,7 +312,7 @@ def zip_(zip_file, sources, template=None, cwd=None, runas=None): except AttributeError: _bad_cwd() - if runas and (euid != uinfo['uid'] or guid != uinfo['gid']): + if runas and (euid != uinfo['uid'] or egid != uinfo['gid']): # Change the egid first, as changing it after the euid will fail # if the runas user is non-privileged. os.setegid(uinfo['gid']) @@ -496,7 +496,7 @@ def unzip(zip_file, dest, excludes=None, template=None, runas=None): zip_file, dest = _render_filenames(zip_file, dest, None, template) - if runas and (euid != uinfo['uid'] or guid != uinfo['gid']): + if runas and (euid != uinfo['uid'] or egid != uinfo['gid']): # Change the egid first, as changing it after the euid will fail # if the runas user is non-privileged. os.setegid(uinfo['gid']) From 769ebe7a9831673a261d9a97a053b0a54a182ab0 Mon Sep 17 00:00:00 2001 From: Erik Johnson Date: Thu, 15 Jan 2015 17:38:51 -0600 Subject: [PATCH 56/67] Fix lint failure --- salt/modules/freebsdkmod.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/salt/modules/freebsdkmod.py b/salt/modules/freebsdkmod.py index 3d58808348..e39d6177f1 100644 --- a/salt/modules/freebsdkmod.py +++ b/salt/modules/freebsdkmod.py @@ -18,7 +18,7 @@ __virtualname__ = 'kmod' _LOAD_MODULE = '{0}_load="YES"' _LOADER_CONF = '/boot/loader.conf' _MODULE_RE = '^{0}_load="YES"' -_MODULES_RE = '^(\w+)_load="YES"' +_MODULES_RE = r'^(\w+)_load="YES"' def __virtual__(): From 08c9bc9c12cdc9443ca88d0a4f774709766727d2 Mon Sep 17 00:00:00 2001 From: "Gareth J. Greenaway" Date: Thu, 15 Jan 2015 20:09:28 -0800 Subject: [PATCH 57/67] fixing a bug where schedule.list would error out if it encountered a configuration item that wasn't in the list of supported items. --- salt/modules/schedule.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/salt/modules/schedule.py b/salt/modules/schedule.py index 9267d367cd..2d9605d639 100644 --- a/salt/modules/schedule.py +++ b/salt/modules/schedule.py @@ -39,7 +39,7 @@ SCHEDULE_CONF = [ 'days', 'enabled', 'cron' - ] +] def list_(show_all=False, return_yaml=True): @@ -72,7 +72,6 @@ def list_(show_all=False, return_yaml=True): for item in schedule[job]: if item not in SCHEDULE_CONF: del schedule[job][item] - continue if schedule[job][item] == 'true': schedule[job][item] = True if schedule[job][item] == 'false': From 6ca91176da59a076249165c44c2dc5d550286a47 Mon Sep 17 00:00:00 2001 From: rallytime Date: Fri, 16 Jan 2015 00:09:34 -0700 Subject: [PATCH 58/67] Pylint fix for 2014.7 --- salt/cloud/clouds/saltify.py | 4 ++-- salt/modules/freebsdkmod.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/salt/cloud/clouds/saltify.py b/salt/cloud/clouds/saltify.py index 1afa968002..ebaf4e25b1 100644 --- a/salt/cloud/clouds/saltify.py +++ b/salt/cloud/clouds/saltify.py @@ -22,7 +22,7 @@ import salt.utils # Import salt cloud libs import salt.utils.cloud import salt.config as config -from salt.exceptions import SaltCloudConfigError, SaltCloudSystemExit +from salt.exceptions import SaltCloudConfigError # Get logging started log = logging.getLogger(__name__) @@ -69,7 +69,7 @@ def create(vm_): 'No Deploy': '\'deploy\' is not enabled. Not deploying.' } } - + key_filename = config.get_cloud_config_value( 'key_filename', vm_, __opts__, search_global=False, default=None ) diff --git a/salt/modules/freebsdkmod.py b/salt/modules/freebsdkmod.py index 411ec77ac4..70d01a101b 100644 --- a/salt/modules/freebsdkmod.py +++ b/salt/modules/freebsdkmod.py @@ -17,7 +17,7 @@ __virtualname__ = 'kmod' _LOAD_MODULE = '{0}_load="YES"' _LOADER_CONF = '/boot/loader.conf' _MODULE_RE = '^{0}_load="YES"' -_MODULES_RE = '^(\w+)_load="YES"' +_MODULES_RE = r'^(\w+)_load="YES"' def __virtual__(): From bb6e22999e65b97f785eda33265a2121058894cc Mon Sep 17 00:00:00 2001 From: Jayesh Kariya Date: Fri, 16 Jan 2015 19:12:19 +0530 Subject: [PATCH 59/67] Adding defaults unit test case --- tests/unit/modules/defaults_test.py | 54 +++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 tests/unit/modules/defaults_test.py diff --git a/tests/unit/modules/defaults_test.py b/tests/unit/modules/defaults_test.py new file mode 100644 index 0000000000..10259d9a3c --- /dev/null +++ b/tests/unit/modules/defaults_test.py @@ -0,0 +1,54 @@ +# -*- coding: utf-8 -*- +''' + :codeauthor: :email:`Jayesh Kariya ` +''' + +# Import Salt Testing Libs +from salttesting import TestCase, skipIf +from salttesting.mock import ( + MagicMock, + patch, + NO_MOCK, + NO_MOCK_REASON +) + +import inspect + +# Import Salt Libs +from salt.modules import defaults + +# Globals +defaults.__grains__ = {} +defaults.__salt__ = {} +defaults.__opts__ = {} + + +@skipIf(NO_MOCK, NO_MOCK_REASON) +class DefaultsTestCase(TestCase): + ''' + Test cases for salt.modules.defaults + ''' + # 'get' function tests: 1 + + def test_get(self): + ''' + Test if it execute a defaults client run and return a dict + ''' + mock = MagicMock(return_value='') + with patch.dict(defaults.__salt__, {'pillar.get': mock}): + self.assertEqual(defaults.get('core:users:root'), '') + + @patch('salt.modules.defaults.get', + MagicMock(return_value={'users': {'root': [0]}})) + def test_get_mock(self): + ''' + Test if it execute a defaults client run and return a dict + ''' + with patch.object(inspect, 'stack', MagicMock(return_value=[])): + self.assertEqual(defaults.get('core:users:root'), + {'users': {'root': [0]}}) + + +if __name__ == '__main__': + from integration import run_tests + run_tests(DefaultsTestCase, needs_daemon=False) From ad02d09821f4344771f9c419753707f82ebfd7fc Mon Sep 17 00:00:00 2001 From: Mike Place Date: Fri, 16 Jan 2015 10:33:53 -0700 Subject: [PATCH 60/67] Fix multi-master event handling bug We had a situation wherein minion generators would race to the sub socket and only a single minion would actually act on the package. The result was that events such as refresh_pillar would cause one (or sometimes both) minions to stop responding. This changes the behaviour so that the first instance to get the request calls action on all instances. While this has the possible side-effect of blocking for O(n) inside the event handling loop, it also ensures that multi-master minions update in a linear fashion. --- salt/minion.py | 26 +++++++++----------------- 1 file changed, 9 insertions(+), 17 deletions(-) diff --git a/salt/minion.py b/salt/minion.py index 2d01162a12..a4b1307edd 100644 --- a/salt/minion.py +++ b/salt/minion.py @@ -557,24 +557,16 @@ class MultiMinion(MinionBase): # run scheduled jobs if you have them loop_interval = self.process_schedule(minion['minion'], loop_interval) - # if you have an event to handle, do it on a single minion - # (first one to not throw an exception) + # If a minion instance receives event, handle the event on all + # instances if package: - # If we need to expand this, we may want to consider a specific header - # or another approach entirely. - if package.startswith('_minion_mine'): - for multi_minion in minions: - try: - minions[master]['minion'].handle_event(package) - except Exception: - pass - else: - try: - minion['minion'].handle_event(package) - package = None - self.epub_sock.send(package) - except Exception: - pass + try: + for master in masters: + minions[master].handle_event(package) + except Exception: + pass + finally: + package = None # Ensure we don't loop # have the Minion class run anything it has to run next(minion['generator']) From 86e4ccf76eff6de6aaa0554c2d43bc03d99d72d4 Mon Sep 17 00:00:00 2001 From: Mike Place Date: Fri, 16 Jan 2015 10:40:23 -0700 Subject: [PATCH 61/67] Remove unnecessary comment --- salt/minion.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/salt/minion.py b/salt/minion.py index a4b1307edd..b9bb197bf0 100644 --- a/salt/minion.py +++ b/salt/minion.py @@ -566,7 +566,7 @@ class MultiMinion(MinionBase): except Exception: pass finally: - package = None # Ensure we don't loop + package = None # have the Minion class run anything it has to run next(minion['generator']) From 843b71965c4ba210b013e77f5507696fc5962b24 Mon Sep 17 00:00:00 2001 From: Justin Findlay Date: Sat, 17 Jan 2015 14:00:44 -0700 Subject: [PATCH 62/67] column_family_definition requires keyspace, column_family args --- salt/modules/cassandra.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/salt/modules/cassandra.py b/salt/modules/cassandra.py index ae09945d19..61b622b647 100644 --- a/salt/modules/cassandra.py +++ b/salt/modules/cassandra.py @@ -179,7 +179,7 @@ def column_families(keyspace=None): return ret -def column_family_definition(keyspace=None, column_family=None): +def column_family_definition(keyspace, column_family): ''' Return a dictionary of column family definitions for the given keyspace/column_family From 9d18f7a8796e8b8e0f31d0f6b2e8b6132648c9ce Mon Sep 17 00:00:00 2001 From: Justin Findlay Date: Sat, 17 Jan 2015 14:02:38 -0700 Subject: [PATCH 63/67] fix cassandra unit tests --- tests/unit/modules/cassandra_test.py | 91 +++++++++++++--------------- 1 file changed, 41 insertions(+), 50 deletions(-) diff --git a/tests/unit/modules/cassandra_test.py b/tests/unit/modules/cassandra_test.py index 0e32918aa1..4d1e7e6382 100644 --- a/tests/unit/modules/cassandra_test.py +++ b/tests/unit/modules/cassandra_test.py @@ -15,15 +15,7 @@ from salttesting.mock import ( # Import Salt Libs from salt.modules import cassandra -HAS_PYCASSA = False -try: - from pycassa.system_manager import SystemManager - HAS_PYCASSA = True -except ImportError: - pass -# Globals -# from pycassa.system_manager.SystemManager import list_keyspaces cassandra.__grains__ = {} cassandra.__salt__ = {} cassandra.__context__ = {} @@ -87,61 +79,60 @@ class CassandraTestCase(TestCase): ''' Test for Return existing keyspaces ''' - mock = MagicMock(side_effect=['8000', 'localhost']) - with patch.dict(cassandra.__salt__, {'config.option': mock}): - with patch.object(SystemManager, - 'list_keyspaces') as mock_method: - mock_method.return_value = ['A'] - self.assertEqual(cassandra.keyspaces(), ['A']) + mock_keyspaces = ['A', 'B', 'C', 'D'] + + class MockSystemManager(object): + def list_keyspaces(self): + return mock_keyspaces + + mock_sys_mgr = MagicMock(return_value=MockSystemManager()) + + with patch.object(cassandra, '_sys_mgr', mock_sys_mgr): + self.assertEqual(cassandra.keyspaces(), mock_keyspaces) def test_column_families(self): ''' Test for Return existing column families for all keyspaces ''' - mock = MagicMock(side_effect=['8000', 'localhost']) - with patch.dict(cassandra.__salt__, {'config.option': mock}): - with patch.object(SystemManager, - 'list_keyspaces') as mock_method: - mock_method.return_value = ['A'] - self.assertEqual(cassandra.column_families('B'), None) + mock_keyspaces = ['A', 'B'] - mock = MagicMock(side_effect=['8000', 'localhost']) - with patch.dict(cassandra.__salt__, {'config.option': mock}): - with patch.object(SystemManager, - 'list_keyspaces') as mock_method: - mock_method.return_value = ['A'] - with patch.object(SystemManager, - 'get_keyspace_column_families') as mock_method: - mock_method.return_value = {'B': 1, 'C': 2} - self.assertEqual(cassandra.column_families('A'), - ['C', 'B']) + class MockSystemManager(object): + def list_keyspaces(self): + return mock_keyspaces - mock = MagicMock(side_effect=['8000', 'localhost']) - with patch.dict(cassandra.__salt__, {'config.option': mock}): - with patch.object(SystemManager, - 'list_keyspaces') as mock_method: - mock_method.return_value = ['A'] - with patch.object(SystemManager, - 'get_keyspace_column_families') as mock_method: - mock_method.return_value = {'B': 1, 'C': 2} - self.assertEqual(cassandra.column_families(), {'A': - ['C', 'B']}) + def get_keyspace_column_families(self, keyspace): + if keyspace == 'A': + return {'a': 'saltines', 'b': 'biscuits'} + if keyspace == 'B': + return {'c': 'cheese', 'd': 'crackers'} + + mock_sys_mgr = MagicMock(return_value=MockSystemManager()) + + with patch.object(cassandra, '_sys_mgr', mock_sys_mgr): + self.assertEqual(cassandra.column_families('A'), + ['a', 'b']) + self.assertEqual(cassandra.column_families('Z'), + None) + self.assertEqual(cassandra.column_families(), + {'A': ['a', 'b'], 'B': ['c', 'd']}) def test_column_family_definition(self): ''' Test for Return a dictionary of column family definitions for the given keyspace/column_family ''' - mock = MagicMock(side_effect=['8000', 'localhost']) - with patch.dict(cassandra.__salt__, {'config.option': mock}): - with patch.object(SystemManager, - 'list_keyspaces') as mock_method: - mock_method.return_value = ['A'] - with patch.object(SystemManager, - 'get_keyspace_column_families') as mock_method: - mock_method.return_value = {'B': 1, 'C': 2} - self.assertEqual(cassandra.column_family_definition('A', 'B'), - None) + class MockSystemManager(object): + def get_keyspace_column_families(self, keyspace): + if keyspace == 'A': + return {'a': object, 'b': object} + if keyspace == 'B': + raise Exception + + mock_sys_mgr = MagicMock(return_value=MockSystemManager()) + + with patch.object(cassandra, '_sys_mgr', mock_sys_mgr): + self.assertEqual(cassandra.column_family_definition('A', 'a'), vars(object)) + self.assertEqual(cassandra.column_family_definition('B', 'a'), None) if __name__ == '__main__': From 770258e5116fa6509c3d910257d60c9aec3776bd Mon Sep 17 00:00:00 2001 From: Justin Findlay Date: Sat, 17 Jan 2015 15:07:25 -0700 Subject: [PATCH 64/67] fix environ module unit tests --- tests/unit/modules/environ_test.py | 36 +++++++++++++++++------------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/tests/unit/modules/environ_test.py b/tests/unit/modules/environ_test.py index 5f26074fa6..bcb75d693a 100644 --- a/tests/unit/modules/environ_test.py +++ b/tests/unit/modules/environ_test.py @@ -41,9 +41,12 @@ class EnvironTestCase(TestCase): with patch.object(os.environ, 'pop', mock): self.assertFalse(environ.setval('key', False, True)) - self.assertEqual(environ.setval('key', False), '') + mock_environ = {} + with patch.dict(os.environ, mock_environ): + self.assertEqual(environ.setval('key', False), '') - self.assertFalse(environ.setval('key', True)) + with patch.dict(os.environ, mock_environ): + self.assertFalse(environ.setval('key', True)) def test_setenv(self): ''' @@ -57,14 +60,11 @@ class EnvironTestCase(TestCase): True, False)) - mock = MagicMock(return_value={}) - with patch.dict(os.environ, mock): - mock = MagicMock(return_value=None) - with patch.object(environ, 'setval', mock): - self.assertEqual(environ.setenv({}, - False, - True, - False)['QT_QPA_PLATFORMTHEME'], + mock_environ = {'key': 'value'} + with patch.dict(os.environ, mock_environ): + mock_setval = MagicMock(return_value=None) + with patch.object(environ, 'setval', mock_setval): + self.assertEqual(environ.setenv({}, False, True, False)['key'], None) def test_get(self): @@ -81,16 +81,20 @@ class EnvironTestCase(TestCase): environment dictionary. Optionally compare the current value of the environment against the supplied value string. ''' - self.assertFalse(environ.has_value(True)) + mock_environ = {} + with patch.dict(os.environ, mock_environ): + self.assertFalse(environ.has_value(True)) - self.assertTrue(environ.has_value('QT_QPA_PLATFORMTHEME', - 'appmenu-qt5')) + os.environ['salty'] = 'yes' + self.assertTrue(environ.has_value('salty', 'yes')) - self.assertFalse(environ.has_value('QT_QPA_PLATFORMTHEME', 'value')) + os.environ['too_salty'] = 'no' + self.assertFalse(environ.has_value('too_salty', 'yes')) - self.assertFalse(environ.has_value('key', 'value')) + self.assertFalse(environ.has_value('key', 'value')) - self.assertFalse(environ.has_value('key')) + os.environ['key'] = 'value' + self.assertTrue(environ.has_value('key')) def test_item(self): ''' From 555dbd66ab34ec6e445833dfe50d1b48d12922d0 Mon Sep 17 00:00:00 2001 From: Justin Findlay Date: Sat, 17 Jan 2015 15:31:52 -0700 Subject: [PATCH 65/67] fix debian_service unit tests --- tests/unit/modules/debian_service_test.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/tests/unit/modules/debian_service_test.py b/tests/unit/modules/debian_service_test.py index 811f50aa2f..96e91a4ffc 100644 --- a/tests/unit/modules/debian_service_test.py +++ b/tests/unit/modules/debian_service_test.py @@ -32,9 +32,13 @@ class DebianServicesTestCase(TestCase): ''' Test for Return a list of service that are enabled on boot ''' - mock = MagicMock(return_value=1) - with patch.object(debian_service, '_get_runlevel', mock): - self.assertEqual(debian_service.get_enabled()[0], 'apparmor') + mock_runlevel = MagicMock(return_value=1) + mock_prefix = '/etc/rc1.d/S' + mock_glob = MagicMock(return_value=[mock_prefix + '01name']) + + with patch.object(debian_service, '_get_runlevel', mock_runlevel): + with patch.object(debian_service.glob, 'glob', mock_glob): + self.assertEqual(debian_service.get_enabled()[0], 'name') def test_get_disabled(self): ''' From 9202cbba787d9ac7e5b9f1229fdeae4937892172 Mon Sep 17 00:00:00 2001 From: Justin Findlay Date: Sat, 17 Jan 2015 15:44:45 -0700 Subject: [PATCH 66/67] fix dnsmasq unit tests --- tests/unit/modules/dnsmasq_test.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/unit/modules/dnsmasq_test.py b/tests/unit/modules/dnsmasq_test.py index d8d59a73e4..abf257717e 100644 --- a/tests/unit/modules/dnsmasq_test.py +++ b/tests/unit/modules/dnsmasq_test.py @@ -77,8 +77,8 @@ class DnsmasqTestCase(TestCase): m.return_value.__iter__.return_value = text_file_data.splitlines() self.assertDictEqual(dnsmasq._parse_dnamasq('filename'), {'A': 'B', - 'unparsed': ['a line here', - 'the second line']}) + 'unparsed': ['line here', + 'second line']}) if __name__ == '__main__': From 4910822280c3ca5c8beac628f60dea1324a67e32 Mon Sep 17 00:00:00 2001 From: Mark McGuire Date: Sun, 18 Jan 2015 00:11:35 -0600 Subject: [PATCH 67/67] fix regex issue --- salt/modules/iptables.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/salt/modules/iptables.py b/salt/modules/iptables.py index de7fd9d9ef..c382ae0c32 100644 --- a/salt/modules/iptables.py +++ b/salt/modules/iptables.py @@ -143,7 +143,7 @@ def build_rule(table=None, chain=None, command=None, position='', full=None, fam rule = '' proto = False - bang_not_pat = re.compile(r'[!|not]\s?') + bang_not_pat = re.compile(r'(!|not)\s?') if 'if' in kwargs: if kwargs['if'].startswith('!') or kwargs['if'].startswith('not'):