Revision c2b5da2f
b/kamaki/clients/__init__.py | ||
---|---|---|
32 | 32 |
# or implied, of GRNET S.A. |
33 | 33 |
|
34 | 34 |
from urllib2 import quote |
35 |
from urlparse import urlparse |
|
35 | 36 |
from threading import Thread |
36 | 37 |
from json import dumps, loads |
37 | 38 |
from time import time |
39 |
from httplib import ResponseNotReady |
|
40 |
from time import sleep |
|
41 |
from random import random |
|
42 |
|
|
43 |
from objpool.http import PooledHTTPConnection |
|
38 | 44 |
|
39 | 45 |
from kamaki.clients.utils import logger |
40 |
from kamaki.clients.connection.kamakicon import KamakiHTTPConnection |
|
41 |
from kamaki.clients.connection.errors import KamakiConnectionError |
|
42 |
from kamaki.clients.connection.errors import KamakiResponseError |
|
43 | 46 |
|
44 | 47 |
LOG_TOKEN = False |
45 | 48 |
DEBUG_LOG = logger.get_log_filename() |
... | ... | |
60 | 63 |
logger.add_file_logger('ClientError', __name__, filename=DEBUG_LOG) |
61 | 64 |
clienterrorlog = logger.get_logger('ClientError') |
62 | 65 |
|
66 |
HTTP_METHODS = ['GET', 'POST', 'PUT', 'HEAD', 'DELETE', 'COPY', 'MOVE'] |
|
67 |
|
|
68 |
|
|
69 |
def _encode(v): |
|
70 |
if v and isinstance(v, unicode): |
|
71 |
return quote(v.encode('utf-8')) |
|
72 |
return v |
|
73 |
|
|
63 | 74 |
|
64 | 75 |
class ClientError(Exception): |
65 | 76 |
def __init__(self, message, status=0, details=None): |
... | ... | |
97 | 108 |
self.details = details if details else [] |
98 | 109 |
|
99 | 110 |
|
111 |
class RequestManager(object): |
|
112 |
"""Handle http request information""" |
|
113 |
|
|
114 |
def _connection_info(self, url, path, params={}): |
|
115 |
""" Set self.url to scheme://netloc/?params |
|
116 |
:param url: (str or unicode) The service url |
|
117 |
|
|
118 |
:param path: (str or unicode) The service path (url/path) |
|
119 |
|
|
120 |
:param params: (dict) Parameters to add to final url |
|
121 |
|
|
122 |
:returns: (scheme, netloc) |
|
123 |
""" |
|
124 |
url = _encode(url) if url else 'http://127.0.0.1/' |
|
125 |
url += '' if url.endswith('/') else '/' |
|
126 |
if path: |
|
127 |
url += _encode(path[1:] if path.startswith('/') else path) |
|
128 |
for i, (key, val) in enumerate(params.items()): |
|
129 |
val = _encode(val) |
|
130 |
url += '%s%s' % ('&' if i else '?', key) |
|
131 |
if val: |
|
132 |
url += '=%s' % val |
|
133 |
parsed = urlparse(url) |
|
134 |
self.url = url |
|
135 |
self.path = parsed.path or '/' |
|
136 |
if parsed.query: |
|
137 |
self.path += '?%s' % parsed.query |
|
138 |
return (parsed.scheme, parsed.netloc) |
|
139 |
|
|
140 |
def __init__( |
|
141 |
self, method, url, path, |
|
142 |
data=None, headers={}, params={}): |
|
143 |
method = method.upper() |
|
144 |
assert method in HTTP_METHODS, 'Invalid http method %s' % method |
|
145 |
if headers: |
|
146 |
assert isinstance(headers, dict) |
|
147 |
self.headers = dict(headers) |
|
148 |
self.method, self.data = method, data |
|
149 |
self.scheme, self.netloc = self._connection_info(url, path, params) |
|
150 |
|
|
151 |
def perform(self, conn): |
|
152 |
""" |
|
153 |
:param conn: (httplib connection object) |
|
154 |
|
|
155 |
:returns: (HTTPResponse) |
|
156 |
""" |
|
157 |
# sendlog.debug( |
|
158 |
# 'RequestManager.perform mthd(%s), url(%s), headrs(%s), bdlen(%s)', |
|
159 |
# self.method, self.url, self.headers, self.data) |
|
160 |
conn.request( |
|
161 |
method=str(self.method.upper()), |
|
162 |
url=str(self.path), |
|
163 |
headers=self.headers, |
|
164 |
body=self.data) |
|
165 |
while True: |
|
166 |
try: |
|
167 |
return conn.getresponse() |
|
168 |
except ResponseNotReady: |
|
169 |
sleep(0.03 * random()) |
|
170 |
|
|
171 |
|
|
172 |
class ResponseManager(object): |
|
173 |
"""Manage the http request and handle the response data, headers, etc.""" |
|
174 |
|
|
175 |
def __init__(self, request, poolsize=None): |
|
176 |
""" |
|
177 |
:param request: (RequestManager) |
|
178 |
""" |
|
179 |
self.request = request |
|
180 |
self._request_performed = False |
|
181 |
self.poolsize = poolsize |
|
182 |
|
|
183 |
def _get_response(self): |
|
184 |
if self._request_performed: |
|
185 |
return |
|
186 |
|
|
187 |
pool_kw = dict(size=self.poolsize) if self.poolsize else dict() |
|
188 |
try: |
|
189 |
with PooledHTTPConnection( |
|
190 |
self.request.netloc, self.request.scheme, |
|
191 |
**pool_kw) as connection: |
|
192 |
r = self.request.perform(connection) |
|
193 |
# recvlog.debug('ResponseManager(%s):' % r) |
|
194 |
self._request_performed = True |
|
195 |
self._headers = dict() |
|
196 |
for k, v in r.getheaders(): |
|
197 |
self.headers[k] = v |
|
198 |
# recvlog.debug('\t%s: %s\t(%s)' % (k, v, r)) |
|
199 |
self._content = r.read() |
|
200 |
self._status_code = r.status |
|
201 |
self._status = r.reason |
|
202 |
except Exception as err: |
|
203 |
from kamaki.clients import recvlog |
|
204 |
from traceback import format_stack |
|
205 |
recvlog.debug('\n'.join(['%s' % type(err)] + format_stack())) |
|
206 |
raise ClientError( |
|
207 |
'Failed while http-connecting to %s (%s)' % ( |
|
208 |
self.request.url, |
|
209 |
err), |
|
210 |
1000) |
|
211 |
|
|
212 |
@property |
|
213 |
def status_code(self): |
|
214 |
self._get_response() |
|
215 |
return self._status_code |
|
216 |
|
|
217 |
@property |
|
218 |
def status(self): |
|
219 |
self._get_response() |
|
220 |
return self._status |
|
221 |
|
|
222 |
@property |
|
223 |
def headers(self): |
|
224 |
self._get_response() |
|
225 |
return self._headers |
|
226 |
|
|
227 |
@property |
|
228 |
def content(self): |
|
229 |
self._get_response() |
|
230 |
return self._content |
|
231 |
|
|
232 |
@property |
|
233 |
def text(self): |
|
234 |
""" |
|
235 |
:returns: (str) content |
|
236 |
""" |
|
237 |
self._get_response() |
|
238 |
return '%s' % self._content |
|
239 |
|
|
240 |
@property |
|
241 |
def json(self): |
|
242 |
""" |
|
243 |
:returns: (dict) squeezed from json-formated content |
|
244 |
""" |
|
245 |
self._get_response() |
|
246 |
try: |
|
247 |
return loads(self._content) |
|
248 |
except ValueError as err: |
|
249 |
ClientError('Response not formated in JSON - %s' % err) |
|
250 |
|
|
251 |
|
|
100 | 252 |
class SilentEvent(Thread): |
101 | 253 |
""" Thread-run method(*args, **kwargs)""" |
102 | 254 |
def __init__(self, method, *args, **kwargs): |
... | ... | |
127 | 279 |
|
128 | 280 |
class Client(object): |
129 | 281 |
|
130 |
def __init__(self, base_url, token, http_client=KamakiHTTPConnection()):
|
|
282 |
def __init__(self, base_url, token): |
|
131 | 283 |
self.base_url = base_url |
132 | 284 |
self.token = token |
133 |
self.headers = {}
|
|
285 |
self.headers, self.params = dict(), dict()
|
|
134 | 286 |
self.DATE_FORMATS = [ |
135 | 287 |
'%a %b %d %H:%M:%S %Y', |
136 | 288 |
'%A, %d-%b-%y %H:%M:%S GMT', |
137 | 289 |
'%a, %d %b %Y %H:%M:%S GMT'] |
138 |
self.http_client = http_client |
|
139 | 290 |
self.MAX_THREADS = 7 |
140 | 291 |
|
141 | 292 |
def _init_thread_limit(self, limit=1): |
... | ... | |
179 | 330 |
def set_header(self, name, value, iff=True): |
180 | 331 |
"""Set a header 'name':'value'""" |
181 | 332 |
if value is not None and iff: |
182 |
self.http_client.set_header(name, value)
|
|
333 |
self.headers[name] = value
|
|
183 | 334 |
|
184 | 335 |
def set_param(self, name, value=None, iff=True): |
185 | 336 |
if iff: |
186 |
self.http_client.set_param(name, value)
|
|
337 |
self.params[name] = value
|
|
187 | 338 |
|
188 | 339 |
def request( |
189 |
self, |
|
190 |
method, |
|
191 |
path, |
|
192 |
async_headers={}, |
|
193 |
async_params={}, |
|
340 |
self, method, path, |
|
341 |
async_headers=dict(), async_params=dict(), |
|
194 | 342 |
**kwargs): |
195 | 343 |
"""In threaded/asynchronous requests, headers and params are not safe |
196 | 344 |
Therefore, the standard self.set_header/param system can be used only |
... | ... | |
205 | 353 |
assert method |
206 | 354 |
assert isinstance(path, str) or isinstance(path, unicode) |
207 | 355 |
try: |
356 |
headers = dict(self.headers) |
|
357 |
headers.update(async_headers) |
|
358 |
params = dict(self.params) |
|
359 |
params.update(async_params) |
|
208 | 360 |
success = kwargs.pop('success', 200) |
209 | 361 |
data = kwargs.pop('data', None) |
210 |
self.http_client.headers.setdefault('X-Auth-Token', self.token) |
|
211 |
|
|
362 |
headers.setdefault('X-Auth-Token', self.token) |
|
212 | 363 |
if 'json' in kwargs: |
213 | 364 |
data = dumps(kwargs.pop('json')) |
214 |
self.http_client.headers.setdefault( |
|
215 |
'Content-Type', |
|
216 |
'application/json') |
|
365 |
headers.setdefault('Content-Type', 'application/json') |
|
217 | 366 |
if data: |
218 |
self.http_client.headers.setdefault( |
|
219 |
'Content-Length', |
|
220 |
'%s' % len(data)) |
|
221 |
|
|
222 |
sendlog.info('perform a %s @ %s', method, self.base_url) |
|
223 |
|
|
224 |
self.http_client.url = self.base_url |
|
225 |
self.http_client.path = quote(path.encode('utf8')) |
|
226 |
r = self.http_client.perform_request( |
|
227 |
method, |
|
228 |
data, |
|
229 |
async_headers, |
|
230 |
async_params) |
|
231 |
|
|
232 |
req = self.http_client |
|
233 |
sendlog.info('%s %s', method, req.url) |
|
234 |
headers = dict(req.headers) |
|
235 |
headers.update(async_headers) |
|
236 |
|
|
237 |
for key, val in headers.items(): |
|
367 |
headers.setdefault('Content-Length', '%s' % len(data)) |
|
368 |
|
|
369 |
req = RequestManager( |
|
370 |
method, self.base_url, path, |
|
371 |
data=data, headers=headers, params=params) |
|
372 |
sendlog.info('commit a %s @ %s\t[%s]', method, self.base_url, self) |
|
373 |
sendlog.info('\tpath: %s\t[%s]', req.path, self) |
|
374 |
for key, val in req.headers.items(): |
|
238 | 375 |
if (not LOG_TOKEN) and key.lower() == 'x-auth-token': |
239 | 376 |
continue |
240 |
sendlog.info('\t%s: %s', key, val) |
|
241 |
sendlog.info('') |
|
377 |
sendlog.info('\t%s: %s [%s]', key, val, self) |
|
242 | 378 |
if data: |
243 | 379 |
datasendlog.info(data) |
380 |
sendlog.info('END HTTP request commit\t[%s]', self) |
|
244 | 381 |
|
382 |
r = ResponseManager(req) |
|
245 | 383 |
recvlog.info('%d %s', r.status_code, r.status) |
246 | 384 |
for key, val in r.headers.items(): |
247 | 385 |
if (not LOG_TOKEN) and key.lower() == 'x-auth-token': |
... | ... | |
249 | 387 |
recvlog.info('%s: %s', key, val) |
250 | 388 |
if r.content: |
251 | 389 |
datarecvlog.info(r.content) |
252 |
|
|
253 |
except (KamakiResponseError, KamakiConnectionError) as err: |
|
254 |
from traceback import format_stack |
|
255 |
recvlog.debug('\n'.join(['%s' % type(err)] + format_stack())) |
|
256 |
self.http_client.reset_headers() |
|
257 |
self.http_client.reset_params() |
|
258 |
errstr = '%s' % err |
|
259 |
if not errstr: |
|
260 |
errstr = ('%s' % type(err))[7:-2] |
|
261 |
status = getattr(err, 'status', getattr(err, 'errno', 0)) |
|
262 |
raise ClientError('%s\n' % errstr, status=status) |
|
263 | 390 |
finally: |
264 |
self.http_client.reset_headers()
|
|
265 |
self.http_client.reset_params()
|
|
391 |
self.headers = dict()
|
|
392 |
self.params = dict()
|
|
266 | 393 |
|
267 | 394 |
if success is not None: |
268 | 395 |
# Success can either be an int or a collection |
269 | 396 |
success = (success,) if isinstance(success, int) else success |
270 | 397 |
if r.status_code not in success: |
271 |
r.release() |
|
272 | 398 |
self._raise_for_status(r) |
273 | 399 |
return r |
274 | 400 |
|
b/kamaki/clients/astakos/test.py | ||
---|---|---|
63 | 63 |
status = None |
64 | 64 |
status_code = 200 |
65 | 65 |
|
66 |
def release(self): |
|
67 |
pass |
|
68 |
|
|
69 | 66 |
astakos_pkg = 'kamaki.clients.astakos.AstakosClient' |
70 | 67 |
|
71 | 68 |
|
b/kamaki/clients/compute/__init__.py | ||
---|---|---|
118 | 118 |
:param new_name: (str) |
119 | 119 |
""" |
120 | 120 |
req = {'server': {'name': new_name}} |
121 |
r = self.servers_put(server_id, json_data=req) |
|
122 |
r.release() |
|
121 |
self.servers_put(server_id, json_data=req) |
|
123 | 122 |
|
124 | 123 |
def delete_server(self, server_id): |
125 | 124 |
"""Submit a deletion request for a server specified by id |
126 | 125 |
|
127 | 126 |
:param server_id: integer (str or int) |
128 | 127 |
""" |
129 |
r = self.servers_delete(server_id) |
|
130 |
r.release() |
|
128 |
self.servers_delete(server_id) |
|
131 | 129 |
|
132 | 130 |
def reboot_server(self, server_id, hard=False): |
133 | 131 |
""" |
... | ... | |
137 | 135 |
""" |
138 | 136 |
boot_type = 'HARD' if hard else 'SOFT' |
139 | 137 |
req = {'reboot': {'type': boot_type}} |
140 |
r = self.servers_post(server_id, 'action', json_data=req) |
|
141 |
r.release() |
|
138 |
self.servers_post(server_id, 'action', json_data=req) |
|
142 | 139 |
|
143 | 140 |
def get_server_metadata(self, server_id, key=''): |
144 | 141 |
""" |
... | ... | |
188 | 185 |
|
189 | 186 |
:param key: (str) the meta key |
190 | 187 |
""" |
191 |
r = self.servers_delete(server_id, 'meta/' + key) |
|
192 |
r.release() |
|
188 |
self.servers_delete(server_id, 'meta/' + key) |
|
193 | 189 |
|
194 | 190 |
def list_flavors(self, detail=False): |
195 | 191 |
""" |
... | ... | |
238 | 234 |
""" |
239 | 235 |
:param image_id: (str) |
240 | 236 |
""" |
241 |
r = self.images_delete(image_id) |
|
242 |
r.release() |
|
237 |
self.images_delete(image_id) |
|
243 | 238 |
|
244 | 239 |
def get_image_metadata(self, image_id, key=''): |
245 | 240 |
""" |
... | ... | |
286 | 281 |
:param key: (str) metadatum key |
287 | 282 |
""" |
288 | 283 |
command = path4url('meta', key) |
289 |
r = self.images_delete(image_id, command) |
|
290 |
r.release() |
|
284 |
self.images_delete(image_id, command) |
b/kamaki/clients/compute/test.py | ||
---|---|---|
106 | 106 |
status = None |
107 | 107 |
status_code = 200 |
108 | 108 |
|
109 |
def release(self): |
|
110 |
pass |
|
111 |
|
|
112 | 109 |
|
113 | 110 |
class ComputeRestClient(TestCase): |
114 | 111 |
|
/dev/null | ||
---|---|---|
1 |
# Copyright 2011-2012 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, self.list of conditions and the following |
|
9 |
# disclaimer. |
|
10 |
# |
|
11 |
# 2. Redistributions in binary form must reproduce the above |
|
12 |
# copyright notice, self.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 |
|
|
35 |
class KamakiResponse(object): |
|
36 |
"""An abstract HTTP Response object to handle a performed HTTPRequest. |
|
37 |
Subclass implementation required |
|
38 |
""" |
|
39 |
|
|
40 |
def __init__(self, request, prefetched=False): |
|
41 |
self.request = request |
|
42 |
self.prefetched = prefetched |
|
43 |
|
|
44 |
def _get_response(self): |
|
45 |
"""Wait for http response as late as possible""" |
|
46 |
if self.prefetched: |
|
47 |
return |
|
48 |
self = self.request.response |
|
49 |
self.prefetched = True |
|
50 |
|
|
51 |
def release(self): |
|
52 |
"""Release the connection. |
|
53 |
""" |
|
54 |
raise NotImplementedError |
|
55 |
|
|
56 |
@property |
|
57 |
def prefetched(self): |
|
58 |
"""flag to avoid downloading more than nessecary""" |
|
59 |
return self._prefetched |
|
60 |
|
|
61 |
@prefetched.setter |
|
62 |
def prefetched(self, p): |
|
63 |
self._prefetched = p |
|
64 |
|
|
65 |
@property |
|
66 |
def content(self): |
|
67 |
""":returns: (binary) request response content (data)""" |
|
68 |
self._get_response() |
|
69 |
return self._content |
|
70 |
|
|
71 |
@content.setter |
|
72 |
def content(self, v): |
|
73 |
self._content = v |
|
74 |
|
|
75 |
@property |
|
76 |
def text(self): |
|
77 |
"""(str)""" |
|
78 |
self._get_response() |
|
79 |
return self._text |
|
80 |
|
|
81 |
@text.setter |
|
82 |
def text(self, v): |
|
83 |
self._text = v |
|
84 |
|
|
85 |
@property |
|
86 |
def json(self): |
|
87 |
"""(dict)""" |
|
88 |
self._get_response() |
|
89 |
return self._json |
|
90 |
|
|
91 |
@json.setter |
|
92 |
def json(self, v): |
|
93 |
self._json = v |
|
94 |
|
|
95 |
@property |
|
96 |
def headers(self): |
|
97 |
"""(dict)""" |
|
98 |
self._get_response() |
|
99 |
return self._headers |
|
100 |
|
|
101 |
@headers.setter |
|
102 |
def headers(self, v): |
|
103 |
self._headers = v |
|
104 |
|
|
105 |
@property |
|
106 |
def status_code(self): |
|
107 |
"""(int) optional""" |
|
108 |
self._get_response() |
|
109 |
return self._status_code |
|
110 |
|
|
111 |
@status_code.setter |
|
112 |
def status_code(self, v): |
|
113 |
self._status_code = v |
|
114 |
|
|
115 |
@property |
|
116 |
def status(self): |
|
117 |
"""(str) useful in server error responses""" |
|
118 |
self._get_response() |
|
119 |
return self._status |
|
120 |
|
|
121 |
@status.setter |
|
122 |
def status(self, v): |
|
123 |
self._status = v |
|
124 |
|
|
125 |
@property |
|
126 |
def request(self): |
|
127 |
"""(KamakiConnection) the source of this response object""" |
|
128 |
return self._request |
|
129 |
|
|
130 |
@request.setter |
|
131 |
def request(self, v): |
|
132 |
self._request = v |
|
133 |
|
|
134 |
|
|
135 |
class KamakiConnection(object): |
|
136 |
"""An abstract HTTP Connection mechanism. Subclass implementation required |
|
137 |
""" |
|
138 |
|
|
139 |
def __init__( |
|
140 |
self, |
|
141 |
method=None, url=None, params={}, headers={}, poolsize=8): |
|
142 |
self.headers = headers |
|
143 |
self.params = params |
|
144 |
self.url = url |
|
145 |
self.path = '' |
|
146 |
self.method = method |
|
147 |
self.poolsize = poolsize |
|
148 |
|
|
149 |
@property |
|
150 |
def poolsize(self): |
|
151 |
return self._poolsize |
|
152 |
|
|
153 |
@poolsize.setter |
|
154 |
def poolsize(self, v): |
|
155 |
assert isinstance(v, (int, long)) and v > 0 |
|
156 |
self._poolsize = v |
|
157 |
|
|
158 |
def set_header(self, name, value): |
|
159 |
assert name, 'KamakiConnection header key cannot be 0 or empty' |
|
160 |
self.headers['%s' % name] = '%s' % value |
|
161 |
|
|
162 |
def remove_header(self, name): |
|
163 |
try: |
|
164 |
self.headers.pop(name) |
|
165 |
except KeyError: |
|
166 |
pass |
|
167 |
|
|
168 |
def replace_headers(self, new_headers): |
|
169 |
self.headers = new_headers |
|
170 |
|
|
171 |
def reset_headers(self): |
|
172 |
self.replace_headers({}) |
|
173 |
|
|
174 |
def set_param(self, name, value=None): |
|
175 |
assert name, 'KamakiConnection param key cannot be 0 or empty' |
|
176 |
self.params[unicode(name)] = value |
|
177 |
|
|
178 |
def remove_param(self, name): |
|
179 |
try: |
|
180 |
self.params.pop(name) |
|
181 |
except KeyError: |
|
182 |
pass |
|
183 |
|
|
184 |
def replace_params(self, new_params): |
|
185 |
self.params = new_params |
|
186 |
|
|
187 |
def reset_params(self): |
|
188 |
self.replace_params({}) |
|
189 |
|
|
190 |
def set_url(self, url): |
|
191 |
self.url = url |
|
192 |
|
|
193 |
def set_path(self, path): |
|
194 |
self.path = path |
|
195 |
|
|
196 |
def set_method(self, method): |
|
197 |
self.method = method |
|
198 |
|
|
199 |
def perform_request( |
|
200 |
self, |
|
201 |
method=None, |
|
202 |
url=None, |
|
203 |
async_headers={}, |
|
204 |
async_params={}, |
|
205 |
data=None): |
|
206 |
raise NotImplementedError |
/dev/null | ||
---|---|---|
1 |
# Copyright 2012 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, self.list of conditions and the following |
|
9 |
# disclaimer. |
|
10 |
# |
|
11 |
# 2. Redistributions in binary form must reproduce the above |
|
12 |
# copyright notice, self.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 |
|
|
35 |
class KamakiConnectionError(Exception): |
|
36 |
|
|
37 |
def __init__(self, message, errno=None): |
|
38 |
super(KamakiConnectionError, self).__init__(message) |
|
39 |
self.errno = errno if errno else 0 |
|
40 |
|
|
41 |
|
|
42 |
class KamakiResponseError(Exception): |
|
43 |
|
|
44 |
def __init__(self, message, errno=None): |
|
45 |
super(KamakiResponseError, self).__init__(message) |
|
46 |
self.errno = errno if errno else 0 |
/dev/null | ||
---|---|---|
1 |
# Copyright 2012 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, self.list of conditions and the following |
|
9 |
# disclaimer. |
|
10 |
# |
|
11 |
# 2. Redistributions in binary form must reproduce the above |
|
12 |
# copyright notice, self.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 urlparse import urlparse |
|
35 |
from urllib2 import quote |
|
36 |
from objpool.http import get_http_connection |
|
37 |
from traceback import format_stack |
|
38 |
|
|
39 |
from kamaki.clients.connection import KamakiConnection, KamakiResponse |
|
40 |
from kamaki.clients.connection.errors import KamakiConnectionError |
|
41 |
from kamaki.clients.connection.errors import KamakiResponseError |
|
42 |
|
|
43 |
from json import loads |
|
44 |
|
|
45 |
from time import sleep |
|
46 |
from httplib import ResponseNotReady |
|
47 |
|
|
48 |
|
|
49 |
def _encode(v): |
|
50 |
if v and isinstance(v, unicode): |
|
51 |
return quote(v.encode('utf-8')) |
|
52 |
return v |
|
53 |
|
|
54 |
|
|
55 |
class KamakiHTTPResponse(KamakiResponse): |
|
56 |
|
|
57 |
def _get_response(self): |
|
58 |
if self.prefetched: |
|
59 |
return |
|
60 |
|
|
61 |
try: |
|
62 |
while True: |
|
63 |
try: |
|
64 |
r = self.request.getresponse() |
|
65 |
except ResponseNotReady: |
|
66 |
sleep(0.001) |
|
67 |
else: |
|
68 |
break |
|
69 |
self.prefetched = True |
|
70 |
headers = {} |
|
71 |
for k, v in r.getheaders(): |
|
72 |
headers.update({k: v}) |
|
73 |
self.headers = headers |
|
74 |
self.content = r.read() |
|
75 |
self.status_code = r.status |
|
76 |
self.status = r.reason |
|
77 |
finally: |
|
78 |
try: |
|
79 |
self.request.close() |
|
80 |
except Exception as err: |
|
81 |
from kamaki.clients import recvlog |
|
82 |
recvlog.debug('\n'.join(['%s' % type(err)] + format_stack())) |
|
83 |
raise |
|
84 |
|
|
85 |
@property |
|
86 |
def text(self): |
|
87 |
""" |
|
88 |
:returns: (str) content |
|
89 |
""" |
|
90 |
self._get_response() |
|
91 |
return '%s' % self._content |
|
92 |
|
|
93 |
@text.setter |
|
94 |
def text(self, v): |
|
95 |
pass |
|
96 |
|
|
97 |
@property |
|
98 |
def json(self): |
|
99 |
""" |
|
100 |
:returns: (dict) the json-formated content |
|
101 |
|
|
102 |
:raises KamakiResponseError: if content is not json formated |
|
103 |
""" |
|
104 |
self._get_response() |
|
105 |
try: |
|
106 |
return loads(self._content) |
|
107 |
except ValueError as err: |
|
108 |
KamakiResponseError('Response not formated in JSON - %s' % err) |
|
109 |
|
|
110 |
@json.setter |
|
111 |
def json(self, v): |
|
112 |
pass |
|
113 |
|
|
114 |
def release(self): |
|
115 |
""" Release the connection. Should always be called if the response |
|
116 |
content hasn't been used. |
|
117 |
""" |
|
118 |
if not self.prefetched: |
|
119 |
try: |
|
120 |
self.request.close() |
|
121 |
except Exception as err: |
|
122 |
from kamaki.clients import recvlog |
|
123 |
recvlog.debug('\n'.join(['%s' % type(err)] + format_stack())) |
|
124 |
raise |
|
125 |
|
|
126 |
|
|
127 |
class KamakiHTTPConnection(KamakiConnection): |
|
128 |
|
|
129 |
def _retrieve_connection_info(self, extra_params={}): |
|
130 |
""" Set self.url to scheme://netloc/?params |
|
131 |
:param extra_params: (dict) key:val for url parameters |
|
132 |
|
|
133 |
:returns: (scheme, netloc) |
|
134 |
""" |
|
135 |
if self.url: |
|
136 |
url = self.url if self.url[-1] == '/' else (self.url + '/') |
|
137 |
else: |
|
138 |
url = 'http://127.0.0.1' |
|
139 |
if self.path: |
|
140 |
url += self.path[1:] if self.path[0] == '/' else self.path |
|
141 |
params = dict(self.params) |
|
142 |
params.update(extra_params) |
|
143 |
for i, (key, val) in enumerate(params.items()): |
|
144 |
val = _encode(val) |
|
145 |
url += '%s%s' % ('&' if i else '?', key) |
|
146 |
if val: |
|
147 |
url += '=%s' % val |
|
148 |
|
|
149 |
parsed = urlparse(url) |
|
150 |
self.url = url |
|
151 |
self.path = parsed.path or '/' |
|
152 |
if parsed.query: |
|
153 |
self.path += '?%s' % parsed.query |
|
154 |
return (parsed.scheme, parsed.netloc) |
|
155 |
|
|
156 |
def perform_request( |
|
157 |
self, |
|
158 |
method=None, data=None, async_headers={}, async_params={}): |
|
159 |
""" |
|
160 |
:param method: (str) http method ('get', 'post', etc.) |
|
161 |
|
|
162 |
:param data: (binary object) |
|
163 |
|
|
164 |
:param async_headers: (dict) key:val headers that are used only for one |
|
165 |
request instance as opposed to self.headers, which remain to be |
|
166 |
used by following or parallel requests |
|
167 |
|
|
168 |
:param async_params: (dict) key:val url parameters that are used only |
|
169 |
for one request instance as opposed to self.params, which remain to |
|
170 |
be used by following or parallel requests |
|
171 |
|
|
172 |
:returns: (KamakiHTTPResponse) a response object |
|
173 |
|
|
174 |
:raises KamakiConnectionError: Connection failures |
|
175 |
""" |
|
176 |
(scheme, netloc) = self._retrieve_connection_info(async_params) |
|
177 |
headers = dict(self.headers) |
|
178 |
for k, v in async_headers.items(): |
|
179 |
v = _encode(v) |
|
180 |
headers[k] = v |
|
181 |
|
|
182 |
#de-unicode headers to prepare them for http |
|
183 |
http_headers = {} |
|
184 |
for k, v in headers.items(): |
|
185 |
v = _encode(v) |
|
186 |
http_headers[k] = v |
|
187 |
|
|
188 |
#get connection from pool |
|
189 |
try: |
|
190 |
conn = get_http_connection( |
|
191 |
netloc=netloc, |
|
192 |
scheme=scheme, |
|
193 |
pool_size=self.poolsize) |
|
194 |
except ValueError as ve: |
|
195 |
raise KamakiConnectionError( |
|
196 |
'Cannot establish connection to %s %s' % (self.url, ve), |
|
197 |
errno=-1) |
|
198 |
try: |
|
199 |
#Be carefull, all non-body variables should not be unicode |
|
200 |
conn.request( |
|
201 |
method=str(method.upper()), |
|
202 |
url=str(self.path), |
|
203 |
headers=http_headers, |
|
204 |
body=data) |
|
205 |
except IOError as ioe: |
|
206 |
raise KamakiConnectionError( |
|
207 |
'Cannot connect to %s: %s' % (self.url, ioe.strerror), |
|
208 |
errno=ioe.errno) |
|
209 |
except Exception as err: |
|
210 |
from kamaki.clients import recvlog |
|
211 |
recvlog.debug('\n'.join(['%s' % type(err)] + format_stack())) |
|
212 |
conn.close() |
|
213 |
raise |
|
214 |
return KamakiHTTPResponse(conn) |
/dev/null | ||
---|---|---|
1 |
# Copyright 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, self.list of conditions and the following |
|
9 |
# disclaimer. |
|
10 |
# |
|
11 |
# 2. Redistributions in binary form must reproduce the above |
|
12 |
# copyright notice, self.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 unittest import TestCase |
|
35 |
from mock import Mock, patch, call |
|
36 |
from random import randrange |
|
37 |
from urllib2 import quote |
|
38 |
|
|
39 |
from kamaki.clients import connection |
|
40 |
from kamaki.clients.connection import errors, kamakicon |
|
41 |
|
|
42 |
|
|
43 |
def _encode(v): |
|
44 |
if v and isinstance(v, unicode): |
|
45 |
return quote(v.encode('utf-8')) |
|
46 |
return v |
|
47 |
|
|
48 |
|
|
49 |
class KamakiConnection(TestCase): |
|
50 |
v_samples = {'title': 'value', 5: 'value'} |
|
51 |
n_samples = {'title': None, 5: None} |
|
52 |
false_samples = {None: 'value', 0: 'value'} |
|
53 |
|
|
54 |
def setUp(self): |
|
55 |
from kamaki.clients.connection import KamakiConnection as HTTPC |
|
56 |
self.conn = HTTPC() |
|
57 |
self.conn.reset_headers() |
|
58 |
self.conn.reset_params() |
|
59 |
|
|
60 |
def test_poolsize(self): |
|
61 |
|
|
62 |
def set_poolsize(poolsize): |
|
63 |
self.conn.poolsize = poolsize |
|
64 |
|
|
65 |
from kamaki.clients.connection import KamakiConnection as HTTPC |
|
66 |
for poolsize in ('non integer', -10, 0): |
|
67 |
err = AssertionError |
|
68 |
self.assertRaises(err, set_poolsize, poolsize) |
|
69 |
for poolsize in (1, 100, 1024 * 1024 * 1024 * 1024): |
|
70 |
self.conn.poolsize = poolsize |
|
71 |
self.assertEquals(self.conn.poolsize, poolsize) |
|
72 |
self.assertEquals(HTTPC(poolsize=poolsize).poolsize, poolsize) |
|
73 |
|
|
74 |
def test_set_header(self): |
|
75 |
cnn = self.conn |
|
76 |
for k, v in self.v_samples.items(): |
|
77 |
cnn.set_header(k, v) |
|
78 |
self.assertEquals(cnn.headers[unicode(k)], unicode(v)) |
|
79 |
for k, v in self.n_samples.items(): |
|
80 |
cnn.set_header(k, v) |
|
81 |
self.assertEquals(cnn.headers[unicode(k)], unicode(v)) |
|
82 |
for k, v in self.false_samples.items(): |
|
83 |
self.assertRaises(AssertionError, cnn.set_header, k, v) |
|
84 |
self.assertEquals(len(cnn.headers), 2) |
|
85 |
|
|
86 |
def test_set_param(self): |
|
87 |
cnn = self.conn |
|
88 |
for k, v in self.v_samples.items(): |
|
89 |
cnn.set_param(k, v) |
|
90 |
self.assertEquals(cnn.params[unicode(k)], v) |
|
91 |
for k, v in self.n_samples.items(): |
|
92 |
cnn.set_param(k, v) |
|
93 |
self.assertEquals(cnn.params[unicode(k)], v) |
|
94 |
for k, v in self.false_samples.items(): |
|
95 |
self.assertRaises(AssertionError, cnn.set_param, k, v) |
|
96 |
self.assertEquals(len(cnn.params), 2) |
|
97 |
|
|
98 |
def test_remove_header(self): |
|
99 |
cnn = self.conn |
|
100 |
for k, v in self.v_samples.items(): |
|
101 |
cnn.headers[unicode(k)] = unicode(v) |
|
102 |
for k in self.v_samples: |
|
103 |
cnn.remove_header(k) |
|
104 |
self.assertFalse(k in cnn.headers) |
|
105 |
|
|
106 |
def test_remove_param(self): |
|
107 |
cnn = self.conn |
|
108 |
for k, v in self.v_samples.items(): |
|
109 |
cnn.params[unicode(k)] = unicode(v) |
|
110 |
for k in self.v_samples: |
|
111 |
cnn.remove_param(k) |
|
112 |
self.assertFalse(k in cnn.params) |
|
113 |
|
|
114 |
def test_replace_headers(self): |
|
115 |
cnn = self.conn |
|
116 |
cnn.headers = self.v_samples |
|
117 |
cnn.replace_headers({1: 'one', 2: 'two'}) |
|
118 |
for k in self.v_samples: |
|
119 |
self.assertFalse(k in cnn.headers) |
|
120 |
|
|
121 |
def test_replace_params(self): |
|
122 |
cnn = self.conn |
|
123 |
cnn.params = self.v_samples |
|
124 |
cnn.replace_params({1: 'one', 2: 'two'}) |
|
125 |
for k in self.v_samples: |
|
126 |
self.assertFalse(k in cnn.params) |
|
127 |
|
|
128 |
def test_reset_headers(self): |
|
129 |
cnn = self.conn |
|
130 |
cnn.headers = self.v_samples |
|
131 |
cnn.reset_headers() |
|
132 |
self.assertFalse(cnn.headers) |
|
133 |
|
|
134 |
def test_reset_params(self): |
|
135 |
cnn = self.conn |
|
136 |
cnn.params = self.v_samples |
|
137 |
cnn.reset_params() |
|
138 |
self.assertFalse(cnn.params) |
|
139 |
|
|
140 |
def test_set_url(self): |
|
141 |
self.assertFalse(self.conn.url) |
|
142 |
sample_url = 'http://example.com' |
|
143 |
self.conn.set_url(sample_url) |
|
144 |
self.assertEquals(self.conn.url, sample_url) |
|
145 |
|
|
146 |
def test_set_path(self): |
|
147 |
self.assertFalse(self.conn.path) |
|
148 |
sample_path = '/example/local/path' |
|
149 |
self.conn.set_path(sample_path) |
|
150 |
self.assertEquals(self.conn.path, sample_path) |
|
151 |
|
|
152 |
def test_set_method(self): |
|
153 |
self.assertFalse(self.conn.method) |
|
154 |
sample_method = 'GET' |
|
155 |
self.conn.set_method(sample_method) |
|
156 |
self.assertEquals(self.conn.method, sample_method) |
|
157 |
|
|
158 |
def test_perform_request(self): |
|
159 |
self.assertRaises(NotImplementedError, self.conn.perform_request) |
|
160 |
|
|
161 |
|
|
162 |
class KamakiHTTPConnection(TestCase): |
|
163 |
|
|
164 |
def setUp(self): |
|
165 |
self.conn = kamakicon.KamakiHTTPConnection() |
|
166 |
self.conn.reset_params() |
|
167 |
self.conn.reset_headers() |
|
168 |
|
|
169 |
def test__retrieve_connection_info(self): |
|
170 |
async_params = dict(param1='val1', param2=None, param3=42) |
|
171 |
r = self.conn._retrieve_connection_info(async_params) |
|
172 |
self.assertEquals(r, ('http', '127.0.0.1')) |
|
173 |
expected = '?%s' % '&'.join([( |
|
174 |
'%s=%s' % (k, v)) if v else ( |
|
175 |
'%s' % k) for k, v in async_params.items()]) |
|
176 |
self.assertEquals('http://127.0.0.1%s' % expected, self.conn.url) |
|
177 |
|
|
178 |
for schnet in ( |
|
179 |
('http', 'www.example.com'), ('https', 'www.example.com'), |
|
180 |
('ftp', 'www.example.com'), ('ftps', 'www.example.com'), |
|
181 |
('http', 'www.example.com/v1'), ('https', 'www.example.com/v1')): |
|
182 |
self.conn = kamakicon.KamakiHTTPConnection(url='%s://%s' % schnet) |
|
183 |
self.conn.url = '%s://%s' % schnet |
|
184 |
r = self.conn._retrieve_connection_info(async_params) |
|
185 |
if schnet[1].endswith('v1'): |
|
186 |
self.assertEquals(r, (schnet[0], schnet[1][:-3])) |
|
187 |
else: |
|
188 |
self.assertEquals(r, schnet) |
|
189 |
self.assertEquals( |
|
190 |
'%s://%s/%s' % (schnet[0], schnet[1], expected), |
|
191 |
self.conn.url) |
|
192 |
|
|
193 |
def test_perform_request(self): |
|
194 |
from httplib import HTTPConnection |
|
195 |
from objpool import http |
|
196 |
pr = self.conn.perform_request |
|
197 |
kwargs = dict( |
|
198 |
data='', |
|
199 |
method='GET', |
|
200 |
async_headers=dict(), |
|
201 |
async_params=dict()) |
|
202 |
utf_test = u'\u03a6\u03bf\u03cd\u03c4\u03c3\u03bf\u03c2' |
|
203 |
utf_dict = dict(utf=utf_test) |
|
204 |
ascii_dict = dict(ascii1='myAscii', ascii2=None) |
|
205 |
kwargs0 = dict( |
|
206 |
data='', |
|
207 |
method='get', |
|
208 |
async_headers=utf_dict, |
|
209 |
async_params=ascii_dict) |
|
210 |
|
|
211 |
def get_expected(): |
|
212 |
expected = [] |
|
213 |
for k, v in kwargs0['async_params'].items(): |
|
214 |
v = _encode(v) |
|
215 |
expected.append(('%s=%s' % (k, v)) if v else ('%s' % k)) |
|
216 |
return '&'.join(expected) |
|
217 |
|
|
218 |
KCError = errors.KamakiConnectionError |
|
219 |
fakecon = HTTPConnection('X', 'Y') |
|
220 |
|
|
221 |
with patch.object(http, 'get_http_connection', return_value=fakecon): |
|
222 |
with patch.object(HTTPConnection, 'request') as request: |
|
223 |
r = pr(**kwargs) |
|
224 |
self.assertTrue(isinstance(r, kamakicon.KamakiHTTPResponse)) |
|
225 |
self.assertEquals( |
|
226 |
request.mock_calls[-1], |
|
227 |
call(body='', headers={}, url='/', method='GET')) |
|
228 |
|
|
229 |
pr(**kwargs0) |
|
230 |
|
|
231 |
exp_headers = dict(kwargs0['async_headers']) |
|
232 |
exp_headers['utf'] = _encode(exp_headers['utf']) |
|
233 |
|
|
234 |
self.assertEquals( |
|
235 |
request.mock_calls[-1], |
|
236 |
call( |
|
237 |
body=kwargs0['data'], |
|
238 |
headers=exp_headers, |
|
239 |
url='/?%s' % get_expected(), |
|
240 |
method=kwargs0['method'].upper())) |
|
241 |
|
|
242 |
self.conn = kamakicon.KamakiHTTPConnection() |
|
243 |
(kwargs0['async_params'], kwargs0['async_headers']) = ( |
|
244 |
kwargs0['async_headers'], kwargs0['async_params']) |
|
245 |
kwargs0['async_headers']['ascii2'] = 'None' |
|
246 |
self.conn.perform_request(**kwargs0) |
|
247 |
self.assertEquals( |
|
248 |
request.mock_calls[-1], |
|
249 |
call( |
|
250 |
body=kwargs0['data'], |
|
251 |
headers=kwargs0['async_headers'], |
|
252 |
url='/?%s' % get_expected(), |
|
253 |
method=kwargs0['method'].upper())) |
|
254 |
|
|
255 |
err = IOError('IO Error') |
|
256 |
with patch.object(HTTPConnection, 'request', side_effect=err): |
|
257 |
self.assertRaises(KCError, pr, **kwargs) |
|
258 |
|
|
259 |
err = ValueError('Cannot Establish connection') |
|
260 |
with patch.object(http, 'get_http_connection', side_effect=err): |
|
261 |
self.assertRaises(KCError, pr, **kwargs) |
|
262 |
|
|
263 |
err = Exception('Any other error') |
|
264 |
with patch.object(http, 'get_http_connection', side_effect=err): |
|
265 |
self.assertRaises(KCError, pr, **kwargs) |
|
266 |
|
|
267 |
|
|
268 |
class KamakiHTTPResponse(TestCase): |
|
269 |
|
|
270 |
class fakeResponse(object): |
|
271 |
sample = 'sample string' |
|
272 |
getheaders = Mock(return_value={}) |
|
273 |
read = Mock(return_value=sample) |
|
274 |
status = Mock(return_value=None) |
|
275 |
reason = Mock(return_value=None) |
|
276 |
|
|
277 |
def setUp(self): |
|
278 |
from httplib import HTTPConnection |
|
279 |
self.HTC = HTTPConnection |
|
280 |
self.FR = self.fakeResponse |
|
281 |
|
|
282 |
def test_text(self): |
|
283 |
with patch.object(self.HTC, 'getresponse', return_value=self.FR()): |
|
284 |
self.resp = kamakicon.KamakiHTTPResponse(self.HTC('X', 'Y')) |
|
285 |
self.assertEquals(self.resp.text, self.FR.sample) |
|
286 |
sample2 = 'some other string' |
|
287 |
self.resp.text = sample2 |
|
288 |
self.assertNotEquals(self.resp.text, sample2) |
|
289 |
|
|
290 |
def test_json(self): |
|
291 |
with patch.object(self.HTC, 'getresponse', return_value=self.FR()): |
|
292 |
self.resp = kamakicon.KamakiHTTPResponse(self.HTC('X', 'Y')) |
|
293 |
self.assertRaises(errors.KamakiResponseError, self.resp.json) |
|
294 |
sample2 = '{"antoher":"sample", "formated":"in_json"}' |
|
295 |
with patch.object(self.FR, 'read', return_value=sample2): |
|
296 |
self.resp = kamakicon.KamakiHTTPResponse(self.HTC('X', 'Y')) |
|
297 |
from json import loads |
|
298 |
self.assertEquals(loads(sample2), self.resp.json) |
|
299 |
|
|
300 |
def test_pool_lock(self): |
|
301 |
exceptions_left = 100 |
|
302 |
while exceptions_left: |
|
303 |
kre = errors.KamakiResponseError |
|
304 |
with patch.object(self.HTC, 'close', return_value=True): |
|
305 |
self.resp = kamakicon.KamakiHTTPResponse(self.HTC('X', 'Y')) |
|
306 |
if randrange(10): |
|
307 |
with patch.object( |
|
308 |
self.HTC, |
|
309 |
'getresponse', |
|
310 |
return_value=self.FR()): |
|
311 |
self.assertEquals(self.resp.text, self.FR.sample) |
|
312 |
else: |
|
313 |
with patch.object( |
|
314 |
self.HTC, |
|
315 |
'getresponse', |
|
316 |
side_effect=kre('A random error')): |
|
317 |
try: |
|
318 |
self.resp.text |
|
319 |
except kre: |
|
320 |
exceptions_left -= 1 |
|
321 |
else: |
|
322 |
self.assertTrue(False) |
|
323 |
self.HTC.close.assert_called_with() |
|
324 |
|
|
325 |
|
|
326 |
class KamakiResponse(TestCase): |
|
327 |
|
|
328 |
def setUp(self): |
|
329 |
self.resp = connection.KamakiResponse( |
|
330 |
'Abstract class, so test with fake request (str)') |
|
331 |
|
|
332 |
def _mock_get_response(foo): |
|
333 |
def mocker(self): |
|
334 |
self.resp._get_response = Mock() |
|
335 |
foo(self) |
|
336 |
return mocker |
|
337 |
|
|
338 |
def test_release(self): |
|
339 |
self.assertRaises(NotImplementedError, self.resp.release) |
|
340 |
|
|
341 |
def test_prefetched(self): |
|
342 |
self.assertFalse(self.resp.prefetched) |
|
343 |
self.resp.prefetched = True |
|
344 |
self.assertTrue(self.resp.prefetched) |
|
345 |
|
|
346 |
@_mock_get_response |
|
347 |
def test_content(self): |
|
348 |
rsp = self.resp |
|
349 |
for cont in ('Sample Content', u'\u03c7\u03cd\u03bd\u03c9\x00'): |
|
350 |
rsp.content = cont |
|
351 |
self.assertEquals(rsp.content, cont) |
|
352 |
|
|
353 |
( |
|
354 |
test_text, |
|
355 |
test_json, |
|
356 |
test_headers, |
|
357 |
test_status, |
|
358 |
test_status_code) = 5 * (test_content,) |
|
359 |
|
|
360 |
def test_request(self): |
|
361 |
r = self.resp.request |
|
362 |
self.assertTrue(isinstance(r, str)) |
|
363 |
|
|
364 |
|
|
365 |
if __name__ == '__main__': |
|
366 |
from sys import argv |
|
367 |
from kamaki.clients.test import runTestCase |
|
368 |
classes = dict( |
|
369 |
KamakiConnection=KamakiConnection, |
|
370 |
KamakiHTTPConnection=KamakiHTTPConnection, |
|
371 |
KamakiHTTPResponse=KamakiHTTPResponse, |
|
372 |
KamakiResponse=KamakiResponse) |
|
373 |
not_found = True |
|
374 |
for k, cls in classes.items(): |
|
375 |
if not argv[1:] or argv[1] == k: |
|
376 |
not_found = False |
|
377 |
runTestCase(cls, '%s Client' % k, argv[2:]) |
|
378 |
if not_found: |
|
379 |
print('TestCase %s not found' % argv[1]) |
b/kamaki/clients/cyclades/__init__.py | ||
---|---|---|
47 | 47 |
:param server_id: integer (str or int) |
48 | 48 |
""" |
49 | 49 |
req = {'start': {}} |
50 |
r = self.servers_post(server_id, 'action', json_data=req, success=202) |
|
51 |
r.release() |
|
50 |
self.servers_post(server_id, 'action', json_data=req, success=202) |
|
52 | 51 |
|
53 | 52 |
def shutdown_server(self, server_id): |
54 | 53 |
"""Submit a shutdown request |
... | ... | |
56 | 55 |
:param server_id: integer (str or int) |
57 | 56 |
""" |
58 | 57 |
req = {'shutdown': {}} |
59 |
r = self.servers_post(server_id, 'action', json_data=req, success=202) |
|
60 |
r.release() |
|
58 |
self.servers_post(server_id, 'action', json_data=req, success=202) |
|
61 | 59 |
|
62 | 60 |
def get_server_console(self, server_id): |
63 | 61 |
""" |
... | ... | |
93 | 91 |
:param profile: (str) ENABLED | DISABLED | PROTECTED |
94 | 92 |
""" |
95 | 93 |
req = {'firewallProfile': {'profile': profile}} |
96 |
r = self.servers_post(server_id, 'action', json_data=req, success=202) |
|
97 |
r.release() |
|
94 |
self.servers_post(server_id, 'action', json_data=req, success=202) |
|
98 | 95 |
|
99 | 96 |
def list_servers(self, detail=False, changes_since=None): |
100 | 97 |
""" |
... | ... | |
189 | 186 |
:param new_name: (str) |
190 | 187 |
""" |
191 | 188 |
req = {'network': {'name': new_name}} |
192 |
r = self.networks_put(network_id=network_id, json_data=req) |
|
193 |
r.release() |
|
189 |
self.networks_put(network_id=network_id, json_data=req) |
|
194 | 190 |
|
195 | 191 |
def delete_network(self, network_id): |
196 | 192 |
""" |
... | ... | |
199 | 195 |
:raises ClientError: 421 Network in use |
200 | 196 |
""" |
201 | 197 |
try: |
202 |
r = self.networks_delete(network_id)
|
|
198 |
self.networks_delete(network_id) |
|
203 | 199 |
except ClientError as err: |
204 | 200 |
if err.status == 421: |
205 | 201 |
err.details = [ |
206 | 202 |
'Network may be still connected to at least one server'] |
207 | 203 |
raise err |
208 |
r.release() |
|
209 | 204 |
|
210 | 205 |
def connect_server(self, server_id, network_id): |
211 | 206 |
""" Connect a server to a network |
... | ... | |
215 | 210 |
:param network_id: integer (str or int) |
216 | 211 |
""" |
217 | 212 |
req = {'add': {'serverRef': server_id}} |
218 |
r = self.networks_post(network_id, 'action', json_data=req) |
|
219 |
r.release() |
|
213 |
self.networks_post(network_id, 'action', json_data=req) |
|
220 | 214 |
|
221 | 215 |
def disconnect_server(self, server_id, nic_id): |
222 | 216 |
""" |
... | ... | |
232 | 226 |
net['id'], |
233 | 227 |
net['network_id']) for net in vm_nets if nic_id == net['id']]: |
234 | 228 |
req = {'remove': {'attachment': '%s' % nic_id}} |
235 |
r = self.networks_post(network_id, 'action', json_data=req) |
|
236 |
r.release() |
|
229 |
self.networks_post(network_id, 'action', json_data=req) |
|
237 | 230 |
num_of_disconnections += 1 |
238 | 231 |
return num_of_disconnections |
239 | 232 |
|
... | ... | |
243 | 236 |
""" |
244 | 237 |
for nic in self.list_network_nics(netid): |
245 | 238 |
req = dict(remove=dict(attachment=nic)) |
246 |
r = self.networks_post(netid, 'action', json_data=req) |
|
247 |
r.release() |
|
239 |
self.networks_post(netid, 'action', json_data=req) |
|
248 | 240 |
|
249 | 241 |
def wait_server( |
250 | 242 |
self, |
b/kamaki/clients/cyclades/test.py | ||
---|---|---|
87 | 87 |
status = None |
88 | 88 |
status_code = 200 |
89 | 89 |
|
90 |
def release(self): |
|
91 |
pass |
|
92 |
|
|
93 | 90 |
rest_pkg = 'kamaki.clients.cyclades.CycladesRestClient' |
94 | 91 |
cyclades_pkg = 'kamaki.clients.cyclades.CycladesClient' |
95 | 92 |
|
b/kamaki/clients/image/__init__.py | ||
---|---|---|
124 | 124 |
for key, val in properties.items(): |
125 | 125 |
async_headers['x-image-meta-property-%s' % key] = val |
126 | 126 |
|
127 |
r = self.post(path, success=200, async_headers=async_headers) |
|
128 |
r.release() |
|
127 |
self.post(path, success=200, async_headers=async_headers) |
|
129 | 128 |
|
130 | 129 |
def list_members(self, image_id): |
131 | 130 |
""" |
... | ... | |
155 | 154 |
:param member: (str) user to allow access to current user's images |
156 | 155 |
""" |
157 | 156 |
path = path4url('images', image_id, 'members', member) |
158 |
r = self.put(path, success=204) |
|
159 |
r.release() |
|
157 |
self.put(path, success=204) |
|
160 | 158 |
|
161 | 159 |
def remove_member(self, image_id, member): |
162 | 160 |
""" |
... | ... | |
165 | 163 |
:param member: (str) user to deprive from current user's images |
166 | 164 |
""" |
167 | 165 |
path = path4url('images', image_id, 'members', member) |
168 |
r = self.delete(path, success=204) |
|
169 |
r.release() |
|
166 |
self.delete(path, success=204) |
|
170 | 167 |
|
171 | 168 |
def set_members(self, image_id, members): |
172 | 169 |
""" |
... | ... | |
176 | 173 |
""" |
177 | 174 |
path = path4url('images', image_id, 'members') |
178 | 175 |
req = {'memberships': [{'member_id': member} for member in members]} |
179 |
r = self.put(path, json=req, success=204) |
|
180 |
r.release() |
|
176 |
self.put(path, json=req, success=204) |
b/kamaki/clients/image/test.py | ||
---|---|---|
132 | 132 |
status = None |
133 | 133 |
status_code = 200 |
134 | 134 |
|
135 |
def release(self): |
|
136 |
pass |
|
137 |
|
|
138 | 135 |
image_pkg = 'kamaki.clients.image.ImageClient' |
139 | 136 |
|
140 | 137 |
|
b/kamaki/clients/livetest/__init__.py | ||
---|---|---|
169 | 169 |
return parser |
170 | 170 |
|
171 | 171 |
|
172 |
""" |
|
172 | 173 |
class Connection(Generic): |
173 | 174 |
|
174 | 175 |
def setUp(self): |
... | ... | |
212 | 213 |
assert False, 'Exception not raised as expected' |
213 | 214 |
response.request.close.assert_called_once_with() |
214 | 215 |
response.request.close() |
216 |
""" |
|
215 | 217 |
|
216 | 218 |
|
217 | 219 |
def main(argv, config=None): |
218 | 220 |
suiteFew = TestSuite() |
219 |
if len(argv) == 0 or argv[0] == 'connection': |
|
220 |
test_method = 'test_%s' % (argv[1] if len(argv) > 1 else '000') |
|
221 |
suiteFew.addTest(Connection(test_method, config)) |
|
221 |
#if len(argv) == 0 or argv[0] == 'connection':
|
|
222 |
# test_method = 'test_%s' % (argv[1] if len(argv) > 1 else '000')
|
|
223 |
# suiteFew.addTest(Connection(test_method, config))
|
|
222 | 224 |
if len(argv) == 0 or argv[0] == 'pithos': |
223 | 225 |
from kamaki.clients.livetest.pithos import Pithos |
224 | 226 |
test_method = 'test_%s' % (argv[1] if len(argv) > 1 else '000') |
b/kamaki/clients/livetest/pithos.py | ||
---|---|---|
154 | 154 |
if_modified_since=now_formated, |
155 | 155 |
success=(204, 304, 412)) |
Also available in: Unified diff