Statistics
| Branch: | Tag: | Revision:

root / kamaki / clients / astakos / __init__.py @ 8b4ba753

History | View | Annotate | Download (11.5 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 AstakosClientError(AstakosClientException, ClientError):
42
    """Join AstakosClientException as ClientError in one class"""
43

    
44

    
45
def _astakos_error(foo):
46
    def wrap(self, *args, **kwargs):
47
        try:
48
            return foo(self, *args, **kwargs)
49
        except AstakosClientException as sace:
50
            raise AstakosClientError('%s' % sace, sace.status, sace.details)
51
    return wrap
52

    
53

    
54
class AstakosClient(OriginalAstakosClient):
55
    """Wrap Original AstakosClient to ensure compatibility in kamaki clients"""
56

    
57
    @_astakos_error
58
    def __init__(self, *args, **kwargs):
59
        if args:
60
            args = list(args)
61
            url = args.pop(0)
62
            token = args.pop(0) if args else kwargs.pop('token', None)
63
            args = tuple([token, url] + args)
64
        elif 'base_url' in kwargs:
65
            kwargs['auth_url'] = kwargs.get('auth_url', kwargs['base_url'])
66
        super(AstakosClient, self).__init__(*args, **kwargs)
67

    
68
    def get_service_endpoints(self, service_type, version=None):
69
        services = parse_endpoints(
70
            self.get_endpoints(), ep_type=service_type, ep_version_id=version)
71
        return services[0]['endpoints'][0] if services else []
72

    
73
    @property
74
    def user_info(self):
75
        return self.authenticate()['access']['user']
76

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

    
80

    
81
#  Wrap AstakosClient public methods to raise AstakosClientError
82
from inspect import getmembers
83
for m in getmembers(AstakosClient):
84
    if hasattr(m[1], '__call__') and not ('%s' % m[0]).startswith('_'):
85
        setattr(AstakosClient, m[0], _astakos_error(m[1]))
86

    
87

    
88
class LoggedAstakosClient(AstakosClient):
89
    """An AstakosClient wrapper with modified logging
90

91
    Logs are adjusted to appear similar to the ones of kamaki clients.
92
    No other changes are made to the parent class.
93
    """
94

    
95
    LOG_TOKEN = False
96
    LOG_DATA = False
97

    
98
    def _dump_response(self, request, status, message, data):
99
        recvlog.info('\n%d %s' % (status, message))
100
        recvlog.info('data size: %s' % len(data))
101
        if not self.LOG_TOKEN:
102
            token = request.headers.get('X-Auth-Token', '')
103
            if self.LOG_DATA:
104
                data = data.replace(token, '...') if token else data
105
        if self.LOG_DATA:
106
            recvlog.info(data)
107
        recvlog.info('-             -        -     -   -  - -')
108

    
109
    def _call_astakos(self, *args, **kwargs):
110
        r = super(LoggedAstakosClient, self)._call_astakos(*args, **kwargs)
111
        try:
112
            log_request = getattr(self, 'log_request', None)
113
            if log_request:
114
                req = RequestManager(
115
                    method=log_request['method'],
116
                    url='%s://%s' % (self.scheme, self.astakos_base_url),
117
                    path=log_request['path'],
118
                    data=log_request.get('body', None),
119
                    headers=log_request.get('headers', dict()))
120
                req.LOG_TOKEN, req.LOG_DATA = self.LOG_TOKEN, self.LOG_DATA
121
                req.dump_log()
122
                log_response = getattr(self, 'log_response', None)
123
                if log_response:
124
                    self._dump_response(
125
                        req,
126
                        status=log_response['status'],
127
                        message=log_response['message'],
128
                        data=log_response.get('data', ''))
129
        except Exception:
130
            pass
131
        finally:
132
            return r
133

    
134

    
135
class CachedAstakosClient(Client):
136
    """Synnefo Astakos cached client wraper"""
137

    
138
    @_astakos_error
139
    def __init__(self, base_url, token=None):
140
        super(CachedAstakosClient, self).__init__(base_url, token)
141
        self._astakos = dict()
142
        self._uuids = dict()
143
        self._cache = dict()
144
        self._uuids2usernames = dict()
145
        self._usernames2uuids = dict()
146

    
147
    def _resolve_token(self, token):
148
        """
149
        :returns: (str) a single token
150

151
        :raises AssertionError: if no token exists (either param or member)
152
        """
153
        token = token or self.token
154
        assert token, 'No token provided'
155
        return token[0] if (
156
            isinstance(token, list) or isinstance(token, tuple)) else token
157

    
158
    def get_client(self, token=None):
159
        """Get the Synnefo AstakosClient instance used by client"""
160
        token = self._resolve_token(token)
161
        self._validate_token(token)
162
        return self._astakos[self._uuids[token]]
163

    
164
    @_astakos_error
165
    def authenticate(self, token=None):
166
        """Get authentication information and store it in this client
167
        As long as the CachedAstakosClient instance is alive, the latest
168
        authentication information for this token will be available
169

170
        :param token: (str) custom token to authenticate
171
        """
172
        token = self._resolve_token(token)
173
        astakos = LoggedAstakosClient(
174
            self.base_url, token, logger=getLogger('astakosclient'))
175
        astakos.LOG_TOKEN = getattr(self, 'LOG_TOKEN', False)
176
        astakos.LOG_DATA = getattr(self, 'LOG_DATA', False)
177
        r = astakos.authenticate()
178
        uuid = r['access']['user']['id']
179
        self._uuids[token] = uuid
180
        self._cache[uuid] = r
181
        self._astakos[uuid] = astakos
182
        self._uuids2usernames[uuid] = dict()
183
        self._usernames2uuids[uuid] = dict()
184
        return self._cache[uuid]
185

    
186
    def remove_user(self, uuid):
187
        self._uuids.pop(self.get_token(uuid))
188
        self._cache.pop(uuid)
189
        self._astakos.pop(uuid)
190
        self._uuids2usernames.pop(uuid)
191
        self._usernames2uuids.pop(uuid)
192

    
193
    def get_token(self, uuid):
194
        return self._cache[uuid]['access']['token']['id']
195

    
196
    def _validate_token(self, token):
197
        if (token not in self._uuids) or (
198
                self.get_token(self._uuids[token]) != token):
199
            self._uuids.pop(token, None)
200
            self.authenticate(token)
201

    
202
    def get_services(self, token=None):
203
        """
204
        :returns: (list) [{name:..., type:..., endpoints:[...]}, ...]
205
        """
206
        token = self._resolve_token(token)
207
        self._validate_token(token)
208
        r = self._cache[self._uuids[token]]
209
        return r['access']['serviceCatalog']
210

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

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

217
        :raises ClientError: (600) if service_type not in service catalog
218
        """
219
        services = self.get_services(token)
220
        for service in services:
221
            try:
222
                if service['type'].lower() == service_type.lower():
223
                    return service
224
            except KeyError:
225
                self.log.warning('Misformated service %s' % service)
226
        raise ClientError(
227
            'Service type "%s" not in service catalog' % service_type, 600)
228

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

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

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

237
        :raises ClientError: (600) if service_type not in service catalog
238

239
        :raises ClientError: (601) if #matching endpoints != 1
240
        """
241
        service = self.get_service_details(service_type, token)
242
        matches = []
243
        for endpoint in service['endpoints']:
244
            if (not version) or (
245
                    endpoint['versionId'].lower() == version.lower()):
246
                matches.append(endpoint)
247
        if len(matches) != 1:
248
            raise ClientError(
249
                '%s endpoints match type %s %s' % (
250
                    len(matches), service_type,
251
                    ('and versionId %s' % version) if version else ''),
252
                601)
253
        return matches[0]
254

    
255
    def list_users(self):
256
        """list cached users information"""
257
        if not self._cache:
258
            self.authenticate()
259
        r = []
260
        for k, v in self._cache.items():
261
            r.append(dict(v['access']['user']))
262
            r[-1].update(dict(auth_token=self.get_token(k)))
263
        return r
264

    
265
    def user_info(self, token=None):
266
        """Get (cached) user information"""
267
        token = self._resolve_token(token)
268
        self._validate_token(token)
269
        r = self._cache[self._uuids[token]]
270
        return r['access']['user']
271

    
272
    def term(self, key, token=None):
273
        """Get (cached) term, from user credentials"""
274
        return self.user_term(key, token)
275

    
276
    def user_term(self, key, token=None):
277
        """Get (cached) term, from user credentials"""
278
        return self.user_info(token).get(key, None)
279

    
280
    def post_user_catalogs(self, uuids=None, displaynames=None, token=None):
281
        """POST base_url/user_catalogs
282

283
        :param uuids: (list or tuple) user uuids
284

285
        :param displaynames: (list or tuple) usernames (mut. excl. to uuids)
286

287
        :returns: (dict) {uuid1: name1, uuid2: name2, ...} or oposite
288
        """
289
        return self.uuids2usernames(uuids, token) if (
290
            uuids) else self.usernames2uuids(displaynames, token)
291

    
292
    @_astakos_error
293
    def uuids2usernames(self, uuids, token=None):
294
        token = self._resolve_token(token)
295
        self._validate_token(token)
296
        uuid = self._uuids[token]
297
        astakos = self._astakos[uuid]
298
        if set(uuids or []).difference(self._uuids2usernames[uuid]):
299
            self._uuids2usernames[uuid].update(astakos.get_usernames(uuids))
300
        return self._uuids2usernames[uuid]
301

    
302
    @_astakos_error
303
    def usernames2uuids(self, usernames, token=None):
304
        token = self._resolve_token(token)
305
        self._validate_token(token)
306
        uuid = self._uuids[token]
307
        astakos = self._astakos[uuid]
308
        if set(usernames or []).difference(self._usernames2uuids[uuid]):
309
            self._usernames2uuids[uuid].update(astakos.get_uuids(usernames))
310
        return self._usernames2uuids[uuid]