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