Update version tag and Changelog
[kamaki] / kamaki / clients / astakos / __init__.py
1 # Copyright 2012-2013 GRNET S.A. All rights reserved.
2 #
3 # Redistribution and use in source and binary forms, with or
4 # without modification, are permitted provided that the following
5 # conditions are met:
6 #
7 #   1. Redistributions of source code must retain the above
8 #      copyright notice, this list of conditions and the following
9 #      disclaimer.
10 #
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.
15 #
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.
28 #
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.
33
34 from logging import getLogger
35 from astakosclient import AstakosClient as OriginalAstakosClient
36 from astakosclient import AstakosClientException, parse_endpoints
37
38 from kamaki.clients import Client, ClientError, RequestManager, recvlog
39
40
41 class AstakosClient(OriginalAstakosClient):
42     """Wrap Original AstakosClient to ensure bw compatibility and ease of use
43
44     Note that this is an ancached class, so each call produces at least one
45     new http request
46     """
47
48     def __init__(self, *args, **kwargs):
49         if args:
50             args = list(args)
51             url = args.pop(0)
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)
57
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 []
62
63     @property
64     def user_info(self):
65         return self.authenticate()['access']['user']
66
67     def user_term(self, term):
68         return self.user_info[term]
69
70
71 def _astakos_error(foo):
72     def wrap(self, *args, **kwargs):
73         try:
74             return foo(self, *args, **kwargs)
75         except AstakosClientException as sace:
76             self._raise_for_status(sace)
77     return wrap
78
79
80 class LoggedAstakosClient(AstakosClient):
81     """An AstakosClient wrapper with modified logging
82
83     Logs are adjusted to appear similar to the ones of kamaki clients.
84     No other changes are made to the parent class.
85     """
86
87     LOG_TOKEN = False
88     LOG_DATA = False
89
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', '')
95             if self.LOG_DATA:
96                 data = data.replace(token, '...') if token else data
97         if self.LOG_DATA:
98             recvlog.info(data)
99         recvlog.info('-             -        -     -   -  - -')
100
101     def _call_astakos(self, *args, **kwargs):
102         r = super(LoggedAstakosClient, self)._call_astakos(*args, **kwargs)
103         try:
104             log_request = getattr(self, 'log_request', None)
105             if log_request:
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
113                 req.dump_log()
114                 log_response = getattr(self, 'log_response', None)
115                 if log_response:
116                     self._dump_response(
117                         req,
118                         status=log_response['status'],
119                         message=log_response['message'],
120                         data=log_response.get('data', ''))
121         except Exception:
122             pass
123         finally:
124             return r
125
126
127 class CachedAstakosClient(Client):
128     """Synnefo Astakos cached client wraper"""
129
130     @_astakos_error
131     def __init__(self, base_url, token=None):
132         super(CachedAstakosClient, self).__init__(base_url, token)
133         self._astakos = dict()
134         self._uuids = dict()
135         self._cache = dict()
136         self._uuids2usernames = dict()
137         self._usernames2uuids = dict()
138
139     def _resolve_token(self, token):
140         """
141         :returns: (str) a single token
142
143         :raises AssertionError: if no token exists (either param or member)
144         """
145         token = token or self.token
146         assert token, 'No token provided'
147         return token[0] if (
148             isinstance(token, list) or isinstance(token, tuple)) else token
149
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]]
155
156     @_astakos_error
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
161
162         :param token: (str) custom token to authenticate
163         """
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]
177
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)
184
185     def get_token(self, uuid):
186         return self._cache[uuid]['access']['token']['id']
187
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)
193
194     def get_services(self, token=None):
195         """
196         :returns: (list) [{name:..., type:..., endpoints:[...]}, ...]
197         """
198         token = self._resolve_token(token)
199         self._validate_token(token)
200         r = self._cache[self._uuids[token]]
201         return r['access']['serviceCatalog']
202
203     def get_service_details(self, service_type, token=None):
204         """
205         :param service_type: (str) compute, object-store, image, account, etc.
206
207         :returns: (dict) {name:..., type:..., endpoints:[...]}
208
209         :raises ClientError: (600) if service_type not in service catalog
210         """
211         services = self.get_services(token)
212         for service in services:
213             try:
214                 if service['type'].lower() == service_type.lower():
215                     return service
216             except KeyError:
217                 self.log.warning('Misformated service %s' % service)
218         raise ClientError(
219             'Service type "%s" not in service catalog' % service_type, 600)
220
221     def get_service_endpoints(self, service_type, version=None, token=None):
222         """
223         :param service_type: (str) can be compute, object-store, etc.
224
225         :param version: (str) the version id of the service
226
227         :returns: (dict) {SNF:uiURL, adminURL, internalURL, publicURL, ...}
228
229         :raises ClientError: (600) if service_type not in service catalog
230
231         :raises ClientError: (601) if #matching endpoints != 1
232         """
233         service = self.get_service_details(service_type, token)
234         matches = []
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:
240             raise ClientError(
241                 '%s endpoints match type %s %s' % (
242                     len(matches), service_type,
243                     ('and versionId %s' % version) if version else ''),
244                 601)
245         return matches[0]
246
247     def list_users(self):
248         """list cached users information"""
249         if not self._cache:
250             self.authenticate()
251         r = []
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)))
255         return r
256
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']
263
264     def term(self, key, token=None):
265         """Get (cached) term, from user credentials"""
266         return self.user_term(key, token)
267
268     def user_term(self, key, token=None):
269         """Get (cached) term, from user credentials"""
270         return self.user_info(token).get(key, None)
271
272     def post_user_catalogs(self, uuids=None, displaynames=None, token=None):
273         """POST base_url/user_catalogs
274
275         :param uuids: (list or tuple) user uuids
276
277         :param displaynames: (list or tuple) usernames (mut. excl. to uuids)
278
279         :returns: (dict) {uuid1: name1, uuid2: name2, ...} or oposite
280         """
281         return self.uuids2usernames(uuids, token) if (
282             uuids) else self.usernames2uuids(displaynames, token)
283
284     @_astakos_error
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]
293
294     @_astakos_error
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]