cloud_test_base.py 13 KB

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