Source code for pysnap.core.appliance

"""Helpers for reading appliance metadata from OVF and OVA files."""

from __future__ import annotations

from pathlib import Path
import tarfile
from xml.etree import ElementTree

from pysnap.errors import PySnapError

OVF_NAMESPACE = {"ovf": "http://schemas.dmtf.org/ovf/envelope/1"}
OVF_ID_ATTRIBUTE = "{http://schemas.dmtf.org/ovf/envelope/1}id"


[docs] def read_appliance_vm_names(image_path: str | Path) -> tuple[str, ...]: """Read original VM names declared inside an OVF or OVA appliance. :param image_path: Appliance path. :returns: Declared appliance VM names in descriptor order. :raises PySnapError: If the descriptor cannot be read or parsed. """ path = Path(image_path) descriptor = _read_ovf_descriptor(path) return _parse_ovf_vm_names(descriptor, path)
def _read_ovf_descriptor(path: Path) -> bytes: """Read the OVF descriptor bytes from an OVF or OVA path. :param path: Appliance path. :returns: Raw OVF descriptor bytes. :raises PySnapError: If no OVF descriptor can be found. """ suffix = path.suffix.lower() try: if suffix == ".ovf": return path.read_bytes() if suffix == ".ova": with tarfile.open(path, "r:*") as archive: member = next( (item for item in archive.getmembers() if item.name.lower().endswith(".ovf")), None, ) if member is None: raise PySnapError(f'Appliance "{path}" does not contain an OVF descriptor.') extracted = archive.extractfile(member) if extracted is None: raise PySnapError( f'Unable to read OVF descriptor "{member.name}" from "{path}".' ) return extracted.read() except (OSError, tarfile.TarError) as error: raise PySnapError(f'Unable to read appliance descriptor from "{path}": {error}') from error raise PySnapError(f'Unsupported appliance format for "{path}".') def _parse_ovf_vm_names(descriptor: bytes, path: Path) -> tuple[str, ...]: """Parse original VM names from raw OVF descriptor bytes. :param descriptor: Raw OVF descriptor bytes. :param path: Appliance path used for diagnostics. :returns: Declared appliance VM names. :raises PySnapError: If the descriptor does not expose any VM names. """ try: root = ElementTree.fromstring(descriptor) except ElementTree.ParseError as error: raise PySnapError(f'Unable to parse OVF descriptor from "{path}": {error}') from error vm_names: list[str] = [] seen: set[str] = set() for virtual_system in root.findall(".//ovf:VirtualSystem", OVF_NAMESPACE): vm_name = (virtual_system.get(OVF_ID_ATTRIBUTE) or "").strip() if not vm_name: name_node = virtual_system.find("ovf:Name", OVF_NAMESPACE) vm_name = ((name_node.text or "") if name_node is not None else "").strip() if not vm_name or vm_name in seen: continue vm_names.append(vm_name) seen.add(vm_name) if not vm_names: raise PySnapError(f'Unable to determine VM names declared by "{path}".') return tuple(vm_names)