test_boto_iot.py 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471
  1. # -*- coding: utf-8 -*-
  2. # Import Python libs
  3. from __future__ import absolute_import, print_function, unicode_literals
  4. import logging
  5. import random
  6. import string
  7. # Import Salt Testing libs
  8. from tests.support.mixins import LoaderModuleMockMixin
  9. from tests.support.unit import skipIf, TestCase
  10. from tests.support.mock import MagicMock, patch
  11. # Import Salt libs
  12. import salt.config
  13. import salt.loader
  14. from salt.utils.versions import LooseVersion
  15. import salt.states.boto_iot as boto_iot
  16. # Import test suite libs
  17. # pylint: disable=import-error,no-name-in-module,unused-import
  18. from tests.unit.modules.test_boto_iot import BotoIoTTestCaseMixin
  19. # Import 3rd-party libs
  20. from salt.ext.six.moves import range # pylint: disable=import-error,redefined-builtin
  21. try:
  22. import boto
  23. import boto3
  24. from botocore.exceptions import ClientError
  25. from botocore import __version__ as found_botocore_version
  26. HAS_BOTO = True
  27. except ImportError:
  28. HAS_BOTO = False
  29. # pylint: enable=import-error,no-name-in-module,unused-import
  30. # the boto_iot module relies on the connect_to_region() method
  31. # which was added in boto 2.8.0
  32. # https://github.com/boto/boto/commit/33ac26b416fbb48a60602542b4ce15dcc7029f12
  33. required_boto3_version = '1.2.1'
  34. required_botocore_version = '1.4.41'
  35. log = logging.getLogger(__name__)
  36. def _has_required_boto():
  37. '''
  38. Returns True/False boolean depending on if Boto is installed and correct
  39. version.
  40. '''
  41. if not HAS_BOTO:
  42. return False
  43. elif LooseVersion(boto3.__version__) < LooseVersion(required_boto3_version):
  44. return False
  45. elif LooseVersion(found_botocore_version) < LooseVersion(required_botocore_version):
  46. return False
  47. else:
  48. return True
  49. if _has_required_boto():
  50. region = 'us-east-1'
  51. access_key = 'GKTADJGHEIQSXMKKRBJ08H'
  52. secret_key = 'askdjghsdfjkghWupUjasdflkdfklgjsdfjajkghs'
  53. conn_parameters = {'region': region, 'key': access_key, 'keyid': secret_key, 'profile': {}}
  54. error_message = 'An error occurred (101) when calling the {0} operation: Test-defined error'
  55. not_found_error = ClientError({
  56. 'Error': {
  57. 'Code': 'ResourceNotFoundException',
  58. 'Message': "Test-defined error"
  59. }
  60. }, 'msg')
  61. topic_rule_not_found_error = ClientError({
  62. 'Error': {
  63. 'Code': 'UnauthorizedException',
  64. 'Message': "Test-defined error"
  65. }
  66. }, 'msg')
  67. error_content = {
  68. 'Error': {
  69. 'Code': 101,
  70. 'Message': "Test-defined error"
  71. }
  72. }
  73. policy_ret = dict(policyName='testpolicy',
  74. policyDocument='{"Version": "2012-10-17", "Statement": [{"Action": ["iot:Publish"], "Resource": ["*"], "Effect": "Allow"}]}',
  75. policyArn='arn:aws:iot:us-east-1:123456:policy/my_policy',
  76. policyVersionId=1,
  77. defaultVersionId=1)
  78. topic_rule_ret = dict(ruleName='testrule',
  79. sql="SELECT * FROM 'iot/test'",
  80. description='topic rule description',
  81. createdAt='1970-01-01',
  82. actions=[{'iot': {'functionArn': 'arn:aws:::function'}}],
  83. ruleDisabled=True)
  84. principal = 'arn:aws:iot:us-east-1:1234:cert/21fc104aaaf6043f5756c1b57bda84ea8395904c43f28517799b19e4c42514'
  85. thing_type_name = 'test_thing_type'
  86. thing_type_desc = 'test_thing_type_desc'
  87. thing_type_attr_1 = 'test_thing_type_search_attr_1'
  88. thing_type_ret = dict(
  89. thingTypeName=thing_type_name,
  90. thingTypeProperties=dict(
  91. thingTypeDescription=thing_type_desc,
  92. searchableAttributes=[thing_type_attr_1],
  93. ),
  94. thingTypeMetadata=dict(
  95. deprecated=False,
  96. creationDate='2010-08-01 15:54:49.699000+00:00'
  97. )
  98. )
  99. deprecated_thing_type_ret = dict(
  100. thingTypeName=thing_type_name,
  101. thingTypeProperties=dict(
  102. thingTypeDescription=thing_type_desc,
  103. searchableAttributes=[thing_type_attr_1],
  104. ),
  105. thingTypeMetadata=dict(
  106. deprecated=True,
  107. creationDate='2010-08-01 15:54:49.699000+00:00',
  108. deprecationDate='2010-08-02 15:54:49.699000+00:00'
  109. )
  110. )
  111. thing_type_arn = 'test_thing_type_arn'
  112. create_thing_type_ret = dict(
  113. thingTypeName=thing_type_name,
  114. thingTypeArn=thing_type_arn
  115. )
  116. @skipIf(HAS_BOTO is False, 'The boto module must be installed.')
  117. @skipIf(_has_required_boto() is False, 'The boto3 module must be greater than'
  118. ' or equal to version {0}'
  119. .format(required_boto3_version))
  120. class BotoIoTStateTestCaseBase(TestCase, LoaderModuleMockMixin):
  121. conn = None
  122. def setup_loader_modules(self):
  123. ctx = {}
  124. utils = salt.loader.utils(
  125. self.opts,
  126. whitelist=['boto3', 'args', 'systemd', 'path', 'platform'],
  127. context=ctx)
  128. serializers = salt.loader.serializers(self.opts)
  129. self.funcs = funcs = salt.loader.minion_mods(self.opts, context=ctx, utils=utils, whitelist=['boto_iot'])
  130. self.salt_states = salt.loader.states(opts=self.opts, functions=funcs, utils=utils, whitelist=['boto_iot'],
  131. serializers=serializers)
  132. return {
  133. boto_iot: {
  134. '__opts__': self.opts,
  135. '__salt__': funcs,
  136. '__utils__': utils,
  137. '__states__': self.salt_states,
  138. '__serializers__': serializers,
  139. }
  140. }
  141. @classmethod
  142. def setUpClass(cls):
  143. cls.opts = salt.config.DEFAULT_MINION_OPTS.copy()
  144. cls.opts['grains'] = salt.loader.grains(cls.opts)
  145. @classmethod
  146. def tearDownClass(cls):
  147. del cls.opts
  148. def setUp(self):
  149. self.addCleanup(delattr, self, 'funcs')
  150. self.addCleanup(delattr, self, 'salt_states')
  151. # Set up MagicMock to replace the boto3 session
  152. # connections keep getting cached from prior tests, can't find the
  153. # correct context object to clear it. So randomize the cache key, to prevent any
  154. # cache hits
  155. conn_parameters['key'] = ''.join(random.choice(string.ascii_lowercase + string.digits) for _ in range(50))
  156. self.patcher = patch('boto3.session.Session')
  157. self.addCleanup(self.patcher.stop)
  158. self.addCleanup(delattr, self, 'patcher')
  159. mock_session = self.patcher.start()
  160. session_instance = mock_session.return_value
  161. self.conn = MagicMock()
  162. self.addCleanup(delattr, self, 'conn')
  163. session_instance.client.return_value = self.conn
  164. class BotoIoTThingTypeTestCase(BotoIoTStateTestCaseBase, BotoIoTTestCaseMixin):
  165. '''
  166. TestCase for salt.modules.boto_iot state.module
  167. '''
  168. def test_present_when_thing_type_does_not_exist(self):
  169. '''
  170. tests present on a thing type that does not exist.
  171. '''
  172. self.conn.describe_thing_type.side_effect = [not_found_error, thing_type_ret]
  173. self.conn.create_thing_type.return_value = create_thing_type_ret
  174. result = self.salt_states['boto_iot.thing_type_present'](
  175. 'thing type present',
  176. thingTypeName=thing_type_name,
  177. thingTypeDescription=thing_type_desc,
  178. searchableAttributesList=[thing_type_attr_1],
  179. **conn_parameters
  180. )
  181. self.assertTrue(result['result'])
  182. self.assertEqual(result['changes']['new']['thing_type']['thingTypeName'],
  183. thing_type_name)
  184. def test_present_when_thing_type_exists(self):
  185. self.conn.describe_thing_type.return_value = thing_type_ret
  186. result = self.salt_states['boto_iot.thing_type_present'](
  187. 'thing type present',
  188. thingTypeName=thing_type_name,
  189. thingTypeDescription=thing_type_desc,
  190. searchableAttributesList=[thing_type_attr_1],
  191. **conn_parameters
  192. )
  193. self.assertTrue(result['result'])
  194. self.assertEqual(result['changes'], {})
  195. self.assertTrue(self.conn.create_thing_type.call_count == 0)
  196. def test_present_with_failure(self):
  197. self.conn.describe_thing_type.side_effect = [not_found_error, thing_type_ret]
  198. self.conn.create_thing_type.side_effect = ClientError(error_content, 'create_thing_type')
  199. result = self.salt_states['boto_iot.thing_type_present'](
  200. 'thing type present',
  201. thingTypeName=thing_type_name,
  202. thingTypeDescription=thing_type_desc,
  203. searchableAttributesList=[thing_type_attr_1],
  204. **conn_parameters
  205. )
  206. self.assertFalse(result['result'])
  207. self.assertTrue('An error occurred' in result['comment'])
  208. def test_absent_when_thing_type_does_not_exist(self):
  209. '''
  210. Tests absent on a thing type does not exist
  211. '''
  212. self.conn.describe_thing_type.side_effect = not_found_error
  213. result = self.salt_states['boto_iot.thing_type_absent']('test', 'mythingtype', **conn_parameters)
  214. self.assertTrue(result['result'])
  215. self.assertEqual(result['changes'], {})
  216. def test_absent_when_thing_type_exists(self):
  217. '''
  218. Tests absent on a thing type
  219. '''
  220. self.conn.describe_thing_type.return_value = deprecated_thing_type_ret
  221. result = self.salt_states['boto_iot.thing_type_absent']('test', thing_type_name, **conn_parameters)
  222. self.assertTrue(result['result'])
  223. self.assertEqual(result['changes']['new']['thing_type'], None)
  224. self.assertTrue(self.conn.deprecate_thing_type.call_count == 0)
  225. def test_absent_with_deprecate_failure(self):
  226. self.conn.describe_thing_type.return_value = thing_type_ret
  227. self.conn.deprecate_thing_type.side_effect = ClientError(error_content, 'deprecate_thing_type')
  228. result = self.salt_states['boto_iot.thing_type_absent']('test', thing_type_name, **conn_parameters)
  229. self.assertFalse(result['result'])
  230. self.assertTrue('An error occurred' in result['comment'])
  231. self.assertTrue('deprecate_thing_type' in result['comment'])
  232. self.assertTrue(self.conn.delete_thing_type.call_count == 0)
  233. def test_absent_with_delete_failure(self):
  234. self.conn.describe_thing_type.return_value = deprecated_thing_type_ret
  235. self.conn.delete_thing_type.side_effect = ClientError(error_content, 'delete_thing_type')
  236. result = self.salt_states['boto_iot.thing_type_absent']('test', thing_type_name, **conn_parameters)
  237. self.assertFalse(result['result'])
  238. self.assertTrue('An error occurred' in result['comment'])
  239. self.assertTrue('delete_thing_type' in result['comment'])
  240. self.assertTrue(self.conn.deprecate_thing_type.call_count == 0)
  241. class BotoIoTPolicyTestCase(BotoIoTStateTestCaseBase, BotoIoTTestCaseMixin):
  242. '''
  243. TestCase for salt.modules.boto_iot state.module
  244. '''
  245. def test_present_when_policy_does_not_exist(self):
  246. '''
  247. Tests present on a policy that does not exist.
  248. '''
  249. self.conn.get_policy.side_effect = [not_found_error, policy_ret]
  250. self.conn.create_policy.return_value = policy_ret
  251. result = self.salt_states['boto_iot.policy_present'](
  252. 'policy present',
  253. policyName=policy_ret['policyName'],
  254. policyDocument=policy_ret['policyDocument'])
  255. self.assertTrue(result['result'])
  256. self.assertEqual(result['changes']['new']['policy']['policyName'],
  257. policy_ret['policyName'])
  258. def test_present_when_policy_exists(self):
  259. self.conn.get_policy.return_value = policy_ret
  260. self.conn.create_policy_version.return_value = policy_ret
  261. result = self.salt_states['boto_iot.policy_present'](
  262. 'policy present',
  263. policyName=policy_ret['policyName'],
  264. policyDocument=policy_ret['policyDocument'])
  265. self.assertTrue(result['result'])
  266. self.assertEqual(result['changes'], {})
  267. def test_present_with_failure(self):
  268. self.conn.get_policy.side_effect = [not_found_error, policy_ret]
  269. self.conn.create_policy.side_effect = ClientError(error_content, 'create_policy')
  270. result = self.salt_states['boto_iot.policy_present'](
  271. 'policy present',
  272. policyName=policy_ret['policyName'],
  273. policyDocument=policy_ret['policyDocument'])
  274. self.assertFalse(result['result'])
  275. self.assertTrue('An error occurred' in result['comment'])
  276. def test_absent_when_policy_does_not_exist(self):
  277. '''
  278. Tests absent on a policy that does not exist.
  279. '''
  280. self.conn.get_policy.side_effect = not_found_error
  281. result = self.salt_states['boto_iot.policy_absent']('test', 'mypolicy')
  282. self.assertTrue(result['result'])
  283. self.assertEqual(result['changes'], {})
  284. def test_absent_when_policy_exists(self):
  285. self.conn.get_policy.return_value = policy_ret
  286. self.conn.list_policy_versions.return_value = {'policyVersions': []}
  287. result = self.salt_states['boto_iot.policy_absent']('test', policy_ret['policyName'])
  288. self.assertTrue(result['result'])
  289. self.assertEqual(result['changes']['new']['policy'], None)
  290. def test_absent_with_failure(self):
  291. self.conn.get_policy.return_value = policy_ret
  292. self.conn.list_policy_versions.return_value = {'policyVersions': []}
  293. self.conn.delete_policy.side_effect = ClientError(error_content, 'delete_policy')
  294. result = self.salt_states['boto_iot.policy_absent']('test', policy_ret['policyName'])
  295. self.assertFalse(result['result'])
  296. self.assertTrue('An error occurred' in result['comment'])
  297. def test_attached_when_policy_not_attached(self):
  298. '''
  299. Tests attached on a policy that is not attached.
  300. '''
  301. self.conn.list_principal_policies.return_value = {'policies': []}
  302. result = self.salt_states['boto_iot.policy_attached']('test', 'myfunc', principal)
  303. self.assertTrue(result['result'])
  304. self.assertTrue(result['changes']['new']['attached'])
  305. def test_attached_when_policy_attached(self):
  306. '''
  307. Tests attached on a policy that is attached.
  308. '''
  309. self.conn.list_principal_policies.return_value = {'policies': [policy_ret]}
  310. result = self.salt_states['boto_iot.policy_attached']('test', policy_ret['policyName'], principal)
  311. self.assertTrue(result['result'])
  312. self.assertEqual(result['changes'], {})
  313. def test_attached_with_failure(self):
  314. '''
  315. Tests attached on a policy that is attached.
  316. '''
  317. self.conn.list_principal_policies.return_value = {'policies': []}
  318. self.conn.attach_principal_policy.side_effect = ClientError(error_content, 'attach_principal_policy')
  319. result = self.salt_states['boto_iot.policy_attached']('test', policy_ret['policyName'], principal)
  320. self.assertFalse(result['result'])
  321. self.assertEqual(result['changes'], {})
  322. def test_detached_when_policy_not_detached(self):
  323. '''
  324. Tests detached on a policy that is not detached.
  325. '''
  326. self.conn.list_principal_policies.return_value = {'policies': [policy_ret]}
  327. result = self.salt_states['boto_iot.policy_detached']('test', policy_ret['policyName'], principal)
  328. self.assertTrue(result['result'])
  329. log.warning(result)
  330. self.assertFalse(result['changes']['new']['attached'])
  331. def test_detached_when_policy_detached(self):
  332. '''
  333. Tests detached on a policy that is detached.
  334. '''
  335. self.conn.list_principal_policies.return_value = {'policies': []}
  336. result = self.salt_states['boto_iot.policy_detached']('test', policy_ret['policyName'], principal)
  337. self.assertTrue(result['result'])
  338. self.assertEqual(result['changes'], {})
  339. def test_detached_with_failure(self):
  340. '''
  341. Tests detached on a policy that is detached.
  342. '''
  343. self.conn.list_principal_policies.return_value = {'policies': [policy_ret]}
  344. self.conn.detach_principal_policy.side_effect = ClientError(error_content, 'detach_principal_policy')
  345. result = self.salt_states['boto_iot.policy_detached']('test', policy_ret['policyName'], principal)
  346. self.assertFalse(result['result'])
  347. self.assertEqual(result['changes'], {})
  348. @skipIf(HAS_BOTO is False, 'The boto module must be installed.')
  349. @skipIf(_has_required_boto() is False, 'The boto3 module must be greater than'
  350. ' or equal to version {0}. The botocore'
  351. ' module must be greater than or equal to'
  352. ' version {1}.'
  353. .format(required_boto3_version, required_botocore_version))
  354. class BotoIoTTopicRuleTestCase(BotoIoTStateTestCaseBase, BotoIoTTestCaseMixin):
  355. '''
  356. TestCase for salt.modules.boto_iot state.module rules
  357. '''
  358. def test_present_when_topic_rule_does_not_exist(self):
  359. '''
  360. Tests present on a topic_rule that does not exist.
  361. '''
  362. self.conn.get_topic_rule.side_effect = [topic_rule_not_found_error, {'rule': topic_rule_ret}]
  363. self.conn.create_topic_rule.return_value = {'created': True}
  364. result = self.salt_states['boto_iot.topic_rule_present'](
  365. 'topic rule present',
  366. ruleName=topic_rule_ret['ruleName'],
  367. sql=topic_rule_ret['sql'],
  368. description=topic_rule_ret['description'],
  369. actions=topic_rule_ret['actions'],
  370. ruleDisabled=topic_rule_ret['ruleDisabled'])
  371. self.assertTrue(result['result'])
  372. self.assertEqual(result['changes']['new']['rule']['ruleName'],
  373. topic_rule_ret['ruleName'])
  374. def test_present_when_policy_exists(self):
  375. self.conn.get_topic_rule.return_value = {'rule': topic_rule_ret}
  376. self.conn.create_topic_rule.return_value = {'created': True}
  377. result = self.salt_states['boto_iot.topic_rule_present'](
  378. 'topic rule present',
  379. ruleName=topic_rule_ret['ruleName'],
  380. sql=topic_rule_ret['sql'],
  381. description=topic_rule_ret['description'],
  382. actions=topic_rule_ret['actions'],
  383. ruleDisabled=topic_rule_ret['ruleDisabled'])
  384. self.assertTrue(result['result'])
  385. self.assertEqual(result['changes'], {})
  386. def test_present_with_failure(self):
  387. self.conn.get_topic_rule.side_effect = [topic_rule_not_found_error, {'rule': topic_rule_ret}]
  388. self.conn.create_topic_rule.side_effect = ClientError(error_content, 'create_topic_rule')
  389. result = self.salt_states['boto_iot.topic_rule_present'](
  390. 'topic rule present',
  391. ruleName=topic_rule_ret['ruleName'],
  392. sql=topic_rule_ret['sql'],
  393. description=topic_rule_ret['description'],
  394. actions=topic_rule_ret['actions'],
  395. ruleDisabled=topic_rule_ret['ruleDisabled'])
  396. self.assertFalse(result['result'])
  397. self.assertTrue('An error occurred' in result['comment'])
  398. def test_absent_when_topic_rule_does_not_exist(self):
  399. '''
  400. Tests absent on a topic rule that does not exist.
  401. '''
  402. self.conn.get_topic_rule.side_effect = topic_rule_not_found_error
  403. result = self.salt_states['boto_iot.topic_rule_absent']('test', 'myrule')
  404. self.assertTrue(result['result'])
  405. self.assertEqual(result['changes'], {})
  406. def test_absent_when_topic_rule_exists(self):
  407. self.conn.get_topic_rule.return_value = topic_rule_ret
  408. result = self.salt_states['boto_iot.topic_rule_absent']('test', topic_rule_ret['ruleName'])
  409. self.assertTrue(result['result'])
  410. self.assertEqual(result['changes']['new']['rule'], None)
  411. def test_absent_with_failure(self):
  412. self.conn.get_topic_rule.return_value = topic_rule_ret
  413. self.conn.delete_topic_rule.side_effect = ClientError(error_content, 'delete_topic_rule')
  414. result = self.salt_states['boto_iot.topic_rule_absent']('test', topic_rule_ret['ruleName'])
  415. self.assertFalse(result['result'])
  416. self.assertTrue('An error occurred' in result['comment'])