# -*- coding: utf-8 -*- # Import python libs from __future__ import absolute_import, print_function, unicode_literals import os import pytest import salt.utils.boto3mod as boto3mod # Import Salt libs import salt.utils.botomod as botomod from salt.exceptions import SaltInvocationError from salt.ext import six from salt.utils.versions import LooseVersion # Import Salt Testing libs from tests.support.mixins import LoaderModuleMockMixin from tests.support.mock import MagicMock, patch from tests.support.runtests import RUNTIME_VARS from tests.support.unit import TestCase, skipIf # Import 3rd-party libs # pylint: disable=import-error try: import boto boto.ENDPOINTS_PATH = os.path.join( RUNTIME_VARS.TESTS_DIR, "unit/files/endpoints.json" ) import boto.exception from boto.exception import BotoServerError HAS_BOTO = True except ImportError: HAS_BOTO = False try: import boto3 HAS_BOTO3 = True except ImportError: HAS_BOTO3 = False try: from moto import mock_ec2 HAS_MOTO = True except ImportError: HAS_MOTO = False def mock_ec2(self): """ if the mock_ec2 function is not available due to import failure this replaces the decorated function with stub_function. Allows unit tests to use the @mock_ec2 decorator without a "NameError: name 'mock_ec2' is not defined" error. """ def stub_function(self): pass return stub_function required_boto_version = "2.0.0" required_boto3_version = "1.2.1" region = "us-east-1" access_key = "GKTADJGHEIQSXMKKRBJ08H" secret_key = "askdjghsdfjkghWupUjasdflkdfklgjsdfjajkghs" conn_parameters = { "region": region, "key": access_key, "keyid": secret_key, "profile": {}, } service = "ec2" resource_name = "test-instance" resource_id = "i-a1b2c3" error_body = """ Error code text Error message request ID """ no_error_body = """ request ID """ def _has_required_boto(): """ Returns True/False boolean depending on if Boto is installed and correct version. """ if not HAS_BOTO: return False elif LooseVersion(boto.__version__) < LooseVersion(required_boto_version): return False else: return True def _has_required_boto3(): """ Returns True/False boolean depending on if Boto is installed and correct version. """ try: if not HAS_BOTO3: return False elif LooseVersion(boto3.__version__) < LooseVersion(required_boto3_version): return False else: return True except AttributeError as exc: if "has no attribute '__version__'" not in six.text_type(exc): raise return False def _has_required_moto(): """ Returns True/False boolean depending on if Moto is installed and correct version. """ if not HAS_MOTO: return False else: import pkg_resources if LooseVersion(pkg_resources.get_distribution("moto").version) < LooseVersion( "0.3.7" ): return False return True class BotoUtilsTestCaseBase(TestCase, LoaderModuleMockMixin): def setup_loader_modules(self): module_globals = { "__salt__": {"config.option": MagicMock(return_value="dummy_opt")} } return {botomod: module_globals, boto3mod: module_globals} class BotoUtilsCacheIdTestCase(BotoUtilsTestCaseBase): def test_set_and_get_with_no_auth_params(self): botomod.cache_id(service, resource_name, resource_id=resource_id) self.assertEqual(botomod.cache_id(service, resource_name), resource_id) def test_set_and_get_with_explicit_auth_params(self): botomod.cache_id( service, resource_name, resource_id=resource_id, **conn_parameters ) self.assertEqual( botomod.cache_id(service, resource_name, **conn_parameters), resource_id ) def test_set_and_get_with_different_region_returns_none(self): botomod.cache_id( service, resource_name, resource_id=resource_id, region="us-east-1" ) self.assertEqual( botomod.cache_id(service, resource_name, region="us-west-2"), None ) def test_set_and_get_after_invalidation_returns_none(self): botomod.cache_id(service, resource_name, resource_id=resource_id) botomod.cache_id( service, resource_name, resource_id=resource_id, invalidate=True ) self.assertEqual(botomod.cache_id(service, resource_name), None) def test_partial(self): cache_id = botomod.cache_id_func(service) cache_id(resource_name, resource_id=resource_id) self.assertEqual(cache_id(resource_name), resource_id) @skipIf(HAS_BOTO is False, "The boto module must be installed.") @skipIf(HAS_MOTO is False, "The moto module must be installed.") @skipIf( _has_required_boto() is False, "The boto module must be greater than" " or equal to version {0}".format(required_boto_version), ) class BotoUtilsGetConnTestCase(BotoUtilsTestCaseBase): @mock_ec2 def test_conn_is_cached(self): conn = botomod.get_connection(service, **conn_parameters) self.assertTrue(conn in botomod.__context__.values()) @mock_ec2 @pytest.mark.slow_test(seconds=1) # Test takes >0.1 and <=1 seconds def test_conn_is_cache_with_profile(self): conn = botomod.get_connection(service, profile=conn_parameters) self.assertTrue(conn in botomod.__context__.values()) @mock_ec2 def test_get_conn_with_no_auth_params_raises_invocation_error(self): with patch( "boto.{0}.connect_to_region".format(service), side_effect=boto.exception.NoAuthHandlerFound(), ): with self.assertRaises(SaltInvocationError): botomod.get_connection(service) @mock_ec2 def test_get_conn_error_raises_command_execution_error(self): with patch( "boto.{0}.connect_to_region".format(service), side_effect=BotoServerError(400, "Mocked error", body=error_body), ): with self.assertRaises(BotoServerError): botomod.get_connection(service) @mock_ec2 def test_partial(self): get_conn = botomod.get_connection_func(service) conn = get_conn(**conn_parameters) self.assertTrue(conn in botomod.__context__.values()) @skipIf(HAS_BOTO is False, "The boto module must be installed.") @skipIf( _has_required_boto() is False, "The boto module must be greater than" " or equal to version {0}".format(required_boto_version), ) class BotoUtilsGetErrorTestCase(BotoUtilsTestCaseBase): def test_error_message(self): e = BotoServerError("400", "Mocked error", body=error_body) r = botomod.get_error(e) expected = { "aws": { "code": "Error code text", "message": "Error message", "reason": "Mocked error", "status": "400", }, "message": "Mocked error: Error message", } self.assertEqual(r, expected) def test_exception_message_with_no_body(self): e = BotoServerError("400", "Mocked error") r = botomod.get_error(e) expected = { "aws": {"reason": "Mocked error", "status": "400"}, "message": "Mocked error", } self.assertEqual(r, expected) def test_exception_message_with_no_error_in_body(self): e = BotoServerError("400", "Mocked error", body=no_error_body) r = botomod.get_error(e) expected = { "aws": {"reason": "Mocked error", "status": "400"}, "message": "Mocked error", } self.assertEqual(r, expected) @skipIf(HAS_BOTO is False, "The boto module must be installed.") @skipIf( _has_required_boto() is False, "The boto module must be greater than" " or equal to version {0}".format(required_boto_version), ) @skipIf(HAS_BOTO3 is False, "The boto3 module must be installed.") @skipIf( _has_required_boto3() is False, "The boto3 module must be greater than" " or equal to version {0}".format(required_boto3_version), ) class BotoBoto3CacheContextCollisionTest(BotoUtilsTestCaseBase): @pytest.mark.slow_test(seconds=1) # Test takes >0.1 and <=1 seconds def test_context_conflict_between_boto_and_boto3_utils(self): botomod.assign_funcs(__name__, "ec2") boto3mod.assign_funcs(__name__, "ec2", get_conn_funcname="_get_conn3") boto_ec2_conn = botomod.get_connection( "ec2", region=region, key=secret_key, keyid=access_key ) boto3_ec2_conn = boto3mod.get_connection( "ec2", region=region, key=secret_key, keyid=access_key ) # These should *not* be the same object! self.assertNotEqual(id(boto_ec2_conn), id(boto3_ec2_conn))