Statistics
| Branch: | Tag: | Revision:

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

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

    
60
def _astakos_error(foo):
61
    def wrap(self, *args, **kwargs):
62
        try:
63
            return foo(self, *args, **kwargs)
64
        except AstakosClientException as sace:
65
            self._raise_for_status(sace)
66
    return wrap
67

    
68

    
69
class LoggedAstakosClient(AstakosClient):
70
    """An AstakosClient wrapper with modified logging
71

72
    Logs are adjusted to appear similar to the ones of kamaki clients.
73
    No other changes are made to the parent class.
74
    """
75

    
76
    LOG_TOKEN = False
77
    LOG_DATA = False
78

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

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

    
115

    
116
class CachedAstakosClient(Client):
117
    """Synnefo Astakos cached client wraper"""
118

    
119
    @_astakos_error
120
    def __init__(self, base_url, token=None):
121
        super(CachedAstakosClient, self).__init__(base_url, token)
122
        self._astakos = dict()
123
        self._uuids = dict()
124
        self._cache = dict()
125
        self._uuids2usernames = dict()
126
        self._usernames2uuids = dict()
127

    
128
    def _resolve_token(self, token):
129
        """
130
        :returns: (str) a single token
131

132
        :raises AssertionError: if no token exists (either param or member)
133
        """
134
        token = token or self.token
135
        assert token, 'No token provided'
136
        return token[0] if (
137
            isinstance(token, list) or isinstance(token, tuple)) else token
138

    
139
    def get_client(self, token=None):
140
        """Get the Synnefo AstakosClient instance used by client"""
141
        token = self._resolve_token(token)
142
        self._validate_token(token)
143
        return self._astakos[self._uuids[token]]
144

    
145
    @_astakos_error
146
    def authenticate(self, token=None):
147
        """Get authentication information and store it in this client
148
        As long as the CachedAstakosClient instance is alive, the latest
149
        authentication information for this token will be available
150

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

    
167
    def remove_user(self, uuid):
168
        self._uuids.pop(self.get_token(uuid))
169
        self._cache.pop(uuid)
170
        self._astakos.pop(uuid)
171
        self._uuids2usernames.pop(uuid)
172
        self._usernames2uuids.pop(uuid)
173

    
174
    def get_token(self, uuid):
175
        return self._cache[uuid]['access']['token']['id']
176

    
177
    def _validate_token(self, token):
178
        if (token not in self._uuids) or (
179
                self.get_token(self._uuids[token]) != token):
180
            self._uuids.pop(token, None)
181
            self.authenticate(token)
182

    
183
    def get_services(self, token=None):
184
        """
185
        :returns: (list) [{name:..., type:..., endpoints:[...]}, ...]
186
        """
187
        token = self._resolve_token(token)
188
        self._validate_token(token)
189
        r = self._cache[self._uuids[token]]
190
        return r['access']['serviceCatalog']
191

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

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

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

    
210
    def get_service_endpoints(self, service_type, version=None, token=None):
211
        """
212
        :param service_type: (str) can be compute, object-store, etc.
213

214
        :param version: (str) the version id of the service
215

216
        :returns: (dict) {SNF:uiURL, adminURL, internalURL, publicURL, ...}
217

218
        :raises ClientError: (600) if service_type not in service catalog
219

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

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

    
246
    def user_info(self, token=None):
247
        """Get (cached) user information"""
248
        token = self._resolve_token(token)
249
        self._validate_token(token)
250
        r = self._cache[self._uuids[token]]
251
        return r['access']['user']
252

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

    
257
    def user_term(self, key, token=None):
258
        """Get (cached) term, from user credentials"""
259
        return self.user_info(token).get(key, None)
260

    
261
    def post_user_catalogs(self, uuids=None, displaynames=None, token=None):
262
        """POST base_url/user_catalogs
263

264
        :param uuids: (list or tuple) user uuids
265

266
        :param displaynames: (list or tuple) usernames (mut. excl. to uuids)
267

268
        :returns: (dict) {uuid1: name1, uuid2: name2, ...} or oposite
269
        """
270
        return self.uuids2usernames(uuids, token) if (
271
            uuids) else self.usernames2uuids(displaynames, token)
272

    
273
    @_astakos_error
274
    def uuids2usernames(self, uuids, token=None):
275
        token = self._resolve_token(token)
276
        self._validate_token(token)
277
        uuid = self._uuids[token]
278
        astakos = self._astakos[uuid]
279
        if set(uuids or []).difference(self._uuids2usernames[uuid]):
280
            self._uuids2usernames[uuid].update(astakos.get_usernames(uuids))
281
        return self._uuids2usernames[uuid]
282

    
283
    @_astakos_error
284
    def usernames2uuids(self, usernames, token=None):
285
        token = self._resolve_token(token)
286
        self._validate_token(token)
287
        uuid = self._uuids[token]
288
        astakos = self._astakos[uuid]
289
        if set(usernames or []).difference(self._usernames2uuids[uuid]):
290
            self._usernames2uuids[uuid].update(astakos.get_uuids(usernames))
291
        return self._usernames2uuids[uuid]