regions.py 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201
  1. # Copyright 2014 Amazon.com, Inc. or its affiliates. All Rights Reserved.
  2. #
  3. # Licensed under the Apache License, Version 2.0 (the "License"). You
  4. # may not use this file except in compliance with the License. A copy of
  5. # the License is located at
  6. #
  7. # http://aws.amazon.com/apache2.0/
  8. #
  9. # or in the "license" file accompanying this file. This file is
  10. # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
  11. # ANY KIND, either express or implied. See the License for the specific
  12. # language governing permissions and limitations under the License.
  13. """Resolves regions and endpoints.
  14. This module implements endpoint resolution, including resolving endpoints for a
  15. given service and region and resolving the available endpoints for a service
  16. in a specific AWS partition.
  17. """
  18. import logging
  19. import re
  20. from botocore.exceptions import NoRegionError
  21. LOG = logging.getLogger(__name__)
  22. DEFAULT_URI_TEMPLATE = '{service}.{region}.{dnsSuffix}'
  23. DEFAULT_SERVICE_DATA = {'endpoints': {}}
  24. class BaseEndpointResolver(object):
  25. """Resolves regions and endpoints. Must be subclassed."""
  26. def construct_endpoint(self, service_name, region_name=None):
  27. """Resolves an endpoint for a service and region combination.
  28. :type service_name: string
  29. :param service_name: Name of the service to resolve an endpoint for
  30. (e.g., s3)
  31. :type region_name: string
  32. :param region_name: Region/endpoint name to resolve (e.g., us-east-1)
  33. if no region is provided, the first found partition-wide endpoint
  34. will be used if available.
  35. :rtype: dict
  36. :return: Returns a dict containing the following keys:
  37. - partition: (string, required) Resolved partition name
  38. - endpointName: (string, required) Resolved endpoint name
  39. - hostname: (string, required) Hostname to use for this endpoint
  40. - sslCommonName: (string) sslCommonName to use for this endpoint.
  41. - credentialScope: (dict) Signature version 4 credential scope
  42. - region: (string) region name override when signing.
  43. - service: (string) service name override when signing.
  44. - signatureVersions: (list<string>) A list of possible signature
  45. versions, including s3, v4, v2, and s3v4
  46. - protocols: (list<string>) A list of supported protocols
  47. (e.g., http, https)
  48. - ...: Other keys may be included as well based on the metadata
  49. """
  50. raise NotImplementedError
  51. def get_available_partitions(self):
  52. """Lists the partitions available to the endpoint resolver.
  53. :return: Returns a list of partition names (e.g., ["aws", "aws-cn"]).
  54. """
  55. raise NotImplementedError
  56. def get_available_endpoints(self, service_name, partition_name='aws',
  57. allow_non_regional=False):
  58. """Lists the endpoint names of a particular partition.
  59. :type service_name: string
  60. :param service_name: Name of a service to list endpoint for (e.g., s3)
  61. :type partition_name: string
  62. :param partition_name: Name of the partition to limit endpoints to.
  63. (e.g., aws for the public AWS endpoints, aws-cn for AWS China
  64. endpoints, aws-us-gov for AWS GovCloud (US) Endpoints, etc.
  65. :type allow_non_regional: bool
  66. :param allow_non_regional: Set to True to include endpoints that are
  67. not regional endpoints (e.g., s3-external-1,
  68. fips-us-gov-west-1, etc).
  69. :return: Returns a list of endpoint names (e.g., ["us-east-1"]).
  70. """
  71. raise NotImplementedError
  72. class EndpointResolver(BaseEndpointResolver):
  73. """Resolves endpoints based on partition endpoint metadata"""
  74. def __init__(self, endpoint_data):
  75. """
  76. :param endpoint_data: A dict of partition data.
  77. """
  78. if 'partitions' not in endpoint_data:
  79. raise ValueError('Missing "partitions" in endpoint data')
  80. self._endpoint_data = endpoint_data
  81. def get_available_partitions(self):
  82. result = []
  83. for partition in self._endpoint_data['partitions']:
  84. result.append(partition['partition'])
  85. return result
  86. def get_available_endpoints(self, service_name, partition_name='aws',
  87. allow_non_regional=False):
  88. result = []
  89. for partition in self._endpoint_data['partitions']:
  90. if partition['partition'] != partition_name:
  91. continue
  92. services = partition['services']
  93. if service_name not in services:
  94. continue
  95. for endpoint_name in services[service_name]['endpoints']:
  96. if allow_non_regional or endpoint_name in partition['regions']:
  97. result.append(endpoint_name)
  98. return result
  99. def construct_endpoint(self, service_name, region_name=None, partition_name=None):
  100. if partition_name is not None:
  101. valid_partition = None
  102. for partition in self._endpoint_data['partitions']:
  103. if partition['partition'] == partition_name:
  104. valid_partition = partition
  105. if valid_partition is not None:
  106. result = self._endpoint_for_partition(valid_partition, service_name,
  107. region_name, True)
  108. return result
  109. return None
  110. # Iterate over each partition until a match is found.
  111. for partition in self._endpoint_data['partitions']:
  112. result = self._endpoint_for_partition(
  113. partition, service_name, region_name)
  114. if result:
  115. return result
  116. def _endpoint_for_partition(self, partition, service_name, region_name,
  117. force_partition=False):
  118. # Get the service from the partition, or an empty template.
  119. service_data = partition['services'].get(
  120. service_name, DEFAULT_SERVICE_DATA)
  121. # Use the partition endpoint if no region is supplied.
  122. if region_name is None:
  123. if 'partitionEndpoint' in service_data:
  124. region_name = service_data['partitionEndpoint']
  125. else:
  126. raise NoRegionError()
  127. # Attempt to resolve the exact region for this partition.
  128. if region_name in service_data['endpoints']:
  129. return self._resolve(
  130. partition, service_name, service_data, region_name)
  131. # Check to see if the endpoint provided is valid for the partition.
  132. if self._region_match(partition, region_name) or force_partition:
  133. # Use the partition endpoint if set and not regionalized.
  134. partition_endpoint = service_data.get('partitionEndpoint')
  135. is_regionalized = service_data.get('isRegionalized', True)
  136. if partition_endpoint and not is_regionalized:
  137. LOG.debug('Using partition endpoint for %s, %s: %s',
  138. service_name, region_name, partition_endpoint)
  139. return self._resolve(
  140. partition, service_name, service_data, partition_endpoint)
  141. LOG.debug('Creating a regex based endpoint for %s, %s',
  142. service_name, region_name)
  143. return self._resolve(
  144. partition, service_name, service_data, region_name)
  145. def _region_match(self, partition, region_name):
  146. if region_name in partition['regions']:
  147. return True
  148. if 'regionRegex' in partition:
  149. return re.compile(partition['regionRegex']).match(region_name)
  150. return False
  151. def _resolve(self, partition, service_name, service_data, endpoint_name):
  152. result = service_data['endpoints'].get(endpoint_name, {})
  153. result['partition'] = partition['partition']
  154. result['endpointName'] = endpoint_name
  155. # Merge in the service defaults then the partition defaults.
  156. self._merge_keys(service_data.get('defaults', {}), result)
  157. self._merge_keys(partition.get('defaults', {}), result)
  158. hostname = result.get('hostname', DEFAULT_URI_TEMPLATE)
  159. result['hostname'] = self._expand_template(
  160. partition, result['hostname'], service_name, endpoint_name)
  161. if 'sslCommonName' in result:
  162. result['sslCommonName'] = self._expand_template(
  163. partition, result['sslCommonName'], service_name,
  164. endpoint_name)
  165. result['dnsSuffix'] = partition['dnsSuffix']
  166. return result
  167. def _merge_keys(self, from_data, result):
  168. for key in from_data:
  169. if key not in result:
  170. result[key] = from_data[key]
  171. def _expand_template(self, partition, template, service_name,
  172. endpoint_name):
  173. return template.format(
  174. service=service_name, region=endpoint_name,
  175. dnsSuffix=partition['dnsSuffix'])