VDS Import fails with UTF-8 decoding error - Pure Python solution needed

Open
#1,117 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Assessment

Difficulty
5/5
Estimated time
Over a week
Newbie friendliness
35/100
Issue type
Bug
Clarity
Needs clarification
Activity status
Active
Tech stack
python
Domain
api, networking

Research direction

Start with the complete reproducible script, especially parse_xml_entity and import_vds, then trace the DVSManager.ImportEntity call in pyVmomi. Reproduce the failure with the supplied environment and inspect how EntityBackup.Config values and the returned SOAP response are handled. Done means a pure Python VDS import works and rejected requests expose a usable error.

Written by the indexing model from the issue text.

Description

enhancement
Is your feature request related to a problem? Please describe.

When attempting to import a Distributed Virtual Switch (VDS) configuration using pure Python and pyVmomi, the DVSManager.ImportEntity method fails with a UTF-8 decoding error:

ERROR: Import failed: DVSManagerImportEntity_Task failed for 'dvs-production-01': 'utf-8' codec can't decode byte 0x9c in position 5: invalid start byte

Technical Analysis:

  • The byte 0x9c is the zlib compression header
  • vCenter rejects the import request and returns a compressed SOAP fault
  • pyVmomi's HTTP client fails to decompress the response before parsing
  • This masks the actual vCenter error message, preventing troubleshooting

Environment:

  • Python: 3.13.x
  • OS: Windows 11(PoC) but Linux for Production.
  • pyVmomi: 9.1.0.0
  • vCenter: VMware vSphere 8.0.3.01000

Current Status:

  • ✅ Export functionality works perfectly
  • Import functionality fails with UTF-8 decoding error
  • ❌ Generated XML is not compatible with vSphere Web UI (expects ZIP format)

The same operation works flawlessly using PowerCLI, confirming the API itself is functional.

Describe the solution you'd like

I need a pure Python/pyVmomi solution that:

  1. Exports VDS configuration (already works ✅)

  2. Imports VDS configuration without UTF-8 decoding errors

  3. Handles proper serialization of EntityBackupConfig objects including:

    • configBlob as raw bytes (not list or string)
    • configVersion as string (pyVmomi requirement)
    • Proper handling of the container field for applyToEntitySpecified mode
  4. Provides clear error messages when vCenter rejects requests (proper decompression of SOAP faults)

Specific Questions:

  1. How should configBlob be serialized when calling DVSManagerImportEntity?

    • Raw bytes?
    • Base64 string?
    • Compressed (zlib/gzip)?
  2. What is the correct type for configVersion?

    • pyVmomi validation requires str, but is this consistent with vSphere 8.0.x WSDL?
  3. Should the container field be populated when using importType="applyToEntitySpecified"?

    • Documentation suggests entityType and key are sufficient
  4. How to handle vCenter's compressed error responses?

    • Is there a configuration to auto-decompress gzip/zlib responses?
  5. Is there a working example of VDS import using pure pyVmomi (not PowerCLI)?

Describe alternatives you've considered

PowerCLI/PowerShell Module:

  • The original PowerCLI script by Luc Dekens works perfectly
  • However, I require a pure Python solution for cross-platform compatibility
  • PowerShell is not available or preferred in our Linux-based automation environment

vSphere Web UI Manual Import:

  • Requires ZIP format with specific structure (separate .bak files + manifest)
  • Not suitable for automated workflows
  • The Python script generates a single XML file with inline base64 blobs

Direct HTTP/REST API calls:

  • Would bypass pyVmomi entirely
  • Extremely complex to implement properly
  • Would lose the benefits of pyVmomi's type safety and object model

Preferred Solution:
A working pyVmomi implementation that properly handles the SOAP serialization and HTTP compression, maintaining compatibility with the existing vSphere API.

Additional context

Complete Reproducible Script

