1
0

cloud_test_base.py 12 KB

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