|
From: <ma...@us...> - 2015-01-22 18:21:35
|
Revision: 727
http://sourceforge.net/p/pywbem/code/727
Author: maiera
Date: 2015-01-22 18:21:33 +0000 (Thu, 22 Jan 2015)
Log Message:
-----------
Fixed bug 34: Added proper timeout support.
Modified Paths:
--------------
pywbem/trunk/pywbem/NEWS
pywbem/trunk/pywbem/cim_http.py
pywbem/trunk/pywbem/cim_operations.py
Modified: pywbem/trunk/pywbem/NEWS
===================================================================
--- pywbem/trunk/pywbem/NEWS 2015-01-22 13:19:25 UTC (rev 726)
+++ pywbem/trunk/pywbem/NEWS 2015-01-22 18:21:33 UTC (rev 727)
@@ -1,4 +1,4 @@
-pywbem-0.8.0-dev.r721
+pywbem-0.8.0-dev.r727
ENHANCEMENTS:
@@ -83,6 +83,9 @@
and improved the information in the exception messages. See description
of WBEMConnection class for details. (Andreas Maier)
+ * Added support for timeouts to WBEMConnection, via a new timeout argument,
+ that defaults to no timeout. (Andreas Maier)
+
BUG FIXES:
* Fix syntax error in CIM DTDVERSION error path. Allow KEYVALUE
Modified: pywbem/trunk/pywbem/cim_http.py
===================================================================
--- pywbem/trunk/pywbem/cim_http.py 2015-01-22 13:19:25 UTC (rev 726)
+++ pywbem/trunk/pywbem/cim_http.py 2015-01-22 18:21:33 UTC (rev 727)
@@ -42,6 +42,8 @@
import httplib
import base64
import urllib
+import threading
+from datetime import timedelta, datetime
from M2Crypto import SSL, Err
@@ -66,6 +68,85 @@
"""This exception is raised when the client timeout is exceeded."""
pass
+class HTTPTimeout (object):
+ """HTTP timeout class that is a context manager (for use by 'with'
+ statement).
+
+ Usage:
+ ::
+ with HTTPTimeout(timeout, http_conn):
+ ... operations using http_conn ...
+
+ If the timeout expires, the socket of the HTTP connection is shut down.
+ Once the http operations return as a result of that or for other reasons,
+ the exit handler of this class raises a `cim_http.Error` exception in the
+ thread that executed the ``with`` statement.
+ """
+
+ def __init__(self, timeout, http_conn):
+ """Initialize the HTTPTimeout object.
+
+ :Parameters:
+
+ timeout : number
+ Timeout in seconds, ``None`` means no timeout.
+
+ http_conn : `httplib.HTTPBaseConnection` (or subclass)
+ The connection that is to be stopped when the timeout expires.
+ """
+
+ self._timeout = timeout
+ self._http_conn = http_conn
+ self._retrytime = 10 # time in seconds after which a retry of the
+ # socket shutdown is scheduled if the socket
+ # is not yet on the connection when the
+ # timeout expires initially.
+ self._timer = None
+ self._ts1 = None
+ self._expired = None
+ return
+
+ def __enter__(self):
+ if self._timeout != None:
+ self._timer = threading.Timer(self._timeout,
+ HTTPTimeout.timer_expired, [self])
+ self._timer.start()
+ self._ts1 = datetime.now()
+ self._expired = False
+ return
+
+ def __exit__(self, exc_type, exc_value, traceback):
+ if self._timeout != None:
+ self._timer.cancel()
+ if self._expired:
+ ts2 = datetime.now()
+ duration = ts2 - self._ts1
+ duration_sec = float(duration.microseconds)/1000000 +\
+ duration.seconds + duration.days*24*3600
+ raise TimeoutError("Timeout error: Client timed out after "\
+ "%.0fs." % duration_sec)
+ return False # re-raise any other exceptions
+
+ def timer_expired(self):
+ """
+ This method is invoked in context of the timer thread, so we cannot
+ directly throw exceptions (we can, but they would be in the wrong
+ thread), so instead we cause the socket of the connection to shut down.
+ """
+ self._expired = True
+ if self._http_conn.sock != None:
+ # Timer expired, shutting down socket of HTTP connection.
+ self._http_conn.sock.shutdown(socket.SHUT_RDWR)
+ else:
+ # Timer expired, no socket yet on HTTP connection to shut down,
+ # so we retry. This should only happen with very short timeouts,
+ # so retrying should not hurt, timewise.
+ self._timer.cancel()
+ self._timer = threading.Timer(self._retrytime,
+ HTTPTimeout.timer_expired, [self])
+ self._timer.start()
+ return
+
def parse_url(url):
"""Return a tuple of ``(host, port, ssl)`` from the URL specified in the
``url`` parameter.
@@ -164,7 +245,7 @@
def wbem_request(url, data, creds, headers=[], debug=0, x509=None,
verify_callback=None, ca_certs=None,
- no_verification=False):
+ no_verification=False, timeout=None):
"""
Send an HTTP or HTTPS request to a WBEM server and return the response.
@@ -214,6 +295,14 @@
For details, see the ``no_verification`` parameter of
`WBEMConnection.__init__`.
+ timeout : number
+ Timeout in seconds, for requests sent to the server. If the server did
+ not respond within the timeout duration, the socket for the connection
+ will be closed, causing a `TimeoutError` to be raised.
+ A value of ``None`` means there is no timeout.
+ A value of ``0`` means the timeout is very short, and does not really
+ make any sense.
+
:Returns:
The CIM-XML formatted response data from the WBEM server, as a `unicode`
object.
@@ -241,13 +330,13 @@
class HTTPConnection(HTTPBaseConnection, httplib.HTTPConnection):
def __init__(self, host, port=None, strict=None):
- httplib.HTTPConnection.__init__(self, host, port, strict)
+ httplib.HTTPConnection.__init__(self, host, port, strict, timeout)
class HTTPSConnection(HTTPBaseConnection, httplib.HTTPSConnection):
def __init__(self, host, port=None, key_file=None, cert_file=None,
strict=None, ca_certs=None, verify_callback=None):
httplib.HTTPSConnection.__init__(self, host, port, key_file,
- cert_file, strict)
+ cert_file, strict, timeout)
self.ca_certs = ca_certs
self.verify_callback = verify_callback
@@ -349,10 +438,13 @@
key_file=key_file,
cert_file=cert_file,
ca_certs=ca_certs,
- verify_callback=verify_callback)
+ verify_callback=verify_callback,
+ timeout=timeout)
else:
if url.startswith('http'):
- h = HTTPConnection(host, port=port)
+ h = HTTPConnection(host,
+ port=port,
+ timeout=timeout)
else:
if url.startswith('file:'):
url = url[5:]
@@ -374,146 +466,153 @@
locallogin = getpass.getuser()
except (KeyError, ImportError):
locallogin = None
- while numTries < tryLimit:
- numTries = numTries + 1
- h.putrequest('POST', '/cimom')
+ with HTTPTimeout(timeout, h):
- h.putheader('Content-type', 'application/xml; charset="utf-8"')
- h.putheader('Content-length', str(len(data)))
- if localAuthHeader is not None:
- h.putheader(*localAuthHeader)
- elif creds is not None:
- h.putheader('Authorization', 'Basic %s' %
- base64.encodestring(
- '%s:%s' %
- (creds[0], creds[1])).replace('\n', ''))
- elif locallogin is not None:
- h.putheader('PegasusAuthorization', 'Local "%s"' % locallogin)
+ while numTries < tryLimit:
+ numTries = numTries + 1
- for hdr in headers:
- if isinstance(hdr, unicode):
- hdr = hdr.encode('utf-8')
- s = map(lambda x: string.strip(x), string.split(hdr, ":", 1))
- h.putheader(urllib.quote(s[0]), urllib.quote(s[1]))
+ h.putrequest('POST', '/cimom')
- try:
- # See RFC 2616 section 8.2.2
- # An http server is allowed to send back an error (presumably
- # a 401), and close the connection without reading the entire
- # request. A server may do this to protect itself from a DoS
- # attack.
- #
- # If the server closes the connection during our h.send(), we
- # will either get a socket exception 104 (TCP RESET), or a
- # socket exception 32 (broken pipe). In either case, thanks
- # to our fixed HTTPConnection classes, we'll still be able to
- # retrieve the response so that we can read and respond to the
- # authentication challenge.
- h.endheaders()
+ h.putheader('Content-type', 'application/xml; charset="utf-8"')
+ h.putheader('Content-length', str(len(data)))
+ if localAuthHeader is not None:
+ h.putheader(*localAuthHeader)
+ elif creds is not None:
+ h.putheader('Authorization', 'Basic %s' %
+ base64.encodestring(
+ '%s:%s' %
+ (creds[0], creds[1])).replace('\n', ''))
+ elif locallogin is not None:
+ h.putheader('PegasusAuthorization', 'Local "%s"' % locallogin)
+
+ for hdr in headers:
+ if isinstance(hdr, unicode):
+ hdr = hdr.encode('utf-8')
+ s = map(lambda x: string.strip(x), string.split(hdr, ":", 1))
+ h.putheader(urllib.quote(s[0]), urllib.quote(s[1]))
+
try:
- h.send(data)
- except socket.error, arg:
- if arg[0] != 104 and arg[0] != 32:
- raise
+ # See RFC 2616 section 8.2.2
+ # An http server is allowed to send back an error (presumably
+ # a 401), and close the connection without reading the entire
+ # request. A server may do this to protect itself from a DoS
+ # attack.
+ #
+ # If the server closes the connection during our h.send(), we
+ # will either get a socket exception 104 (TCP RESET), or a
+ # socket exception 32 (broken pipe). In either case, thanks
+ # to our fixed HTTPConnection classes, we'll still be able to
+ # retrieve the response so that we can read and respond to the
+ # authentication challenge.
+ h.endheaders()
+ try:
+ h.send(data)
+ except socket.error, arg:
+ if arg[0] != 104 and arg[0] != 32:
+ raise
- response = h.getresponse()
+ response = h.getresponse()
- if response.status != 200:
- if response.status == 401:
- if numTries >= tryLimit:
- raise AuthError(response.reason)
- if not local:
- raise AuthError(response.reason)
- authChal = response.getheader('WWW-Authenticate', '')
- if 'openwbem' in response.getheader('Server', ''):
- if 'OWLocal' not in authChal:
+ if response.status != 200:
+ if response.status == 401:
+ if numTries >= tryLimit:
+ raise AuthError(response.reason)
+ if not local:
+ raise AuthError(response.reason)
+ authChal = response.getheader('WWW-Authenticate', '')
+ if 'openwbem' in response.getheader('Server', ''):
+ if 'OWLocal' not in authChal:
+ try:
+ uid = os.getuid()
+ except AttributeError:
+ raise Error("OWLocal authorization for "\
+ "openwbem server not supported "\
+ "on %s platform due to missing "\
+ "os.getuid()" % platform.system())
+ localAuthHeader = ('Authorization',
+ 'OWLocal uid="%d"' % uid)
+ continue
+ else:
+ try:
+ nonceIdx = authChal.index('nonce=')
+ nonceBegin = authChal.index('"', nonceIdx)
+ nonceEnd = authChal.index('"', nonceBegin+1)
+ nonce = authChal[nonceBegin+1:nonceEnd]
+ cookieIdx = authChal.index('cookiefile=')
+ cookieBegin = authChal.index('"', cookieIdx)
+ cookieEnd = authChal.index('"', cookieBegin+1)
+ cookieFile = authChal[cookieBegin+1:cookieEnd]
+ f = open(cookieFile, 'r')
+ cookie = f.read().strip()
+ f.close()
+ localAuthHeader = (
+ 'Authorization',
+ 'OWLocal nonce="%s", cookie="%s"' % \
+ (nonce, cookie))
+ continue
+ except:
+ localAuthHeader = None
+ continue
+ elif 'Local' in authChal:
try:
- uid = os.getuid()
- except AttributeError:
- raise Error("OWLocal authorization for "\
- "openwbem server not supported on %s "\
- "platform due to missing os.getuid()"%\
- platform.system())
- localAuthHeader = ('Authorization',
- 'OWLocal uid="%d"' % uid)
- continue
- else:
- try:
- nonceIdx = authChal.index('nonce=')
- nonceBegin = authChal.index('"', nonceIdx)
- nonceEnd = authChal.index('"', nonceBegin+1)
- nonce = authChal[nonceBegin+1:nonceEnd]
- cookieIdx = authChal.index('cookiefile=')
- cookieBegin = authChal.index('"', cookieIdx)
- cookieEnd = authChal.index('"', cookieBegin+1)
- cookieFile = authChal[cookieBegin+1:cookieEnd]
- f = open(cookieFile, 'r')
- cookie = f.read().strip()
- f.close()
- localAuthHeader = (
- 'Authorization',
- 'OWLocal nonce="%s", cookie="%s"' % \
- (nonce, cookie))
- continue
- except:
- localAuthHeader = None
- continue
- elif 'Local' in authChal:
- try:
- beg = authChal.index('"') + 1
- end = authChal.rindex('"')
- if end > beg:
- file = authChal[beg:end]
- fo = open(file, 'r')
- cookie = fo.read().strip()
- fo.close()
- localAuthHeader = (
- 'PegasusAuthorization',
- 'Local "%s:%s:%s"' % \
- (locallogin, file, cookie))
- continue
- except ValueError:
- pass
+ beg = authChal.index('"') + 1
+ end = authChal.rindex('"')
+ if end > beg:
+ file = authChal[beg:end]
+ fo = open(file, 'r')
+ cookie = fo.read().strip()
+ fo.close()
+ localAuthHeader = (
+ 'PegasusAuthorization',
+ 'Local "%s:%s:%s"' % \
+ (locallogin, file, cookie))
+ continue
+ except ValueError:
+ pass
- raise AuthError(response.reason)
- if response.getheader('CIMError', None) is not None and \
- response.getheader('PGErrorDetail', None) is not None:
- raise Error(
- 'CIMError: %s: %s' %
- (response.getheader('CIMError'),
- urllib.unquote(response.getheader('PGErrorDetail'))))
- raise Error('HTTP error: %s' % response.reason)
+ raise AuthError(response.reason)
+ if response.getheader('CIMError', None) is not None and \
+ response.getheader('PGErrorDetail', None) is not None:
+ raise Error(
+ 'CIMError: %s: %s' %
+ (response.getheader('CIMError'),
+ urllib.unquote(response.getheader(
+ 'PGErrorDetail'))))
+ raise Error('HTTP error: %s' % response.reason)
- body = response.read()
+ body = response.read()
- except httplib.BadStatusLine, arg:
- # Background: BadStatusLine is documented to be raised only when
- # strict=True is used (that is not the case here). However, httplib
- # currently raises BadStatusLine also independent of strict when a
- # keep-alive connection times out (e.g. because the server went
- # down). See http://bugs.python.org/issue8450.
- # On how to detect this: A connection timeout definitely causes
- # arg==None, but it is not clear whether other situations could
- # also cause arg==None.
- if arg.line.strip().strip("'") == '':
- raise ConnectionError("Connection error: The CIM server "\
- "closed the connection without "\
- "returning any data, or the client "\
- "timed out")
- else:
- raise Error("HTTP error: The CIM server returned a bad HTTP "\
- "status line: '%s'" % arg.line)
- except httplib.IncompleteRead, arg:
- raise ConnectionError("Connection error: HTTP incomplete read: %s" % arg)
- except httplib.NotConnected, arg:
- raise ConnectionError("Connection error: HTTP not connected: %s" % arg)
- except socket.error, arg:
- raise ConnectionError("Connection error: Socket error %s" % arg)
- except socket.sslerror, arg:
- raise ConnectionError("Connection error: SSL error %s" % arg)
+ except httplib.BadStatusLine, arg:
+ # Background: BadStatusLine is documented to be raised only
+ # when strict=True is used (that is not the case here).
+ # However, httplib currently raises BadStatusLine also
+ # independent of strict when a keep-alive connection times out
+ # (e.g. because the server went down).
+ # See http://bugs.python.org/issue8450.
+ # On how to detect this: A connection timeout definitely causes
+ # arg==None, but it is not clear whether other situations could
+ # also cause arg==None.
+ if arg.line.strip().strip("'") == '':
+ raise ConnectionError("Connection error: The CIM server "\
+ "closed the connection without "\
+ "returning any data, or the client "\
+ "timed out")
+ else:
+ raise Error("HTTP error: The CIM server returned a bad "\
+ "HTTP status line: '%s'" % arg.line)
+ except httplib.IncompleteRead, arg:
+ raise ConnectionError("Connection error: HTTP incomplete "\
+ "read: %s" % arg)
+ except httplib.NotConnected, arg:
+ raise ConnectionError("Connection error: HTTP not "\
+ "connected: %s" % arg)
+ except socket.error, arg:
+ raise ConnectionError("Connection error: Socket error %s" %arg)
+ except socket.sslerror, arg:
+ raise ConnectionError("Connection error: SSL error %s" % arg)
- break
+ break
return body
Modified: pywbem/trunk/pywbem/cim_operations.py
===================================================================
--- pywbem/trunk/pywbem/cim_operations.py 2015-01-22 13:19:25 UTC (rev 726)
+++ pywbem/trunk/pywbem/cim_operations.py 2015-01-22 18:21:33 UTC (rev 727)
@@ -398,7 +398,7 @@
def __init__(self, url, creds=None, default_namespace=DEFAULT_NAMESPACE,
x509=None, verify_callback=None, ca_certs=None,
- no_verification=False):
+ no_verification=False, timeout=None):
"""
Initialize the `WBEMConnection` object.
@@ -512,6 +512,14 @@
Default: `False`.
+ timeout : number
+ Timeout in seconds, for requests sent to the server. If the server
+ did not respond within the timeout duration, the socket for the
+ connection will be closed, causing a `TimeoutError` to be raised.
+ A value of ``None`` means there is no timeout.
+ A value of ``0`` means the timeout is very short, and does not
+ really make any sense.
+
:Exceptions:
See the list of exceptions described in `WBEMConnection`.
@@ -524,6 +532,7 @@
self.ca_certs = ca_certs
self.no_verification = no_verification
self.default_namespace = default_namespace
+ self.timeout = timeout
self.debug = False
self.last_raw_request = None
@@ -609,7 +618,8 @@
x509=self.x509,
verify_callback=self.verify_callback,
ca_certs=self.ca_certs,
- no_verification=self.no_verification)
+ no_verification=self.no_verification,
+ timeout=self.timeout)
except (cim_http.AuthError, cim_http.ConnectionError,
cim_http.TimeoutError, cim_http.Error) as exc:
raise CIMError(0, str(exc), exc)
@@ -825,7 +835,8 @@
x509=self.x509,
verify_callback=self.verify_callback,
ca_certs=self.ca_certs,
- no_verification=self.no_verification)
+ no_verification=self.no_verification,
+ timeout=self.timeout)
except (cim_http.AuthError, cim_http.ConnectionError,
cim_http.TimeoutError, cim_http.Error) as exc:
raise CIMError(0, str(exc), exc)
This was sent by the SourceForge.net collaborative development platform, the world's largest Open Source development site.
|