Below is the complete, self-contained Python script that demonstrates the issue. Maintainers can copy, paste, and run this directly.

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
VDS Backup & Restore Manager (XML) - Minimal Reproducible Example
Demonstrates the UTF-8 decoding crash during DVSManager.ImportEntity in pyVmomi.
"""
import os
import sys
import ssl
import base64
import argparse
import urllib3
from datetime import datetime
from xml.etree import ElementTree as ET

try:
    from pyVmomi import vim, vmodl, VmomiSupport
    from pyVim import connect as vmware_connect
except ImportError as e:
    print(f"ERROR: Cannot import pyVmomi: {e}")
    sys.exit(1)

urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
ssl._create_default_https_context = ssl._create_unverified_context

BACKUP_ROOT = r"C:\backups\VDS"
IMPORT_TYPE = "applyToEntitySpecified"

def sanitize_filename(name: str) -> str:
    invalid = '<>:"/\\|?*\x00-\x1f'
    for ch in invalid:
        name = name.replace(ch, '_')
    return name.strip(' .') or '_'

def create_xml_entity(entity):
    elem = ET.Element("entity")
    elem.set("type", getattr(entity, 'entityType', ''))
    elem.set("key", getattr(entity, 'key', ''))
    elem.set("name", getattr(entity, 'name', ''))
    
    container = getattr(entity, 'container', None)
    if container and hasattr(container, '_moId'):
        elem.set("container", container._moId)
    else:
        elem.set("container", str(container) if container else '')
    
    elem.set("configVersion", str(getattr(entity, 'configVersion', '0')))
    
    if getattr(entity, 'configBlob', None):
        blob_b64 = base64.b64encode(entity.configBlob).decode('ascii')
        blob_elem = ET.SubElement(elem, "configBlob")
        blob_elem.text = blob_b64
    else:
        blob_elem = ET.SubElement(elem, "configBlob")
        blob_elem.text = ""
    return elem

def parse_xml_entity(elem):
    config_type = VmomiSupport.GetVmodlType("vim.dvs.EntityBackup.Config")
    config = config_type()
    
    config.entityType = elem.get("type", "")
    config.key = elem.get("key", "")
    config.name = elem.get("name", "")
    
    config_version_str = elem.get("configVersion", "0")
    config.configVersion = str(config_version_str) if config_version_str else "0"
    
    blob_text = elem.findtext("configBlob", default="")
    if blob_text:
        config.configBlob = base64.b64decode(blob_text)
    else:
        config.configBlob = b""
        
    return config

def connect_to_vcenter(host, user, pwd):
    ctx = ssl.create_default_context()
    ctx.check_hostname = False
    ctx.verify_mode = ssl.CERT_NONE
    try:
        si = vmware_connect.SmartConnect(host=host, user=user, pwd=pwd, port=443, sslContext=ctx)
        return si, si.RetrieveContent()
    except Exception as e:
        raise RuntimeError(f"Connection to '{host}' failed: {e}")

def export_vds(content, dvs_name, output_dir):
    container_view = content.viewManager.CreateContainerView(content.rootFolder, [vim.DistributedVirtualSwitch], True)
    dvs = next((d for d in container_view.view if d.name == dvs_name), None)
    container_view.Destroy()
    
    if not dvs:
        raise RuntimeError(f"VDS '{dvs_name}' not found")
    
    selection_sets = []
    dvs_ss = vim.dvs.DistributedVirtualSwitchSelection()
    dvs_ss.dvsUuid = dvs.uuid
    selection_sets.append(dvs_ss)
    
    for pg in dvs.portgroup:
        pg_ss = vim.dvs.DistributedVirtualPortgroupSelection()
        pg_ss.dvsUuid = dvs.uuid
        pg_ss.portgroupKey = [pg.key]
        selection_sets.append(pg_ss)
    
    try:
        task = content.dvSwitchManager.DVSManagerExportEntity_Task(selection_sets)
        while task.info.state not in [vim.TaskInfo.State.success, vim.TaskInfo.State.error]:
            import time; time.sleep(0.5)
        if task.info.state == vim.TaskInfo.State.error:
            raise RuntimeError(f"Export failed: {task.info.error.msg}")
        export_result = task.info.result
    except Exception as e:
        raise RuntimeError(f"DVSManagerExportEntity_Task failed: {e}")
    
    os.makedirs(output_dir, exist_ok=True)
    root = ET.Element("vdsBackup")
    root.set("dvsName", dvs.name)
    root.set("dvsUuid", dvs.uuid)
    root.set("exportTime", datetime.now().isoformat())
    
    entities_elem = ET.SubElement(root, "entities")
    for entity in export_result:
        entities_elem.append(create_xml_entity(entity))
        
    filepath = os.path.join(output_dir, f"VDS_{sanitize_filename(dvs.name)}_{datetime.now().strftime('%Y%m%d_%H%M%S')}.xml")
    ET.ElementTree(root).write(filepath, encoding="utf-8", xml_declaration=True)
    print(f"SUCCESS: Exported to {filepath}")
    return filepath

def import_vds(content, backup_file, dvs_name):
    if not os.path.isfile(backup_file):
        raise FileNotFoundError(f"File not found: {backup_file}")
        
    tree = ET.parse(backup_file)
    root = tree.getroot()
    entities_elem = root.find("entities")
    if entities_elem is None:
        raise ValueError("Invalid XML: missing 'entities' element")
        
    entity_list = [parse_xml_entity(elem) for elem in entities_elem.findall("entity")]
    if not entity_list:
        raise ValueError("No entities found in XML file")
    
    try:
        content.dvSwitchManager.ImportEntity(
            entityBackup=entity_list,
            importType=IMPORT_TYPE
        )
        print(f"SUCCESS: Imported VDS '{dvs_name}'")
    except vim.fault.NotFound:
        raise RuntimeError(f"VDS '{dvs_name}' not found. '{IMPORT_TYPE}' requires the VDS to exist.")
    except Exception as e:
        err_msg = str(e)
        if "utf-8" in err_msg.lower() or "codec" in err_msg.lower() or "decode" in err_msg.lower():
            print("\n" + "="*60)
            print("[DEBUG] pyVmomi parsing crash intercepted.")
            print("[DEBUG] vCenter rejected the request and returned a compressed (zlib) SOAP fault.")
            print("[DEBUG] pyVmomi failed to decompress it, masking the actual vCenter error.")
            print("="*60 + "\n")
        raise RuntimeError(f"ImportEntity failed for '{dvs_name}': {e}")

def main():
    parser = argparse.ArgumentParser(description="VDS Backup & Restore - Minimal Reproducible Example")
    parser.add_argument("--export", action="store_true", help="Export VDS")
    parser.add_argument("--import", dest="import_mode", action="store_true", help="Import VDS")
    parser.add_argument("--vcenter", required=True, help="vCenter FQDN")
    parser.add_argument("--vds", required=True, help="VDS Name")
    parser.add_argument("--path", help="Path for import/export")
    parser.add_argument("--user", required=True, help="vCenter Username")
    parser.add_argument("--password", required=True, help="vCenter Password")
    
    args = parser.parse_args()
    
    try:
        si, content = connect_to_vcenter(args.vcenter, args.user, args.password)
        print(f"SUCCESS: Connected to '{args.vcenter}'")
        
        if args.export:
            out_dir = args.path or os.path.join(BACKUP_ROOT, sanitize_filename(args.vcenter), sanitize_filename(args.vds))
            export_vds(content, args.vds, out_dir)
            
        elif args.import_mode:
            if not args.path:
                parser.error("--import requires --path")
            import_vds(content, args.path, args.vds)
            
    except Exception as e:
        print(f"ERROR: {e}")
        sys.exit(1)
    finally:
        if 'si' in locals() and si:
            try: vmware_connect.Disconnect(si)
            except: pass

if __name__ == "__main__":
    main()

Sample Generated XML Snippet:

<?xml version='1.0' encoding='utf-8'?>
<vdsBackup dvsName="dvs-production-01" dvsUuid="50 39 18 c4 04 fc cf 76-97 50 1f ee e9 0c 0a 27" exportTime="2026-08-31T18:21:11.676457">
  <entities>
    <entity type="distributedVirtualSwitch" key="50 39 18 c4 04 fc cf 76-97 50 1f ee e9 0c 0a 27" name="dvs-production-01" container="group-n11" configVersion="0">
      <configBlob>AEUAAHic7RzZbuM48leEPCxmgY3lI4njrNpAzulgciFOB4t9adASbbNDiWqKcpz9+i2S...</configBlob>
    </entity>
    <entity type="distributedVirtualPortgroup" key="dvportgroup-1234" name="PG-Production-01" container="dvs-837" configVersion="0">
      <configBlob>DBQAAHic7Vjbkto4EP0VF++DuQwzMOVxVeayW1ObSaiFUPuWEpYMCrLklWQD+fptWwZs...</configBlob>
    </entity>
  </entities>
</vdsBackup>

References

 $dvSwMgr.DVSManagerImportEntity($dvswImport, "applyToEntitySpecified").
Dominant language
Python
Stars
2.3k
Forks
763
PR merge metrics
No merged PRs in 30d

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from vmware/pyvmomi

All issues in vmware/pyvmomi

Similar issues

More Python issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.