Statistics
| Branch: | Tag: | Revision:

root / kamaki / clients / astakos / __init__.py @ 3a190998

History | View | Annotate | Download (11.2 kB)

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]