Statistics
| Branch: | Tag: | Revision:

root / kamaki / clients / astakos / __init__.py @ cd42f8d8

History | View | Annotate | Download (10.7 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

    
55
def _astakos_error(foo):
56
    def wrap(self, *args, **kwargs):
57
        try:
58
            return foo(self, *args, **kwargs)
59
        except AstakosClientException as sace:
60
            self._raise_for_status(sace)
61
    return wrap
62

    
63

    
64
class LoggedAstakosClient(AstakosClient):
65
    """An AstakosClient wrapper with modified logging
66

67
    Logs are adjusted to appear similar to the ones of kamaki clients.
68
    No other changes are made to the parent class.
69
    """
70

    
71
    LOG_TOKEN = False
72
    LOG_DATA = False
73

    
74
    def _dump_response(self, request, status, message, data):
75
        recvlog.info('\n%d %s' % (status, message))
76
        recvlog.info('data size: %s' % len(data))
77
        if not self.LOG_TOKEN:
78
            token = request.headers.get('X-Auth-Token', '')
79
            if self.LOG_DATA:
80
                data = data.replace(token, '...') if token else data
81
        if self.LOG_DATA:
82
            recvlog.info(data)
83
        recvlog.info('-             -        -     -   -  - -')
84

    
85
    def _call_astakos(self, *args, **kwargs):
86
        r = super(LoggedAstakosClient, self)._call_astakos(*args, **kwargs)
87
        try:
88
            log_request = getattr(self, 'log_request', None)
89
            if log_request:
90
                req = RequestManager(
91
                    method=log_request['method'],
92
                    url='%s://%s' % (self.scheme, self.astakos_base_url),
93
                    path=log_request['path'],
94
                    data=log_request.get('body', None),
95
                    headers=log_request.get('headers', dict()))
96
                req.LOG_TOKEN, req.LOG_DATA = self.LOG_TOKEN, self.LOG_DATA
97
                req.dump_log()
98
                log_response = getattr(self, 'log_response', None)
99
                if log_response:
100
                    self._dump_response(
101
                        req,
102
                        status=log_response['status'],
103
                        message=log_response['message'],
104
                        data=log_response.get('data', ''))
105
        except Exception:
106
            pass
107
        finally:
108
            return r
109

    
110

    
111
class CachedAstakosClient(Client):
112
    """Synnefo Astakos cached client wraper"""
113

    
114
    @_astakos_error
115
    def __init__(self, base_url, token=None):
116
        super(CachedAstakosClient, self).__init__(base_url, token)
117
        self._astakos = dict()
118
        self._uuids = dict()
119
        self._cache = dict()
120
        self._uuids2usernames = dict()
121
        self._usernames2uuids = dict()
122

    
123
    def _resolve_token(self, token):
124
        """
125
        :returns: (str) a single token
126

127
        :raises AssertionError: if no token exists (either param or member)
128
        """
129
        token = token or self.token
130
        assert token, 'No token provided'
131
        return token[0] if (
132
            isinstance(token, list) or isinstance(token, tuple)) else token
133

    
134
    def get_client(self, token=None):
135
        """Get the Synnefo AstakosClient instance used by client"""
136
        token = self._resolve_token(token)
137
        self._validate_token(token)
138
        return self._astakos[self._uuids[token]]
139

    
140
    @_astakos_error
141
    def authenticate(self, token=None):
142
        """Get authentication information and store it in this client
143
        As long as the CachedAstakosClient instance is alive, the latest
144
        authentication information for this token will be available
145

146
        :param token: (str) custom token to authenticate
147
        """
148
        token = self._resolve_token(token)
149
        astakos = LoggedAstakosClient(
150
            self.base_url, token, logger=getLogger('astakosclient'))
151
        astakos.LOG_TOKEN = getattr(self, 'LOG_TOKEN', False)
152
        astakos.LOG_DATA = getattr(self, 'LOG_DATA', False)
153
        r = astakos.authenticate()
154
        uuid = r['access']['user']['id']
155
        self._uuids[token] = uuid
156
        self._cache[uuid] = r
157
        self._astakos[uuid] = astakos
158
        self._uuids2usernames[uuid] = dict()
159
        self._usernames2uuids[uuid] = dict()
160
        return self._cache[uuid]
161

    
162
    def remove_user(self, uuid):
163
        self._uuids.pop(self.get_token(uuid))
164
        self._cache.pop(uuid)
165
        self._astakos.pop(uuid)
166
        self._uuids2usernames.pop(uuid)
167
        self._usernames2uuids.pop(uuid)
168

    
169
    def get_token(self, uuid):
170
        return self._cache[uuid]['access']['token']['id']
171

    
172
    def _validate_token(self, token):
173
        if (token not in self._uuids) or (
174
                self.get_token(self._uuids[token]) != token):
175
            self._uuids.pop(token, None)
176
            self.authenticate(token)
177

    
178
    def get_services(self, token=None):
179
        """
180
        :returns: (list) [{name:..., type:..., endpoints:[...]}, ...]
181
        """
182
        token = self._resolve_token(token)
183
        self._validate_token(token)
184
        r = self._cache[self._uuids[token]]
185
        return r['access']['serviceCatalog']
186

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

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

193
        :raises ClientError: (600) if service_type not in service catalog
194
        """
195
        services = self.get_services(token)
196
        for service in services:
197
            try:
198
                if service['type'].lower() == service_type.lower():
199
                    return service
200
            except KeyError:
201
                self.log.warning('Misformated service %s' % service)
202
        raise ClientError(
203
            'Service type "%s" not in service catalog' % service_type, 600)
204

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

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

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

213
        :raises ClientError: (600) if service_type not in service catalog
214

215
        :raises ClientError: (601) if #matching endpoints != 1
216
        """
217
        service = self.get_service_details(service_type, token)
218
        matches = []
219
        for endpoint in service['endpoints']:
220
            if (not version) or (
221
                    endpoint['versionId'].lower() == version.lower()):
222
                matches.append(endpoint)
223
        if len(matches) != 1:
224
            raise ClientError(
225
                '%s endpoints match type %s %s' % (
226
                    len(matches), service_type,
227
                    ('and versionId %s' % version) if version else ''),
228
                601)
229
        return matches[0]
230

    
231
    def list_users(self):
232
        """list cached users information"""
233
        if not self._cache:
234
            self.authenticate()
235
        r = []
236
        for k, v in self._cache.items():
237
            r.append(dict(v['access']['user']))
238
            r[-1].update(dict(auth_token=self.get_token(k)))
239
        return r
240

    
241
    def user_info(self, token=None):
242
        """Get (cached) user information"""
243
        token = self._resolve_token(token)
244
        self._validate_token(token)
245
        r = self._cache[self._uuids[token]]
246
        return r['access']['user']
247

    
248
    def term(self, key, token=None):
249
        """Get (cached) term, from user credentials"""
250
        return self.user_term(key, token)
251

    
252
    def user_term(self, key, token=None):
253
        """Get (cached) term, from user credentials"""
254
        return self.user_info(token).get(key, None)
255

    
256
    def post_user_catalogs(self, uuids=None, displaynames=None, token=None):
257
        """POST base_url/user_catalogs
258

259
        :param uuids: (list or tuple) user uuids
260

261
        :param displaynames: (list or tuple) usernames (mut. excl. to uuids)
262

263
        :returns: (dict) {uuid1: name1, uuid2: name2, ...} or oposite
264
        """
265
        return self.uuids2usernames(uuids, token) if (
266
            uuids) else self.usernames2uuids(displaynames, token)
267

    
268
    @_astakos_error
269
    def uuids2usernames(self, uuids, token=None):
270
        token = self._resolve_token(token)
271
        self._validate_token(token)
272
        uuid = self._uuids[token]
273
        astakos = self._astakos[uuid]
274
        if set(uuids or []).difference(self._uuids2usernames[uuid]):
275
            self._uuids2usernames[uuid].update(astakos.get_usernames(uuids))
276
        return self._uuids2usernames[uuid]
277

    
278
    @_astakos_error
279
    def usernames2uuids(self, usernames, token=None):
280
        token = self._resolve_token(token)
281
        self._validate_token(token)
282
        uuid = self._uuids[token]
283
        astakos = self._astakos[uuid]
284
        if set(usernames or []).difference(self._usernames2uuids[uuid]):
285
            self._usernames2uuids[uuid].update(astakos.get_uuids(usernames))
286
        return self._usernames2uuids[uuid]