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. class CloudTest(ShellCase):
  23. PROVIDER = ''
  24. REQUIRED_PROVIDER_CONFIG_ITEMS = tuple()
  25. TMP_PROVIDER_DIR = os.path.join(RUNTIME_VARS.TMP_CONF_DIR, 'cloud.providers.d')
  26. __RE_RUN_DELAY = 30
  27. __RE_TRIES = 12
  28. @staticmethod
  29. def clean_cloud_dir(tmp_dir):
  30. '''
  31. Clean the cloud.providers.d tmp directory
  32. '''
  33. # make sure old provider configs are deleted
  34. for i in os.listdir(tmp_dir):
  35. os.remove(os.path.join(tmp_dir, i))
  36. def query_instances(self):
  37. '''
  38. Standardize the data returned from a salt-cloud --query
  39. '''
  40. return set(x.strip(': ') for x in self.run_cloud('--query') if x.lstrip().lower().startswith('cloud-test-'))
  41. def _instance_exists(self, instance_name=None, query=None):
  42. '''
  43. :param instance_name: The name of the instance to check for in salt-cloud.
  44. For example this is may used when a test temporarily renames an instance
  45. :param query: The result of a salt-cloud --query run outside of this function
  46. '''
  47. if not instance_name:
  48. instance_name = self.instance_name
  49. if not query:
  50. query = self.query_instances()
  51. log.debug('Checking for "{}" in {}'.format(instance_name, query))
  52. if isinstance(query, set):
  53. return instance_name in query
  54. return any(instance_name == q.strip(': ') for q in query)
  55. def assertInstanceExists(self, creation_ret=None, instance_name=None):
  56. '''
  57. :param instance_name: Override the checked instance name, otherwise the class default will be used.
  58. :param creation_ret: The return value from the run_cloud() function that created the instance
  59. '''
  60. if not instance_name:
  61. instance_name = self.instance_name
  62. # If it exists but doesn't show up in the creation_ret, there was probably an error during creation
  63. if creation_ret:
  64. self.assertIn(instance_name, [i.strip(': ') for i in creation_ret],
  65. 'An error occured during instance creation: |\n\t{}\n\t|'.format(
  66. '\n\t'.join(creation_ret)
  67. ))
  68. else:
  69. # Verify that the instance exists via query
  70. query = self.query_instances()
  71. for tries in range(self.__RE_TRIES):
  72. if self._instance_exists(instance_name, query):
  73. log.debug(
  74. 'Instance "{}" reported after {} seconds'.format(instance_name, tries * self.__RE_RUN_DELAY))
  75. break
  76. else:
  77. sleep(self.__RE_RUN_DELAY)
  78. query = self.query_instances()
  79. # Assert that the last query was successful
  80. self.assertTrue(self._instance_exists(instance_name, query),
  81. 'Instance "{}" was not created successfully: {}'.format(self.instance_name,
  82. ', '.join(query)))
  83. log.debug('Instance exists and was created: "{}"'.format(instance_name))
  84. def assertDestroyInstance(self, instance_name=None, timeout=None):
  85. if timeout is None:
  86. timeout = TIMEOUT
  87. if not instance_name:
  88. instance_name = self.instance_name
  89. log.debug('Deleting instance "{}"'.format(instance_name))
  90. delete_str = self.run_cloud('-d {0} --assume-yes --out=yaml'.format(instance_name), timeout=timeout)
  91. if delete_str:
  92. delete = safe_load('\n'.join(delete_str))
  93. self.assertIn(self.profile_str, delete)
  94. self.assertIn(self.PROVIDER, delete[self.profile_str])
  95. self.assertIn(instance_name, delete[self.profile_str][self.PROVIDER])
  96. delete_status = delete[self.profile_str][self.PROVIDER][instance_name]
  97. if isinstance(delete_status, str):
  98. self.assertEqual(delete_status, 'True')
  99. return
  100. elif isinstance(delete_status, dict):
  101. current_state = delete_status.get('currentState')
  102. if current_state:
  103. if current_state.get('ACTION'):
  104. self.assertIn('.delete', current_state.get('ACTION'))
  105. return
  106. else:
  107. self.assertEqual(current_state.get('name'), 'shutting-down')
  108. return
  109. # It's not clear from the delete string that deletion was successful, ask salt-cloud after a delay
  110. query = self.query_instances()
  111. # some instances take a while to report their destruction
  112. for tries in range(6):
  113. if self._instance_exists(query=query):
  114. sleep(30)
  115. log.debug('Instance "{}" still found in query after {} tries: {}'
  116. .format(instance_name, tries, query))
  117. query = self.query_instances()
  118. # The last query should have been successful
  119. self.assertNotIn(instance_name, self.query_instances())
  120. @property
  121. def instance_name(self):
  122. if not hasattr(self, '_instance_name'):
  123. # Create the cloud instance name to be used throughout the tests
  124. subclass = self.__class__.__name__.strip('Test')
  125. # Use the first three letters of the subclass, fill with '-' if too short
  126. self._instance_name = generate_random_name('cloud-test-{:-<3}-'.format(subclass[:3])).lower()
  127. return self._instance_name
  128. @property
  129. def providers(self):
  130. if not hasattr(self, '_providers'):
  131. self._providers = self.run_cloud('--list-providers')
  132. return self._providers
  133. @property
  134. def provider_config(self):
  135. if not hasattr(self, '_provider_config'):
  136. self._provider_config = cloud_providers_config(
  137. os.path.join(
  138. self.config_dir,
  139. 'cloud.providers.d',
  140. self.PROVIDER + '.conf'
  141. )
  142. )
  143. return self._provider_config[self.profile_str][self.PROVIDER]
  144. @property
  145. def config(self):
  146. if not hasattr(self, '_config'):
  147. self._config = cloud_config(
  148. os.path.join(
  149. self.config_dir,
  150. 'cloud.profiles.d',
  151. self.PROVIDER + '.conf'
  152. )
  153. )
  154. return self._config
  155. @property
  156. def profile_str(self):
  157. return self.PROVIDER + '-config'
  158. @expensiveTest
  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)))