__init__.py 12.4 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
from astakosclient import AstakosClientException, parse_endpoints
37
import astakosclient
38

39
from kamaki.clients import Client, ClientError, RequestManager, recvlog
40

41
42
from kamaki.clients.utils import https

43

44
class AstakosClientError(ClientError, AstakosClientException):
45
46
    """Join AstakosClientException as ClientError in one class"""

47
48
49
    def __init__(self, message='Astakos Client Error', details='', status=0):
        super(ClientError, self).__init__(message, details, status)

50
51
52
53
54
55

def _astakos_error(foo):
    def wrap(self, *args, **kwargs):
        try:
            return foo(self, *args, **kwargs)
        except AstakosClientException as sace:
56
57
58
            raise AstakosClientError(
                getattr(sace, 'message', '%s' % sace),
                details=sace.details, status=sace.status)
59
60
61
    return wrap


62
63
64
65
66
67
#  Patch AstakosClient to support SSLAuthentication
astakosclient.utils.PooledHTTPConnection = https.PooledHTTPConnection
astakosclient.utils.HTTPSConnection = https.HTTPSClientAuthConnection
OriginalAstakosClient = astakosclient.AstakosClient


68
69
70
class AstakosClient(OriginalAstakosClient):
    """Wrap Original AstakosClient to ensure compatibility in kamaki clients"""

71
    @_astakos_error
72
73
74
75
76
77
    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)
78
79
80
        else:
            kwargs['auth_url'] = kwargs.get('auth_url', kwargs.get(
                'endpoint_url', kwargs['base_url']))
81
82
        super(AstakosClient, self).__init__(*args, **kwargs)

83
84
85
    def get_service_endpoints(self, service_type, version=None):
        services = parse_endpoints(
            self.get_endpoints(), ep_type=service_type, ep_version_id=version)
86
87
88
89
        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']
90

91
92
93
94
95
96
97
    @property
    def user_info(self):
        return self.authenticate()['access']['user']

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

98

99
#  Wrap AstakosClient public methods to raise AstakosClientError
100
for m in inspect.getmembers(AstakosClient):
101
102
    if hasattr(m[1], '__call__') and not ('%s' % m[0]).startswith('_'):
        setattr(AstakosClient, m[0], _astakos_error(m[1]))
103
104


105
106
107
108
109
110
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.
    """
111
112
113

    LOG_TOKEN = False
    LOG_DATA = False
114
115

    def _dump_response(self, request, status, message, data):
116
117
        recvlog.info('\n%d %s' % (status, message))
        recvlog.info('data size: %s' % len(data))
118
119
120
121
122
123
        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
124
        recvlog.info('-             -        -     -   -  - -')
125
126

    def _call_astakos(self, *args, **kwargs):
127
        r = super(LoggedAstakosClient, self)._call_astakos(*args, **kwargs)
128
129
130
131
132
        try:
            log_request = getattr(self, 'log_request', None)
            if log_request:
                req = RequestManager(
                    method=log_request['method'],
133
                    url='%s://%s' % (self.scheme, self.astakos_base_url),
134
135
136
                    path=log_request['path'],
                    data=log_request.get('body', None),
                    headers=log_request.get('headers', dict()))
137
                req.LOG_TOKEN, req.LOG_DATA = self.LOG_TOKEN, self.LOG_DATA
138
139
140
141
142
143
144
145
                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', ''))
146
        except Exception:
147
            recvlog.debug('Kamaki failed to log an AstakosClient call')
148
149
150
151
        finally:
            return r


152
class CachedAstakosClient(Client):
153
    """Synnefo Astakos cached client wraper"""
154
    service_type = 'identity'
Giorgos Verigakis's avatar
Giorgos Verigakis committed
155

156
    @_astakos_error
157
158
    def __init__(self, endpoint_url, token=None):
        super(CachedAstakosClient, self).__init__(endpoint_url, token)
159
160
161
162
163
164
165
166
167
168
169
170
        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)
        """
171
        token = token or self.token
172
173
174
175
        assert token, 'No token provided'
        return token[0] if (
            isinstance(token, list) or isinstance(token, tuple)) else token

176
177
178
179
180
181
    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]]

182
    @_astakos_error
183
    def authenticate(self, token=None):
184
        """Get authentication information and store it in this client
185
        As long as the CachedAstakosClient instance is alive, the latest
186
187
        authentication information for this token will be available

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

204
205
206
207
208
209
210
    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)

211
212
    def get_token(self, uuid):
        return self._cache[uuid]['access']['token']['id']
213

214
215
216
217
218
219
    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)

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

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

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

235
        :raises AstakosClientError: if service_type not in service catalog
236
237
238
239
240
241
242
243
        """
        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)
244
245
        raise AstakosClientError(
            'Service type "%s" not in service catalog' % service_type)
246
247
248
249
250
251
252
253
254

    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, ...}

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

271
272
273
274
    def get_endpoint_url(self, service_type, version=None, token=None):
        r = self.get_service_endpoints(service_type, version, token)
        return r['publicURL']

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

285
    def user_info(self, token=None):
286
        """Get (cached) user information"""
287
288
289
        token = self._resolve_token(token)
        self._validate_token(token)
        r = self._cache[self._uuids[token]]
290
        return r['access']['user']
291

292
    def term(self, key, token=None):
293
294
295
296
        """Get (cached) term, from user credentials"""
        return self.user_term(key, token)

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

300
    def post_user_catalogs(self, uuids=None, displaynames=None, token=None):
301
        """POST endpoint_url/user_catalogs
302
303
304

        :param uuids: (list or tuple) user uuids

305
306
307
        :param displaynames: (list or tuple) usernames (mut. excl. to uuids)

        :returns: (dict) {uuid1: name1, uuid2: name2, ...} or oposite
308
        """
309
        return self.uuids2usernames(uuids, token) if (
310
            uuids) else self.usernames2uuids(displaynames, token)
311

312
    @_astakos_error
313
314
315
    def uuids2usernames(self, uuids, token=None):
        token = self._resolve_token(token)
        self._validate_token(token)
316
317
        uuid = self._uuids[token]
        astakos = self._astakos[uuid]
318
        if set(uuids or []).difference(self._uuids2usernames[uuid]):
319
320
            self._uuids2usernames[uuid].update(astakos.get_usernames(uuids))
        return self._uuids2usernames[uuid]
321

322
    @_astakos_error
323
324
325
    def usernames2uuids(self, usernames, token=None):
        token = self._resolve_token(token)
        self._validate_token(token)
326
327
        uuid = self._uuids[token]
        astakos = self._astakos[uuid]
328
        if set(usernames or []).difference(self._usernames2uuids[uuid]):
329
330
            self._usernames2uuids[uuid].update(astakos.get_uuids(usernames))
        return self._usernames2uuids[uuid]