standard_interface_tests.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362
  1. """
  2. Standard tests for behaviour common across the whole of cloudbridge.
  3. This includes:
  4. 1. Checking that every resource has an id property
  5. 2. Checking for object equality and repr
  6. 3. Checking standard behaviour for list, iter, find, get, delete
  7. """
  8. import uuid
  9. from six.moves.urllib.parse import quote_plus
  10. import tenacity
  11. from cloudbridge.base import helpers as cb_helpers
  12. from cloudbridge.interfaces.exceptions \
  13. import InvalidNameException
  14. from cloudbridge.interfaces.exceptions import InvalidParamException
  15. from cloudbridge.interfaces.resources import LabeledCloudResource
  16. from cloudbridge.interfaces.resources import ObjectLifeCycleMixin
  17. from cloudbridge.interfaces.resources import ResultList
  18. from cloudbridge.providers.aws.services import AWSImageService
  19. import tests.helpers as helpers
  20. def check_repr(test, obj):
  21. test.assertTrue(
  22. obj.id in repr(obj),
  23. "repr(obj) for %s contain the object id so that the object"
  24. " can be reconstructed, but does not. eval(repr(obj)) == obj"
  25. % (type(obj).__name__,))
  26. def check_json(test, obj):
  27. val = obj.to_json()
  28. test.assertEqual(val.get('id'), obj.id)
  29. test.assertEqual(val.get('name'), obj.name)
  30. if isinstance(obj, LabeledCloudResource):
  31. test.assertEqual(val.get('label'), obj.label)
  32. def check_obj_properties(test, obj):
  33. test.assertEqual(obj, obj, "Object should be equal to itself")
  34. test.assertFalse(obj != obj, "Object inequality should be false")
  35. check_obj_id(test, obj)
  36. check_obj_name(test, obj)
  37. check_obj_label(test, obj)
  38. @tenacity.retry(stop=tenacity.stop_after_attempt(10),
  39. retry=tenacity.retry_if_exception_type(AssertionError),
  40. wait=tenacity.wait_fixed(10),
  41. reraise=True)
  42. def check_list(test, service, obj):
  43. list_objs = service.list()
  44. test.assertIsInstance(list_objs, ResultList)
  45. all_records = list_objs
  46. while list_objs.is_truncated:
  47. list_objs = service.list(marker=list_objs.marker)
  48. all_records += list_objs
  49. match_objs = [o for o in all_records if o.id == obj.id]
  50. test.assertTrue(
  51. len(match_objs) == 1,
  52. "List objects for %s does not return the expected object id %s. Got %s"
  53. % (type(obj).__name__, obj.id, match_objs))
  54. return match_objs
  55. def check_iter(test, service, obj):
  56. # check iteration
  57. iter_objs = list(service)
  58. iter_ids = [o.id for o in service]
  59. test.assertEqual(len(set(iter_ids)), len(iter_ids),
  60. "Iteration should not return duplicates")
  61. match_objs = [o for o in iter_objs if o.id == obj.id]
  62. test.assertTrue(
  63. len(match_objs) == 1,
  64. "Iter objects for %s does not return the expected object id %s. Got %s"
  65. % (type(obj).__name__, obj.id, match_objs))
  66. return match_objs
  67. def check_find(test, service, obj):
  68. # check find
  69. if isinstance(obj, LabeledCloudResource):
  70. find_objs = service.find(label=obj.label)
  71. else:
  72. find_objs = service.find(name=obj.name)
  73. test.assertTrue(
  74. len(find_objs) == 1,
  75. "Find objects for %s does not return the expected object: %s. Got %s"
  76. % (type(obj).__name__, getattr(obj, 'label', obj.name), find_objs))
  77. test.assertEqual(find_objs[0].id, obj.id)
  78. return find_objs
  79. def check_find_non_existent(test, service, obj):
  80. args = {}
  81. # AWSImageService.find looks through all public images by default
  82. # In order to get tests to run faster, looking for these non existent
  83. # values only in images owned by the current user
  84. if isinstance(service, AWSImageService):
  85. args = {'owners': ['self']}
  86. if isinstance(obj, LabeledCloudResource):
  87. find_objs = service.find(label="random_imagined_obj_name", **args)
  88. else:
  89. find_objs = service.find(name="random_imagined_obj_name")
  90. with test.assertRaises(InvalidParamException):
  91. service.find(notaparameter="random_imagined_obj_name")
  92. test.assertTrue(
  93. len(find_objs) == 0,
  94. "Find non-existent object for %s returned unexpected objects: %s"
  95. % (type(service).__name__, find_objs))
  96. def check_get(test, service, obj):
  97. get_obj = service.get(obj.id)
  98. test.assertEqual(get_obj.id, obj.id)
  99. test.assertIsInstance(get_obj, type(obj))
  100. return get_obj
  101. def check_get_non_existent(test, service):
  102. # check get
  103. get_objs = service.get('tmp-' + str(uuid.uuid4()))
  104. test.assertIsNone(
  105. get_objs,
  106. "Get non-existent object for %s returned unexpected objects: %s"
  107. % (type(service).__name__, get_objs))
  108. @tenacity.retry(stop=tenacity.stop_after_attempt(10),
  109. retry=tenacity.retry_if_exception_type(AssertionError),
  110. wait=tenacity.wait_fixed(10),
  111. reraise=True)
  112. def check_delete(test, service, obj, perform_delete=False):
  113. if perform_delete:
  114. obj.delete()
  115. objs = service.list()
  116. found_objs = [o for o in objs if o.id == obj.id]
  117. test.assertTrue(
  118. len(found_objs) == 0,
  119. "Object %s in service %s should have been deleted but still exists."
  120. % (found_objs, type(service).__name__))
  121. def check_obj_id(test, obj):
  122. id_property = getattr(type(obj), 'id', None)
  123. test.assertIsInstance(id_property, property)
  124. test.assertIsNone(id_property.fset, "Id should not have a setter")
  125. # Non-url safe characters trip up djcloudbridge or anything that needs to
  126. # use the ID in a url so make sure ids do not contain them
  127. test.assertEqual(quote_plus(obj.id), obj.id,
  128. "IDs should only contain URL friendly chars that do not "
  129. "require encoding but contains: %s" % (obj.id,))
  130. def check_obj_name(test, obj):
  131. name_property = getattr(type(obj), 'name', None)
  132. test.assertIsInstance(name_property, property)
  133. test.assertIsNone(name_property.fset, "Name should not have a setter")
  134. def check_obj_label(test, obj):
  135. """
  136. Cloudbridge identifiers must be 1-63 characters long, and comply with
  137. RFC1035. In addition, identifiers should contain only lowercase letters,
  138. numeric characters, underscores, and dashes. International
  139. characters are allowed.
  140. """
  141. # if label property exists, make sure invalid values cannot be set
  142. label_property = getattr(type(obj), 'label', None)
  143. if isinstance(label_property, property):
  144. test.assertIsInstance(obj, LabeledCloudResource)
  145. original_label = obj.label
  146. # Three character labels should be allowed
  147. obj.label = "abc"
  148. VALID_LABEL = u"hello-world-123"
  149. obj.label = VALID_LABEL
  150. # Two character labels should not be allowed
  151. with test.assertRaises(InvalidNameException):
  152. obj.label = "ab"
  153. # A none value should not be allowed
  154. with test.assertRaises(InvalidNameException):
  155. obj.label = None
  156. # setting spaces should raise an exception
  157. with test.assertRaises(InvalidNameException):
  158. obj.label = "hello world"
  159. # setting upper case characters should raise an exception
  160. with test.assertRaises(InvalidNameException):
  161. obj.label = "helloWorld"
  162. # setting special characters should raise an exception
  163. with test.assertRaises(InvalidNameException):
  164. obj.label = "hello.world:how_goes_it"
  165. # Starting with a dash should raise an exception
  166. with test.assertRaises(InvalidNameException):
  167. obj.label = "-hello"
  168. # Ending with a dash should raise an exception
  169. with test.assertRaises(InvalidNameException):
  170. obj.label = "hello-"
  171. # setting a length > 63 should result in an exception
  172. with test.assertRaises(InvalidNameException,
  173. msg="Label of length > 64 is not allowed"):
  174. obj.label = "a" * 64
  175. # refreshing should yield the last successfully set label
  176. obj.refresh()
  177. test.assertEqual(obj.label, VALID_LABEL)
  178. obj.label = original_label
  179. def check_standard_behaviour(test, service, obj):
  180. """
  181. Checks standard behaviour in a given cloudbridge resource
  182. of a given service.
  183. """
  184. check_repr(test, obj)
  185. check_json(test, obj)
  186. check_obj_properties(test, obj)
  187. objs_list = check_list(test, service, obj)
  188. objs_iter = check_iter(test, service, obj)
  189. objs_find = check_find(test, service, obj)
  190. check_find_non_existent(test, service, obj)
  191. obj_get = check_get(test, service, obj)
  192. check_get_non_existent(test, service)
  193. test.assertTrue(
  194. obj.id == objs_list[0].id == objs_iter[0].id ==
  195. objs_find[0].id == obj_get.id,
  196. "Object Ids returned by list: {0}, iter: {1}, find: {2} and get: {3} "
  197. " are not as expected: {4}".format(objs_list[0].id, objs_iter[0].id,
  198. objs_find[0].id, obj_get.id,
  199. obj.id))
  200. test.assertTrue(
  201. obj.name == objs_list[0].name == objs_iter[0].name ==
  202. objs_find[0].name == obj_get.name,
  203. "Names returned by list: {0}, iter: {1}, find: {2} and get: {3} "
  204. " are not as expected: {4}".format(objs_list[0].id, objs_iter[0].id,
  205. objs_find[0].id, obj_get.id,
  206. obj.id))
  207. if isinstance(obj, LabeledCloudResource):
  208. test.assertTrue(
  209. obj.label == objs_list[0].label == objs_iter[0].label ==
  210. objs_find[0].label == obj_get.label,
  211. "Labels returned by list: {0}, iter: {1}, find: {2} and get: {3} "
  212. " are not as expected: {4}".format(objs_list[0].id,
  213. objs_iter[0].id,
  214. objs_find[0].id, obj_get.id,
  215. obj.id))
  216. def check_create(test, service, iface, name_prefix,
  217. create_func, cleanup_func):
  218. # check create with invalid label
  219. with test.assertRaises(InvalidNameException):
  220. # spaces should raise an exception
  221. create_func("hello world")
  222. # check create with invalid label
  223. with test.assertRaises(InvalidNameException):
  224. # uppercase characters should raise an exception
  225. create_func("helloWorld")
  226. # setting special characters should raise an exception
  227. with test.assertRaises(InvalidNameException):
  228. create_func("hello.world:how_goes_it")
  229. # Starting with a dash should raise an exception
  230. with test.assertRaises(InvalidNameException):
  231. create_func("-hello")
  232. # Ending with a dash should raise an exception
  233. with test.assertRaises(InvalidNameException):
  234. create_func("hello-")
  235. # underscores are not allowed
  236. with test.assertRaises(InvalidNameException):
  237. create_func("hello_bucket")
  238. # setting a length > 63 should result in an exception
  239. with test.assertRaises(InvalidNameException,
  240. msg="Label of length > 63 should be disallowed"):
  241. create_func("a" * 64)
  242. # name cannot be an IP address
  243. with test.assertRaises(InvalidNameException):
  244. create_func("197.10.100.42")
  245. # empty name are not allowed
  246. with test.assertRaises(InvalidNameException):
  247. create_func(None)
  248. # names of length less than 3 should raise an exception
  249. with test.assertRaises(InvalidNameException):
  250. create_func("cb")
  251. def check_crud(test, service, iface, label_prefix,
  252. create_func, cleanup_func, extra_test_func=None,
  253. custom_check_delete=None, skip_name_check=False):
  254. """
  255. Checks crud behaviour of a given cloudbridge service. The create_func will
  256. be used as a factory function to create a service object and the
  257. cleanup_func will be used to destroy the object. Once an object is created
  258. using the create_func, all other standard behavioural tests can be run
  259. against that object.
  260. :type test: ``TestCase``
  261. :param test: The TestCase object to use
  262. :type service: ``CloudService``
  263. :param service: The CloudService object under test. For example,
  264. a VolumeService object.
  265. :type iface: ``type``
  266. :param iface: The type to test behaviour against. This type must be a
  267. subclass of ``CloudResource``.
  268. :type label_prefix: ``str``
  269. :param label_prefix: The label to prefix all created objects with. This
  270. function will generated a new label with the
  271. specified label_prefix for each test object created
  272. and pass that label into the create_func
  273. :type create_func: ``func``
  274. :param create_func: The create_func must accept the label of the object to
  275. create as a parameter and return the constructed
  276. object.
  277. :type cleanup_func: ``func``
  278. :param cleanup_func: The cleanup_func must accept the created object
  279. and perform all cleanup tasks required to delete the
  280. object.
  281. :type extra_test_func: ``func``
  282. :param extra_test_func: This function will be called to perform additional
  283. tests after object construction and initialization,
  284. but before object cleanup. It will receive the
  285. created object as a parameter.
  286. :type custom_check_delete: ``func``
  287. :param custom_check_delete: If provided, this function will be called
  288. instead of the standard check_delete function
  289. to make sure that the object has been deleted.
  290. :type skip_name_check: ``boolean``
  291. :param skip_name_check: If True, the name related checking will be
  292. skipped.
  293. """
  294. obj = None
  295. with cb_helpers.cleanup_action(lambda: cleanup_func(obj)):
  296. label = "{0}-{1}".format(label_prefix, helpers.get_uuid())
  297. if not skip_name_check:
  298. check_create(test, service, iface, label_prefix,
  299. create_func, cleanup_func)
  300. obj = create_func(label)
  301. if issubclass(iface, ObjectLifeCycleMixin):
  302. obj.wait_till_ready()
  303. check_standard_behaviour(test, service, obj)
  304. if extra_test_func:
  305. extra_test_func(obj)
  306. if custom_check_delete:
  307. custom_check_delete(obj)
  308. else:
  309. check_delete(test, service, obj)