Statistics
| Branch: | Tag: | Revision:

root / kamaki / clients / astakos / __init__.py @ 493f5877

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 AstakosClientError: 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 AstakosClientError(
227
            'Service type "%s" not in service catalog' % service_type)
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 AstakosClientError: if service_type not in service catalog, or
238
            if #matching endpoints != 1
239
        """
240
        service = self.get_service_details(service_type, token)
241
        matches = []
242
        for endpoint in service['endpoints']:
243
            if (not version) or (
244
                    endpoint['versionId'].lower() == version.lower()):
245
                matches.append(endpoint)
246
        if len(matches) != 1:
247
            raise AstakosClientError(
248
                '%s endpoints match type %s %s' % (
249
                    len(matches), service_type,
250
                    ('and versionId %s' % version) if version else ''),
251
                601)
252
        return matches[0]
253

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

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

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

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

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

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

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

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

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

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