deploy_ova: support OVF deployment options + add inspect_ova
Cisco-style OVAs (CUCM/CUC/UCCX) ship multiple deployment configurations (e.g. Small/Medium/Large) in a DeploymentOptionSection and default to one. deploy_ova now takes deployment_option to select a config; without it the OVF default is used. inspect_ova reads an OVA (local path or URL) and reports product, guest OS, hardware version, selectable deployment options, networks, and disks so clients can choose deployment_option/network before deploying. Verified against CUCM 15 OVA: Small config yields 2 vCPU / 10 GB / 110 GB / VMXNET3 exactly per Cisco's spec.
This commit is contained in:
parent
4fa2286e57
commit
539aa913e9
@ -363,6 +363,7 @@ class OVFManagementMixin(MCPMixin):
|
||||
network: str | None = None,
|
||||
power_on: bool = False,
|
||||
disk_provisioning: str = "thin",
|
||||
deployment_option: str | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Deploy a virtual machine from an OVA file.
|
||||
|
||||
@ -378,6 +379,9 @@ class OVFManagementMixin(MCPMixin):
|
||||
network: Target port group; every network in the OVF is mapped to it
|
||||
power_on: Power on the VM after deployment
|
||||
disk_provisioning: 'thin', 'thick', or 'eagerZeroedThick'
|
||||
deployment_option: OVF configuration id for multi-config templates
|
||||
(e.g. Cisco 'S'/'M'/'L'); defaults to the OVF's default config.
|
||||
Use inspect_ova to list available options.
|
||||
|
||||
Returns:
|
||||
Dict with deployment details
|
||||
@ -413,6 +417,8 @@ class OVFManagementMixin(MCPMixin):
|
||||
entityName=vm_name,
|
||||
diskProvisioning=disk_provisioning,
|
||||
)
|
||||
if deployment_option:
|
||||
spec_params.deploymentOption = deployment_option
|
||||
if network:
|
||||
net = self.conn.find_network(network)
|
||||
if not net:
|
||||
@ -507,6 +513,103 @@ class OVFManagementMixin(MCPMixin):
|
||||
result["power_state"] = "poweredOff"
|
||||
return result
|
||||
|
||||
@mcp_tool(
|
||||
name="inspect_ova",
|
||||
description="Inspect an OVA (local path or URL): product, deployment configurations, networks, and disks",
|
||||
annotations=ToolAnnotations(readOnlyHint=True),
|
||||
)
|
||||
def inspect_ova(self, ova_path: str) -> dict[str, Any]:
|
||||
"""Inspect an OVA without deploying it.
|
||||
|
||||
Reads the OVF descriptor and reports the product, guest OS, hardware
|
||||
version, selectable deployment configurations (e.g. Cisco S/M/L), the
|
||||
networks the OVF expects, and its disks. Useful for picking a
|
||||
``deployment_option`` and ``network`` before calling ``deploy_ova``.
|
||||
|
||||
Args:
|
||||
ova_path: Local path to the .ova file, or an http(s) URL
|
||||
|
||||
Returns:
|
||||
Dict describing the OVA
|
||||
"""
|
||||
local_ova, is_temp = self._resolve_ova_file(ova_path)
|
||||
try:
|
||||
with tarfile.open(local_ova) as tar:
|
||||
ovf_member = next(
|
||||
(m for m in tar.getmembers() if m.name.lower().endswith(".ovf")),
|
||||
None,
|
||||
)
|
||||
if not ovf_member:
|
||||
raise ValueError("No .ovf descriptor found in OVA")
|
||||
ovf_xml = tar.extractfile(ovf_member).read().decode("utf-8")
|
||||
disk_files = [
|
||||
m.name for m in tar.getmembers() if m.name.lower().endswith(".vmdk")
|
||||
]
|
||||
finally:
|
||||
if is_temp:
|
||||
Path(local_ova).unlink(missing_ok=True)
|
||||
|
||||
def lname(tag: str) -> str:
|
||||
return tag.rsplit("}", 1)[-1]
|
||||
|
||||
root = ET.fromstring(ovf_xml)
|
||||
product: str | None = None
|
||||
os_type: str | None = None
|
||||
hw_version: str | None = None
|
||||
configs: list[dict[str, Any]] = []
|
||||
networks: list[str] = []
|
||||
disks: list[dict[str, Any]] = []
|
||||
|
||||
for el in root.iter():
|
||||
tag = lname(el.tag)
|
||||
if tag == "Product" and product is None:
|
||||
product = (el.text or "").strip()
|
||||
elif tag == "OperatingSystemSection":
|
||||
for key, val in el.attrib.items():
|
||||
if lname(key) == "osType":
|
||||
os_type = val
|
||||
elif tag == "VirtualSystemType" and hw_version is None:
|
||||
hw_version = (el.text or "").strip()
|
||||
elif tag == "Configuration":
|
||||
cid = None
|
||||
default = False
|
||||
for key, val in el.attrib.items():
|
||||
if lname(key) == "id":
|
||||
cid = val
|
||||
elif lname(key) == "default":
|
||||
default = val == "true"
|
||||
label = ""
|
||||
for child in el:
|
||||
if lname(child.tag) == "Label":
|
||||
label = (child.text or "").strip()
|
||||
configs.append({"id": cid, "label": label, "default": default})
|
||||
elif tag == "Network":
|
||||
for key, val in el.attrib.items():
|
||||
if lname(key) == "name":
|
||||
networks.append(val)
|
||||
elif tag == "Disk":
|
||||
disk: dict[str, Any] = {}
|
||||
for key, val in el.attrib.items():
|
||||
name = lname(key)
|
||||
if name == "diskId":
|
||||
disk["disk_id"] = val
|
||||
elif name == "capacity":
|
||||
disk["capacity"] = val
|
||||
elif name == "capacityAllocationUnits":
|
||||
disk["units"] = val
|
||||
disks.append(disk)
|
||||
|
||||
return {
|
||||
"source": ova_path,
|
||||
"product": product,
|
||||
"os_type": os_type,
|
||||
"hardware_version": hw_version,
|
||||
"deployment_options": configs,
|
||||
"networks": networks,
|
||||
"disks": disks,
|
||||
"disk_files": disk_files,
|
||||
}
|
||||
|
||||
@mcp_tool(
|
||||
name="export_vm_ovf",
|
||||
description="Export a VM to OVF format on a datastore",
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user