diff --git a/.travis.yml b/.travis.yml index df8f020..b27d9b4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,13 +1,11 @@ language: python python: - - "2.6" - "2.7" - "pypy" - "3.3" - "3.4" before_install: - - if [[ $TRAVIS_PYTHON_VERSION == '2.6' ]]; then pip install unittest2; fi - pip install -r requirements.txt - pip install -r test-requirements.txt diff --git a/pyVim/connect.py b/pyVim/connect.py index 574d142..5691097 100644 --- a/pyVim/connect.py +++ b/pyVim/connect.py @@ -34,6 +34,7 @@ import requests from requests.auth import HTTPBasicAuth from pyVmomi import vim, vmodl, SoapStubAdapter, SessionOrientedStub +from pyVmomi.SoapAdapter import CONNECTION_POOL_IDLE_TIMEOUT_SEC from pyVmomi.VmomiSupport import nsMap, versionIdMap, versionMap, IsChildVersion from pyVmomi.VmomiSupport import GetServiceVersions @@ -177,7 +178,7 @@ class VimSessionOrientedStub(SessionOrientedStub): def Connect(host='localhost', port=443, user='root', pwd='', service="hostd", adapter="SOAP", namespace=None, path="/sdk", - version=None, keyFile=None, certFile=None, + version=None, keyFile=None, certFile=None, thumbprint=None, sslContext=None): """ Connect to the specified server, login and return the service @@ -212,6 +213,8 @@ def Connect(host='localhost', port=443, user='root', pwd='', @type keyFile: string @param certFile: ssl cert file path @type certFile: string + @param thumbprint: host cert thumbprint + @type thumbprint: string @param sslContext: SSL Context describing the various SSL options. It is only supported in Python 2.7.9 or higher. @type sslContext: SSL.Context @@ -233,7 +236,7 @@ def Connect(host='localhost', port=443, user='root', pwd='', elif not version: version="vim.version.version6" si, stub = __Login(host, port, user, pwd, service, adapter, version, path, - keyFile, certFile, sslContext) + keyFile, certFile, thumbprint, sslContext) SetSi(si) return si @@ -268,7 +271,7 @@ def GetLocalTicket(si, user): ## connected service instance object. def __Login(host, port, user, pwd, service, adapter, version, path, - keyFile, certFile, sslContext): + keyFile, certFile, thumbprint, sslContext): """ Private method that performs the actual Connect and returns a connected service instance object. @@ -293,6 +296,8 @@ def __Login(host, port, user, pwd, service, adapter, version, path, @type keyFile: string @param certFile: ssl cert file path @type certFile: string + @param thumbprint: host cert thumbprint + @type thumbprint: string @param sslContext: SSL Context describing the various SSL options. It is only supported in Python 2.7.9 or higher. @type sslContext: SSL.Context @@ -304,7 +309,8 @@ def __Login(host, port, user, pwd, service, adapter, version, path, # Create the SOAP stub adapter stub = SoapStubAdapter(host, port, version=version, path=path, - certKeyFile=keyFile, certFile=certFile, sslContext=sslContext) + certKeyFile=keyFile, certFile=certFile, + thumbprint=thumbprint, sslContext=sslContext) # Get Service instance si = vim.ServiceInstance("ServiceInstance", stub) @@ -555,10 +561,54 @@ def __FindSupportedVersion(protocol, server, port, path, preferredApiVersions, s return desiredVersion return None +def SmartStubAdapter(host='localhost', port=443, path='/sdk', + url=None, sock=None, poolSize=5, + certFile=None, certKeyFile=None, + httpProxyHost=None, httpProxyPort=80, sslProxyPath=None, + thumbprint=None, cacertsFile=None, preferredApiVersions=None, + acceptCompressedResponses=True, + connectionPoolTimeout=CONNECTION_POOL_IDLE_TIMEOUT_SEC, + samlToken=None, sslContext=None): + """ + Determine the most preferred API version supported by the specified server, + then create a soap stub adapter using that version + + The parameters are the same as for pyVmomi.SoapStubAdapter except for + version which is renamed to prefferedApiVersions + + @param preferredApiVersions: Acceptable API version(s) (e.g. vim.version.version3) + If a list of versions is specified the versions should + be ordered from most to least preferred. If None is + specified, the list of versions support by pyVmomi will + be used. + @type preferredApiVersions: string or string list + """ + if preferredApiVersions is None: + preferredApiVersions = GetServiceVersions('vim25') + + supportedVersion = __FindSupportedVersion('https' if port > 0 else 'http', + host, + port, + path, + preferredApiVersions, + sslContext) + if supportedVersion is None: + raise Exception("%s:%s is not a VIM server" % (host, port)) + + return SoapStubAdapter(host=host, port=port, path=path, + url=url, sock=sock, poolSize=poolSize, + certFile=certFile, certKeyFile=certKeyFile, + httpProxyHost=httpProxyHost, httpProxyPort=httpProxyPort, + sslProxyPath=sslProxyPath, thumbprint=thumbprint, + cacertsFile=cacertsFile, version=supportedVersion, + acceptCompressedResponses=acceptCompressedResponses, + connectionPoolTimeout=connectionPoolTimeout, + samlToken=samlToken, sslContext=sslContext) def SmartConnect(protocol='https', host='localhost', port=443, user='root', pwd='', service="hostd", path="/sdk", - preferredApiVersions=None, sslContext=None): + preferredApiVersions=None, + keyFile=None, certFile=None, thumbprint=None, sslContext=None): """ Determine the most preferred API version supported by the specified server, then connect to the specified server using that API version, login and return @@ -591,6 +641,12 @@ def SmartConnect(protocol='https', host='localhost', port=443, user='root', pwd= specified, the list of versions support by pyVmomi will be used. @type preferredApiVersions: string or string list + @param keyFile: ssl key file path + @type keyFile: string + @param certFile: ssl cert file path + @type certFile: string + @param thumbprint: host cert thumbprint + @type thumbprint: string @param sslContext: SSL Context describing the various SSL options. It is only supported in Python 2.7.9 or higher. @type sslContext: SSL.Context @@ -618,6 +674,9 @@ def SmartConnect(protocol='https', host='localhost', port=443, user='root', pwd= adapter='SOAP', version=supportedVersion, path=path, + keyFile=keyFile, + certFile=certFile, + thumbprint=thumbprint, sslContext=sslContext) def OpenUrlWithBasicAuth(url, user='root', pwd=''): diff --git a/pyVmomi/SoapAdapter.py b/pyVmomi/SoapAdapter.py index 42179ca..8c04499 100644 --- a/pyVmomi/SoapAdapter.py +++ b/pyVmomi/SoapAdapter.py @@ -104,6 +104,17 @@ def encode(string, encoding): return string.encode(encoding) return u(string) +## Thumbprint mismatch exception +# +class ThumbprintMismatchException(Exception): + def __init__(self, expected, actual): + Exception.__init__(self, "Server has wrong SHA1 thumbprint: %s " + "(required) != %s (server)" % ( + expected, actual)) + + self.expected = expected + self.actual = actual + ## Escape <, >, & def XmlEscape(xmlStr): escaped = xmlStr.replace("&", "&").replace(">", ">").replace("<", "<") @@ -236,7 +247,7 @@ class SoapSerializer: # @param info the field def SerializeFaultDetail(self, val, info): """ Serialize an object """ - self._SerializeDataObject(val, info, '', self.defaultNS) + self._SerializeDataObject(val, info, ' xsi:typ="{1}"'.format(val._wsdlName), self.defaultNS) def _NSPrefix(self, ns): """ Get xml ns prefix. self.nsMap must be set """ @@ -494,12 +505,7 @@ class ExpatDeserializerNSHandlers: ## Get current default ns def GetCurrDefNS(self): - namespaces = self.nsMap.get(None) - if namespaces: - ns = namespaces[-1] - else: - ns = "" - return ns + return self._GetNamespaceFromPrefix() ## Get namespace and wsdl name from tag def GetNSAndWsdlname(self, tag): @@ -510,9 +516,17 @@ class ExpatDeserializerNSHandlers: else: prefix, name = None, tag # Map prefix to ns - ns = self.nsMap[prefix][-1] + ns = self._GetNamespaceFromPrefix(prefix) return ns, name + def _GetNamespaceFromPrefix(self, prefix = None): + namespaces = self.nsMap.get(prefix) + if namespaces: + ns = namespaces[-1] + else: + ns = "" + return ns + ## Handle namespace begin def StartNamespaceDeclHandler(self, prefix, uri): namespaces = self.nsMap.get(prefix) @@ -930,9 +944,7 @@ try: sha1.update(derCert) sha1Digest = sha1.hexdigest().lower() if sha1Digest != thumbprint: - raise Exception("Server has wrong SHA1 thumbprint: {0} " - "(required) != {1} (server)".format( - thumbprint, sha1Digest)) + raise ThumbprintMismatchException(thumbprint, sha1Digest) # Function used to wrap sockets with SSL _SocketWrapper = ssl.wrap_socket @@ -963,7 +975,7 @@ except ImportError: class HTTPSConnectionWrapper(object): def __init__(self, *args, **kwargs): wrapped = http_client.HTTPSConnection(*args, **kwargs) - # Extract ssl.wrap_socket param unknown to httplib.HTTPConnection, + # Extract ssl.wrap_socket param unknown to httplib.HTTPSConnection, # and push back the params in connect() self._sslArgs = {} tmpKwargs = kwargs.copy() @@ -1028,11 +1040,13 @@ class SSLTunnelConnection(object): # @param kwargs In case caller passed in extra parameters not handled by # SSLTunnelConnection def __call__(self, path, key_file=None, cert_file=None, **kwargs): - # Don't pass any keyword args that HTTPConnection won't understand. - for arg in kwargs.keys(): - if arg not in ("port", "strict", "timeout", "source_address"): - del kwargs[arg] - tunnel = http_client.HTTPConnection(path, **kwargs) + # Only pass in the named arguments that HTTPConnection constructor + # understands + tmpKwargs = {} + for key in http_client.HTTPConnection.__init__.__code__.co_varnames: + if key in kwargs and key != 'self': + tmpKwargs[key] = kwargs[key] + tunnel = http_client.HTTPConnection(path, **tmpKwargs) tunnel.request('CONNECT', self.proxyPath) resp = tunnel.getresponse() if resp.status != 200: @@ -1164,7 +1178,7 @@ class SoapStubAdapter(SoapStubAdapterBase): # the UnixSocketConnection ctor expects to find it -- see above self.host = sock elif url: - scheme, self.host, urlpath = urlparse.urlparse(url)[:3] + scheme, self.host, urlpath = urlparse(url)[:3] # Only use the URL path if it's sensible, otherwise use the path # keyword argument as passed in. if urlpath not in ('', '/'): @@ -1219,6 +1233,20 @@ class SoapStubAdapter(SoapStubAdapterBase): self.requestModifierList = [] self._acceptCompressedResponses = acceptCompressedResponses + # Force a socket shutdown. Before python 2.7, ssl will fail to close + # the socket (http://bugs.python.org/issue10127). + # Not making this a part of the actual _HTTPSConnection since the internals + # of the httplib.HTTP*Connection seem to pass around the descriptors and + # depend on the behavior that close() still leaves the socket semi-functional. + if sys.version_info[:2] < (2,7): + def _CloseConnection(self, conn): + # import pdb; pdb.set_trace() + if self.scheme == HTTPSConnectionWrapper and conn.sock: + conn.sock.shutdown(socket.SHUT_RDWR) + conn.close() + else: + def _CloseConnection(self, conn): + conn.close() # Context modifier used to modify the SOAP request. # @param func The func that takes in the serialized message and modifies the @@ -1287,7 +1315,7 @@ class SoapStubAdapter(SoapStubAdapterBase): deserializer = SoapResponseDeserializer(outerStub) obj = deserializer.Deserialize(fd, info.result) except Exception as exc: - conn.close() + self._CloseConnection(conn) # NOTE (hartsock): This feels out of place. As a rule the lexical # context that opens a connection should also close it. However, # in this code the connection is passed around and closed in other @@ -1306,7 +1334,7 @@ class SoapStubAdapter(SoapStubAdapterBase): else: raise obj # pylint: disable-msg=E0702 else: - conn.close() + self._CloseConnection(conn) raise http_client.HTTPException("{0} {1}".format(resp.status, resp.reason)) ## Clean up connection pool to throw away idle timed-out connections @@ -1324,7 +1352,7 @@ class SoapStubAdapter(SoapStubAdapterBase): break for conn, _ in idleConnections: - conn.close() + self._CloseConnection(conn) ## Get a HTTP connection from the pool def GetConnection(self): @@ -1365,7 +1393,7 @@ class SoapStubAdapter(SoapStubAdapterBase): self.pool = [] self.lock.release() for conn, _ in oldConnections: - conn.close() + self._CloseConnection(conn) ## Return a HTTP connection to the pool def ReturnConnection(self, conn): @@ -1380,7 +1408,7 @@ class SoapStubAdapter(SoapStubAdapterBase): # NOTE (hartsock): this seems to violate good coding practice in that # the lexical context that opens a connection should also be the # same context responsible for closing it. - conn.close() + self._CloseConnection(conn) ## Disable nagle on a http connections def DisableNagle(self, conn): diff --git a/pyVmomi/VmomiSupport.py b/pyVmomi/VmomiSupport.py index c0eafdd..245611f 100644 --- a/pyVmomi/VmomiSupport.py +++ b/pyVmomi/VmomiSupport.py @@ -465,6 +465,9 @@ class ManagedObject(object): self.__class__ == other.__class__ and \ self._serverGuid == other._serverGuid + def __ne__(self, other): + return not(self == other) + def __hash__(self): return str(self).__hash__() @@ -1248,11 +1251,15 @@ class _BuildVersions: self._nsMap = {} def Add(self, version): - vmodlNs = version.split(".",1)[0] - if not (vmodlNs in self._verMap): - self._verMap[vmodlNs] = version - if not (vmodlNs in self._nsMap): - self._nsMap[vmodlNs] = GetVersionNamespace(version) + assert '.version.' in version, 'Invalid version %s' % version + + vmodlNs = version.split(".version.", 1)[0].split(".") + for idx in [1, len(vmodlNs)]: + subVmodlNs = ".".join(vmodlNs[:idx]) + if not (subVmodlNs in self._verMap): + self._verMap[subVmodlNs] = version + if not (subVmodlNs in self._nsMap): + self._nsMap[subVmodlNs] = GetVersionNamespace(version) def Get(self, vmodlNs): return self._verMap[vmodlNs] diff --git a/pyVmomi/__init__.py b/pyVmomi/__init__.py index 999204e..47dc56f 100644 --- a/pyVmomi/__init__.py +++ b/pyVmomi/__init__.py @@ -198,7 +198,7 @@ except ImportError: pyVmomi.VmomiSupport.GetVmodlType("vmodl.DynamicData") from pyVmomi.SoapAdapter import SoapStubAdapter, StubAdapterBase, SoapCmdStubAdapter, \ - SessionOrientedStub + SessionOrientedStub, ThumbprintMismatchException types = pyVmomi.VmomiSupport.types diff --git a/tox.ini b/tox.ini index 0d419a4..f434dfe 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py26,py27,py33,py34 +envlist = py27,py33,py34 [testenv] deps = -rtest-requirements.txt commands =