4 # Copyright (C) 2007, 2008, 2010 Google Inc.
6 # This program is free software; you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation; either version 2 of the License, or
9 # (at your option) any later version.
11 # This program is distributed in the hope that it will be useful, but
12 # WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14 # General Public License for more details.
16 # You should have received a copy of the GNU General Public License
17 # along with this program; if not, write to the Free Software
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
21 """HTTP client module.
27 from cStringIO import StringIO
29 from ganeti import http
30 from ganeti import compat
33 class HttpClientRequest(object):
34 def __init__(self, host, port, method, path, headers=None, post_data=None,
35 read_timeout=None, curl_config_fn=None):
36 """Describes an HTTP request.
43 @param method: Method name
45 @param path: Request path
46 @type headers: list or None
47 @param headers: Additional headers to send, list of strings
48 @type post_data: string or None
49 @param post_data: Additional data to send
50 @type read_timeout: int
51 @param read_timeout: if passed, it will be used as the read
52 timeout while reading the response from the server
53 @type curl_config_fn: callable
54 @param curl_config_fn: Function to configure cURL object before request
55 (Note: if the function configures the connection in
56 a way where it wouldn't be efficient to reuse them,
57 a "identity" property should be defined, see
58 L{HttpClientRequest.identity})
61 assert path.startswith("/"), "Path must start with slash (/)"
62 assert curl_config_fn is None or callable(curl_config_fn)
69 self.read_timeout = read_timeout
70 self.curl_config_fn = curl_config_fn
75 self.post_data = post_data
79 elif isinstance(headers, dict):
80 # Support for old interface
81 self.headers = ["%s: %s" % (name, value)
82 for name, value in headers.items()]
84 self.headers = headers
91 self.resp_status_code = None
95 status = ["%s.%s" % (self.__class__.__module__, self.__class__.__name__),
96 "%s:%s" % (self.host, self.port),
100 return "<%s at %#x>" % (" ".join(status), id(self))
104 """Returns the full URL for this requests.
107 # TODO: Support for non-SSL requests
108 return "https://%s:%s%s" % (self.host, self.port, self.path)
112 """Returns identifier for retrieving a pooled connection for this request.
114 This allows cURL client objects to be re-used and to cache information
115 (e.g. SSL session IDs or connections).
118 parts = [self.host, self.port]
120 if self.curl_config_fn:
122 parts.append(self.curl_config_fn.identity)
123 except AttributeError:
126 return "/".join(str(i) for i in parts)
129 class _HttpClient(object):
130 def __init__(self, curl_config_fn):
131 """Initializes this class.
133 @type curl_config_fn: callable
134 @param curl_config_fn: Function to configure cURL object after
140 curl = self._CreateCurlHandle()
141 curl.setopt(pycurl.VERBOSE, False)
142 curl.setopt(pycurl.NOSIGNAL, True)
143 curl.setopt(pycurl.USERAGENT, http.HTTP_GANETI_VERSION)
144 curl.setopt(pycurl.PROXY, "")
146 # Disable SSL session ID caching (pycurl >= 7.16.0)
147 if hasattr(pycurl, "SSL_SESSIONID_CACHE"):
148 curl.setopt(pycurl.SSL_SESSIONID_CACHE, False)
150 # Pass cURL object to external config function
157 def _CreateCurlHandle():
158 """Returns a new cURL object.
163 def GetCurlHandle(self):
164 """Returns the cURL object.
169 def GetCurrentRequest(self):
170 """Returns the current request.
172 @rtype: L{HttpClientRequest} or None
177 def StartRequest(self, req):
178 """Starts a request on this client.
180 @type req: L{HttpClientRequest}
181 @param req: HTTP request
184 assert not self._req, "Another request is already started"
187 self._resp_buffer = StringIO()
191 post_data = req.post_data
192 headers = req.headers
194 # PycURL requires strings to be non-unicode
195 assert isinstance(method, str)
196 assert isinstance(url, str)
197 assert isinstance(post_data, str)
198 assert compat.all(isinstance(i, str) for i in headers)
200 # Configure cURL object for request
202 curl.setopt(pycurl.CUSTOMREQUEST, str(method))
203 curl.setopt(pycurl.URL, url)
204 curl.setopt(pycurl.POSTFIELDS, post_data)
205 curl.setopt(pycurl.WRITEFUNCTION, self._resp_buffer.write)
206 curl.setopt(pycurl.HTTPHEADER, headers)
208 if req.read_timeout is None:
209 curl.setopt(pycurl.TIMEOUT, 0)
211 curl.setopt(pycurl.TIMEOUT, int(req.read_timeout))
213 # Pass cURL object to external config function
214 if req.curl_config_fn:
215 req.curl_config_fn(curl)
217 def Done(self, errmsg):
218 """Finishes a request.
220 @type errmsg: string or None
221 @param errmsg: Error message if request failed
225 assert req, "No request"
227 logging.debug("Request %s finished, errmsg=%s", req, errmsg)
231 req.success = not bool(errmsg)
234 # Get HTTP response code
235 req.resp_status_code = curl.getinfo(pycurl.RESPONSE_CODE)
236 req.resp_body = self._resp_buffer.getvalue()
238 # Reset client object
240 self._resp_buffer = None
242 # Ensure no potentially large variables are referenced
243 curl.setopt(pycurl.POSTFIELDS, "")
244 curl.setopt(pycurl.WRITEFUNCTION, lambda _: None)
247 class _PooledHttpClient:
248 """Data structure for HTTP client pool.
251 def __init__(self, identity, client):
252 """Initializes this class.
254 @type identity: string
255 @param identity: Client identifier for pool
256 @type client: L{_HttpClient}
257 @param client: HTTP client
260 self.identity = identity
265 status = ["%s.%s" % (self.__class__.__module__, self.__class__.__name__),
266 "id=%s" % self.identity,
267 "lastuse=%s" % self.lastused,
270 return "<%s at %#x>" % (" ".join(status), id(self))
273 class HttpClientPool:
274 """A simple HTTP client pool.
276 Supports one pooled connection per identity (see
277 L{HttpClientRequest.identity}).
280 #: After how many generations to drop unused clients
281 _MAX_GENERATIONS_DROP = 25
283 def __init__(self, curl_config_fn):
284 """Initializes this class.
286 @type curl_config_fn: callable
287 @param curl_config_fn: Function to configure cURL object after
291 self._curl_config_fn = curl_config_fn
296 def _GetHttpClientCreator():
297 """Returns callable to create HTTP client.
302 def _Get(self, identity):
303 """Gets an HTTP client from the pool.
305 @type identity: string
306 @param identity: Client identifier
310 pclient = self._pool.pop(identity)
312 # Need to create new client
313 client = self._GetHttpClientCreator()(self._curl_config_fn)
314 pclient = _PooledHttpClient(identity, client)
315 logging.debug("Created new client %s", pclient)
317 logging.debug("Reusing client %s", pclient)
319 assert pclient.identity == identity
323 def _StartRequest(self, req):
326 @type req: L{HttpClientRequest}
327 @param req: HTTP request
330 logging.debug("Starting request %r", req)
331 pclient = self._Get(req.identity)
333 assert req.identity not in self._pool
335 pclient.client.StartRequest(req)
336 pclient.lastused = self._generation
340 def _Return(self, pclients):
341 """Returns HTTP clients to the pool.
345 logging.debug("Returning client %s to pool", pc)
346 assert pc.identity not in self._pool
347 assert pc not in self._pool.values()
348 self._pool[pc.identity] = pc
350 # Check for unused clients
351 for pc in self._pool.values():
352 if (pc.lastused + self._MAX_GENERATIONS_DROP) < self._generation:
353 logging.debug("Removing client %s which hasn't been used"
354 " for %s generations",
355 pc, self._MAX_GENERATIONS_DROP)
356 self._pool.pop(pc.identity, None)
358 assert compat.all(pc.lastused >= (self._generation -
359 self._MAX_GENERATIONS_DROP)
360 for pc in self._pool.values())
363 def _CreateCurlMultiHandle():
364 """Creates new cURL multi handle.
367 return pycurl.CurlMulti()
369 def ProcessRequests(self, requests):
370 """Processes any number of HTTP client requests using pooled objects.
372 @type requests: list of L{HttpClientRequest}
373 @param requests: List of all requests
376 multi = self._CreateCurlMultiHandle()
379 self._generation += 1
381 assert compat.all((req.error is None and
382 req.success is None and
383 req.resp_status_code is None and
384 req.resp_body is None)
389 pclient = self._StartRequest(req)
390 curl = pclient.client.GetCurlHandle()
391 curl_to_pclient[curl] = pclient
392 multi.add_handle(curl)
393 assert pclient.client.GetCurrentRequest() == req
394 assert pclient.lastused >= 0
396 assert len(curl_to_pclient) == len(requests)
400 (ret, _) = multi.perform()
401 assert ret in (pycurl.E_MULTI_OK, pycurl.E_CALL_MULTI_PERFORM)
403 if ret == pycurl.E_CALL_MULTI_PERFORM:
404 # cURL wants to be called again
408 (remaining_messages, successful, failed) = multi.info_read()
410 for curl in successful:
411 multi.remove_handle(curl)
413 pclient = curl_to_pclient[curl]
414 req = pclient.client.GetCurrentRequest()
415 pclient.client.Done(None)
417 assert not pclient.client.GetCurrentRequest()
419 for curl, errnum, errmsg in failed:
420 multi.remove_handle(curl)
422 pclient = curl_to_pclient[curl]
423 req = pclient.client.GetCurrentRequest()
424 pclient.client.Done("Error %s: %s" % (errnum, errmsg))
426 assert not pclient.client.GetCurrentRequest()
428 if remaining_messages == 0:
431 assert done_count <= len(requests)
433 if done_count == len(requests):
436 # Wait for I/O. The I/O timeout shouldn't be too long so that HTTP
437 # timeouts, which are only evaluated in multi.perform, aren't
438 # unnecessarily delayed.
441 assert compat.all(pclient.client.GetCurrentRequest() is None
442 for pclient in curl_to_pclient.values())
444 # Return clients to pool
445 self._Return(curl_to_pclient.values())
447 assert done_count == len(requests)
448 assert compat.all(req.error is not None or
450 req.resp_status_code is not None and
451 req.resp_body is not None)