Statistics
| Branch: | Tag: | Revision:

root / kamaki / clients / astakos / __init__.py @ 736072e2

History | View | Annotate | Download (11.1 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 compatibility in kamaki clients"""
43

    
44
    def __init__(self, *args, **kwargs):
45
        if args:
46
            args = list(args)
47
            url = args.pop(0)
48
            token = args.pop(0) if args else kwargs.pop('token', None)
49
            args = tuple([token, url] + args)
50
        elif 'base_url' in kwargs:
51
            kwargs['auth_url'] = kwargs.get('auth_url', kwargs['base_url'])
52
        super(AstakosClient, self).__init__(*args, **kwargs)
53

    
54
    def get_service_endpoints(self, service_type, version=None):
55
        services = parse_endpoints(
56
            self.get_endpoints(), ep_type=service_type, ep_version_id=version)
57
        return services[0]['endpoints'][0] if services else []
58

    
59
    @property
60
    def user_info(self):
61
        return self.authenticate()['access']['user']
62

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

    
66

    
67
def _astakos_error(foo):
68
    def wrap(self, *args, **kwargs):
69
        try:
70
            return foo(self, *args, **kwargs)
71
        except AstakosClientException as sace:
72
            self._raise_for_status(sace)
73
    return wrap
74

    
75

    
76
class LoggedAstakosClient(AstakosClient):
77
    """An AstakosClient wrapper with modified logging
78

79
    Logs are adjusted to appear similar to the ones of kamaki clients.
80
    No other changes are made to the parent class.
81
    """
82

    
83
    LOG_TOKEN = False
84
    LOG_DATA = False
85

    
86
    def _dump_response(self, request, status, message, data):
87
        recvlog.info('\n%d %s' % (status, message))
88
        recvlog.info('data size: %s' % len(data))
89
        if not self.LOG_TOKEN:
90
            token = request.headers.get('X-Auth-Token', '')
91
            if self.LOG_DATA:
92
                data = data.replace(token, '...') if token else data
93
        if self.LOG_DATA:
94
            recvlog.info(data)
95
        recvlog.info('-             -        -     -   -  - -')
96

    
97
    def _call_astakos(self, *args, **kwargs):
98
        r = super(LoggedAstakosClient, self)._call_astakos(*args, **kwargs)
99
        try:
100
            log_request = getattr(self, 'log_request', None)
101
            if log_request:
102
                req = RequestManager(
103
                    method=log_request['method'],
104
                    url='%s://%s' % (self.scheme, self.astakos_base_url),
105
                    path=log_request['path'],
106
                    data=log_request.get('body', None),
107
                    headers=log_request.get('headers', dict()))
108
                req.LOG_TOKEN, req.LOG_DATA = self.LOG_TOKEN, self.LOG_DATA
109
                req.dump_log()
110
                log_response = getattr(self, 'log_response', None)
111
                if log_response:
112
                    self._dump_response(
113
                        req,
114
                        status=log_response['status'],
115
                        message=log_response['message'],
116
                        data=log_response.get('data', ''))
117
        except Exception:
118
            pass
119
        finally:
120
            return r
121

    
122

    
123
class CachedAstakosClient(Client):
124
    """Synnefo Astakos cached client wraper"""
125

    
126
    @_astakos_error
127
    def __init__(self, base_url, token=None):
128
        super(CachedAstakosClient, self).__init__(base_url, token)
129
        self._astakos = dict()
130
        self._uuids = dict()
131
        self._cache = dict()
132
        self._uuids2usernames = dict()
133
        self._usernames2uuids = dict()
134

    
135
    def _resolve_token(self, token):
136
        """
137
        :returns: (str) a single token
138

139
        :raises AssertionError: if no token exists (either param or member)
140
        """
141
        token = token or self.token
142
        assert token, 'No token provided'
143
        return token[0] if (
144
            isinstance(token, list) or isinstance(token, tuple)) else token
145

    
146
    def get_client(self, token=None):
147
        """Get the Synnefo AstakosClient instance used by client"""
148
        token = self._resolve_token(token)
149
        self._validate_token(token)
150
        return self._astakos[self._uuids[token]]
151

    
152
    @_astakos_error
153
    def authenticate(self, token=None):
154
        """Get authentication information and store it in this client
155
        As long as the CachedAstakosClient instance is alive, the latest
156
        authentication information for this token will be available
157

158
        :param token: (str) custom token to authenticate
159
        """
160
        token = self._resolve_token(token)
161
        astakos = LoggedAstakosClient(
162
            self.base_url, token, logger=getLogger('astakosclient'))
163
        astakos.LOG_TOKEN = getattr(self, 'LOG_TOKEN', False)
164
        astakos.LOG_DATA = getattr(self, 'LOG_DATA', False)
165
        r = astakos.authenticate()
166
        uuid = r['access']['user']['id']
167
        self._uuids[token] = uuid
168
        self._cache[uuid] = r
169
        self._astakos[uuid] = astakos
170
        self._uuids2usernames[uuid] = dict()
171
        self._usernames2uuids[uuid] = dict()
172
        return self._cache[uuid]
173

    
174
    def remove_user(self, uuid):
175
        self._uuids.pop(self.get_token(uuid))
176
        self._cache.pop(uuid)
177
        self._astakos.pop(uuid)
178
        self._uuids2usernames.pop(uuid)
179
        self._usernames2uuids.pop(uuid)
180

    
181
    def get_token(self, uuid):
182
        return self._cache[uuid]['access']['token']['id']
183

    
184
    def _validate_token(self, token):
185
        if (token not in self._uuids) or (
186
                self.get_token(self._uuids[token]) != token):
187
            self._uuids.pop(token, None)
188
            self.authenticate(token)
189

    
190
    def get_services(self, token=None):
191
        """
192
        :returns: (list) [{name:..., type:..., endpoints:[...]}, ...]
193
        """
194
        token = self._resolve_token(token)
195
        self._validate_token(token)
196
        r = self._cache[self._uuids[token]]
197
        return r['access']['serviceCatalog']
198

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

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

205
        :raises ClientError: (600) if service_type not in service catalog
206
        """
207
        services = self.get_services(token)
208
        for service in services:
209
            try:
210
                if service['type'].lower() == service_type.lower():
211
                    return service
212
            except KeyError:
213
                self.log.warning('Misformated service %s' % service)
214
        raise ClientError(
215
            'Service type "%s" not in service catalog' % service_type, 600)
216

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

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

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

225
        :raises ClientError: (600) if service_type not in service catalog
226

227
        :raises ClientError: (601) if #matching endpoints != 1
228
        """
229
        service = self.get_service_details(service_type, token)
230
        matches = []
231
        for endpoint in service['endpoints']:
232
            if (not version) or (
233
                    endpoint['versionId'].lower() == version.lower()):
234
                matches.append(endpoint)
235
        if len(matches) != 1:
236
            raise ClientError(
237
                '%s endpoints match type %s %s' % (
238
                    len(matches), service_type,
239
                    ('and versionId %s' % version) if version else ''),
240
                601)
241
        return matches[0]
242

    
243
    def list_users(self):
244
        """list cached users information"""
245
        if not self._cache:
246
            self.authenticate()
247
        r = []
248
        for k, v in self._cache.items():
249
            r.append(dict(v['access']['user']))
250
            r[-1].update(dict(auth_token=self.get_token(k)))
251
        return r
252

    
253
    def user_info(self, token=None):
254
        """Get (cached) user information"""
255
        token = self._resolve_token(token)
256
        self._validate_token(token)
257
        r = self._cache[self._uuids[token]]
258
        return r['access']['user']
259

    
260
    def term(self, key, token=None):
261
        """Get (cached) term, from user credentials"""
262
        return self.user_term(key, token)
263

    
264
    def user_term(self, key, token=None):
265
        """Get (cached) term, from user credentials"""
266
        return self.user_info(token).get(key, None)
267

    
268
    def post_user_catalogs(self, uuids=None, displaynames=None, token=None):
269
        """POST base_url/user_catalogs
270

271
        :param uuids: (list or tuple) user uuids
272

273
        :param displaynames: (list or tuple) usernames (mut. excl. to uuids)
274

275
        :returns: (dict) {uuid1: name1, uuid2: name2, ...} or oposite
276
        """
277
        return self.uuids2usernames(uuids, token) if (
278
            uuids) else self.usernames2uuids(displaynames, token)
279

    
280
    @_astakos_error
281
    def uuids2usernames(self, uuids, token=None):
282
        token = self._resolve_token(token)
283
        self._validate_token(token)
284
        uuid = self._uuids[token]
285
        astakos = self._astakos[uuid]
286
        if set(uuids or []).difference(self._uuids2usernames[uuid]):
287
            self._uuids2usernames[uuid].update(astakos.get_usernames(uuids))
288
        return self._uuids2usernames[uuid]
289

    
290
    @_astakos_error
291
    def usernames2uuids(self, usernames, token=None):
292
        token = self._resolve_token(token)
293
        self._validate_token(token)
294
        uuid = self._uuids[token]
295
        astakos = self._astakos[uuid]
296
        if set(usernames or []).difference(self._usernames2uuids[uuid]):
297
            self._usernames2uuids[uuid].update(astakos.get_uuids(usernames))
298
        return self._usernames2uuids[uuid]