test_compute_service.py 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469
  1. import datetime
  2. import ipaddress
  3. import six
  4. from cloudbridge.base import helpers as cb_helpers
  5. from cloudbridge.base.resources import BaseNetwork
  6. from cloudbridge.factory import ProviderList
  7. from cloudbridge.interfaces import InstanceState
  8. from cloudbridge.interfaces import InvalidConfigurationException
  9. from cloudbridge.interfaces.exceptions import WaitStateException
  10. from cloudbridge.interfaces.resources import Instance
  11. from cloudbridge.interfaces.resources import SnapshotState
  12. from cloudbridge.interfaces.resources import VMType
  13. from tests import helpers
  14. from tests.helpers import ProviderTestBase
  15. from tests.helpers import standard_interface_tests as sit
  16. class CloudComputeServiceTestCase(ProviderTestBase):
  17. _multiprocess_can_split_ = True
  18. @helpers.skipIfNoService(['compute.instances'])
  19. def test_storage_services_event_pattern(self):
  20. # pylint:disable=protected-access
  21. self.assertEqual(
  22. self.provider.compute.instances._service_event_pattern,
  23. "provider.compute.instances",
  24. "Event pattern for {} service should be '{}', "
  25. "but found '{}'.".format("instances",
  26. "provider.compute.instances",
  27. self.provider.compute.instances.
  28. _service_event_pattern))
  29. # moto 5.x regression: DescribeInstances returns an empty list
  30. # immediately after RunInstances completes, so the list-after-create
  31. # check in standard_interface_tests.check_list fails. A secondary
  32. # symptom shows in cleanup, where post-delete state remains
  33. # "deleted" instead of becoming UNKNOWN. Last observed on moto
  34. # 5.2.1. Tighten the specifier when an upstream fix lands.
  35. @helpers.skipIfMockMotoVersion(
  36. ">=5.0.0",
  37. "moto 5.x RunInstances/DescribeInstances state-sync bug")
  38. @helpers.skipIfNoService(['compute.instances', 'networking.networks'])
  39. def test_crud_instance(self):
  40. label = "cb-instcrud-{0}".format(helpers.get_uuid())
  41. # Declare these variables and late binding will allow
  42. # the cleanup method access to the most current values
  43. subnet = None
  44. def create_inst(label):
  45. # Also test whether sending in an empty_dict for user_data
  46. # results in an automatic conversion to string.
  47. return helpers.get_test_instance(self.provider, label,
  48. subnet=subnet, user_data={})
  49. def cleanup_inst(inst):
  50. if inst:
  51. inst.delete()
  52. inst.wait_for([InstanceState.DELETED, InstanceState.UNKNOWN])
  53. inst.refresh()
  54. self.assertTrue(
  55. inst.state == InstanceState.UNKNOWN,
  56. "Instance.state must be unknown when refreshing after a "
  57. "delete but got %s"
  58. % inst.state)
  59. def check_deleted(inst):
  60. deleted_inst = self.provider.compute.instances.get(
  61. inst.id)
  62. self.assertTrue(
  63. deleted_inst is None or deleted_inst.state in (
  64. InstanceState.DELETED,
  65. InstanceState.UNKNOWN),
  66. "Instance %s should have been deleted but still exists." %
  67. label)
  68. subnet = helpers.get_or_create_default_subnet(self.provider)
  69. sit.check_crud(self, self.provider.compute.instances, Instance,
  70. "cb-instcrud", create_inst, cleanup_inst,
  71. custom_check_delete=check_deleted)
  72. def _is_valid_ip(self, address):
  73. try:
  74. ipaddress.ip_address(address)
  75. except ValueError:
  76. return False
  77. return True
  78. @helpers.skipIfNoService(['compute.instances', 'networking.networks',
  79. 'security.vm_firewalls',
  80. 'security.key_pairs'])
  81. def test_instance_properties(self):
  82. label = "cb-inst-props-{0}".format(helpers.get_uuid())
  83. # Declare these variables and late binding will allow
  84. # the cleanup method access to the most current values
  85. test_instance = None
  86. fw = None
  87. kp = None
  88. with cb_helpers.cleanup_action(lambda: helpers.cleanup_test_resources(
  89. test_instance, fw, kp)):
  90. subnet = helpers.get_or_create_default_subnet(self.provider)
  91. net = subnet.network
  92. kp = self.provider.security.key_pairs.create(name=label)
  93. fw = self.provider.security.vm_firewalls.create(
  94. label=label, description=label, network=net.id)
  95. test_instance = helpers.get_test_instance(self.provider,
  96. label, key_pair=kp,
  97. vm_firewalls=[fw],
  98. subnet=subnet)
  99. self.assertEqual(
  100. test_instance.label, label,
  101. "Instance label {0} is not equal to the expected label"
  102. " {1}".format(test_instance.label, label))
  103. image_id = helpers.get_provider_test_data(self.provider, "image")
  104. self.assertEqual(test_instance.image_id, image_id,
  105. "Image id {0} is not equal to the expected id"
  106. " {1}".format(test_instance.image_id, image_id))
  107. self.assertIsInstance(test_instance.zone_id,
  108. six.string_types)
  109. self.assertEqual(
  110. test_instance.image_id,
  111. helpers.get_provider_test_data(self.provider, "image"))
  112. self.assertIsInstance(test_instance.public_ips, list)
  113. if test_instance.public_ips:
  114. self.assertTrue(
  115. test_instance.public_ips[0], "public ip should contain a"
  116. " valid value if a list of public_ips exist")
  117. self.assertIsInstance(test_instance.private_ips, list)
  118. self.assertTrue(test_instance.private_ips[0],
  119. "private ip should contain a valid value")
  120. self.assertEqual(
  121. test_instance.key_pair_id,
  122. kp.id)
  123. self.assertIsInstance(test_instance.vm_firewalls, list)
  124. self.assertEqual(
  125. test_instance.vm_firewalls[0],
  126. fw)
  127. self.assertIsInstance(test_instance.vm_firewall_ids, list)
  128. self.assertEqual(
  129. test_instance.vm_firewall_ids[0],
  130. fw.id)
  131. # Must have either a public or a private ip
  132. ip_private = test_instance.private_ips[0] \
  133. if test_instance.private_ips else None
  134. ip_address = test_instance.public_ips[0] \
  135. if test_instance.public_ips and test_instance.public_ips[0] \
  136. else ip_private
  137. # Convert to unicode for py27 compatibility with ipaddress()
  138. ip_address = u"{}".format(ip_address)
  139. self.assertIsNotNone(
  140. ip_address,
  141. "Instance must have either a public IP or a private IP")
  142. self.assertTrue(
  143. self._is_valid_ip(ip_address),
  144. "Instance must have a valid IP address. Got: %s" % ip_address)
  145. self.assertIsInstance(test_instance.vm_type_id,
  146. six.string_types)
  147. vm_type = self.provider.compute.vm_types.get(
  148. test_instance.vm_type_id)
  149. self.assertEqual(
  150. vm_type, test_instance.vm_type,
  151. "VM type {0} does not match expected type {1}".format(
  152. vm_type.name, test_instance.vm_type))
  153. self.assertIsInstance(vm_type, VMType)
  154. expected_type = helpers.get_provider_test_data(self.provider,
  155. 'vm_type')
  156. self.assertEqual(
  157. vm_type.name, expected_type,
  158. "VM type {0} does not match expected type {1}".format(
  159. vm_type.name, expected_type))
  160. find_zone = [zone for zone in
  161. self.provider.compute.regions.current.zones
  162. if zone.id == test_instance.zone_id]
  163. self.assertEqual(len(find_zone), 1,
  164. "Instance's placement zone could not be "
  165. " found in zones list")
  166. self.assertIsNotNone(
  167. test_instance.create_time,
  168. "Instance must have its creation time")
  169. self.assertIsInstance(
  170. test_instance.create_time,
  171. datetime.datetime)
  172. @helpers.skipIfNoService(['compute.instances', 'compute.images',
  173. 'compute.vm_types'])
  174. def test_block_device_mapping_launch_config(self):
  175. lc = self.provider.compute.instances.create_launch_config()
  176. # specifying an invalid size should raise
  177. # an exception
  178. with self.assertRaises(InvalidConfigurationException):
  179. lc.add_volume_device(size=-1)
  180. # Attempting to add a blank volume without specifying a size
  181. # should raise an exception
  182. with self.assertRaises(InvalidConfigurationException):
  183. lc.add_volume_device(source=None)
  184. # block_devices should be empty so far
  185. self.assertListEqual(
  186. lc.block_devices, [], "No block devices should have been"
  187. " added to mappings list since the configuration was invalid")
  188. # Add a new volume
  189. lc.add_volume_device(size=1, delete_on_terminate=True)
  190. # Override root volume size
  191. image_id = helpers.get_provider_test_data(self.provider, "image")
  192. img = self.provider.compute.images.get(image_id)
  193. # The size should be greater then the ami size
  194. # and therefore, img.min_disk is used.
  195. lc.add_volume_device(
  196. is_root=True,
  197. source=img,
  198. size=img.min_disk if img and img.min_disk else 30,
  199. delete_on_terminate=True)
  200. # Attempting to add more than one root volume should raise an
  201. # exception.
  202. with self.assertRaises(InvalidConfigurationException):
  203. lc.add_volume_device(size=1, is_root=True)
  204. # Attempting to add an incorrect source should raise an exception
  205. with self.assertRaises(InvalidConfigurationException):
  206. lc.add_volume_device(
  207. source="invalid_source",
  208. delete_on_terminate=True)
  209. # Add all available ephemeral devices
  210. vm_type_name = helpers.get_provider_test_data(
  211. self.provider,
  212. "vm_type")
  213. vm_type = self.provider.compute.vm_types.find(
  214. name=vm_type_name)[0]
  215. for _ in range(vm_type.num_ephemeral_disks):
  216. lc.add_ephemeral_device()
  217. # block_devices should be populated
  218. self.assertTrue(
  219. len(lc.block_devices) == 2 + vm_type.num_ephemeral_disks,
  220. "Expected %d total block devices bit found %d" %
  221. (2 + vm_type.num_ephemeral_disks, len(lc.block_devices)))
  222. @helpers.skipIfNoService(['compute.instances', 'compute.images',
  223. 'compute.vm_types', 'storage.volumes'])
  224. def test_block_device_mapping_attachments(self):
  225. label = "cb-blkattch-{0}".format(helpers.get_uuid())
  226. if self.provider.PROVIDER_ID == ProviderList.OPENSTACK:
  227. raise self.skipTest("Not running BDM tests because OpenStack is"
  228. " not stable enough yet")
  229. test_vol = self.provider.storage.volumes.create(
  230. label, 1)
  231. with cb_helpers.cleanup_action(lambda: test_vol.delete()):
  232. test_vol.wait_till_ready()
  233. test_snap = test_vol.create_snapshot(label=label,
  234. description=label)
  235. def cleanup_snap(snap):
  236. if snap:
  237. snap.delete()
  238. snap.wait_for([SnapshotState.UNKNOWN],
  239. terminal_states=[SnapshotState.ERROR])
  240. with cb_helpers.cleanup_action(lambda: cleanup_snap(test_snap)):
  241. test_snap.wait_till_ready()
  242. lc = self.provider.compute.instances.create_launch_config()
  243. # Add a new blank volume
  244. lc.add_volume_device(size=1, delete_on_terminate=True)
  245. # Attach an existing volume
  246. lc.add_volume_device(size=1, source=test_vol,
  247. delete_on_terminate=True)
  248. # Add a new volume based on a snapshot
  249. lc.add_volume_device(size=1, source=test_snap,
  250. delete_on_terminate=True)
  251. # Override root volume size
  252. image_id = helpers.get_provider_test_data(
  253. self.provider,
  254. "image")
  255. img = self.provider.compute.images.get(image_id)
  256. # The size should be greater then the ami size
  257. # and therefore, img.min_disk is used.
  258. lc.add_volume_device(
  259. is_root=True,
  260. source=img,
  261. size=img.min_disk if img and img.min_disk else 30,
  262. delete_on_terminate=True)
  263. # Add all available ephemeral devices
  264. vm_type_name = helpers.get_provider_test_data(
  265. self.provider,
  266. "vm_type")
  267. vm_type = self.provider.compute.vm_types.find(
  268. name=vm_type_name)[0]
  269. # Some providers, e.g. GCP, has a limit on total number of
  270. # attached disks; it does not matter how many of them are
  271. # ephemeral or persistent. So, wee keep in mind that we have
  272. # attached 4 disks already, and add ephemeral disks accordingly
  273. # to not exceed the limit.
  274. for _ in range(vm_type.num_ephemeral_disks - 4):
  275. lc.add_ephemeral_device()
  276. subnet = helpers.get_or_create_default_subnet(
  277. self.provider)
  278. inst = None
  279. with cb_helpers.cleanup_action(
  280. lambda: helpers.delete_instance(inst)):
  281. inst = helpers.create_test_instance(
  282. self.provider,
  283. label,
  284. subnet=subnet,
  285. launch_config=lc)
  286. try:
  287. inst.wait_till_ready()
  288. except WaitStateException as e:
  289. self.fail("The block device mapped launch did not "
  290. " complete successfully: %s" % e)
  291. # TODO: Check instance attachments and make sure they
  292. # correspond to requested mappings
  293. @helpers.skipIfNoService(['compute.instances', 'networking.networks',
  294. 'security.vm_firewalls'])
  295. def test_instance_methods(self):
  296. label = "cb-instmethods-{0}".format(helpers.get_uuid())
  297. # Declare these variables and late binding will allow
  298. # the cleanup method access to the most current values
  299. net = None
  300. test_inst = None
  301. fw = None
  302. with cb_helpers.cleanup_action(lambda: helpers.cleanup_test_resources(
  303. instance=test_inst, vm_firewall=fw, network=net)):
  304. net = self.provider.networking.networks.create(
  305. label=label, cidr_block=BaseNetwork.CB_DEFAULT_IPV4RANGE)
  306. cidr = '10.0.1.0/24'
  307. subnet = net.subnets.create(label=label, cidr_block=cidr)
  308. test_inst = helpers.get_test_instance(self.provider, label,
  309. subnet=subnet)
  310. fw = self.provider.security.vm_firewalls.create(
  311. label=label, description=label, network=net.id)
  312. # Check adding a VM firewall to a running instance
  313. test_inst.add_vm_firewall(fw)
  314. test_inst.refresh()
  315. self.assertTrue(
  316. fw in test_inst.vm_firewalls, "Expected VM firewall '%s'"
  317. " to be among instance vm_firewalls: [%s]" %
  318. (fw, test_inst.vm_firewalls))
  319. # Check removing a VM firewall from a running instance
  320. test_inst.remove_vm_firewall(fw)
  321. test_inst.refresh()
  322. self.assertTrue(
  323. fw not in test_inst.vm_firewalls, "Expected VM firewall"
  324. " '%s' to be removed from instance vm_firewalls: [%s]" %
  325. (fw, test_inst.vm_firewalls))
  326. # check floating ips
  327. router = self.provider.networking.routers.create(label, net)
  328. gateway = net.gateways.get_or_create()
  329. def cleanup_router(router, gateway):
  330. with cb_helpers.cleanup_action(lambda: router.delete()):
  331. with cb_helpers.cleanup_action(lambda: gateway.delete()):
  332. router.detach_subnet(subnet)
  333. router.detach_gateway(gateway)
  334. with cb_helpers.cleanup_action(lambda: cleanup_router(router,
  335. gateway)):
  336. router.attach_subnet(subnet)
  337. router.attach_gateway(gateway)
  338. fip = None
  339. with cb_helpers.cleanup_action(
  340. lambda: helpers.cleanup_fip(fip)):
  341. # check whether adding an elastic ip works
  342. fip = gateway.floating_ips.create()
  343. self.assertFalse(
  344. fip.in_use,
  345. "Newly created floating IP %s should not be in use." %
  346. fip.public_ip)
  347. with cb_helpers.cleanup_action(
  348. lambda: test_inst.remove_floating_ip(fip)):
  349. test_inst.add_floating_ip(fip)
  350. test_inst.refresh()
  351. # On Devstack, FloatingIP is listed under private_ips.
  352. self.assertIn(fip.public_ip, test_inst.public_ips +
  353. test_inst.private_ips)
  354. fip.refresh()
  355. self.assertTrue(
  356. fip.in_use,
  357. "Attached floating IP %s address should be in use."
  358. % fip.public_ip)
  359. test_inst.refresh()
  360. test_inst.reboot()
  361. test_inst.wait_till_ready()
  362. self.assertNotIn(
  363. fip.public_ip,
  364. test_inst.public_ips + test_inst.private_ips)
  365. with cb_helpers.cleanup_action(
  366. lambda: test_inst.remove_floating_ip(fip.id)):
  367. test_inst.add_floating_ip(fip.id)
  368. test_inst.refresh()
  369. # On Devstack, FloatingIP is listed under private_ips.
  370. self.assertIn(fip.public_ip, test_inst.public_ips +
  371. test_inst.private_ips)
  372. fip.refresh()
  373. self.assertTrue(
  374. fip.in_use,
  375. "Attached floating IP %s address should be in use."
  376. % fip.public_ip)
  377. @helpers.skipIfNoService(['compute.instances', 'networking.networks',
  378. 'security.vm_firewalls'])
  379. def test_instance_start_stop_methods(self):
  380. label = "cb-instmethods-{0}".format(helpers.get_uuid())
  381. if self.provider.PROVIDER_ID != ProviderList.AWS:
  382. raise self.skipTest(f"Instance start/stop methods not implemented"
  383. f"for provider: {self.provider.PROVIDER_ID}")
  384. # Declare these variables and late binding will allow
  385. # the cleanup method access to the most current values
  386. subnet = None
  387. test_inst = None
  388. with cb_helpers.cleanup_action(lambda: helpers.cleanup_test_resources(
  389. instance=test_inst)):
  390. subnet = helpers.get_or_create_default_subnet(self.provider)
  391. test_inst = helpers.get_test_instance(self.provider, label,
  392. subnet=subnet)
  393. # check whether stopping aws instance works
  394. resp = test_inst.stop()
  395. test_inst.wait_for([InstanceState.STOPPED])
  396. test_inst.refresh()
  397. self.assertTrue(
  398. test_inst.state == InstanceState.STOPPED,
  399. "Instance state must be stopped when refreshing after a "
  400. "'stop' operation but got %s"
  401. % test_inst.state)
  402. self.assertTrue(resp, "Response from method was suppose to be"
  403. + " True but got False")
  404. # check whether starting aws instance works
  405. resp = test_inst.start()
  406. test_inst.wait_for([InstanceState.RUNNING])
  407. test_inst.refresh()
  408. self.assertTrue(
  409. test_inst.state == InstanceState.RUNNING,
  410. "Instance state must be running when refreshing after a "
  411. "'start' operation but got %s"
  412. % test_inst.state)
  413. self.assertTrue(resp, "Response from method was suppose to be"
  414. + " True but got False")