__init__.py 12.5 KB
Newer Older
1
# Copyright 2012-2014 GRNET S.A. All rights reserved.
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#
# Redistribution and use in source and binary forms, with or
# without modification, are permitted provided that the following
# conditions are met:
#
#   1. Redistributions of source code must retain the above
#      copyright notice, this list of conditions and the following
#      disclaimer.
#
#   2. Redistributions in binary form must reproduce the above
#      copyright notice, this list of conditions and the following
#      disclaimer in the documentation and/or other materials
#      provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
#
# The views and conclusions contained in the software and
# documentation are those of the authors and should not be
# interpreted as representing official policies, either expressed
# or implied, of GRNET S.A.

34
from logging import getLogger
35
import inspect
36
37
import ssl

38
from astakosclient import AstakosClientException, parse_endpoints
39
import astakosclient
40

41
42
from kamaki.clients import (
    Client, ClientError, KamakiSSLError, RequestManager, recvlog)
43

44
45
from kamaki.clients.utils import https

46

47
class AstakosClientError(ClientError, AstakosClientException):
48
49
    """Join AstakosClientException as ClientError in one class"""

50
51
52
    def __init__(self, message='Astakos Client Error', details='', status=0):
        super(ClientError, self).__init__(message, details, status)

53
54
55
56
57
58

def _astakos_error(foo):
    def wrap(self, *args, **kwargs):
        try:
            return foo(self, *args, **kwargs)
        except AstakosClientException as sace:
59
60
            if isinstance(getattr(sace, 'errobject', None), ssl.SSLError):
                raise KamakiSSLError('SSL Connection error (%s)' % sace)
61
62
63
            raise AstakosClientError(
                getattr(sace, 'message', '%s' % sace),
                details=sace.details, status=sace.status)
64
65
66
    return wrap


67
68
69
70
71
72
#  Patch AstakosClient to support SSLAuthentication
astakosclient.utils.PooledHTTPConnection = https.PooledHTTPConnection
astakosclient.utils.HTTPSConnection = https.HTTPSClientAuthConnection
OriginalAstakosClient = astakosclient.AstakosClient


73
74
75
class AstakosClient(OriginalAstakosClient):
    """Wrap Original AstakosClient to ensure compatibility in kamaki clients"""

76
    @_astakos_error
77
78
79
80
81
82
    def __init__(self, *args, **kwargs):
        if args:
            args = list(args)
            url = args.pop(0)
            token = args.pop(0) if args else kwargs.pop('token', None)
            args = tuple([token, url] + args)
83
84
85
        else:
            kwargs['auth_url'] = kwargs.get('auth_url', kwargs.get(
                'endpoint_url', kwargs['base_url']))
86
87
        super(AstakosClient, self).__init__(*args, **kwargs)

88
89
90
    def get_service_endpoints(self, service_type, version=None):
        services = parse_endpoints(
            self.get_endpoints(), ep_type=service_type, ep_version_id=version)
91
92
93
94
        return services[0]['endpoints'][0] if services else {}

    def get_endpoint_url(self, service_type, version=None):
        return self.get_service_endpoints(service_type, version)['publicURL']
95

96
97
98
99
100
101
102
    @property
    def user_info(self):
        return self.authenticate()['access']['user']

    def user_term(self, term):
        return self.user_info[term]

103

104
#  Wrap AstakosClient public methods to raise AstakosClientError
105
for m in inspect.getmembers(AstakosClient):
106
107
    if hasattr(m[1], '__call__') and not ('%s' % m[0]).startswith('_'):
        setattr(AstakosClient, m[0], _astakos_error(m[1]))
108
109


110
111
112
113
114
115
class LoggedAstakosClient(AstakosClient):
    """An AstakosClient wrapper with modified logging

    Logs are adjusted to appear similar to the ones of kamaki clients.
    No other changes are made to the parent class.
    """
116
117
118

    LOG_TOKEN = False
    LOG_DATA = False
119
120

    def _dump_response(self, request, status, message, data):
121
122
        recvlog.info('\n%d %s' % (status, message))
        recvlog.info('data size: %s' % len(data))
