1 # Copyright 2012-2013 GRNET S.A. All rights reserved.
3 # Redistribution and use in source and binary forms, with or
4 # without modification, are permitted provided that the following
7 # 1. Redistributions of source code must retain the above
8 # copyright notice, this list of conditions and the following
11 # 2. Redistributions in binary form must reproduce the above
12 # copyright notice, this list of conditions and the following
13 # disclaimer in the documentation and/or other materials
14 # provided with the distribution.
16 # THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
17 # OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
18 # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
19 # PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
20 # CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21 # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22 # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
23 # USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
24 # AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
25 # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
26 # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
27 # POSSIBILITY OF SUCH DAMAGE.
29 # The views and conclusions contained in the software and
30 # documentation are those of the authors and should not be
31 # interpreted as representing official policies, either expressed
32 # or implied, of GRNET S.A.
34 from logging import getLogger
35 from astakosclient import AstakosClient as OriginalAstakosClient
36 from astakosclient import AstakosClientException, parse_endpoints
38 from kamaki.clients import Client, ClientError, RequestManager, recvlog
41 class AstakosClient(OriginalAstakosClient):
42 """Wrap Original AstakosClient to ensure bw compatibility and ease of use
44 Note that this is an ancached class, so each call produces at least one
48 def __init__(self, *args, **kwargs):
52 token = args.pop(0) if args else kwargs.pop('token', None)
53 args = tuple([token, url] + args)
54 elif 'base_url' in kwargs:
55 kwargs['auth_url'] = kwargs.get('auth_url', kwargs['base_url'])
56 super(AstakosClient, self).__init__(*args, **kwargs)
58 def get_service_endpoints(self, service_type, version=None):
59 services = parse_endpoints(
60 self.get_endpoints(), ep_type=service_type, ep_version_id=version)
61 return services[0]['endpoints'][0] if services else []
65 return self.authenticate()['access']['user']
67 def user_term(self, term):
68 return self.user_info[term]
71 def _astakos_error(foo):
72 def wrap(self, *args, **kwargs):
74 return foo(self, *args, **kwargs)
75 except AstakosClientException as sace:
76 self._raise_for_status(sace)
80 class LoggedAstakosClient(AstakosClient):
81 """An AstakosClient wrapper with modified logging
83 Logs are adjusted to appear similar to the ones of kamaki clients.
84 No other changes are made to the parent class.
90 def _dump_response(self, request, status, message, data):
91 recvlog.info('\n%d %s' % (status, message))
92 recvlog.info('data size: %s' % len(data))
93 if not self.LOG_TOKEN:
94 token = request.headers.get('X-Auth-Token', '')
96 data = data.replace(token, '...') if token else data
99 recvlog.info('- - - - - - -')
101 def _call_astakos(self, *args, **kwargs):
102 r = super(LoggedAstakosClient, self)._call_astakos(*args, **kwargs)
104 log_request = getattr(self, 'log_request', None)
106 req = RequestManager(
107 method=log_request['method'],
108 url='%s://%s' % (self.scheme, self.astakos_base_url),
109 path=log_request['path'],
110 data=log_request.get('body', None),
111 headers=log_request.get('headers', dict()))
112 req.LOG_TOKEN, req.LOG_DATA = self.LOG_TOKEN, self.LOG_DATA
114 log_response = getattr(self, 'log_response', None)
118 status=log_response['status'],
119 message=log_response['message'],
120 data=log_response.get('data', ''))
127 class CachedAstakosClient(Client):
128 """Synnefo Astakos cached client wraper"""
131 def __init__(self, base_url, token=None):
132 super(CachedAstakosClient, self).__init__(base_url, token)
133 self._astakos = dict()
136 self._uuids2usernames = dict()
137 self._usernames2uuids = dict()
139 def _resolve_token(self, token):
141 :returns: (str) a single token
143 :raises AssertionError: if no token exists (either param or member)
145 token = token or self.token
146 assert token, 'No token provided'
148 isinstance(token, list) or isinstance(token, tuple)) else token
150 def get_client(self, token=None):
151 """Get the Synnefo AstakosClient instance used by client"""
152 token = self._resolve_token(token)
153 self._validate_token(token)
154 return self._astakos[self._uuids[token]]
157 def authenticate(self, token=None):
158 """Get authentication information and store it in this client
159 As long as the CachedAstakosClient instance is alive, the latest
160 authentication information for this token will be available
162 :param token: (str) custom token to authenticate
164 token = self._resolve_token(token)
165 astakos = LoggedAstakosClient(
166 self.base_url, token, logger=getLogger('astakosclient'))
167 astakos.LOG_TOKEN = getattr(self, 'LOG_TOKEN', False)
168 astakos.LOG_DATA = getattr(self, 'LOG_DATA', False)
169 r = astakos.authenticate()
170 uuid = r['access']['user']['id']
171 self._uuids[token] = uuid
172 self._cache[uuid] = r
173 self._astakos[uuid] = astakos
174 self._uuids2usernames[uuid] = dict()
175 self._usernames2uuids[uuid] = dict()
176 return self._cache[uuid]
178 def remove_user(self, uuid):
179 self._uuids.pop(self.get_token(uuid))
180 self._cache.pop(uuid)
181 self._astakos.pop(uuid)
182 self._uuids2usernames.pop(uuid)
183 self._usernames2uuids.pop(uuid)
185 def get_token(self, uuid):
186 return self._cache[uuid]['access']['token']['id']
188 def _validate_token(self, token):
189 if (token not in self._uuids) or (
190 self.get_token(self._uuids[token]) != token):
191 self._uuids.pop(token, None)
192 self.authenticate(token)
194 def get_services(self, token=None):
196 :returns: (list) [{name:..., type:..., endpoints:[...]}, ...]
198 token = self._resolve_token(token)
199 self._validate_token(token)
200 r = self._cache[self._uuids[token]]
201 return r['access']['serviceCatalog']
203 def get_service_details(self, service_type, token=None):
205 :param service_type: (str) compute, object-store, image, account, etc.
207 :returns: (dict) {name:..., type:..., endpoints:[...]}
209 :raises ClientError: (600) if service_type not in service catalog
211 services = self.get_services(token)
212 for service in services:
214 if service['type'].lower() == service_type.lower():
217 self.log.warning('Misformated service %s' % service)
219 'Service type "%s" not in service catalog' % service_type, 600)
221 def get_service_endpoints(self, service_type, version=None, token=None):
223 :param service_type: (str) can be compute, object-store, etc.
225 :param version: (str) the version id of the service
227 :returns: (dict) {SNF:uiURL, adminURL, internalURL, publicURL, ...}
229 :raises ClientError: (600) if service_type not in service catalog
231 :raises ClientError: (601) if #matching endpoints != 1
233 service = self.get_service_details(service_type, token)
235 for endpoint in service['endpoints']:
236 if (not version) or (
237 endpoint['versionId'].lower() == version.lower()):
238 matches.append(endpoint)
239 if len(matches) != 1:
241 '%s endpoints match type %s %s' % (
242 len(matches), service_type,
243 ('and versionId %s' % version) if version else ''),
247 def list_users(self):
248 """list cached users information"""
252 for k, v in self._cache.items():
253 r.append(dict(v['access']['user']))
254 r[-1].update(dict(auth_token=self.get_token(k)))
257 def user_info(self, token=None):
258 """Get (cached) user information"""
259 token = self._resolve_token(token)
260 self._validate_token(token)
261 r = self._cache[self._uuids[token]]
262 return r['access']['user']
264 def term(self, key, token=None):
265 """Get (cached) term, from user credentials"""
266 return self.user_term(key, token)
268 def user_term(self, key, token=None):
269 """Get (cached) term, from user credentials"""
270 return self.user_info(token).get(key, None)
272 def post_user_catalogs(self, uuids=None, displaynames=None, token=None):
273 """POST base_url/user_catalogs
275 :param uuids: (list or tuple) user uuids
277 :param displaynames: (list or tuple) usernames (mut. excl. to uuids)
279 :returns: (dict) {uuid1: name1, uuid2: name2, ...} or oposite
281 return self.uuids2usernames(uuids, token) if (
282 uuids) else self.usernames2uuids(displaynames, token)
285 def uuids2usernames(self, uuids, token=None):
286 token = self._resolve_token(token)
287 self._validate_token(token)
288 uuid = self._uuids[token]
289 astakos = self._astakos[uuid]
290 if set(uuids or []).difference(self._uuids2usernames[uuid]):
291 self._uuids2usernames[uuid].update(astakos.get_usernames(uuids))
292 return self._uuids2usernames[uuid]
295 def usernames2uuids(self, usernames, token=None):
296 token = self._resolve_token(token)
297 self._validate_token(token)
298 uuid = self._uuids[token]
299 astakos = self._astakos[uuid]
300 if set(usernames or []).difference(self._usernames2uuids[uuid]):
301 self._usernames2uuids[uuid].update(astakos.get_uuids(usernames))
302 return self._usernames2uuids[uuid]