1# VMware vSphere Python SDK
2# Copyright (c) 2008-2016 VMware, Inc. All Rights Reserved.
3#
4# Licensed under the Apache License, Version 2.0 (the "License");
5# you may not use this file except in compliance with the License.
6# You may obtain a copy of the License at
7#
8# http://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS,
12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13# See the License for the specific language governing permissions and
14# limitations under the License.
15from __future__ import absolute_import
16
17import six
18from six import reraise
19from six.moves import http_client
20from six.moves import StringIO
21from six.moves import zip
22from six import u
23from six import iteritems
24
25import sys
26import os
27import platform
28import socket
29import subprocess
30import time
31from six.moves.urllib.parse import urlparse
32from datetime import datetime
33from xml.parsers.expat import ParserCreate
34# We have our own escape functionality.
35# from xml.sax.saxutils import escape
36
37from pyVmomi.VmomiSupport import *
38from pyVmomi.StubAdapterAccessorImpl import StubAdapterAccessorMixin
39import pyVmomi.Iso8601
40import base64
41from xml.parsers.expat import ExpatError
42import copy
43import contextlib
44
45try:
46   USERWORLD = os.uname()[0] == 'VMkernel'
47except:
48   USERWORLD = False
49
50# Timeout value used for idle connections in client connection pool.
51# Default value is 900 seconds (15 minutes).
52CONNECTION_POOL_IDLE_TIMEOUT_SEC = 900
53
54NS_SEP = " "
55
56XML_ENCODING = 'UTF-8'
57XML_HEADER = '<?xml version="1.0" encoding="{0}"?>'.format(XML_ENCODING)
58
59XMLNS_SOAPENC = "http://schemas.xmlsoap.org/soap/encoding/"
60XMLNS_SOAPENV = "http://schemas.xmlsoap.org/soap/envelope/"
61
62XSI_TYPE = XMLNS_XSI + NS_SEP + u('type')
63
64# Note: Must make a copy to use the SOAP_NSMAP
65# TODO: Change to frozendict, if available
66SOAP_NSMAP = { XMLNS_SOAPENC: 'soapenc', XMLNS_SOAPENV: 'soapenv',
67               XMLNS_XSI: 'xsi', XMLNS_XSD: 'xsd' }
68
69SOAP_ENVELOPE_TAG = "{0}:Envelope".format(SOAP_NSMAP[XMLNS_SOAPENV])
70SOAP_HEADER_TAG = "{0}:Header".format(SOAP_NSMAP[XMLNS_SOAPENV])
71SOAP_FAULT_TAG = "{0}:Fault".format(SOAP_NSMAP[XMLNS_SOAPENV])
72SOAP_BODY_TAG = "{0}:Body".format(SOAP_NSMAP[XMLNS_SOAPENV])
73
74SOAP_ENVELOPE_START = '<{0} '.format(SOAP_ENVELOPE_TAG) + \
75                      ' '.join(['xmlns:' + prefix + '="' + urn + '"' \
76                                for urn, prefix in iteritems(SOAP_NSMAP)]) + \
77                      '>\n'
78SOAP_ENVELOPE_END = "\n</{0}>".format(SOAP_ENVELOPE_TAG)
79SOAP_HEADER_START = "<{0}>".format(SOAP_HEADER_TAG)
80SOAP_HEADER_END = "</{0}>".format(SOAP_HEADER_TAG)
81SOAP_BODY_START = "<{0}>".format(SOAP_BODY_TAG)
82SOAP_BODY_END = "</{0}>".format(SOAP_BODY_TAG)
83SOAP_START = SOAP_ENVELOPE_START + SOAP_BODY_START + '\n'
84SOAP_END = '\n' + SOAP_BODY_END + SOAP_ENVELOPE_END
85
86WSSE_PREFIX = "wsse"
87WSSE_HEADER_TAG = "{0}:Security".format(WSSE_PREFIX)
88WSSE_NS_URL = "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"
89WSSE_NS = 'xmlns:{0}="{1}"'.format(WSSE_PREFIX, WSSE_NS_URL)
90WSSE_HEADER_START = "<{0} {1}>".format(WSSE_HEADER_TAG, WSSE_NS)
91WSSE_HEADER_END = "</{0}>".format(WSSE_HEADER_TAG)
92
93## MethodFault type
94MethodFault = GetVmodlType("vmodl.MethodFault")
95## Localized MethodFault type
96LocalizedMethodFault = GetVmodlType("vmodl.LocalizedMethodFault")
97
98# These info are included in the http user-agent header
99PYTHON_VERSION = platform.python_version()
100OS_NAME = platform.uname()[0]
101OS_VERSION = platform.uname()[2]
102OS_ARCH = platform.uname()[4]
103
104SOAP_ADAPTER_ARGS = [
105   "server_side", "cert_reqs", "ssl_version", "ca_certs", "do_handshake_on_connect",
106   "suppress_ragged_eofs", "ciphers"]
107
108
109## Thumbprint mismatch exception
110#
111class ThumbprintMismatchException(Exception):
112   def __init__(self, expected, actual):
113      Exception.__init__(self, "Server has wrong SHA1 thumbprint: %s "
114                               "(required) != %s (server)" % (
115                                 expected, actual))
116
117      self.expected = expected
118      self.actual = actual
119
120## Escape <, >, &
121def XmlEscape(xmlStr):
122    escaped = xmlStr.replace("&", "&amp;").replace(">", "&gt;").replace("<", "&lt;")
123    return escaped
124
125## Get the start tag, end tag, and text handlers of a class
126def GetHandlers(obj):
127   return (obj.StartElementHandler,
128           obj.EndElementHandler,
129           obj.CharacterDataHandler,
130           obj.StartNamespaceDeclHandler,
131           obj.EndNamespaceDeclHandler)
132
133## Set the start tag, end tag, and text handlers of a parser
134def SetHandlers(obj, handlers):
135   (obj.StartElementHandler,
136    obj.EndElementHandler,
137    obj.CharacterDataHandler,
138    obj.StartNamespaceDeclHandler,
139    obj.EndNamespaceDeclHandler) = handlers
140
141## Serialize an object to bytes
142#
143# This function assumes CheckField(info, val) was already called
144# @param val the value to serialize
145# @param info the field
146# @param version the version
147# @param nsMap a dict of xml ns -> prefix
148# @return the serialized object as bytes
149# @param encoding Deprecated this is not used during serialization since we always
150#        use utf-8 to encode a request message. We didn't remove the
151#        parameter so it is still compatible with clients that are still using it.
152def Serialize(val, info=None, version=None, nsMap=None, encoding=None):
153   return _SerializeToUnicode(val, info=info, version=version, nsMap=nsMap).encode(XML_ENCODING)
154
155## Serialize an object to unicode
156#
157# This function assumes CheckField(info, val) was already called
158# @param val the value to serialize
159# @param info the field
160# @param version the version
161# @param nsMap a dict of xml ns -> prefix
162# @return the serialized object as unicode
163def SerializeToUnicode(val, info=None, version=None, nsMap=None):
164   return _SerializeToUnicode(val, info=info, version=version, nsMap=nsMap)
165
166## Serialize an object to unicode
167#
168# This function assumes CheckField(info, val) was already called
169# @param val the value to serialize
170# @param info the field
171# @param version the version
172# @param nsMap a dict of xml ns -> prefix
173# @return the serialized object as unicode
174def _SerializeToUnicode(val, info=None, version=None, nsMap=None):
175   if version is None:
176      try:
177         if isinstance(val, list):
178            itemType = val.Item
179            version = itemType._version
180         else:
181            if val is None:
182               # neither val nor version is given
183               return ''
184            # Pick up the version from val
185            version = val._version
186      except AttributeError:
187         version = BASE_VERSION
188   if info is None:
189      info = Object(name="object", type=object, version=version, flags=0)
190
191   writer = StringIO()
192   SoapSerializer(writer, version, nsMap).Serialize(val, info)
193   return writer.getvalue()
194
195## Serialize fault detail
196#
197# Serializes a fault as the content of the detail element in a
198# soapenv:Fault (i.e. without a LocalizedMethodFault wrapper).
199#
200# This function assumes CheckField(info, val) was already called
201# @param val the value to serialize
202# @param info the field
203# @param version the version
204# @param nsMap a dict of xml ns -> prefix
205# @return the serialized object as a unicode string
206def SerializeFaultDetail(val, info=None, version=None, nsMap=None, encoding=None):
207   if version is None:
208      try:
209         if not isinstance(val, MethodFault):
210            raise TypeError('{0} is not a MethodFault'.format(str(val)))
211         version = val._version
212      except AttributeError:
213         version = BASE_VERSION
214   if info is None:
215      info = Object(name="object", type=object, version=version, flags=0)
216
217   writer = StringIO()
218   SoapSerializer(writer, version, nsMap, encoding).SerializeFaultDetail(val, info)
219   return writer.getvalue()
220
221## SOAP serializer
222#
223class SoapSerializer:
224   """ SoapSerializer """
225   ## Serializer constructor
226   #
227   # @param writer File writer
228   # @param version the version
229   # @param nsMap a dict of xml ns -> prefix
230   # @param encoding Deprecated this is not used during serialization since we always
231   #        use utf-8 to encode a request message. We didn't remove the
232   #        parameter so it is still compatible with clients that are still using it.
233   def __init__(self, writer, version, nsMap, encoding=None):
234      """ Constructor """
235      self.writer = writer
236      self.version = version
237      self.nsMap = nsMap and nsMap or {}
238      for ns, prefix in iteritems(self.nsMap):
239         if prefix == '':
240            self.defaultNS = ns
241            break
242      else:
243         self.defaultNS = ''
244
245      # Additional attr for outermost tag
246      self.outermostAttrs = ''
247
248      # Fill in required xmlns, if not defined
249      for nsPrefix, ns, attrName in [('xsi', XMLNS_XSI, 'xsiPrefix'),
250                                     ('xsd', XMLNS_XSD, 'xsdPrefix')]:
251         prefix = self.nsMap.get(ns)
252         if not prefix:
253            prefix = nsPrefix
254            self.outermostAttrs += ' xmlns:{0}="{1}"'.format(prefix, ns)
255            self.nsMap = self.nsMap.copy()
256            self.nsMap[ns] = prefix
257         setattr(self, attrName, prefix + ":")
258
259
260   ## Serialize an object
261   #
262   # This function assumes CheckField(info, val) was already called
263   # @param val the value to serialize
264   # @param info the field
265   def Serialize(self, val, info):
266      """ Serialize an object """
267      self._Serialize(val, info, self.defaultNS)
268
269   ## Serialize fault detail
270   #
271   # Serializes a fault as the content of the detail element in a
272   # soapenv:Fault (i.e. without a LocalizedMethodFault wrapper).
273   #
274   # This function assumes CheckField(info, val) was already called
275   # @param val the value to serialize
276   # @param info the field
277   def SerializeFaultDetail(self, val, info):
278      """ Serialize an object """
279      self._SerializeDataObject(val, info, ' xsi:typ="{1}"'.format(val._wsdlName), self.defaultNS)
280
281   def _NSPrefix(self, ns):
282      """ Get xml ns prefix. self.nsMap must be set """
283      if ns == self.defaultNS:
284         return ''
285      prefix = self.nsMap[ns]
286      return prefix and prefix + ':' or ''
287
288   def _QName(self, typ, defNS):
289      """ Get fully qualified wsdl name (prefix:name) """
290      attr = ''
291      ns, name = GetQualifiedWsdlName(typ)
292      if ns == defNS:
293         prefix = ''
294      else:
295         try:
296            prefix = self.nsMap[ns]
297         except KeyError:
298            # We have not seen this ns before
299            prefix = ns.split(':', 1)[-1]
300            attr = ' xmlns:{0}="{1}"'.format(prefix, ns)
301      return attr, prefix and prefix + ':' + name or name
302
303   ## Serialize an object to unicode (internal)
304   #
305   # @param val the value to serialize
306   # @param info the field
307   # @param defNS the default namespace
308   def _Serialize(self, val, info, defNS):
309      """ Serialize an object """
310      if not IsChildVersion(self.version, info.version):
311         return
312
313      if val is None:
314         if info.flags & F_OPTIONAL:
315            return
316         else:
317            raise TypeError('Field "{0}" is not optional'.format(info.name))
318      elif isinstance(val, list) and len(val) == 0:
319         if info.type is object:
320            # Make sure an empty array assigned to Any is typed
321            if not isinstance(val, Array):
322               raise TypeError('Field "{0}": Cannot assign empty native python array to an Any'.format(info.name))
323         elif info.flags & F_OPTIONAL:
324            # Skip optional non-Any
325            return
326         else:
327             raise TypeError('Field "{0}" not optional'.format(info.name))
328
329      if self.outermostAttrs:
330         attr = self.outermostAttrs
331         self.outermostAttrs = None
332      else:
333         attr = ''
334      currDefNS = defNS
335      # Emit default ns if tag ns is not the same
336      currTagNS = GetWsdlNamespace(info.version)
337      if currTagNS != defNS:
338         attr += ' xmlns="{0}"'.format(currTagNS)
339         currDefNS = currTagNS
340
341      if isinstance(val, DataObject):
342         if isinstance(val, MethodFault):
343            newVal = LocalizedMethodFault(fault=val, localizedMessage=val.msg)
344            if info.type is object:
345               faultType = object
346            else:
347               faultType = LocalizedMethodFault
348            newInfo = Object(name=info.name, type=faultType,
349                             version=info.version, flags=info.flags)
350            self._SerializeDataObject(newVal, newInfo, attr, currDefNS)
351         else:
352            self._SerializeDataObject(val, info, attr, currDefNS)
353      elif isinstance(val, ManagedObject):
354         if info.type is object:
355            nsattr, qName = self._QName(ManagedObject, currDefNS)
356            attr += '{0} {1}type="{2}"'.format(nsattr, self.xsiPrefix, qName)
357         if val._serverGuid is not None:
358            attr += ' serverGuid="{0}"'.format(val._serverGuid)
359         # val in vim type attr is not namespace qualified
360         # TODO: Add a new "typens" attr?
361         ns, name = GetQualifiedWsdlName(Type(val))
362         attr += ' type="{0}"'.format(name)
363         self.writer.write('<{0}{1}>{2}</{3}>'.format(info.name, attr,
364                                              val._moId,
365                                              info.name))
366      elif isinstance(val, list):
367         if info.type is object:
368            itemType = val.Item
369            if (itemType is ManagedMethod or itemType is PropertyPath
370            or  itemType is type):
371               tag = 'string'
372               typ = GetVmodlType("string[]")
373            elif issubclass(itemType, ManagedObject):
374               tag = 'ManagedObjectReference'
375               typ = ManagedObject.Array
376            else:
377               tag = GetWsdlName(itemType)
378               typ = Type(val)
379            nsattr, qName = self._QName(typ, currDefNS)
380
381            # For WSDL, since we set tag of ManagedObjects to ManagedObjectReferences,
382            # the name of its array should be ArrayOfManagedObjectReference
383            if qName.endswith("ArrayOfManagedObject"):
384               qName += "Reference"
385
386            attr += '{0} {1}type="{2}"'.format(nsattr, self.xsiPrefix, qName)
387            self.writer.write('<{0}{1}>'.format(info.name, attr))
388
389            itemInfo = Object(name=tag, type=itemType,
390                              version=info.version, flags=info.flags)
391            for it in val:
392               self._Serialize(it, itemInfo, currDefNS)
393            self.writer.write('</{0}>'.format(info.name))
394         else:
395            itemType = info.type.Item
396            itemInfo = Object(name=info.name, type=itemType,
397                              version=info.version, flags=info.flags)
398            for it in val:
399               self._Serialize(it, itemInfo, defNS)
400      elif isinstance(val, type) or isinstance(val, type(Exception)):
401         if info.type is object:
402            attr += ' {0}type="{1}string"'.format(self.xsiPrefix, self.xsdPrefix)
403         self.writer.write('<{0}{1}>{2}</{0}>'.format(
404                           info.name, attr, GetWsdlName(val)))
405      elif isinstance(val, ManagedMethod):
406         if info.type is object:
407            attr += ' {0}type="{1}string"'.format(self.xsiPrefix, self.xsdPrefix)
408         self.writer.write('<{0}{1}>{2}</{0}>'.format(
409                              info.name, attr, val.info.wsdlName))
410      elif isinstance(val, datetime):
411         if info.type is object:
412            nsattr, qName = self._QName(Type(val), currDefNS)
413            attr += '{0} {1}type="{2}"'.format(nsattr, self.xsiPrefix, qName)
414         result = Iso8601.ISO8601Format(val)
415         self.writer.write('<{0}{1}>{2}</{0}>'.format(info.name, attr, result))
416      elif isinstance(val, binary):
417         if info.type is object:
418            nsattr, qName = self._QName(Type(val), currDefNS)
419            attr += '{0} {1}type="{2}"'.format(nsattr, self.xsiPrefix, qName)
420         result = base64.b64encode(val)
421         if PY3:
422            # In python3 the bytes result after the base64 encoding has a
423            # leading 'b' which causes error when we use it to construct the
424            # soap message. Workaround the issue by converting the result to
425            # string. Since the result of base64 encoding contains only subset
426            # of ASCII chars, converting to string will not change the value.
427            result = str(result, XML_ENCODING)
428         self.writer.write('<{0}{1}>{2}</{0}>'.format(info.name, attr, result))
429      elif isinstance(val, bool):
430         if info.type is object:
431            nsattr, qName = self._QName(Type(val), currDefNS)
432            attr += '{0} {1}type="{2}"'.format(nsattr, self.xsiPrefix, qName)
433         result = val and "true" or "false"
434         self.writer.write('<{0}{1}>{2}</{0}>'.format(info.name, attr, result))
435      elif isinstance(val, six.integer_types) or isinstance(val, float):
436         if info.type is object:
437            nsattr, qName = self._QName(Type(val), currDefNS)
438            attr += '{0} {1}type="{2}"'.format(nsattr, self.xsiPrefix, qName)
439         result = six.text_type(val)
440         self.writer.write('<{0}{1}>{2}</{0}>'.format(info.name, attr, result))
441      elif isinstance(val, Enum):
442         if info.type is object:
443            nsattr, qName = self._QName(Type(val), currDefNS)
444            attr += '{0} {1}type="{2}"'.format(nsattr, self.xsiPrefix, qName)
445         self.writer.write('<{0}{1}>{2}</{0}>'.format(info.name, attr, val))
446      else:
447         if info.type is object:
448            if isinstance(val, PropertyPath):
449               attr += ' {0}type="{1}string"'.format(self.xsiPrefix, self.xsdPrefix)
450            else:
451               nsattr, qName = self._QName(Type(val), currDefNS)
452               attr += '{0} {1}type="{2}"'.format(nsattr, self.xsiPrefix, qName)
453
454         if isinstance(val, six.binary_type):
455            # Use UTF-8 rather than self.encoding.  self.encoding is for
456            # output of serializer, while 'val' is our input.  And regardless
457            # of what our output is, our input should be always UTF-8.  Yes,
458            # it means that if you emit output in other encoding than UTF-8,
459            # you cannot serialize it again once more.  That's feature, not
460            # a bug.
461            val = val.decode(XML_ENCODING)
462         result = XmlEscape(val)
463         self.writer.write(u'<{0}{1}>{2}</{0}>'.format(info.name, attr, result))
464
465   ## Serialize a a data object (internal)
466   #
467   # @param val the value to serialize
468   # @param info the field
469   # @param attr attributes to serialized in the outermost elementt
470   # @param currDefNS the current default namespace
471   def _SerializeDataObject(self, val, info, attr, currDefNS):
472      if info.flags & F_LINK:
473         # Attribute is a link and Object is present instead of its key.
474         # We need to serialize just the key and not the entire object
475         self._Serialize(val.key, info, currDefNS)
476         return
477      dynType = GetCompatibleType(Type(val), self.version)
478      if dynType != info.type:
479         nsattr, qName = self._QName(dynType, currDefNS)
480         attr += '{0} {1}type="{2}"'.format(nsattr, self.xsiPrefix, qName)
481      self.writer.write('<{0}{1}>'.format(info.name, attr))
482      if dynType is LocalizedMethodFault:
483         # Serialize a MethodFault as LocalizedMethodFault on wire
484         # See PR 670229
485         for prop in val._GetPropertyList():
486            propVal = getattr(val, prop.name)
487            if prop.name == 'fault':
488               propVal = copy.copy(propVal)
489               propVal.msg = None
490               self._SerializeDataObject(propVal, prop, '', currDefNS)
491            else:
492               self._Serialize(propVal, prop, currDefNS)
493      else:
494         for prop in val._GetPropertyList():
495            self._Serialize(getattr(val, prop.name), prop, currDefNS)
496
497      self.writer.write('</{0}>'.format(info.name))
498
499
500class ParserError(KeyError):
501    # NOTE (hartsock): extends KeyError since parser logic is written to
502    # catch KeyError types. Normally, I would want PerserError to be a root
503    # type for all parser faults.
504    pass
505
506def ParseData(parser, data):
507   # NOTE (hartsock): maintaining library internal consistency here, this is
508   # a refactoring that rolls up some repeated code blocks into a method so
509   # that we can refactor XML parsing behavior in a single place.
510   try:
511      if isinstance(data, six.binary_type) or isinstance(data, six.text_type):
512         parser.Parse(data)
513      else:
514         parser.ParseFile(data)
515   except Exception:
516      # wrap all parser faults with additional information for later
517      # bug reporting on the XML parser code itself.
518      (ec, ev, tb) = sys.exc_info()
519      line = parser.CurrentLineNumber
520      col = parser.CurrentColumnNumber
521      pe = ParserError("xml document: "
522                       "{0} parse error at: "
523                       "line:{1}, col:{2}".format(data, line, col))
524      # use six.reraise for python 2.x and 3.x compatability
525      reraise(ParserError, pe, tb)
526
527## Deserialize an object from a file or string
528#
529# This function will deserialize one top-level XML node.
530# @param data the data to deserialize (a file object or string)
531# @param resultType expected result type
532# @param stub stub for moRef deserialization
533# @return the deserialized object
534def Deserialize(data, resultType=object, stub=None):
535   parser = ParserCreate(namespace_separator=NS_SEP)
536   ds = SoapDeserializer(stub)
537   ds.Deserialize(parser, resultType)
538   ParseData(parser, data)
539   return ds.GetResult()
540
541
542## Expat deserializer namespace handler
543class ExpatDeserializerNSHandlers:
544   def __init__(self, nsMap=None):
545      # nsMap is a dict of ns prefix to a stack (list) of namespaces
546      # The last element of the stack is current namespace
547      if not nsMap:
548         nsMap = {}
549      self.nsMap = nsMap
550
551   ## Get current default ns
552   def GetCurrDefNS(self):
553      return self._GetNamespaceFromPrefix()
554
555   ## Get namespace and wsdl name from tag
556   def GetNSAndWsdlname(self, tag):
557      """ Map prefix:name tag into ns, name """
558      idx = tag.find(":")
559      if idx >= 0:
560         prefix, name = tag[:idx], tag[idx + 1:]
561      else:
562         prefix, name = None, tag
563      # Map prefix to ns
564      ns = self._GetNamespaceFromPrefix(prefix)
565      return ns, name
566
567   def _GetNamespaceFromPrefix(self, prefix = None):
568      namespaces = self.nsMap.get(prefix)
569      if namespaces:
570         ns = namespaces[-1]
571      else:
572         ns = ""
573      return ns
574
575   ## Handle namespace begin
576   def StartNamespaceDeclHandler(self, prefix, uri):
577      namespaces = self.nsMap.get(prefix)
578      if namespaces:
579         namespaces.append(uri)
580      else:
581         self.nsMap[prefix] = [uri]
582
583   ## Handle namespace end
584   def EndNamespaceDeclHandler(self, prefix):
585      self.nsMap[prefix].pop()
586
587
588## SOAP -> Python Deserializer
589class SoapDeserializer(ExpatDeserializerNSHandlers):
590   ## Constructor
591   #
592   # @param self self
593   # @param stub Stub adapter to use for deserializing moRefs
594   def __init__(self, stub=None, version=None):
595      ExpatDeserializerNSHandlers.__init__(self)
596      self.stub = stub
597      if version:
598         self.version = version
599      elif self.stub:
600         self.version = self.stub.version
601      else:
602         self.version = None
603      self.result = None
604
605   ## Deserialize a SOAP object
606   #
607   # @param self self
608   # @param parser an expat parser
609   # @param resultType the static type of the result
610   # @param isFault true if the response is a fault response
611   # @param nsMap a dict of prefix -> [xml ns stack]
612   # @return the deserialized object
613   def Deserialize(self, parser, resultType=object, isFault=False, nsMap=None):
614      self.isFault = isFault
615      self.parser = parser
616      self.origHandlers = GetHandlers(parser)
617      SetHandlers(parser, GetHandlers(self))
618      self.resultType = resultType
619      self.stack = []
620      self.data = ""
621      self.serverGuid = None
622      if issubclass(resultType, list):
623         self.result = resultType()
624      else:
625         self.result = None
626      if not nsMap:
627         nsMap = {}
628      self.nsMap = nsMap
629
630   ## Get the result of deserialization
631   #  The links will not be resolved. User needs to explicitly resolve them
632   #  using LinkResolver.
633   def GetResult(self):
634      return self.result
635
636   def SplitTag(self, tag):
637      """ Split tag into ns, name """
638      idx = tag.find(NS_SEP)
639      if idx >= 0:
640         return tag[:idx], tag[idx + 1:]
641      else:
642         return "", tag
643
644   def LookupWsdlType(self, ns, name, allowManagedObjectReference=False):
645      """ Lookup wsdl type. Handle special case for some vmodl version """
646      try:
647         return GetWsdlType(ns, name)
648      except KeyError:
649         if allowManagedObjectReference:
650            if name.endswith('ManagedObjectReference') and ns == XMLNS_VMODL_BASE:
651               return GetWsdlType(ns, name[:-len('Reference')])
652         # WARNING!!! This is a temporary hack to get around server not
653         # honoring @service tag (see bug 521744). Once it is fix, I am
654         # going to back out this change
655         if name.endswith('ManagedObjectReference') and allowManagedObjectReference:
656            return GetWsdlType(XMLNS_VMODL_BASE, name[:-len('Reference')])
657         return GuessWsdlType(name)
658
659   ## Handle an opening XML tag
660   def StartElementHandler(self, tag, attr):
661      self.data = ""
662      self.serverGuid = None
663      deserializeAsLocalizedMethodFault = True
664      if not self.stack:
665         if self.isFault:
666            ns, name = self.SplitTag(tag)
667            objType = self.LookupWsdlType(ns, name[:-5])
668            # Only top level soap fault should be deserialized as method fault
669            deserializeAsLocalizedMethodFault = False
670         else:
671            objType = self.resultType
672      elif isinstance(self.stack[-1], list):
673         objType = self.stack[-1].Item
674      elif isinstance(self.stack[-1], DataObject):
675         # TODO: Check ns matches DataObject's namespace
676         ns, name = self.SplitTag(tag)
677         objType = self.stack[-1]._GetPropertyInfo(name).type
678
679         # LocalizedMethodFault <fault> tag should be deserialized as method fault
680         if name == "fault" and isinstance(self.stack[-1], LocalizedMethodFault):
681            deserializeAsLocalizedMethodFault = False
682      else:
683         raise TypeError("Invalid type for tag {0}".format(tag))
684
685      xsiType = attr.get(XSI_TYPE)
686      if xsiType:
687         # Ignore dynamic type for TypeName, MethodName, PropertyPath
688         # @bug 150459
689         if not (objType is type or objType is ManagedMethod or \
690                                    objType is PropertyPath):
691            ns, name = self.GetNSAndWsdlname(xsiType)
692            dynType = self.LookupWsdlType(ns, name, allowManagedObjectReference=True)
693            # TODO: Should be something like...
694            #   dynType must be narrower than objType, except for
695            #   ManagedObjectReference
696            if not (issubclass(dynType, list) and issubclass(objType, list)):
697               objType = dynType
698      else:
699         if issubclass(objType, list):
700            objType = objType.Item
701
702      if self.version:
703         objType = GetCompatibleType(objType, self.version)
704      if issubclass(objType, ManagedObject):
705         typeAttr = attr[u('type')]
706         # val in vim type attr is not namespace qualified
707         # However, this doesn't hurt to strip out namespace
708         # TODO: Get the ns from "typens" attr?
709         ns, name = self.GetNSAndWsdlname(typeAttr)
710         if u('serverGuid') in attr:
711            self.serverGuid = attr[u('serverGuid')]
712         self.stack.append(GuessWsdlType(name))
713      elif issubclass(objType, DataObject) or issubclass(objType, list):
714         if deserializeAsLocalizedMethodFault and issubclass(objType, Exception):
715            objType = LocalizedMethodFault
716         self.stack.append(objType())
717      else:
718         self.stack.append(objType)
719
720   ## Handle a closing XML tag
721   def EndElementHandler(self, tag):
722      try:
723         obj = self.stack.pop()
724      except IndexError:
725         SetHandlers(self.parser, self.origHandlers)
726         handler = self.parser.EndElementHandler
727         del self.parser, self.origHandlers, self.stack, self.resultType
728         if handler:
729            return handler(tag)
730         return
731
732      data = self.data
733      if isinstance(obj, type) or isinstance(obj, type(Exception)):
734         if obj is type:
735            if data is None or data == '':
736               obj = None
737            else:
738               try:
739                  # val in type val is not namespace qualified
740                  # However, this doesn't hurt to strip out namespace
741                  ns, name = self.GetNSAndWsdlname(data)
742                  obj = GuessWsdlType(name)
743               except KeyError:
744                  raise TypeError(data)
745         elif obj is ManagedMethod:
746            # val in Method val is not namespace qualified
747            # However, this doesn't hurt to strip out namespace
748            ns, name = self.GetNSAndWsdlname(data)
749            try:
750               obj = GuessWsdlMethod(name)
751            except KeyError:
752               obj = UncallableManagedMethod(name)
753         elif obj is bool:
754            if data == "0" or data.lower() == "false":
755               obj = bool(False)
756            elif data == "1" or data.lower() == "true":
757               obj = bool(True)
758            else:
759               raise TypeError(data)
760         elif obj is binary:
761            # Raise type error if decode failed
762            obj = obj(base64.b64decode(data))
763         elif obj is str:
764            try:
765               obj = str(data)
766            except ValueError:
767               obj = data
768         elif obj is datetime:
769            obj = pyVmomi.Iso8601.ParseISO8601(data)
770            if not obj:
771               raise TypeError(data)
772         # issubclass is very expensive. Test last
773         elif issubclass(obj, ManagedObject):
774            obj = obj(data, self.stub, self.serverGuid)
775         elif issubclass(obj, Enum):
776            obj = getattr(obj, data)
777         else:
778            obj = obj(data)
779      elif isinstance(obj, LocalizedMethodFault):
780         obj.fault.msg = obj.localizedMessage
781         obj = obj.fault
782
783      if self.stack:
784         top = self.stack[-1]
785         if isinstance(top, list):
786            top.append(obj)
787         elif isinstance(top, DataObject):
788            ns, name = self.SplitTag(tag)
789            info = top._GetPropertyInfo(name)
790
791            if not isinstance(obj, list) and issubclass(info.type, list):
792               getattr(top, info.name).append(obj)
793            else:
794               setattr(top, info.name, obj)
795         else:
796            ns, name = self.SplitTag(tag)
797            setattr(top, name, obj)
798      else:
799         if not isinstance(obj, list) and issubclass(self.resultType, list):
800            self.result.append(obj)
801         else:
802            self.result = obj
803            SetHandlers(self.parser, self.origHandlers)
804            del self.parser, self.origHandlers, self.stack, self.resultType
805
806   ## Handle text data
807   def CharacterDataHandler(self, data):
808      self.data += data
809
810
811## SOAP Response Deserializer class
812class SoapResponseDeserializer(ExpatDeserializerNSHandlers):
813   ## Constructor
814   #
815   # @param self self
816   # @param stub Stub adapter to use for deserializing moRefs
817   def __init__(self, stub):
818      ExpatDeserializerNSHandlers.__init__(self)
819      self.stub = stub
820      self.deser = SoapDeserializer(stub)
821      self.soapFaultTag = XMLNS_SOAPENV + NS_SEP + "Fault"
822
823   ## Deserialize a SOAP response
824   #
825   # @param self self
826   # @param response the response (a file object or a string)
827   # @param resultType expected result type
828   # @param nsMap a dict of prefix -> [xml ns stack]
829   # @return the deserialized object
830   def Deserialize(self, response, resultType, nsMap=None):
831      self.resultType = resultType
832      self.stack = []
833      self.msg = ""
834      self.deser.result = None
835      self.isFault = False
836      self.parser = ParserCreate(namespace_separator=NS_SEP)
837      try: # buffer_text only in python >= 2.3
838         self.parser.buffer_text = True
839      except AttributeError:
840         pass
841      if not nsMap:
842         nsMap = {}
843      self.nsMap = nsMap
844      SetHandlers(self.parser, GetHandlers(self))
845      ParseData(self.parser, response)
846      result = self.deser.GetResult()
847      if self.isFault:
848         if result is None:
849            result = GetVmodlType("vmodl.RuntimeFault")()
850         result.msg = self.msg
851      del self.resultType, self.stack, self.parser, self.msg, self.data, self.nsMap
852      return result
853
854   ## Handle an opening XML tag
855   def StartElementHandler(self, tag, attr):
856      self.data = ""
857      if tag == self.soapFaultTag:
858         self.isFault = True
859      elif self.isFault and tag == "detail":
860         self.deser.Deserialize(self.parser, object, True, self.nsMap)
861      elif tag.endswith("Response"):
862         self.deser.Deserialize(self.parser, self.resultType, False, self.nsMap)
863
864   ## Handle text data
865   def CharacterDataHandler(self, data):
866      self.data += data
867
868   ## Handle a closing XML tag
869   def EndElementHandler(self, tag):
870      if self.isFault and tag == "faultstring":
871         try:
872            self.msg = str(self.data)
873         except ValueError:
874            self.msg = self.data
875
876## Base class that implements common functionality for stub adapters.
877## Method that must be provided by the implementation class:
878## -- InvokeMethod(ManagedObject mo, Object methodInfo, Object[] args)
879class StubAdapterBase(StubAdapterAccessorMixin):
880   def __init__(self, version):
881      StubAdapterAccessorMixin.__init__(self)
882      self.ComputeVersionInfo(version)
883
884   ## Compute the version information for the specified namespace
885   #
886   # @param ns the namespace
887   def ComputeVersionInfo(self, version):
888      # Make sure we do NOT fallback to an older version
889      if hasattr(self, 'version') and IsChildVersion(self.version, version):
890         # print("WARNING: stub degrading: " + self.version + " -> " + version)
891         return
892
893      versionNS = GetVersionNamespace(version)
894      if versionNS.find("/") >= 0:
895         self.versionId = '"urn:{0}"'.format(versionNS)
896      else:
897         self.versionId = ''
898      self.version = version
899
900## Base class that implements common functionality for SOAP-based stub adapters.
901## Method that must be provided by the implementation class:
902## -- InvokeMethod(ManagedObject mo, Object methodInfo, Object[] args)
903class SoapStubAdapterBase(StubAdapterBase):
904   ## Serialize a VMOMI request to SOAP
905   #
906   # @param version API version
907   # @param mo the 'this'
908   # @param info method info
909   # @param args method arguments
910   # @return the serialized request
911   def SerializeRequest(self, mo, info, args):
912      if not IsChildVersion(self.version, info.version):
913         raise GetVmodlType("vmodl.fault.MethodNotFound")(receiver=mo,
914                                                          method=info.name)
915      nsMap = SOAP_NSMAP.copy()
916      defaultNS = GetWsdlNamespace(self.version)
917      nsMap[defaultNS] = ''
918
919      # Add xml header and soap envelope
920      result = [XML_HEADER, '\n', SOAP_ENVELOPE_START]
921
922      # Add request context and samlToken to soap header, if exists
923      reqContexts = GetRequestContext()
924      if self.requestContext:
925         reqContexts.update(self.requestContext)
926      samlToken = getattr(self, 'samlToken', None)
927
928      if reqContexts or samlToken:
929         result.append(SOAP_HEADER_START)
930         for key, val in iteritems(reqContexts):
931            # Note: Support req context of string type only
932            if not isinstance(val, six.string_types):
933               raise TypeError("Request context key ({0}) has non-string value ({1}) of {2}".format(key, val, type(val)))
934            ret = _SerializeToUnicode(val,
935                            Object(name=key, type=str, version=self.version),
936                            self.version,
937                            nsMap)
938            result.append(ret)
939         if samlToken:
940            result.append('{0} {1} {2}'.format(WSSE_HEADER_START,
941                                               samlToken,
942                                               WSSE_HEADER_END))
943         result.append(SOAP_HEADER_END)
944         result.append('\n')
945
946      # Serialize soap body
947      result.extend([SOAP_BODY_START,
948                       '<{0} xmlns="{1}">'.format(info.wsdlName, defaultNS),
949                       _SerializeToUnicode(mo, Object(name="_this", type=ManagedObject,
950                                            version=self.version),
951                                 self.version, nsMap)])
952
953      # Serialize soap request parameters
954      for (param, arg) in zip(info.params, args):
955         result.append(_SerializeToUnicode(arg, param, self.version, nsMap))
956      result.extend(['</{0}>'.format(info.wsdlName), SOAP_BODY_END, SOAP_ENVELOPE_END])
957      return ''.join(result).encode(XML_ENCODING)
958
959## Subclass of HTTPConnection that connects over a Unix domain socket
960## instead of a TCP port.  The path of the socket is passed in place of
961## the hostname.  Fairly gross but does the job.
962class UnixSocketConnection(http_client.HTTPConnection):
963   # The HTTPConnection ctor expects a single argument, which it interprets
964   # as the host to connect to; for UnixSocketConnection, we instead interpret
965   # the parameter as the filesystem path of the Unix domain socket.
966   def __init__(self, path):
967      # Pass '' as the host to HTTPConnection; it doesn't really matter
968      # what we pass (since we've overridden the connect method) as long
969      # as it's a valid string.
970      http_client.HTTPConnection.__init__(self, '')
971      self.path = path
972
973   def connect(self):
974      # Hijack the connect method of HTTPConnection to connect to the
975      # specified Unix domain socket instead.  Obey the same contract
976      # as HTTPConnection.connect, which puts the socket in self.sock.
977      sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
978      sock.connect(self.path)
979      self.sock = sock
980
981try:
982   # The ssl module is not available in python versions less than 2.6
983   SSL_THUMBPRINTS_SUPPORTED = True
984
985   import ssl
986   import hashlib
987
988   def _VerifyThumbprint(thumbprint, connection):
989      '''If there is a thumbprint, connect to the server and verify that the
990      SSL certificate matches the given thumbprint.  An exception is thrown
991      if there is a mismatch.'''
992      if thumbprint and isinstance(connection, http_client.HTTPSConnection):
993         if not connection.sock:
994            connection.connect()
995         derCert = connection.sock.getpeercert(True)
996         sha1 = hashlib.sha1()
997         sha1.update(derCert)
998         sha1Digest = sha1.hexdigest().lower()
999         if sha1Digest != thumbprint:
1000            raise ThumbprintMismatchException(thumbprint, sha1Digest)
1001
1002   # Function used to wrap sockets with SSL
1003   _SocketWrapper = ssl.wrap_socket
1004
1005except ImportError:
1006   SSL_THUMBPRINTS_SUPPORTED = False
1007
1008   def _VerifyThumbprint(thumbprint, connection):
1009      if thumbprint and isinstance(connection, http_client.HTTPSConnection):
1010         raise Exception(
1011            "Thumbprint verification not supported on python < 2.6")
1012
1013   def _SocketWrapper(rawSocket, keyfile, certfile, *args, **kwargs):
1014      wrappedSocket = socket.ssl(rawSocket, keyfile, certfile)
1015      return http_client.FakeSocket(rawSocket, wrappedSocket)
1016
1017
1018## Internal version of https connection
1019#
1020# Support ssl.wrap_socket params which are missing from httplib
1021# HTTPSConnection (e.g. ca_certs)
1022# Note: Only works if the ssl params are passing in as kwargs
1023class _HTTPSConnection(http_client.HTTPSConnection):
1024   def __init__(self, *args, **kwargs):
1025      # Extract ssl.wrap_socket param unknown to httplib.HTTPSConnection,
1026      # and push back the params in connect()
1027      self._sslArgs = {}
1028      tmpKwargs = kwargs.copy()
1029      for key in SOAP_ADAPTER_ARGS:
1030         if key in tmpKwargs:
1031            self._sslArgs[key] = tmpKwargs.pop(key)
1032      http_client.HTTPSConnection.__init__(self, *args, **tmpKwargs)
1033
1034   ## Override connect to allow us to pass in additional ssl paramters to
1035   #  ssl.wrap_socket (e.g. cert_reqs, ca_certs for ca cert verification)
1036   def connect(self):
1037      if len(self._sslArgs) == 0:
1038         # No override
1039         http_client.HTTPSConnection.connect(self)
1040         return
1041
1042      # Big hack. We have to copy and paste the httplib connect fn for
1043      # each python version in order to handle extra ssl paramters. Yuk!
1044      if hasattr(self, "source_address"):
1045         # Python 2.7
1046         sock = socket.create_connection((self.host, self.port),
1047                                         self.timeout, self.source_address)
1048         if self._tunnel_host:
1049            self.sock = sock
1050            self._tunnel()
1051         self.sock = ssl.wrap_socket(sock, self.key_file, self.cert_file,
1052                                     **self._sslArgs)
1053      elif hasattr(self, "timeout"):
1054         # Python 2.6
1055         sock = socket.create_connection((self.host, self.port), self.timeout)
1056         self.sock = ssl.wrap_socket(sock, self.key_file, self.cert_file,
1057                                     **self._sslArgs)
1058      else:
1059         # Unknown python version. Do nothing
1060         http_client.HTTPSConnection.connect(self)
1061         return
1062
1063         # TODO: Additional verification of peer cert if needed
1064         # cert_reqs = self._sslArgs.get("cert_reqs", ssl.CERT_NONE)
1065         # ca_certs = self._sslArgs.get("ca_certs", None)
1066         # if cert_reqs != ssl.CERT_NONE and ca_certs:
1067         #   if hasattr(self.sock, "getpeercert"):
1068         #      # TODO: verify peer cert
1069         #      dercert = self.sock.getpeercert(False)
1070         #      # pemcert = ssl.DER_cert_to_PEM_cert(dercert)
1071
1072
1073## Stand-in for the HTTPSConnection class that will connect to a SSL proxy,
1074## VCenter's /sdkTunnel endpoint. It will issue a CONNECT command to start
1075## an SSL tunnel.
1076class SSLTunnelConnection(object):
1077   # @param proxyPath The path to pass to the CONNECT command.
1078   def __init__(self, proxyPath):
1079      self.proxyPath = proxyPath
1080
1081   # Connects to a proxy server and initiates a tunnel to the destination
1082   # specified by proxyPath.  If successful, a new HTTPSConnection is returned.
1083   #
1084   # @param path The destination URL path.
1085   # @param key_file The SSL key file to use when wrapping the socket.
1086   # @param cert_file The SSL certificate file to use when wrapping the socket.
1087   # @param kwargs In case caller passed in extra parameters not handled by
1088   #        SSLTunnelConnection
1089   def __call__(self, path, key_file=None, cert_file=None, **kwargs):
1090      # Only pass in the named arguments that HTTPConnection constructor
1091      # understands
1092      tmpKwargs = {}
1093      for key in http_client.HTTPConnection.__init__.__code__.co_varnames:
1094         if key in kwargs and key != 'self':
1095            tmpKwargs[key] = kwargs[key]
1096      tunnel = http_client.HTTPConnection(path, **tmpKwargs)
1097      tunnel.request('CONNECT', self.proxyPath)
1098      resp = tunnel.getresponse()
1099      if resp.status != 200:
1100        raise http_client.HTTPException("{0} {1}".format(resp.status, resp.reason))
1101      retval = http_client.HTTPSConnection(path)
1102      retval.sock = _SocketWrapper(tunnel.sock,
1103                                   keyfile=key_file, certfile=cert_file)
1104      return retval
1105
1106
1107## Stand-in for the HTTPSConnection class that will connect to a regular HTTP
1108## proxy.
1109class HTTPProxyConnection(object):
1110   # @param proxyPath The path to pass to the CONNECT command.
1111   def __init__(self, proxyPath):
1112      self.proxyPath = proxyPath
1113
1114   # Connects to a HTTP proxy server and initiates a tunnel to the destination
1115   # specified by proxyPath.  If successful, a new HTTPSConnection is returned.
1116   #
1117   # @param path The destination URL path.
1118   # @param args Arguments are ignored
1119   # @param kwargs Arguments for HTTPSConnection
1120   def __call__(self, path, *args, **kwargs):
1121      httpsConnArgs = {k: kwargs[k] for k in kwargs if k not in SOAP_ADAPTER_ARGS}
1122      conn = http_client.HTTPSConnection(path, **httpsConnArgs)
1123      conn.set_tunnel(self.proxyPath)
1124      return conn
1125
1126class GzipReader:
1127   GZIP        = 1
1128   DEFLATE     = 2
1129
1130   def __init__(self, rfile, encoding=GZIP, readChunkSize=512):
1131      self.rfile = rfile
1132      self.chunks = []
1133      self.bufSize = 0 # Remaining buffer
1134      assert(encoding in (GzipReader.GZIP, GzipReader.DEFLATE))
1135      self.encoding = encoding
1136      self.unzip = None
1137      self.readChunkSize = readChunkSize
1138
1139   def _CreateUnzip(self, firstChunk):
1140      import zlib
1141      if self.encoding == GzipReader.GZIP:
1142         wbits = zlib.MAX_WBITS + 16
1143      elif self.encoding == GzipReader.DEFLATE:
1144         # Sniff out real deflate format
1145         chunkLen = len(firstChunk)
1146         # Assume raw deflate
1147         wbits = -zlib.MAX_WBITS
1148         if firstChunk[:3] == ['\x1f', '\x8b', '\x08']:
1149            # gzip: Apache mod_deflate will send gzip. Yurk!
1150            wbits = zlib.MAX_WBITS + 16
1151         elif chunkLen >= 2:
1152            b0 = ord(firstChunk[0])
1153            b1 = ord(firstChunk[1])
1154            if (b0 & 0xf) == 8 and (((b0 * 256 + b1)) % 31) == 0:
1155               # zlib deflate
1156               wbits = min(((b0 & 0xf0) >> 4) + 8, zlib.MAX_WBITS)
1157      else:
1158         assert(False)
1159      self.unzip = zlib.decompressobj(wbits)
1160      return self.unzip
1161
1162   def read(self, bytes=-1):
1163      chunks = self.chunks
1164      bufSize = self.bufSize
1165
1166      while bufSize < bytes or bytes == -1:
1167         # Read and decompress
1168         chunk = self.rfile.read(self.readChunkSize)
1169
1170         if self.unzip == None:
1171            self._CreateUnzip(chunk)
1172
1173         if chunk:
1174            inflatedChunk = self.unzip.decompress(chunk)
1175            bufSize += len(inflatedChunk)
1176            chunks.append(inflatedChunk)
1177         else:
1178            # Returns whatever we have
1179            break
1180
1181      if bufSize <= bytes or bytes == -1:
1182         leftoverBytes = 0
1183         leftoverChunks = []
1184      else:
1185         leftoverBytes = bufSize - bytes
1186         # Adjust last chunk to hold only the left over bytes
1187         lastChunk = chunks.pop()
1188         chunks.append(lastChunk[:-leftoverBytes])
1189         leftoverChunks = [lastChunk[-leftoverBytes:]]
1190
1191      self.chunks = leftoverChunks
1192      self.bufSize = leftoverBytes
1193
1194      buf = b"".join(chunks)
1195      return buf
1196
1197## SOAP stub adapter object
1198class SoapStubAdapter(SoapStubAdapterBase):
1199   ## Constructor
1200   #
1201   # The endpoint can be specified individually as either a host/port
1202   # combination, or with a URL (using a url= keyword).
1203   #
1204   # @param self self
1205   # @param host host
1206   # @param port port (pass negative port number for no SSL)
1207   # @param **** Deprecated. Please use version instead **** ns API namespace
1208   # @param path location of SOAP VMOMI service
1209   # @param url URL (overrides host, port, path if set)
1210   # @param sock unix domain socket path (overrides host, port, url if set)
1211   # @param poolSize size of HTTP connection pool
1212   # @param certKeyFile The path to the PEM-encoded SSL private key file.
1213   # @param certFile The path to the PEM-encoded SSL certificate file.
1214   # @param httpProxyHost The host name of the proxy server.
1215   # @param httpProxyPort The proxy server port.
1216   # @param sslProxyPath Path to use when tunneling through VC's reverse proxy.
1217   # @param thumbprint The SHA1 thumbprint of the server's SSL certificate.
1218   #   Some use a thumbprint of the form xx:xx:xx..:xx.  We ignore the ":"
1219   #   characters.  If set to None, any thumbprint is accepted.
1220   # @param cacertsFile CA certificates file in PEM format
1221   # @param version API version
1222   # @param connectionPoolTimeout Timeout in secs for idle connections in client pool. Use -1 to disable any timeout.
1223   # @param samlToken SAML Token that should be used in SOAP security header for login
1224   # @param sslContext SSL Context describing the various SSL options. It is only
1225   #                   supported in Python 2.7.9 or higher.
1226   def __init__(self, host='localhost', port=443, ns=None, path='/sdk',
1227                url=None, sock=None, poolSize=5,
1228                certFile=None, certKeyFile=None,
1229                httpProxyHost=None, httpProxyPort=80, sslProxyPath=None,
1230                thumbprint=None, cacertsFile=None, version=None,
1231                acceptCompressedResponses=True,
1232                connectionPoolTimeout=CONNECTION_POOL_IDLE_TIMEOUT_SEC,
1233                samlToken=None, sslContext=None, requestContext=None):
1234      if ns:
1235         assert(version is None)
1236         version = versionMap[ns]
1237      elif not version:
1238         version = 'vim.version.version1'
1239      SoapStubAdapterBase.__init__(self, version=version)
1240      self.cookie = ""
1241      if sock:
1242         self.scheme = UnixSocketConnection
1243         # Store sock in the host member variable because that's where
1244         # the UnixSocketConnection ctor expects to find it -- see above
1245         self.host = sock
1246      elif url:
1247         scheme, self.host, urlpath = urlparse(url)[:3]
1248         # Only use the URL path if it's sensible, otherwise use the path
1249         # keyword argument as passed in.
1250         if urlpath not in ('', '/'):
1251            path = urlpath
1252         self.scheme = scheme == "http" and http_client.HTTPConnection \
1253                    or scheme == "https" and _HTTPSConnection
1254      else:
1255         port, self.scheme = port < 0 and (-port, http_client.HTTPConnection) \
1256                                       or (port, _HTTPSConnection)
1257         if host.find(':') != -1:  # is IPv6?
1258            host = '[' + host + ']'
1259         self.host = '{0}:{1}'.format(host, port)
1260
1261      self.path = path
1262      if thumbprint:
1263         self.thumbprint = thumbprint.replace(":", "").lower()
1264         if len(self.thumbprint) != 40:
1265           raise Exception("Invalid SHA1 thumbprint -- {0}".format(thumbprint))
1266      else:
1267         self.thumbprint = None
1268
1269      self.is_tunnel = False
1270      if sslProxyPath:
1271         self.scheme = SSLTunnelConnection(sslProxyPath)
1272         self.is_tunnel = True
1273      elif httpProxyHost:
1274         self.scheme = HTTPProxyConnection(self.host)
1275         self.is_tunnel = True
1276         self.host = "{0}:{1}".format(httpProxyHost, httpProxyPort)
1277      self.poolSize = poolSize
1278      self.pool = []
1279      self.connectionPoolTimeout = connectionPoolTimeout
1280      self.lock = threading.Lock()
1281      self.schemeArgs = {}
1282      if certKeyFile:
1283         self.schemeArgs['key_file'] = certKeyFile
1284      if certFile:
1285         self.schemeArgs['cert_file'] = certFile
1286      if cacertsFile:
1287         self.schemeArgs['ca_certs'] = cacertsFile
1288         self.schemeArgs['cert_reqs'] = ssl.CERT_REQUIRED
1289      if sslContext:
1290         self.schemeArgs['context'] = sslContext
1291      self.samlToken = samlToken
1292      self.requestContext = requestContext
1293      self.requestModifierList = []
1294      self._acceptCompressedResponses = acceptCompressedResponses
1295
1296   # Force a socket shutdown. Before python 2.7, ssl will fail to close
1297   # the socket (http://bugs.python.org/issue10127).
1298   # Not making this a part of the actual _HTTPSConnection since the internals
1299   # of the httplib.HTTP*Connection seem to pass around the descriptors and
1300   # depend on the behavior that close() still leaves the socket semi-functional.
1301   if sys.version_info[:2] < (2,7):
1302      def _CloseConnection(self, conn):
1303         if self.scheme == _HTTPSConnection and conn.sock:
1304           conn.sock.shutdown(socket.SHUT_RDWR)
1305         conn.close()
1306   else:
1307      def _CloseConnection(self, conn):
1308         conn.close()
1309
1310   # Context modifier used to modify the SOAP request.
1311   # @param func The func that takes in the serialized message and modifies the
1312   #   the request. The func is appended to the requestModifierList and then
1313   #   popped after the request is modified.
1314   @contextlib.contextmanager
1315   def requestModifier(self, func):
1316      self.requestModifierList.append(func)
1317      try:
1318         yield
1319      finally:
1320         self.requestModifierList.pop()
1321   ## Invoke a managed method
1322   #
1323   # @param self self
1324   # @param mo the 'this'
1325   # @param info method info
1326   # @param args arguments
1327   # @param outerStub If not-None, this should be a reference to the wrapping
1328   #   stub adapter.  Any ManagedObject references returned from this method
1329   #   will have outerStub in their _stub field.  Note that this also changes
1330   #   the return type to a tuple containing the HTTP status and the
1331   #   deserialized object so that it's easier to distinguish an API error from
1332   #   a connection error.
1333   def InvokeMethod(self, mo, info, args, outerStub=None):
1334      if outerStub is None:
1335         outerStub = self
1336
1337      headers = {'Cookie' : self.cookie,
1338                 'SOAPAction' : self.versionId,
1339                 'Content-Type': 'text/xml; charset={0}'.format(XML_ENCODING),
1340                 'User-Agent': 'pyvmomi Python/{0} ({1}; {2}; {3})'.
1341                    format(PYTHON_VERSION, OS_NAME, OS_VERSION, OS_ARCH)}
1342      if self._acceptCompressedResponses:
1343         headers['Accept-Encoding'] = 'gzip, deflate'
1344
1345      req = self.SerializeRequest(mo, info, args)
1346      for modifier in self.requestModifierList:
1347         req = modifier(req)
1348      conn = self.GetConnection()
1349      try:
1350         conn.request('POST', self.path, req, headers)
1351         resp = conn.getresponse()
1352      except (socket.error, http_client.HTTPException):
1353         # The server is probably sick, drop all of the cached connections.
1354         self.DropConnections()
1355         raise
1356      # NOTE (hartsocks): this cookie handling code should go away in a future
1357      # release. The string 'set-cookie' and 'Set-Cookie' but both are
1358      # acceptable, but the supporting library may have a bug making it
1359      # case sensitive when it shouldn't be. The term 'set-cookie' will occur
1360      # more frequently than 'Set-Cookie' based on practical testing.
1361      cookie = resp.getheader('set-cookie')
1362      if cookie is None:
1363          # try case-sensitive header for compatibility
1364          cookie = resp.getheader('Set-Cookie')
1365      status = resp.status
1366
1367      if cookie:
1368         self.cookie = cookie
1369      if status == 200 or status == 500:
1370         try:
1371            fd = resp
1372            encoding = resp.getheader('Content-Encoding', 'identity').lower()
1373            if encoding == 'gzip':
1374               fd = GzipReader(resp, encoding=GzipReader.GZIP)
1375            elif encoding == 'deflate':
1376               fd = GzipReader(resp, encoding=GzipReader.DEFLATE)
1377            deserializer = SoapResponseDeserializer(outerStub)
1378            obj = deserializer.Deserialize(fd, info.result)
1379         except Exception as exc:
1380            self._CloseConnection(conn)
1381            # NOTE (hartsock): This feels out of place. As a rule the lexical
1382            # context that opens a connection should also close it. However,
1383            # in this code the connection is passed around and closed in other
1384            # contexts (ie: methods) that we are blind to here. Refactor this.
1385
1386            # The server might be sick, drop all of the cached connections.
1387            self.DropConnections()
1388            raise exc
1389         else:
1390            resp.read()
1391            self.ReturnConnection(conn)
1392         if outerStub != self:
1393            return (status, obj)
1394         if status == 200:
1395            return obj
1396         else:
1397            raise obj # pylint: disable-msg=E0702
1398      else:
1399         self._CloseConnection(conn)
1400         raise http_client.HTTPException("{0} {1}".format(resp.status, resp.reason))
1401
1402   ## Clean up connection pool to throw away idle timed-out connections
1403   #  SoapStubAdapter lock must be acquired before this method is called.
1404   def _CloseIdleConnections(self):
1405      if self.connectionPoolTimeout >= 0:
1406         currentTime = time.time()
1407         idleConnections = []
1408         for conn, lastAccessTime in self.pool:
1409            idleTime = currentTime - lastAccessTime
1410            if idleTime >= self.connectionPoolTimeout:
1411               i = self.pool.index((conn, lastAccessTime))
1412               idleConnections = self.pool[i:]
1413               self.pool = self.pool[:i]
1414               break
1415
1416         for conn, _ in idleConnections:
1417            self._CloseConnection(conn)
1418
1419   ## Get a HTTP connection from the pool
1420   def GetConnection(self):
1421      self.lock.acquire()
1422      self._CloseIdleConnections()
1423      if self.pool:
1424         result, _ = self.pool.pop(0)
1425         self.lock.release()
1426      else:
1427         self.lock.release()
1428         result = self.scheme(self.host, **self.schemeArgs)
1429
1430         # Always disable NAGLE algorithm
1431         #
1432         # Python httplib (2.6 and below) is splitting a http request into 2
1433         # packets (header and body). It first send the header, but will not
1434         # send the body until it receives the ack (for header) from server
1435         # [NAGLE at work]. The delayed ack time on ESX is around 40 - 100 ms
1436         # (depends on version) and can go up to 200 ms. This effectively slow
1437         # down each pyVmomi call by the same amount of time.
1438         #
1439         # Disable NAGLE on client will force both header and body packets to
1440         # get out immediately, and eliminated the delay
1441         #
1442         # This bug is fixed in python 2.7, however, only if the request
1443         # body is a string (which is true for now)
1444         if sys.version_info[:2] < (2,7):
1445            self.DisableNagle(result)
1446
1447         _VerifyThumbprint(self.thumbprint, result)
1448
1449      return result
1450
1451   ## Drop all cached connections to the server.
1452   def DropConnections(self):
1453      self.lock.acquire()
1454      oldConnections = self.pool
1455      self.pool = []
1456      self.lock.release()
1457      for conn, _ in oldConnections:
1458         self._CloseConnection(conn)
1459
1460   ## Return a HTTP connection to the pool
1461   def ReturnConnection(self, conn):
1462      self.lock.acquire()
1463      self._CloseIdleConnections()
1464      # In case of ssl tunneling, only add the conn if the conn has not been closed
1465      if len(self.pool) < self.poolSize and (not self.is_tunnel or conn.sock):
1466         self.pool.insert(0, (conn, time.time()))
1467         self.lock.release()
1468      else:
1469         self.lock.release()
1470         # NOTE (hartsock): this seems to violate good coding practice in that
1471         # the lexical context that opens a connection should also be the
1472         # same context responsible for closing it.
1473         self._CloseConnection(conn)
1474
1475   ## Disable nagle on a http connections
1476   def DisableNagle(self, conn):
1477      # Override connections' connect function to force disable NAGLE
1478      if self.scheme != UnixSocketConnection and getattr(conn, "connect"):
1479         orgConnect = conn.connect
1480         def ConnectDisableNagle(*args, **kwargs):
1481            orgConnect(*args, **kwargs)
1482            sock = getattr(conn, "sock")
1483            if sock:
1484               try:
1485                  sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
1486               except Exception:
1487                  pass
1488         conn.connect = ConnectDisableNagle
1489
1490## Need to override the depcopy method. Since, the stub is not deep copyable
1491#  due to the thread lock and connection pool, deep copy of a managed object
1492#  fails. Further different instances of a managed object still share the
1493#  same soap stub. Hence, returning self here is fine.
1494def __deepcopy__(self, memo):
1495   return self
1496
1497HEADER_SECTION_END = '\r\n\r\n'
1498
1499## Parse an HTTP response into its headers and body
1500def ParseHttpResponse(httpResponse):
1501   headerEnd = httpResponse.find(HEADER_SECTION_END)
1502   if headerEnd == -1:
1503      return ('', '')
1504   headerEnd += len(HEADER_SECTION_END)
1505   headerText = httpResponse[:headerEnd]
1506   bodyText = httpResponse[headerEnd:]
1507   return (headerText, bodyText)
1508
1509
1510## SOAP-over-stdio stub adapter object
1511class SoapCmdStubAdapter(SoapStubAdapterBase):
1512   ## Constructor
1513   #
1514   # @param self self
1515   # @param cmd command to execute
1516   # @param ns API namespace
1517   def __init__(self, cmd, version='vim.version.version1'):
1518      SoapStubAdapterBase.__init__(self, version=version)
1519      self.cmd = cmd
1520      self.systemError = GetVmodlType('vmodl.fault.SystemError')
1521
1522   ## Invoke a managed method
1523   #
1524   # @param self self
1525   # @param mo the 'this'
1526   # @param info method info
1527   # @param args arguments
1528   def InvokeMethod(self, mo, info, args):
1529      argv = self.cmd.split()
1530      req = self.SerializeRequest(mo, info, args)
1531      env = dict(os.environ)
1532      env['REQUEST_METHOD'] = 'POST'
1533      env['CONTENT_LENGTH'] = str(len(req))
1534      env['HTTP_SOAPACTION'] = self.versionId[1:-1]
1535      p = subprocess.Popen(argv,
1536                           stdin=subprocess.PIPE,
1537                           stdout=subprocess.PIPE,
1538                           stderr=subprocess.PIPE,
1539                           env=env)
1540      (outText, errText) = p.communicate(req)
1541      if p.returncode < 0:
1542         # Process died with a signal
1543         errText = "Process terminated with signal {0}\n{1}".format(-p.returncode, errText)
1544         raise self.systemError(msg=errText, reason=errText)
1545
1546      try:
1547         (responseHeaders, responseBody) = ParseHttpResponse(outText)
1548         obj = SoapResponseDeserializer(self).Deserialize(responseBody, info.result)
1549      except:
1550         errText = "Failure parsing SOAP response ({0})\n{1}}".format(outText, errText)
1551         raise self.systemError(msg=errText, reason=errText)
1552
1553      if p.returncode == 0:
1554         return obj
1555      elif obj is None:
1556         raise self.systemError(msg=errText, reason=errText)
1557      else:
1558         raise obj # pylint: disable-msg=E0702
1559
1560
1561class SessionOrientedStub(StubAdapterBase):
1562   '''A session-oriented stub adapter that will relogin to the destination if a
1563   session-oriented exception is thrown.
1564
1565
1566   Here's an example.  First, we setup the communication substrate:
1567
1568   >>> soapStub = SoapStubAdapter(host="192.168.1.2", ns="vim25/5.0")
1569
1570   Create a SessionOrientedStub that uses the stub we just created for talking
1571   to the server:
1572
1573   >>> from pyVim.connect import VimSessionOrientedStub
1574   >>> sessionStub = VimSessionOrientedStub(
1575   ...     soapStub,
1576   ...     VimSessionOrientedStub.makeUserLoginMethod("root", "vmware"))
1577
1578   Perform some privileged operations without needing to explicitly login:
1579
1580   >>> si = Vim.ServiceInstance("ServiceInstance", sessionStub)
1581   >>> si.content.sessionManager.sessionList
1582   >>> si.content.sessionManager.Logout()
1583   >>> si.content.sessionManager.sessionList
1584   '''
1585
1586   STATE_UNAUTHENTICATED = 0
1587   STATE_AUTHENTICATED = 1
1588
1589   SESSION_EXCEPTIONS = tuple()
1590
1591   def __init__(self, soapStub, loginMethod, retryDelay=0.1, retryCount=4):
1592      '''Construct a SessionOrientedStub.
1593
1594      The stub starts off in the "unauthenticated" state, so it will call the
1595      loginMethod on the first invocation of a method.  If a communication error
1596      is encountered, the stub will wait for retryDelay seconds and then try to
1597      call the method again.  If the server throws an exception that is in the
1598      SESSION_EXCEPTIONS tuple, it will be caught and the stub will transition
1599      back into the "unauthenticated" state so that another login will be
1600      performed.
1601
1602      @param soapStub The communication substrate.
1603      @param loginMethod A function that takes a single parameter, soapStub, and
1604        performs the necessary operations to authenticate with the server.
1605      @param retryDelay The amount of time to sleep before retrying after a
1606        communication error.
1607      @param retryCount The number of times to retry connecting to the server.
1608      '''
1609      assert callable(loginMethod)
1610      assert retryCount >= 0
1611      StubAdapterBase.__init__(self, version=soapStub.version)
1612
1613      self.lock = threading.Lock()
1614      self.soapStub = soapStub
1615      self.state = self.STATE_UNAUTHENTICATED
1616
1617      self.loginMethod = loginMethod
1618      self.retryDelay = retryDelay
1619      self.retryCount = retryCount
1620
1621   def InvokeMethod(self, mo, info, args):
1622      # This retry logic is replicated in InvokeAccessor and the two copies need
1623      # to be in sync
1624      retriesLeft = self.retryCount
1625      while retriesLeft > 0:
1626         try:
1627            if self.state == self.STATE_UNAUTHENTICATED:
1628               self._CallLoginMethod()
1629            # Invoke the method
1630            status, obj = self.soapStub.InvokeMethod(mo, info, args, self)
1631         except (socket.error, http_client.HTTPException, ExpatError):
1632            if self.retryDelay and retriesLeft:
1633               time.sleep(self.retryDelay)
1634            retriesLeft -= 1
1635            continue
1636
1637         if status == 200:
1638            # Normal return from the server, return the object to the caller.
1639            return obj
1640
1641         # An exceptional return from the server
1642         if isinstance(obj, self.SESSION_EXCEPTIONS):
1643            # Our session might've timed out, change our state and retry.
1644            self._SetStateUnauthenticated()
1645         else:
1646            # It's an exception from the method that was called, send it up.
1647            raise obj
1648
1649      # Raise any socket/httplib errors caught above.
1650      raise SystemError()
1651
1652   ## Retrieve a managed property
1653   #
1654   # @param self self
1655   # @param mo managed object
1656   # @param info property info
1657   def InvokeAccessor(self, mo, info):
1658      # This retry logic is replicated in InvokeMethod and the two copies need
1659      # to be in sync
1660      retriesLeft = self.retryCount
1661      while retriesLeft > 0:
1662         try:
1663            if self.state == self.STATE_UNAUTHENTICATED:
1664               self._CallLoginMethod()
1665            # Invoke the method
1666            obj = StubAdapterBase.InvokeAccessor(self, mo, info)
1667         except (socket.error, http_client.HTTPException, ExpatError):
1668            if self.retryDelay and retriesLeft:
1669               time.sleep(self.retryDelay)
1670            retriesLeft -= 1
1671            continue
1672         except Exception as e:
1673            if isinstance(e, self.SESSION_EXCEPTIONS):
1674               # Our session might've timed out, change our state and retry.
1675               self._SetStateUnauthenticated()
1676               retriesLeft -= 1
1677               continue
1678            else:
1679               raise e
1680         return obj
1681      # Raise any socket/httplib errors caught above.
1682      raise SystemError()
1683
1684   ## Handle the login method call
1685   #
1686   #  This method calls the login method on the soap stub and changes the state
1687   #  to authenticated
1688   def _CallLoginMethod(self):
1689      try:
1690         self.lock.acquire()
1691         if self.state == self.STATE_UNAUTHENTICATED:
1692            self.loginMethod(self.soapStub)
1693            self.state = self.STATE_AUTHENTICATED
1694      finally:
1695         self.lock.release()
1696
1697   ## Change the state to unauthenticated
1698   def _SetStateUnauthenticated(self):
1699      self.lock.acquire()
1700      if self.state == self.STATE_AUTHENTICATED:
1701         self.state = self.STATE_UNAUTHENTICATED
1702      self.lock.release()
1703