__init__.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321
  1. import functools
  2. import operator
  3. import os
  4. import sys
  5. import unittest
  6. import uuid
  7. import tenacity
  8. from cloudbridge.base import helpers as cb_helpers
  9. from cloudbridge.factory import CloudProviderFactory
  10. from cloudbridge.interfaces import CloudProvider
  11. from cloudbridge.interfaces import InstanceState
  12. from cloudbridge.interfaces import TestMockHelperMixin
  13. from cloudbridge.interfaces.resources import FloatingIpState
  14. from cloudbridge.interfaces.resources import NetworkState
  15. from cloudbridge.interfaces.resources import SubnetState
  16. def parse_bool(val):
  17. if val:
  18. return str(val).upper() in ['TRUE', 'YES']
  19. else:
  20. return False
  21. def skipIfNoService(services):
  22. """
  23. A decorator for skipping tests if the provider
  24. does not implement a given service.
  25. """
  26. def wrap(func):
  27. """
  28. The actual wrapper
  29. """
  30. @functools.wraps(func)
  31. def wrapper(self, *args, **kwargs):
  32. provider = getattr(self, 'provider')
  33. if provider:
  34. for service in services:
  35. if not provider.has_service(service):
  36. self.skipTest("Skipping test because '%s' service is"
  37. " not implemented" % (service,))
  38. func(self, *args, **kwargs)
  39. return wrapper
  40. return wrap
  41. def skipIfPython(op, major, minor):
  42. """
  43. A decorator for skipping tests if the python
  44. version doesn't match
  45. """
  46. def stringToOperator(op):
  47. op_map = {
  48. "=": operator.eq,
  49. "==": operator.eq,
  50. "<": operator.lt,
  51. "<=": operator.le,
  52. ">": operator.gt,
  53. ">=": operator.ge,
  54. }
  55. return op_map.get(op)
  56. def wrap(func):
  57. """
  58. The actual wrapper
  59. """
  60. @functools.wraps(func)
  61. def wrapper(self, *args, **kwargs):
  62. op_func = stringToOperator(op)
  63. if op_func(sys.version_info, (major, minor)):
  64. self.skipTest(
  65. "Skipping test because python version {0} is {1} expected"
  66. " version {2}".format(sys.version_info[:2],
  67. op, (major, minor)))
  68. func(self, *args, **kwargs)
  69. return wrapper
  70. return wrap
  71. def env_or(varname, default_value):
  72. """
  73. Environment variable ``varname``, treating unset and empty alike.
  74. The cloud workflow sets every CB_* variable in every matrix cell, as
  75. ``${{ matrix.cloud-provider == '<x>' && secrets.<VAR> || '' }}``, so a
  76. variable belonging to another cell - or one whose secret is simply not
  77. configured - arrives as an empty string rather than absent. A plain
  78. ``os.environ.get`` hands that empty string straight back, silently
  79. blanking the test data instead of falling back to the default.
  80. """
  81. return cb_helpers.get_env(varname) or default_value
  82. TEST_DATA_CONFIG = {
  83. "AWSCloudProvider": {
  84. # Ubuntu 24.04 LTS, us-east-1, amd64, gp3 (Canonical, 20260714). AMI
  85. # ids are per-region, so a run anywhere but us-east-1 has to set
  86. # CB_IMAGE_AWS - as does anyone wanting a different distribution.
  87. #
  88. # Being Nitro-era matters: the AWS suite's wall time is dominated by
  89. # launching an instance, snapshotting it into an AMI, launching a
  90. # second instance from that AMI and stop/start cycling, and all of
  91. # those are markedly slower on the Xen-era image this replaced
  92. # (Ubuntu 16.04, built 2017). Pair it with a Nitro instance type -
  93. # t3.micro or larger - via CB_VM_TYPE_AWS; on a t2.* the gain is
  94. # mostly lost, and 24.04 is a tight fit in t2.nano's 512 MB.
  95. #
  96. # moto does not validate instance-launch AMI ids, so the mock
  97. # provider is indifferent to this value.
  98. "image": env_or('CB_IMAGE_AWS', 'ami-052355af2a014bd2c'),
  99. "vm_type": env_or('CB_VM_TYPE_AWS', 't2.nano'),
  100. "placement": env_or('CB_PLACEMENT_AWS', 'us-east-1a'),
  101. "placement_cfg_key": "aws_zone_name"
  102. },
  103. 'OpenStackCloudProvider': {
  104. 'image': env_or('CB_IMAGE_OS',
  105. 'c66bdfa1-62b1-43be-8964-e9ce208ac6a5'),
  106. "vm_type": env_or('CB_VM_TYPE_OS', 'm1.tiny'),
  107. "placement": env_or('CB_PLACEMENT_OS', 'nova'),
  108. "placement_cfg_key": "os_zone_name"
  109. },
  110. 'GCPCloudProvider': {
  111. 'image': env_or(
  112. 'CB_IMAGE_GCP',
  113. 'https://www.googleapis.com/compute/v1/projects/ubuntu-os-cloud/'
  114. 'global/images/ubuntu-1804-bionic-v20200908'),
  115. 'vm_type': env_or('CB_VM_TYPE_GCP', 'f1-micro'),
  116. 'placement': env_or('GCP_ZONE_NAME', 'us-central1-a'),
  117. "placement_cfg_key": "gcp_zone_name"
  118. },
  119. "AzureCloudProvider": {
  120. "image":
  121. env_or('CB_IMAGE_AZURE',
  122. 'Canonical:0001-com-ubuntu-minimal-jammy:minimal-22_04-lts-gen2:latest'),
  123. "vm_type": env_or('CB_VM_TYPE_AZURE', 'Standard_DC1ds_v3'),
  124. "placement": env_or('CB_PLACEMENT_AZURE', 'eastus'),
  125. "placement_cfg_key": "azure_zone_name"
  126. }
  127. }
  128. def get_provider_test_data(provider, key):
  129. provider_id = (provider.PROVIDER_ID if isinstance(provider, CloudProvider)
  130. else provider)
  131. if "aws" == provider_id:
  132. return TEST_DATA_CONFIG.get("AWSCloudProvider").get(key)
  133. if "mock" == provider_id:
  134. return TEST_DATA_CONFIG.get("AWSCloudProvider").get(key)
  135. elif "openstack" == provider_id:
  136. return TEST_DATA_CONFIG.get("OpenStackCloudProvider").get(key)
  137. elif "gcp" == provider_id:
  138. return TEST_DATA_CONFIG.get("GCPCloudProvider").get(key)
  139. elif "azure" == provider_id:
  140. return TEST_DATA_CONFIG.get("AzureCloudProvider").get(key)
  141. return None
  142. @tenacity.retry(
  143. stop=tenacity.stop_after_attempt(10),
  144. wait=tenacity.wait_fixed(5),
  145. reraise=True,
  146. )
  147. def get_or_create_default_subnet(provider):
  148. """
  149. Return the default subnet to be used for tests.
  150. Multiple pytest-xdist workers can call this concurrently at cold-start
  151. and race on the singleton default vnet/subnet (on Azure ARM that shows
  152. up as AnotherOperationInProgress / cancelled-and-superseded / subnet
  153. IP-range overlap). Retry with a short backoff so the loser re-runs the
  154. find() and inherits whatever the winner just created.
  155. """
  156. return provider.networking.subnets.get_or_create_default()
  157. def cleanup_subnet(subnet):
  158. if subnet:
  159. subnet.delete()
  160. subnet.wait_for([SubnetState.UNKNOWN],
  161. terminal_states=[SubnetState.ERROR])
  162. def cleanup_network(network):
  163. """
  164. Delete the supplied network, first deleting any contained subnets.
  165. """
  166. if network:
  167. try:
  168. for sn in network.subnets:
  169. with cb_helpers.cleanup_action(lambda: cleanup_subnet(sn)):
  170. pass
  171. finally:
  172. network.delete()
  173. network.wait_for([NetworkState.UNKNOWN],
  174. terminal_states=[NetworkState.ERROR])
  175. def cleanup_fip(fip):
  176. if fip:
  177. fip.delete()
  178. fip.wait_for([FloatingIpState.UNKNOWN],
  179. terminal_states=[FloatingIpState.ERROR])
  180. def get_test_gateway(provider):
  181. """
  182. Get an internet gateway for testing.
  183. This includes creating a network for the gateway, which is also returned.
  184. """
  185. sn = get_or_create_default_subnet(provider)
  186. net = sn.network
  187. return net.gateways.get_or_create()
  188. def cleanup_gateway(gateway):
  189. """
  190. Delete the supplied network and gateway.
  191. """
  192. with cb_helpers.cleanup_action(lambda: gateway.delete()):
  193. pass
  194. def create_test_instance(
  195. provider, instance_label, subnet, launch_config=None,
  196. key_pair=None, vm_firewalls=None, user_data=None):
  197. instance = provider.compute.instances.create(
  198. instance_label, get_provider_test_data(provider, 'image'),
  199. get_provider_test_data(provider, 'vm_type'),
  200. subnet=subnet,
  201. key_pair=key_pair,
  202. vm_firewalls=vm_firewalls,
  203. launch_config=launch_config,
  204. user_data=user_data)
  205. return instance
  206. def get_test_instance(provider, label, key_pair=None, vm_firewalls=None,
  207. subnet=None, user_data=None):
  208. launch_config = None
  209. instance = create_test_instance(
  210. provider,
  211. label,
  212. subnet=subnet,
  213. key_pair=key_pair,
  214. vm_firewalls=vm_firewalls,
  215. launch_config=launch_config,
  216. user_data=user_data)
  217. instance.wait_till_ready()
  218. return instance
  219. def get_test_fixtures_folder():
  220. return os.path.join(os.path.dirname(__file__), '../fixtures/')
  221. def delete_instance(instance):
  222. if instance:
  223. instance.delete()
  224. instance.wait_for([InstanceState.DELETED, InstanceState.UNKNOWN],
  225. terminal_states=[InstanceState.ERROR])
  226. def cleanup_test_resources(instance=None, vm_firewall=None,
  227. key_pair=None, network=None):
  228. """Clean up any combination of supplied resources."""
  229. with cb_helpers.cleanup_action(
  230. lambda: cleanup_network(network) if network else None):
  231. with cb_helpers.cleanup_action(
  232. lambda: key_pair.delete() if key_pair else None):
  233. with cb_helpers.cleanup_action(
  234. lambda: vm_firewall.delete() if vm_firewall else None):
  235. delete_instance(instance)
  236. def get_uuid():
  237. return str(uuid.uuid4())[:6]
  238. class ProviderTestBase(unittest.TestCase):
  239. _provider = None
  240. def setUp(self):
  241. if isinstance(self.provider, TestMockHelperMixin):
  242. self.provider.setUpMock()
  243. def tearDown(self):
  244. if isinstance(self.provider, TestMockHelperMixin):
  245. self.provider.tearDownMock()
  246. self._provider = None
  247. def get_provider_wait_interval(self, provider_class):
  248. if issubclass(provider_class, TestMockHelperMixin):
  249. return 0
  250. else:
  251. return 1
  252. def create_provider_instance(self):
  253. provider_name = cb_helpers.get_env("CB_TEST_PROVIDER", "aws")
  254. zone_cfg_key = get_provider_test_data(provider_name,
  255. 'placement_cfg_key')
  256. factory = CloudProviderFactory()
  257. provider_class = factory.get_provider_class(provider_name)
  258. config = {
  259. 'default_wait_interval': self.get_provider_wait_interval(
  260. provider_class),
  261. 'default_result_limit': 5,
  262. zone_cfg_key: get_provider_test_data(provider_name, 'placement')
  263. }
  264. return provider_class(config)
  265. @property
  266. def provider(self):
  267. if not self._provider:
  268. self._provider = self.create_provider_instance()
  269. return self._provider