helpers.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335
  1. """A set of AWS-specific helper methods used by the framework."""
  2. import logging as log
  3. from boto3.resources.params import create_request_parameters
  4. from botocore import xform_name
  5. from botocore.exceptions import ClientError
  6. from botocore.utils import merge_dicts
  7. from cloudbridge.cloud.base.resources import ClientPagedResultList
  8. from cloudbridge.cloud.base.resources import ServerPagedResultList
  9. def trim_empty_params(params_dict):
  10. """
  11. Given a dict containing potentially null values, trims out
  12. all the null values. This is to please Boto, which throws
  13. a parameter validation exception for NoneType arguments.
  14. e.g. Given
  15. {
  16. 'GroupName': 'abc',
  17. 'Description': None,
  18. 'VpcId': 'xyz',
  19. }
  20. returns:
  21. {
  22. 'GroupName': 'abc',
  23. 'VpcId': 'xyz'
  24. }
  25. """
  26. log.debug("Removing null values from %s", params_dict)
  27. return {k: v for k, v in params_dict.items() if v is not None}
  28. def find_tag_value(tags, key):
  29. """
  30. Finds the value associated with a given key from a list of AWS tags.
  31. :type tags: list of ``dict``
  32. :param tags: The AWS tag list to search through
  33. :type key: ``str``
  34. :param key: Name of the tag to search for
  35. """
  36. log.info("Searching for %s in %s", key, tags)
  37. for tag in tags or []:
  38. if tag.get('Key') == key:
  39. log.info("Found %s, returning %s", key, tag.get('Value'))
  40. return tag.get('Value')
  41. return None
  42. class BotoGenericService(object):
  43. """
  44. Generic implementation of a Boto3 AWS service. Uses Boto3
  45. resource, collection and paging support to implement
  46. basic cloudbridge methods.
  47. """
  48. def __init__(self, provider, cb_resource, boto_conn, boto_collection_name):
  49. """
  50. :type provider: :class:`AWSCloudProvider`
  51. :param provider: CloudBridge AWS provider to use
  52. :type cb_resource: :class:`CloudResource`
  53. :param cb_resource: CloudBridge Resource class to wrap results in
  54. :type boto_conn: :class:`Boto3.Resource`
  55. :param boto_conn: Boto top level service resource (e.g. EC2, S3)
  56. connection.
  57. :type boto_collection_name: ``str``
  58. :param boto_collection_name: Boto collection name that corresponds
  59. to the CloudBridge resource (e.g. key_pair)
  60. """
  61. self.provider = provider
  62. self.cb_resource = cb_resource
  63. self.boto_conn = boto_conn
  64. self.boto_collection_model = self._infer_collection_model(
  65. boto_conn, boto_collection_name)
  66. # Perform an empty filter to convert to a ResourceCollection
  67. self.boto_collection = (getattr(self.boto_conn, boto_collection_name)
  68. .filter())
  69. self.boto_resource = self._infer_boto_resource(
  70. boto_conn, self.boto_collection_model)
  71. def _infer_collection_model(self, conn, collection_name):
  72. log.debug("Retrieving boto model for collection: %s", collection_name)
  73. return next(col for col in conn.meta.resource_model.collections
  74. if col.name == collection_name)
  75. def _infer_boto_resource(self, conn, collection_model):
  76. log.debug("Retrieving resource model for collection: %s",
  77. collection_model.name)
  78. resource_model = next(
  79. sr for sr in conn.meta.resource_model.subresources
  80. if sr.resource.model.name == collection_model.resource.model.name)
  81. return getattr(self.boto_conn, resource_model.name)
  82. def get_raw(self, resource_id):
  83. """
  84. Returns a single resource.
  85. :type resource_id: ``str``
  86. :param resource_id: ID of the boto resource to fetch
  87. :returns An unwrapped AWS resource
  88. """
  89. try:
  90. log.debug("Retrieving resource: %s with id: %s",
  91. self.boto_collection_model.name, resource_id)
  92. obj = self.boto_resource(resource_id)
  93. obj.load()
  94. log.debug("Successfully Retrieved: %s", obj)
  95. return obj
  96. except ClientError as exc:
  97. error_code = exc.response['Error']['Code']
  98. if any(status in error_code for status in
  99. ('NotFound', 'InvalidParameterValue', 'Malformed', '404')):
  100. log.debug("Object not found: %s", resource_id)
  101. return None
  102. else:
  103. raise exc
  104. def get(self, resource_id):
  105. """
  106. Returns a single resource.
  107. :type resource_id: ``str``
  108. :param resource_id: ID of the boto resource to fetch
  109. :returns A CloudBridge wrapped resource
  110. """
  111. aws_res = self.get_raw(resource_id)
  112. if aws_res:
  113. return self.cb_resource(self.provider, aws_res)
  114. else:
  115. return None
  116. def _get_list_operation(self):
  117. """
  118. This function discovers the list operation for a particular resource
  119. collection. For example, given the resource collection model for
  120. KeyPair, it returns the list operation for it, as describe_key_pairs.
  121. """
  122. return xform_name(self.boto_collection_model.request.operation)
  123. def _to_boto_resource(self, collection, params, page):
  124. """
  125. This function duplicates some of the logic of the pages() method in
  126. boto.resources.collection.ResourceCollection. It will convert a raw
  127. json response to the corresponding Boto resource. It's necessary
  128. because paginators() return json responses, and there's no direct way
  129. to convert a paginated json response to a Boto Resource.
  130. """
  131. # pylint:disable=protected-access
  132. return collection._handler(collection._parent, params, page)
  133. def _get_paginated_results(self, limit, marker, collection):
  134. """
  135. If a Boto Paginator is available, use it. The results
  136. are converted back into BotoResources by directly accessing
  137. protected members of ResourceCollection. This logic can be removed
  138. depending on issue: https://github.com/boto/boto3/issues/1268.
  139. """
  140. # pylint:disable=protected-access
  141. cleaned_params = collection._params.copy()
  142. cleaned_params.pop('limit', None)
  143. cleaned_params.pop('page_size', None)
  144. # pylint:disable=protected-access
  145. params = create_request_parameters(
  146. collection._parent, collection._model.request)
  147. merge_dicts(params, cleaned_params, append_lists=True)
  148. client = self.boto_conn.meta.client
  149. list_op = self._get_list_operation()
  150. paginator = client.get_paginator(list_op)
  151. PaginationConfig = {}
  152. if limit:
  153. PaginationConfig = {'MaxItems': limit, 'PageSize': limit}
  154. if marker:
  155. PaginationConfig.update({'StartingToken': marker})
  156. params.update({'PaginationConfig': PaginationConfig})
  157. args = trim_empty_params(params)
  158. pages = paginator.paginate(**args)
  159. # resume_token is not populated unless the iterator is used
  160. items = pages.build_full_result()
  161. boto_objs = self._to_boto_resource(collection, args, items)
  162. resume_token = pages.resume_token
  163. return (resume_token, boto_objs)
  164. def _make_query(self, collection, limit, marker):
  165. """
  166. Decide between server or client pagination,
  167. depending on the availability of a Boto Paginator.
  168. See issue: https://github.com/boto/boto3/issues/1268
  169. """
  170. client = self.boto_conn.meta.client
  171. list_op = self._get_list_operation()
  172. if client.can_paginate(list_op):
  173. log.debug("Supports server side pagination. Server will"
  174. " limit and page results.")
  175. res_token, items = self._get_paginated_results(limit, marker,
  176. collection)
  177. return 'server', res_token, items
  178. else:
  179. log.debug("Does not support server side pagination. Client will"
  180. " limit and page results.")
  181. return 'client', None, collection
  182. def list(self, limit=None, marker=None, collection=None, **kwargs):
  183. """
  184. List a set of resources.
  185. :type collection: ``ResourceCollection``
  186. :param collection: Boto resource collection object corresponding to the
  187. current resource. See http://boto3.readthedocs.io/
  188. en/latest/guide/collections.html
  189. """
  190. limit = limit or self.provider.config.default_result_limit
  191. collection = collection or self.boto_collection.filter(**kwargs)
  192. pag_type, resume_token, boto_objs = self._make_query(collection,
  193. limit,
  194. marker)
  195. # Wrap in CB objects.
  196. results = [self.cb_resource(self.provider, obj) for obj in boto_objs]
  197. if pag_type == 'server':
  198. log.debug("Using server pagination.")
  199. return ServerPagedResultList(is_truncated=True if resume_token
  200. else False,
  201. marker=resume_token if resume_token
  202. else None,
  203. supports_total=False,
  204. data=results)
  205. else:
  206. log.debug("Did not received a resume token, will page in client"
  207. " if necessary.")
  208. return ClientPagedResultList(self.provider, results,
  209. limit=limit, marker=marker)
  210. def find(self, filters, limit=None, marker=None,
  211. **kwargs):
  212. """
  213. Return a list of resources by filter.
  214. :type filters: A ``dict`` of filters
  215. :param filters: A list of filters, where the dict key is the filter
  216. name and the value is the value to filter by.
  217. """
  218. boto_filters = [{'Name': key, 'Values': [value]}
  219. for key, value in filters.items()]
  220. collection = self.boto_collection
  221. collection = collection.filter(Filters=boto_filters)
  222. if kwargs:
  223. collection = collection.filter(**kwargs)
  224. return self.list(limit=limit, marker=marker, collection=collection)
  225. def create(self, boto_method, **kwargs):
  226. """
  227. Creates a resource
  228. :type boto_method: ``str``
  229. :param boto_method: AWS Service method to invoke
  230. :type kwargs: ``dict``
  231. :param kwargs: Arguments to be passed as-is to the service method
  232. """
  233. log.debug("Creating a resource by invoking %s on these arguments: %s",
  234. boto_method, kwargs)
  235. trimmed_args = trim_empty_params(kwargs)
  236. result = getattr(self.boto_conn, boto_method)(**trimmed_args)
  237. if isinstance(result, list):
  238. return [self.cb_resource(self.provider, obj)
  239. for obj in result if obj]
  240. else:
  241. return self.cb_resource(self.provider, result) if result else None
  242. def delete(self, resource_id):
  243. """
  244. Deletes a resource by id
  245. :type resource_id: ``str``
  246. :param resource_id: ID of the resource
  247. """
  248. log.info("Delete the resource with the id %s", resource_id)
  249. res = self.get(resource_id)
  250. if res:
  251. res.delete()
  252. class BotoEC2Service(BotoGenericService):
  253. """
  254. Boto EC2 service implementation
  255. """
  256. def __init__(self, provider, cb_resource,
  257. boto_collection_name):
  258. """
  259. :type provider: :class:`AWSCloudProvider`
  260. :param provider: CloudBridge AWS provider to use
  261. :type cb_resource: :class:`CloudResource`
  262. :param cb_resource: CloudBridge Resource class to wrap results in
  263. :type boto_collection_name: ``str``
  264. :param boto_collection_name: Boto collection name that corresponds
  265. to the CloudBridge resource (e.g. key_pair)
  266. """
  267. super(BotoEC2Service, self).__init__(
  268. provider, cb_resource, provider.ec2_conn,
  269. boto_collection_name)
  270. class BotoS3Service(BotoGenericService):
  271. """
  272. Boto S3 service implementation.
  273. """
  274. def __init__(self, provider, cb_resource,
  275. boto_collection_name):
  276. """
  277. :type provider: :class:`AWSCloudProvider`
  278. :param provider: CloudBridge AWS provider to use
  279. :type cb_resource: :class:`CloudResource`
  280. :param cb_resource: CloudBridge Resource class to wrap results in
  281. :type boto_collection_name: ``str``
  282. :param boto_collection_name: Boto collection name that corresponds
  283. to the CloudBridge resource (e.g. key_pair)
  284. """
  285. super(BotoS3Service, self).__init__(
  286. provider, cb_resource, provider.s3_conn,
  287. boto_collection_name)