cloud_test_base.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282
  1. # -*- coding: utf-8 -*-
  2. '''
  3. Tests for the Openstack Cloud Provider
  4. '''
  5. # Import python libs
  6. from __future__ import absolute_import, print_function, unicode_literals
  7. from time import sleep
  8. import logging
  9. import os
  10. import shutil
  11. # Import Salt Testing libs
  12. from tests.support.case import ShellCase
  13. from tests.support.helpers import generate_random_name, expensiveTest
  14. from tests.support.paths import FILES
  15. from tests.support.runtests import RUNTIME_VARS
  16. # Import Salt Libs
  17. from salt.config import cloud_config, cloud_providers_config
  18. from salt.ext.six.moves import range
  19. from salt.utils.yaml import safe_load
  20. TIMEOUT = 500
  21. log = logging.getLogger(__name__)
  22. @expensiveTest
  23. class CloudTest(ShellCase):
  24. PROVIDER = ''
  25. REQUIRED_PROVIDER_CONFIG_ITEMS = tuple()
  26. TMP_PROVIDER_DIR = os.path.join(RUNTIME_VARS.TMP_CONF_DIR, 'cloud.providers.d')
  27. __RE_RUN_DELAY = 30
  28. __RE_TRIES = 12
  29. @staticmethod
  30. def clean_cloud_dir(tmp_dir):
  31. '''
  32. Clean the cloud.providers.d tmp directory
  33. '''
  34. # make sure old provider configs are deleted
  35. for i in os.listdir(tmp_dir):
  36. os.remove(os.path.join(tmp_dir, i))
  37. def query_instances(self):
  38. '''
  39. Standardize the data returned from a salt-cloud --query
  40. '''
  41. return set(x.strip(': ') for x in self.run_cloud('--query') if x.lstrip().lower().startswith('cloud-test-'))
  42. def _instance_exists(self, instance_name=None, query=None):
  43. '''
  44. :param instance_name: The name of the instance to check for in salt-cloud.
  45. For example this is may used when a test temporarily renames an instance
  46. :param query: The result of a salt-cloud --query run outside of this function
  47. '''
  48. if not instance_name:
  49. instance_name = self.instance_name
  50. if not query:
  51. query = self.query_instances()
  52. log.debug('Checking for "{}" in {}'.format(instance_name, query))
  53. if isinstance(query, set):
  54. return instance_name in query
  55. return any(instance_name == q.strip(': ') for q in query)
  56. def assertInstanceExists(self, creation_ret=None, instance_name=None):
  57. '''
  58. :param instance_name: Override the checked instance name, otherwise the class default will be used.
  59. :param creation_ret: The return value from the run_cloud() function that created the instance
  60. '''
  61. if not instance_name:
  62. instance_name = self.instance_name
  63. # If it exists but doesn't show up in the creation_ret, there was probably an error during creation
  64. if creation_ret:
  65. self.assertIn(instance_name, [i.strip(': ') for i in creation_ret],
  66. 'An error occured during instance creation: |\n\t{}\n\t|'.format(
  67. '\n\t'.join(creation_ret)
  68. ))
  69. else:
  70. # Verify that the instance exists via query
  71. query = self.query_instances()
  72. for tries in range(self.__RE_TRIES):
  73. if self._instance_exists(instance_name, query):
  74. log.debug(
  75. 'Instance "{}" reported after {} seconds'.format(instance_name, tries * self.__RE_RUN_DELAY))
  76. break
  77. else:
  78. sleep(self.__RE_RUN_DELAY)
  79. query = self.query_instances()
  80. # Assert that the last query was successful
  81. self.assertTrue(self._instance_exists(instance_name, query),
  82. 'Instance "{}" was not created successfully: {}'.format(self.instance_name,
  83. ', '.join(query)))
  84. log.debug('Instance exists and was created: "{}"'.format(instance_name))
  85. def assertDestroyInstance(self, instance_name=None, timeout=None):
  86. if timeout is None:
  87. timeout = TIMEOUT
  88. if not instance_name:
  89. instance_name = self.instance_name
  90. log.debug('Deleting instance "{}"'.format(instance_name))
  91. delete_str = self.run_cloud('-d {0} --assume-yes --out=yaml'.format(instance_name), timeout=timeout)
  92. if delete_str:
  93. delete = safe_load('\n'.join(delete_str))
  94. self.assertIn(self.profile_str, delete)
  95. self.assertIn(self.PROVIDER, delete[self.profile_str])
  96. self.assertIn(instance_name, delete[self.profile_str][self.PROVIDER])
  97. delete_status = delete[self.profile_str][self.PROVIDER][instance_name]
  98. if isinstance(delete_status, str):
  99. self.assertEqual(delete_status, 'True')
  100. return
  101. elif isinstance(delete_status, dict):
  102. current_state = delete_status.get('currentState')
  103. if current_state:
  104. if current_state.get('ACTION'):
  105. self.assertIn('.delete', current_state.get('ACTION'))
  106. return
  107. else:
  108. self.assertEqual(current_state.get('name'), 'shutting-down')
  109. return
  110. # It's not clear from the delete string that deletion was successful, ask salt-cloud after a delay
  111. query = self.query_instances()
  112. # some instances take a while to report their destruction
  113. for tries in range(6):
  114. if self._instance_exists(query=query):
  115. sleep(30)
  116. log.debug('Instance "{}" still found in query after {} tries: {}'
  117. .format(instance_name, tries, query))
  118. query = self.query_instances()
  119. # The last query should have been successful
  120. self.assertNotIn(instance_name, self.query_instances())
  121. @property
  122. def instance_name(self):
  123. if not hasattr(self, '_instance_name'):
  124. # Create the cloud instance name to be used throughout the tests
  125. subclass = self.__class__.__name__.strip('Test')
  126. # Use the first three letters of the subclass, fill with '-' if too short
  127. self._instance_name = generate_random_name('cloud-test-{:-<3}-'.format(subclass[:3])).lower()
  128. return self._instance_name
  129. @property
  130. def providers(self):
  131. if not hasattr(self, '_providers'):
  132. self._providers = self.run_cloud('--list-providers')
  133. return self._providers
  134. @property
  135. def provider_config(self):
  136. if not hasattr(self, '_provider_config'):
  137. self._provider_config = cloud_providers_config(
  138. os.path.join(
  139. self.config_dir,
  140. 'cloud.providers.d',
  141. self.PROVIDER + '.conf'
  142. )
  143. )
  144. return self._provider_config[self.profile_str][self.PROVIDER]
  145. @property
  146. def config(self):
  147. if not hasattr(self, '_config'):
  148. self._config = cloud_config(
  149. os.path.join(
  150. self.config_dir,
  151. 'cloud.profiles.d',
  152. self.PROVIDER + '.conf'
  153. )
  154. )
  155. return self._config
  156. @property
  157. def profile_str(self):
  158. return self.PROVIDER + '-config'
  159. def setUp(self):
  160. '''
  161. Sets up the test requirements. In child classes, define PROVIDER and REQUIRED_CONFIG_ITEMS or this will fail
  162. '''
  163. super(CloudTest, self).setUp()
  164. if not self.PROVIDER:
  165. self.fail('A PROVIDER must be defined for this test')
  166. # check if appropriate cloud provider and profile files are present
  167. if self.profile_str + ':' not in self.providers:
  168. self.skipTest(
  169. 'Configuration file for {0} was not found. Check {0}.conf files '
  170. 'in tests/integration/files/conf/cloud.*.d/ to run these tests.'
  171. .format(self.PROVIDER)
  172. )
  173. missing_conf_item = []
  174. for att in self.REQUIRED_PROVIDER_CONFIG_ITEMS:
  175. if not self.provider_config.get(att):
  176. missing_conf_item.append(att)
  177. if missing_conf_item:
  178. self.skipTest('Conf items are missing that must be provided to run these tests: {}'
  179. .format(', '.join(missing_conf_item)) +
  180. '\nCheck tests/integration/files/conf/cloud.providers.d/{0}.conf'.format(self.PROVIDER))
  181. def _alt_names(self):
  182. '''
  183. Check for an instances created alongside this test's instance that weren't cleaned up
  184. '''
  185. query = self.query_instances()
  186. instances = set()
  187. for q in query:
  188. # Verify but this is a new name and not a shutting down ec2 instance
  189. if q.startswith(self.instance_name) and not q.split('-')[-1].startswith('DEL'):
  190. instances.add(q)
  191. log.debug('Adding "{}" to the set of instances that needs to be deleted'.format(q))
  192. return instances
  193. def _ensure_deletion(self, instance_name=None):
  194. '''
  195. Make sure that the instance absolutely gets deleted, but fail the test if it happens in the tearDown
  196. :return True if an instance was deleted, False if no instance was deleted; and a message
  197. '''
  198. destroyed = False
  199. if not instance_name:
  200. instance_name = self.instance_name
  201. if self._instance_exists(instance_name):
  202. for tries in range(3):
  203. try:
  204. self.assertDestroyInstance(instance_name)
  205. return False, 'The instance "{}" was deleted during the tearDown, not the test.'.format(
  206. instance_name)
  207. except AssertionError as e:
  208. log.error('Failed to delete instance "{}". Tries: {}\n{}'.format(instance_name, tries, str(e)))
  209. if not self._instance_exists():
  210. destroyed = True
  211. break
  212. else:
  213. sleep(30)
  214. if not destroyed:
  215. # Destroying instances in the tearDown is a contingency, not the way things should work by default.
  216. return False, 'The Instance "{}" was not deleted after multiple attempts'.format(instance_name)
  217. return True, 'The instance "{}" cleaned up properly after the test'.format(instance_name)
  218. def tearDown(self):
  219. '''
  220. Clean up after tests, If the instance still exists for any reason, delete it.
  221. Instances should be destroyed before the tearDown, assertDestroyInstance() should be called exactly
  222. one time in a test for each instance created. This is a failSafe and something went wrong
  223. if the tearDown is where an instance is destroyed.
  224. '''
  225. success = True
  226. fail_messages = []
  227. alt_names = self._alt_names()
  228. for instance in alt_names:
  229. alt_destroyed, alt_destroy_message = self._ensure_deletion(instance)
  230. if not alt_destroyed:
  231. success = False
  232. fail_messages.append(alt_destroy_message)
  233. log.error('Failed to destroy instance "{}": {}'.format(instance, alt_destroy_message))
  234. self.assertTrue(success, '\n'.join(fail_messages))
  235. self.assertFalse(alt_names, 'Cleanup should happen in the test, not the TearDown')
  236. @classmethod
  237. def tearDownClass(cls):
  238. cls.clean_cloud_dir(cls.TMP_PROVIDER_DIR)
  239. @classmethod
  240. def setUpClass(cls):
  241. # clean up before setup
  242. cls.clean_cloud_dir(cls.TMP_PROVIDER_DIR)
  243. # add the provider config for only the cloud we are testing
  244. provider_file = cls.PROVIDER + '.conf'
  245. shutil.copyfile(os.path.join(os.path.join(FILES, 'conf', 'cloud.providers.d'), provider_file),
  246. os.path.join(os.path.join(cls.TMP_PROVIDER_DIR, provider_file)))