test_botomod.py 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265
  1. # -*- coding: utf-8 -*-
  2. # Import python libs
  3. from __future__ import absolute_import, print_function, unicode_literals
  4. import os
  5. # Import Salt Testing libs
  6. from tests.support.mixins import LoaderModuleMockMixin
  7. from tests.support.unit import skipIf, TestCase
  8. from tests.support.mock import NO_MOCK, NO_MOCK_REASON, patch, MagicMock
  9. from tests.support.paths import TESTS_DIR
  10. # Import Salt libs
  11. import salt.utils.botomod as botomod
  12. import salt.utils.boto3mod as boto3mod
  13. from salt.ext import six
  14. from salt.exceptions import SaltInvocationError
  15. from salt.utils.versions import LooseVersion
  16. # Import 3rd-party libs
  17. # pylint: disable=import-error
  18. try:
  19. import boto
  20. boto.ENDPOINTS_PATH = os.path.join(TESTS_DIR, 'unit/files/endpoints.json')
  21. import boto.exception
  22. from boto.exception import BotoServerError
  23. HAS_BOTO = True
  24. except ImportError:
  25. HAS_BOTO = False
  26. try:
  27. import boto3
  28. HAS_BOTO3 = True
  29. except ImportError:
  30. HAS_BOTO3 = False
  31. try:
  32. from moto import mock_ec2
  33. HAS_MOTO = True
  34. except ImportError:
  35. HAS_MOTO = False
  36. def mock_ec2(self):
  37. '''
  38. if the mock_ec2 function is not available due to import failure
  39. this replaces the decorated function with stub_function.
  40. Allows unit tests to use the @mock_ec2 decorator
  41. without a "NameError: name 'mock_ec2' is not defined" error.
  42. '''
  43. def stub_function(self):
  44. pass
  45. return stub_function
  46. required_boto_version = '2.0.0'
  47. required_boto3_version = '1.2.1'
  48. region = 'us-east-1'
  49. access_key = 'GKTADJGHEIQSXMKKRBJ08H'
  50. secret_key = 'askdjghsdfjkghWupUjasdflkdfklgjsdfjajkghs'
  51. conn_parameters = {'region': region, 'key': access_key, 'keyid': secret_key, 'profile': {}}
  52. service = 'ec2'
  53. resource_name = 'test-instance'
  54. resource_id = 'i-a1b2c3'
  55. error_body = '''
  56. <Response>
  57. <Errors>
  58. <Error>
  59. <Code>Error code text</Code>
  60. <Message>Error message</Message>
  61. </Error>
  62. </Errors>
  63. <RequestID>request ID</RequestID>
  64. </Response>
  65. '''
  66. no_error_body = '''
  67. <Response>
  68. <Errors />
  69. <RequestID>request ID</RequestID>
  70. </Response>
  71. '''
  72. def _has_required_boto():
  73. '''
  74. Returns True/False boolean depending on if Boto is installed and correct
  75. version.
  76. '''
  77. if not HAS_BOTO:
  78. return False
  79. elif LooseVersion(boto.__version__) < LooseVersion(required_boto_version):
  80. return False
  81. else:
  82. return True
  83. def _has_required_boto3():
  84. '''
  85. Returns True/False boolean depending on if Boto is installed and correct
  86. version.
  87. '''
  88. try:
  89. if not HAS_BOTO3:
  90. return False
  91. elif LooseVersion(boto3.__version__) < LooseVersion(required_boto3_version):
  92. return False
  93. else:
  94. return True
  95. except AttributeError as exc:
  96. if "has no attribute '__version__'" not in six.text_type(exc):
  97. raise
  98. return False
  99. def _has_required_moto():
  100. '''
  101. Returns True/False boolean depending on if Moto is installed and correct
  102. version.
  103. '''
  104. if not HAS_MOTO:
  105. return False
  106. else:
  107. import pkg_resources
  108. if LooseVersion(pkg_resources.get_distribution('moto').version) < LooseVersion('0.3.7'):
  109. return False
  110. return True
  111. class BotoUtilsTestCaseBase(TestCase, LoaderModuleMockMixin):
  112. def setup_loader_modules(self):
  113. module_globals = {
  114. '__salt__': {'config.option': MagicMock(return_value='dummy_opt')}
  115. }
  116. return {botomod: module_globals, boto3mod: module_globals}
  117. class BotoUtilsCacheIdTestCase(BotoUtilsTestCaseBase):
  118. def test_set_and_get_with_no_auth_params(self):
  119. botomod.cache_id(service, resource_name, resource_id=resource_id)
  120. self.assertEqual(botomod.cache_id(service, resource_name), resource_id)
  121. def test_set_and_get_with_explicit_auth_params(self):
  122. botomod.cache_id(service, resource_name, resource_id=resource_id, **conn_parameters)
  123. self.assertEqual(botomod.cache_id(service, resource_name, **conn_parameters), resource_id)
  124. def test_set_and_get_with_different_region_returns_none(self):
  125. botomod.cache_id(service, resource_name, resource_id=resource_id, region='us-east-1')
  126. self.assertEqual(botomod.cache_id(service, resource_name, region='us-west-2'), None)
  127. def test_set_and_get_after_invalidation_returns_none(self):
  128. botomod.cache_id(service, resource_name, resource_id=resource_id)
  129. botomod.cache_id(service, resource_name, resource_id=resource_id, invalidate=True)
  130. self.assertEqual(botomod.cache_id(service, resource_name), None)
  131. def test_partial(self):
  132. cache_id = botomod.cache_id_func(service)
  133. cache_id(resource_name, resource_id=resource_id)
  134. self.assertEqual(cache_id(resource_name), resource_id)
  135. @skipIf(NO_MOCK, NO_MOCK_REASON)
  136. @skipIf(HAS_BOTO is False, 'The boto module must be installed.')
  137. @skipIf(HAS_MOTO is False, 'The moto module must be installed.')
  138. @skipIf(_has_required_boto() is False, 'The boto module must be greater than'
  139. ' or equal to version {0}'
  140. .format(required_boto_version))
  141. class BotoUtilsGetConnTestCase(BotoUtilsTestCaseBase):
  142. @mock_ec2
  143. def test_conn_is_cached(self):
  144. conn = botomod.get_connection(service, **conn_parameters)
  145. self.assertTrue(conn in botomod.__context__.values())
  146. @mock_ec2
  147. def test_conn_is_cache_with_profile(self):
  148. conn = botomod.get_connection(service, profile=conn_parameters)
  149. self.assertTrue(conn in botomod.__context__.values())
  150. @mock_ec2
  151. def test_get_conn_with_no_auth_params_raises_invocation_error(self):
  152. with patch('boto.{0}.connect_to_region'.format(service),
  153. side_effect=boto.exception.NoAuthHandlerFound()):
  154. with self.assertRaises(SaltInvocationError):
  155. botomod.get_connection(service)
  156. @mock_ec2
  157. def test_get_conn_error_raises_command_execution_error(self):
  158. with patch('boto.{0}.connect_to_region'.format(service),
  159. side_effect=BotoServerError(400, 'Mocked error', body=error_body)):
  160. with self.assertRaises(BotoServerError):
  161. botomod.get_connection(service)
  162. @mock_ec2
  163. def test_partial(self):
  164. get_conn = botomod.get_connection_func(service)
  165. conn = get_conn(**conn_parameters)
  166. self.assertTrue(conn in botomod.__context__.values())
  167. @skipIf(HAS_BOTO is False, 'The boto module must be installed.')
  168. @skipIf(_has_required_boto() is False, 'The boto module must be greater than'
  169. ' or equal to version {0}'
  170. .format(required_boto_version))
  171. class BotoUtilsGetErrorTestCase(BotoUtilsTestCaseBase):
  172. def test_error_message(self):
  173. e = BotoServerError('400', 'Mocked error', body=error_body)
  174. r = botomod.get_error(e)
  175. expected = {'aws': {'code': 'Error code text',
  176. 'message': 'Error message',
  177. 'reason': 'Mocked error',
  178. 'status': '400'},
  179. 'message': 'Mocked error: Error message'}
  180. self.assertEqual(r, expected)
  181. def test_exception_message_with_no_body(self):
  182. e = BotoServerError('400', 'Mocked error')
  183. r = botomod.get_error(e)
  184. expected = {'aws': {'reason': 'Mocked error',
  185. 'status': '400'},
  186. 'message': 'Mocked error'}
  187. self.assertEqual(r, expected)
  188. def test_exception_message_with_no_error_in_body(self):
  189. e = BotoServerError('400', 'Mocked error', body=no_error_body)
  190. r = botomod.get_error(e)
  191. expected = {'aws': {'reason': 'Mocked error', 'status': '400'},
  192. 'message': 'Mocked error'}
  193. self.assertEqual(r, expected)
  194. @skipIf(HAS_BOTO is False, 'The boto module must be installed.')
  195. @skipIf(_has_required_boto() is False, 'The boto module must be greater than'
  196. ' or equal to version {0}'
  197. .format(required_boto_version))
  198. @skipIf(HAS_BOTO3 is False, 'The boto3 module must be installed.')
  199. @skipIf(_has_required_boto3() is False, 'The boto3 module must be greater than'
  200. ' or equal to version {0}'
  201. .format(required_boto3_version))
  202. class BotoBoto3CacheContextCollisionTest(BotoUtilsTestCaseBase):
  203. def test_context_conflict_between_boto_and_boto3_utils(self):
  204. botomod.assign_funcs(__name__, 'ec2')
  205. boto3mod.assign_funcs(__name__, 'ec2', get_conn_funcname="_get_conn3")
  206. boto_ec2_conn = botomod.get_connection('ec2',
  207. region=region,
  208. key=secret_key,
  209. keyid=access_key)
  210. boto3_ec2_conn = boto3mod.get_connection('ec2',
  211. region=region,
  212. key=secret_key,
  213. keyid=access_key)
  214. # These should *not* be the same object!
  215. self.assertNotEqual(id(boto_ec2_conn), id(boto3_ec2_conn))