123
124
125
126
127
128
        if not self.LOG_TOKEN:
            token = request.headers.get('X-Auth-Token', '')
            if self.LOG_DATA:
                data = data.replace(token, '...') if token else data
        if self.LOG_DATA:
            recvlog.info(data)
Stavros Sachtouris's avatar
Stavros Sachtouris committed
129
        recvlog.info('-             -        -     -   -  - -')
130
131

    def _call_astakos(self, *args, **kwargs):
132
        r = super(LoggedAstakosClient, self)._call_astakos(*args, **kwargs)
133
134
135
136
137
        try:
            log_request = getattr(self, 'log_request', None)
            if log_request:
                req = RequestManager(
                    method=log_request['method'],
138
                    url='%s://%s' % (self.scheme, self.astakos_base_url),
139
140
141
                    path=log_request['path'],
                    data=log_request.get('body', None),
                    headers=log_request.get('headers', dict()))
142
                req.LOG_TOKEN, req.LOG_DATA = self.LOG_TOKEN, self.LOG_DATA
143
144
145
146
147
148
149
150
                req.dump_log()
                log_response = getattr(self, 'log_response', None)
                if log_response:
                    self._dump_response(
                        req,
                        status=log_response['status'],
                        message=log_response['message'],
                        data=log_response.get('data', ''))
151
        except Exception:
152
            recvlog.debug('Kamaki failed to log an AstakosClient call')
153
154
155
156
        finally:
            return r


157
class CachedAstakosClient(Client):
158
    """Synnefo Astakos cached client wraper"""
159
    service_type = 'identity'
Giorgos Verigakis's avatar
Giorgos Verigakis committed
160

161
    @_astakos_error
162
163
    def __init__(self, endpoint_url, token=None):
        super(CachedAstakosClient, self).__init__(endpoint_url, token)
164
165
166
167
168
169
170
171
172
173
174
175
        self._astakos = dict()
        self._uuids = dict()
        self._cache = dict()
        self._uuids2usernames = dict()
        self._usernames2uuids = dict()

    def _resolve_token(self, token):
        """
        :returns: (str) a single token

        :raises AssertionError: if no token exists (either param or member)
        """
176
        token = token or self.token
177
178
179
180
        assert token, 'No token provided'
        return token[0] if (
            isinstance(token, list) or isinstance(token, tuple)) else token

181
182
183
184
185
186
    def get_client(self, token=None):
        """Get the Synnefo AstakosClient instance used by client"""
        token = self._resolve_token(token)
        self._validate_token(token)
        return self._astakos[self._uuids[token]]

187
    @_astakos_error
188
    def authenticate(self, token=None):
189
        """Get authentication information and store it in this client
190
        As long as the CachedAstakosClient instance is alive, the latest
191
192
        authentication information for this token will be available

193
194
        :param token: (str) custom token to authenticate
        """
195
        token = self._resolve_token(token)
196
        astakos = LoggedAstakosClient(
197
            self.endpoint_url, token, logger=getLogger('astakosclient'))
198
199
        astakos.LOG_TOKEN = getattr(self, 'LOG_TOKEN', False)
        astakos.LOG_DATA = getattr(self, 'LOG_DATA', False)
200
        r = astakos.authenticate()
201
        uuid = r['access']['user']['id']
202
        self._uuids[token] = uuid
203
        self._cache[uuid] = r
204
        self._astakos[uuid] = astakos
205
206
        self._uuids2usernames[uuid] = dict()
        self._usernames2uuids[uuid] = dict()
207
        return self._cache[uuid]
208

209
210
211
212
213
214
215
    def remove_user(self, uuid):
        self._uuids.pop(self.get_token(uuid))
        self._cache.pop(uuid)
        self._astakos.pop(uuid)
        self._uuids2usernames.pop(uuid)
        self._usernames2uuids.pop(uuid)

216
217
    def get_token(self, uuid):
        return self._cache[uuid]['access']['token']['id']
218

219
220
221
222
223
224
    def _validate_token(self, token):
        if (token not in self._uuids) or (
                self.get_token(self._uuids[token]) != token):
            self._uuids.pop(token, None)
            self.authenticate(token)

225
226
227
228
    def get_services(self, token=None):
        """
        :returns: (list) [{name:..., type:..., endpoints:[...]}, ...]
        """
