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("&", "&").replace(">", ">").replace("<", "<") 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