1# Copyright (c) 2006-2010 Mitch Garnaat http://garnaat.org/
2# Copyright (c) 2010, Eucalyptus Systems, Inc.
3# All rights reserved.
4#
5# Permission is hereby granted, free of charge, to any person obtaining a
6# copy of this software and associated documentation files (the
7# "Software"), to deal in the Software without restriction, including
8# without limitation the rights to use, copy, modify, merge, publish, dis-
9# tribute, sublicense, and/or sell copies of the Software, and to permit
10# persons to whom the Software is furnished to do so, subject to the fol-
11# lowing conditions:
12#
13# The above copyright notice and this permission notice shall be included
14# in all copies or substantial portions of the Software.
15#
16# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
17# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABIL-
18# ITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
19# SHALL THE AUTHOR BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
20# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
22# IN THE SOFTWARE.
23import os
24
25import boto
26from boto.compat import json
27from boto.exception import BotoClientError
28
29
30def load_endpoint_json(path):
31    """
32    Loads a given JSON file & returns it.
33
34    :param path: The path to the JSON file
35    :type path: string
36
37    :returns: The loaded data
38    """
39    with open(path, 'r') as endpoints_file:
40        return json.load(endpoints_file)
41
42
43def merge_endpoints(defaults, additions):
44    """
45    Given an existing set of endpoint data, this will deep-update it with
46    any similarly structured data in the additions.
47
48    :param defaults: The existing endpoints data
49    :type defaults: dict
50
51    :param defaults: The additional endpoints data
52    :type defaults: dict
53
54    :returns: The modified endpoints data
55    :rtype: dict
56    """
57    # We can't just do an ``defaults.update(...)`` here, as that could
58    # *overwrite* regions if present in both.
59    # We'll iterate instead, essentially doing a deeper merge.
60    for service, region_info in additions.items():
61        # Set the default, if not present, to an empty dict.
62        defaults.setdefault(service, {})
63        defaults[service].update(region_info)
64
65    return defaults
66
67
68def load_regions():
69    """
70    Actually load the region/endpoint information from the JSON files.
71
72    By default, this loads from the default included ``boto/endpoints.json``
73    file.
74
75    Users can override/extend this by supplying either a ``BOTO_ENDPOINTS``
76    environment variable or a ``endpoints_path`` config variable, either of
77    which should be an absolute path to the user's JSON file.
78
79    :returns: The endpoints data
80    :rtype: dict
81    """
82    # Load the defaults first.
83    endpoints = load_endpoint_json(boto.ENDPOINTS_PATH)
84    additional_path = None
85
86    # Try the ENV var. If not, check the config file.
87    if os.environ.get('BOTO_ENDPOINTS'):
88        additional_path = os.environ['BOTO_ENDPOINTS']
89    elif boto.config.get('Boto', 'endpoints_path'):
90        additional_path = boto.config.get('Boto', 'endpoints_path')
91
92    # If there's a file provided, we'll load it & additively merge it into
93    # the endpoints.
94    if additional_path:
95        additional = load_endpoint_json(additional_path)
96        endpoints = merge_endpoints(endpoints, additional)
97
98    return endpoints
99
100
101def get_regions(service_name, region_cls=None, connection_cls=None):
102    """
103    Given a service name (like ``ec2``), returns a list of ``RegionInfo``
104    objects for that service.
105
106    This leverages the ``endpoints.json`` file (+ optional user overrides) to
107    configure/construct all the objects.
108
109    :param service_name: The name of the service to construct the ``RegionInfo``
110        objects for. Ex: ``ec2``, ``s3``, ``sns``, etc.
111    :type service_name: string
112
113    :param region_cls: (Optional) The class to use when constructing. By
114        default, this is ``RegionInfo``.
115    :type region_cls: class
116
117    :param connection_cls: (Optional) The connection class for the
118        ``RegionInfo`` object. Providing this allows the ``connect`` method on
119        the ``RegionInfo`` to work. Default is ``None`` (no connection).
120    :type connection_cls: class
121
122    :returns: A list of configured ``RegionInfo`` objects
123    :rtype: list
124    """
125    endpoints = load_regions()
126
127    if service_name not in endpoints:
128        raise BotoClientError(
129            "Service '%s' not found in endpoints." % service_name
130        )
131
132    if region_cls is None:
133        region_cls = RegionInfo
134
135    region_objs = []
136
137    for region_name, endpoint in endpoints.get(service_name, {}).items():
138        region_objs.append(
139            region_cls(
140                name=region_name,
141                endpoint=endpoint,
142                connection_cls=connection_cls
143            )
144        )
145
146    return region_objs
147
148
149class RegionInfo(object):
150    """
151    Represents an AWS Region
152    """
153
154    def __init__(self, connection=None, name=None, endpoint=None,
155                 connection_cls=None):
156        self.connection = connection
157        self.name = name
158        self.endpoint = endpoint
159        self.connection_cls = connection_cls
160
161    def __repr__(self):
162        return 'RegionInfo:%s' % self.name
163
164    def startElement(self, name, attrs, connection):
165        return None
166
167    def endElement(self, name, value, connection):
168        if name == 'regionName':
169            self.name = value
170        elif name == 'regionEndpoint':
171            self.endpoint = value
172        else:
173            setattr(self, name, value)
174
175    def connect(self, **kw_params):
176        """
177        Connect to this Region's endpoint. Returns an connection
178        object pointing to the endpoint associated with this region.
179        You may pass any of the arguments accepted by the connection
180        class's constructor as keyword arguments and they will be
181        passed along to the connection object.
182
183        :rtype: Connection object
184        :return: The connection to this regions endpoint
185        """
186        if self.connection_cls:
187            return self.connection_cls(region=self, **kw_params)
188