229
230
231
        token = self._resolve_token(token)
        self._validate_token(token)
        r = self._cache[self._uuids[token]]
232
        return r['access']['serviceCatalog']
233
234
235
236
237
238
239

    def get_service_details(self, service_type, token=None):
        """
        :param service_type: (str) compute, object-store, image, account, etc.

        :returns: (dict) {name:..., type:..., endpoints:[...]}

240
        :raises AstakosClientError: if service_type not in service catalog
241
242
243
244
245
246
247
248
        """
        services = self.get_services(token)
        for service in services:
            try:
                if service['type'].lower() == service_type.lower():
                    return service
            except KeyError:
                self.log.warning('Misformated service %s' % service)
249
250
        raise AstakosClientError(
            'Service type "%s" not in service catalog' % service_type)
251
252
253
254
255
256
257
258
259

    def get_service_endpoints(self, service_type, version=None, token=None):
        """
        :param service_type: (str) can be compute, object-store, etc.

        :param version: (str) the version id of the service

        :returns: (dict) {SNF:uiURL, adminURL, internalURL, publicURL, ...}

260
261
        :raises AstakosClientError: if service_type not in service catalog, or
            if #matching endpoints != 1
262
263
264
265
266
        """
        service = self.get_service_details(service_type, token)
        matches = []
        for endpoint in service['endpoints']:
            if (not version) or (
267
                    endpoint['versionId'].lower() == version.lower()):
268
269
                matches.append(endpoint)
        if len(matches) != 1:
270
            raise AstakosClientError(
271
272
                '%s endpoints match type %s %s' % (
                    len(matches), service_type,
273
                    ('and versionId %s' % version) if version else ''))
274
275
        return matches[0]

276
277
278
279
    def get_endpoint_url(self, service_type, version=None, token=None):
        r = self.get_service_endpoints(service_type, version, token)
        return r['publicURL']

280
281
    def list_users(self):
        """list cached users information"""
282
283
        if not self._cache:
            self.authenticate()
284
285
        r = []
        for k, v in self._cache.items():
286
            r.append(dict(v['access']['user']))
287
            r[-1].update(dict(auth_token=self.get_token(k)))
288
        return r
289

290
    def user_info(self, token=None):
291
        """Get (cached) user information"""
292
293
294
        token = self._resolve_token(token)
        self._validate_token(token)
        r = self._cache[self._uuids[token]]
295
        return r['access']['user']
296

297
    def term(self, key, token=None):
298
299
300
301
        """Get (cached) term, from user credentials"""
        return self.user_term(key, token)

    def user_term(self, key, token=None):
302
        """Get (cached) term, from user credentials"""
303
        return self.user_info(token).get(key, None)
304

305
    def post_user_catalogs(self, uuids=None, displaynames=None, token=None):
306
        """POST endpoint_url/user_catalogs
307
308
309

        :param uuids: (list or tuple) user uuids

310
311
312
        :param displaynames: (list or tuple) usernames (mut. excl. to uuids)

        :returns: (dict) {uuid1: name1, uuid2: name2, ...} or oposite
313
        """
314
        return self.uuids2usernames(uuids, token) if (
315
            uuids) else self.usernames2uuids(displaynames, token)
316

317
    @_astakos_error
318
319
320
    def uuids2usernames(self, uuids, token=None):
        token = self._resolve_token(token)
        self._validate_token(token)
321
322
        uuid = self._uuids[token]
        astakos = self._astakos[uuid]
323
        if set(uuids or []).difference(self._uuids2usernames[uuid]):
324
325
            self._uuids2usernames[uuid].update(astakos.get_usernames(uuids))
        return self._uuids2usernames[uuid]
326

327
    @_astakos_error
328
329
330
    def usernames2uuids(self, usernames, token=None):
        token = self._resolve_token(token)
        self._validate_token(token)
331
332
        uuid = self._uuids[token]
        astakos = self._astakos[uuid]
333
        if set(usernames or []).difference(self._usernames2uuids[uuid]):
334
335
            self._usernames2uuids[uuid].update(astakos.get_uuids(usernames))
        return self._usernames2uuids[uuid]