summaryrefslogtreecommitdiff
path: root/boto/endpoints.py
blob: 96b11092a6b2372e3057bac3d0542f29050eafac (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
# Copyright 2016 Amazon.com, Inc. or its affiliates. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"). You
# may not use this file except in compliance with the License. A copy of
# the License is located at
#
# http://aws.amazon.com/apache2.0/
#
# or in the "license" file accompanying this file. This file is
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
# ANY KIND, either express or implied. See the License for the specific
# language governing permissions and limitations under the License.
import logging

from boto.vendored.regions.regions import EndpointResolver


# Since we will be resolving every region, it's worth not cluttering up the
# logs with all that data.
_endpoint_logger = logging.getLogger('boto.vendored.regions.regions')
_endpoint_logger.disabled = True


class BotoEndpointResolver(EndpointResolver):
    """Endpoint resolver which handles boto2 compatibility concerns."""

    SERVICE_RENAMES = {
        # The botocore resolver is based on endpoint prefix.
        # These don't always sync up to the name that boto2 uses.
        # A mapping can be provided that handles the mapping between
        # "service names" and endpoint prefixes.
        'awslambda': 'lambda',
        'cloudwatch': 'monitoring',
        'ses': 'email',
        'ec2containerservice': 'ecs',
        'configservice': 'config',
    }

    def __init__(self, endpoint_data, service_rename_map=None):
        """
        :type endpoint_data: dict
        :param endpoint_data: Regions and endpoints data in the same format
            as is used by botocore / boto3.

        :type legacy_endpoint_data: dict
        :param legacy_endpoint_data: Regions and endpoints data in the legacy
            format. This data takes precedence over any data found in
            `endpoint_data`.

        :type service_rename_map: dict
        :param service_rename_map: A mapping of boto2 service name to
            endpoint prefix.
        """
        super(BotoEndpointResolver, self).__init__(endpoint_data)
        if service_rename_map is None:
            service_rename_map = self.SERVICE_RENAMES
        # Mapping of boto2 service name to endpoint prefix
        self._endpoint_prefix_map = service_rename_map
        # Mapping of endpoint prefix to boto2 service name
        self._service_name_map = dict(
            (v, k) for k, v in service_rename_map.items())

    def get_available_endpoints(self, service_name, partition_name='aws',
                                allow_non_regional=False):
        endpoint_prefix = self._endpoint_prefix(service_name)
        return super(BotoEndpointResolver, self).get_available_endpoints(
            endpoint_prefix, partition_name, allow_non_regional)

    def get_all_available_regions(self, service_name):
        """Retrieve every region across partitions for a service."""
        regions = set()
        endpoint_prefix = self._endpoint_prefix(service_name)

        # Get every region for every partition in the new endpoint format
        for partition_name in self.get_available_partitions():
            if self._is_global_service(service_name, partition_name):
                # Global services are available in every region in the
                # partition in which they are considered global.
                partition = self._get_partition_data(partition_name)
                regions.update(partition['regions'].keys())
            else:
                regions.update(
                    super(BotoEndpointResolver, self).get_available_endpoints(
                        endpoint_prefix, partition_name
                    )
                )

        return list(regions)

    def construct_endpoint(self, service_name, region_name=None):
        endpoint_prefix = self._endpoint_prefix(service_name)
        return super(BotoEndpointResolver, self).construct_endpoint(
            endpoint_prefix, region_name)

    def resolve_hostname(self, service, region_name):
        """Resolve the hostname for a service in a particular region."""
        endpoint = self.construct_endpoint(service, region_name)
        if endpoint is None:
            return None
        return endpoint.get('sslCommonName', endpoint['hostname'])

    def get_available_services(self):
        """Get a list of all the available services in the endpoints file(s)"""
        services = set()

        for partition in self._endpoint_data['partitions']:
            services.update(partition['services'].keys())

        return [self._service_name(s) for s in services]

    def _is_global_service(self, service_name, partition_name='aws'):
        """Determines whether a service uses a global endpoint.

        In theory a service can be 'global' in one partition but regional in
        another. In practice, each service is all global or all regional.
        """
        endpoint_prefix = self._endpoint_prefix(service_name)
        partition = self._get_partition_data(partition_name)
        service = partition['services'].get(endpoint_prefix, {})
        return 'partitionEndpoint' in service

    def _get_partition_data(self, partition_name):
        """Get partition information for a particular partition.

        This should NOT be used to get service endpoint data because it only
        loads from the new endpoint format. It should only be used for
        partition metadata and partition specific service metadata.

        :type partition_name: str
        :param partition_name: The name of the partition to search for.

        :returns: Partition info from the new endpoints format.
        :rtype: dict or None
        """
        for partition in self._endpoint_data['partitions']:
            if partition['partition'] == partition_name:
                return partition
        raise ValueError(
            "Could not find partition data for: %s" % partition_name)

    def _endpoint_prefix(self, service_name):
        """Given a boto2 service name, get the endpoint prefix."""
        return self._endpoint_prefix_map.get(service_name, service_name)

    def _service_name(self, endpoint_prefix):
        """Given an endpoint prefix, get the boto2 service name."""
        return self._service_name_map.get(endpoint_prefix, endpoint_prefix)


class StaticEndpointBuilder(object):
    """Builds a static mapping of endpoints in the legacy format."""

    def __init__(self, resolver):
        """
        :type resolver: BotoEndpointResolver
        :param resolver: An endpoint resolver.
        """
        self._resolver = resolver

    def build_static_endpoints(self, service_names=None):
        """Build a set of static endpoints in the legacy boto2 format.

        :param service_names: The names of the services to build. They must
            use the names that boto2 uses, not boto3, e.g "ec2containerservice"
            and not "ecs". If no service names are provided, all available
            services will be built.

        :return: A dict consisting of::
            {"service": {"region": "full.host.name"}}
        """
        if service_names is None:
            service_names = self._resolver.get_available_services()

        static_endpoints = {}
        for name in service_names:
            endpoints_for_service = self._build_endpoints_for_service(name)
            if endpoints_for_service:
                # It's possible that when we try to build endpoints for
                # services we get an empty hash.  In that case we don't
                # bother adding it to the final list of static endpoints.
                static_endpoints[name] = endpoints_for_service
        self._handle_special_cases(static_endpoints)
        return static_endpoints

    def _build_endpoints_for_service(self, service_name):
        # Given a service name, 'ec2', build a dict of
        # 'region' -> 'hostname'
        endpoints = {}
        regions = self._resolver.get_all_available_regions(service_name)
        for region_name in regions:
            endpoints[region_name] = self._resolver.resolve_hostname(
                service_name, region_name)
        return endpoints

    def _handle_special_cases(self, static_endpoints):
        # cloudsearchdomain endpoints use the exact same set of endpoints as
        # cloudsearch.
        if 'cloudsearch' in static_endpoints:
            cloudsearch_endpoints = static_endpoints['cloudsearch']
            static_endpoints['cloudsearchdomain'] = cloudsearch_endpoints