Implement user session commands for kamaki
[kamaki] / kamaki / clients / astakos / __init__.py
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 SynnefoAstakosClient
36 from astakosclient import AstakosClientException as SynnefoAstakosClientError
37
38 from kamaki.clients import Client, ClientError
39
40
41 def _astakos_error(foo):
42     def wrap(self, *args, **kwargs):
43         try:
44             return foo(self, *args, **kwargs)
45         except SynnefoAstakosClientError as sace:
46             self._raise_for_status(sace)
47     return wrap
48
49
50 class AstakosClient(Client):
51     """Synnefo Astakos cached client wraper"""
52
53     @_astakos_error
54     def __init__(self, base_url, token=None):
55         super(AstakosClient, self).__init__(base_url, token)
56         self._astakos = dict()
57         self._uuids = dict()
58         self._cache = dict()
59         self._uuids2usernames = dict()
60         self._usernames2uuids = dict()
61
62     def _resolve_token(self, token):
63         """
64         :returns: (str) a single token
65
66         :raises AssertionError: if no token exists (either param or member)
67         """
68         token = token or self.token
69         assert token, 'No token provided'
70         return token[0] if (
71             isinstance(token, list) or isinstance(token, tuple)) else token
72
73     def get_client(self, token=None):
74         """Get the Synnefo AstakosClient instance used by client"""
75         token = self._resolve_token(token)
76         self._validate_token(token)
77         return self._astakos[self._uuids[token]]
78
79     @_astakos_error
80     def authenticate(self, token=None):
81         """Get authentication information and store it in this client
82         As long as the AstakosClient instance is alive, the latest
83         authentication information for this token will be available
84
85         :param token: (str) custom token to authenticate
86         """
87         token = self._resolve_token(token)
88         astakos = SynnefoAstakosClient(
89             token, self.base_url, logger=getLogger('astakosclient'))
90         r = astakos.authenticate()
91         uuid = r['access']['user']['id']
92         self._uuids[token] = uuid
93         self._cache[uuid] = r
94         self._astakos[uuid] = astakos
95         self._uuids2usernames[uuid] = dict()
96         self._usernames2uuids[uuid] = dict()
97         return self._cache[uuid]
98
99     def remove_user(self, uuid):
100         self._uuids.pop(self.get_token(uuid))
101         self._cache.pop(uuid)
102         self._astakos.pop(uuid)
103         self._uuids2usernames.pop(uuid)
104         self._usernames2uuids.pop(uuid)
105
106     def get_token(self, uuid):
107         return self._cache[uuid]['access']['token']['id']
108
109     def _validate_token(self, token):
110         if (token not in self._uuids) or (
111                 self.get_token(self._uuids[token]) != token):
112             self._uuids.pop(token, None)
113             self.authenticate(token)
114
115     def get_services(self, token=None):
116         """
117         :returns: (list) [{name:..., type:..., endpoints:[...]}, ...]
118         """
119         token = self._resolve_token(token)
120         self._validate_token(token)
121         r = self._cache[self._uuids[token]]
122         return r['access']['serviceCatalog']
123
124     def get_service_details(self, service_type, token=None):
125         """
126         :param service_type: (str) compute, object-store, image, account, etc.
127
128         :returns: (dict) {name:..., type:..., endpoints:[...]}
129
130         :raises ClientError: (600) if service_type not in service catalog
131         """
132         services = self.get_services(token)
133         for service in services:
134             try:
135                 if service['type'].lower() == service_type.lower():
136                     return service
137             except KeyError:
138                 self.log.warning('Misformated service %s' % service)
139         raise ClientError(
140             'Service type "%s" not in service catalog' % service_type, 600)
141
142     def get_service_endpoints(self, service_type, version=None, token=None):
143         """
144         :param service_type: (str) can be compute, object-store, etc.
145
146         :param version: (str) the version id of the service
147
148         :returns: (dict) {SNF:uiURL, adminURL, internalURL, publicURL, ...}
149
150         :raises ClientError: (600) if service_type not in service catalog
151
152         :raises ClientError: (601) if #matching endpoints != 1
153         """
154         service = self.get_service_details(service_type, token)
155         matches = []
156         for endpoint in service['endpoints']:
157             if (not version) or (
158                     endpoint['versionId'].lower() == version.lower()):
159                 matches.append(endpoint)
160         if len(matches) != 1:
161             raise ClientError(
162                 '%s endpoints match type %s %s' % (
163                     len(matches), service_type,
164                     ('and versionId %s' % version) if version else ''),
165                 601)
166         return matches[0]
167
168     def list_users(self):
169         """list cached users information"""
170         if not self._cache:
171             self.authenticate()
172         r = []
173         for k, v in self._cache.items():
174             r.append(dict(v['access']['user']))
175             r[-1].update(dict(auth_token=self.get_token(k)))
176         return r
177
178     def user_info(self, token=None):
179         """Get (cached) user information"""
180         token = self._resolve_token(token)
181         self._validate_token(token)
182         r = self._cache[self._uuids[token]]
183         return r['access']['user']
184
185     def term(self, key, token=None):
186         """Get (cached) term, from user credentials"""
187         return self.user_term(key, token)
188
189     def user_term(self, key, token=None):
190         """Get (cached) term, from user credentials"""
191         return self.user_info(token).get(key, None)
192
193     def post_user_catalogs(self, uuids=None, displaynames=None, token=None):
194         """POST base_url/user_catalogs
195
196         :param uuids: (list or tuple) user uuids
197
198         :param displaynames: (list or tuple) usernames (mut. excl. to uuids)
199
200         :returns: (dict) {uuid1: name1, uuid2: name2, ...} or oposite
201         """
202         return self.uuids2usernames(uuids, token) if (
203             uuids) else self.usernames2uuids(displaynames, token)
204
205     @_astakos_error
206     def uuids2usernames(self, uuids, token=None):
207         token = self._resolve_token(token)
208         self._validate_token(token)
209         uuid = self._uuids[token]
210         astakos = self._astakos[uuid]
211         if set(uuids).difference(self._uuids2usernames[uuid]):
212             self._uuids2usernames[uuid].update(astakos.get_usernames(uuids))
213         return self._uuids2usernames[uuid]
214
215     @_astakos_error
216     def usernames2uuids(self, usernames, token=None):
217         token = self._resolve_token(token)
218         self._validate_token(token)
219         uuid = self._uuids[token]
220         astakos = self._astakos[uuid]
221         if set(usernames).difference(self._usernames2uuids[uuid]):
222             self._usernames2uuids[uuid].update(astakos.get_uuids(usernames))
223         return self._usernames2uuids[uuid]