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,
|
network: str | None = None,
|
||||||
power_on: bool = False,
|
power_on: bool = False,
|
||||||
disk_provisioning: str = "thin",
|
disk_provisioning: str = "thin",
|
||||||
|
deployment_option: str | None = None,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
"""Deploy a virtual machine from an OVA file.
|
"""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
|
network: Target port group; every network in the OVF is mapped to it
|
||||||
power_on: Power on the VM after deployment
|
power_on: Power on the VM after deployment
|
||||||
disk_provisioning: 'thin', 'thick', or 'eagerZeroedThick'
|
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:
|
Returns:
|
||||||
Dict with deployment details
|
Dict with deployment details
|
||||||
@ -413,6 +417,8 @@ class OVFManagementMixin(MCPMixin):
|
|||||||
entityName=vm_name,
|
entityName=vm_name,
|
||||||
diskProvisioning=disk_provisioning,
|
diskProvisioning=disk_provisioning,
|
||||||
)
|
)
|
||||||
|
if deployment_option:
|
||||||
|
spec_params.deploymentOption = deployment_option
|
||||||
if network:
|
if network:
|
||||||
net = self.conn.find_network(network)
|
net = self.conn.find_network(network)
|
||||||
if not net:
|
if not net:
|
||||||
@ -507,6 +513,103 @@ class OVFManagementMixin(MCPMixin):
|
|||||||
result["power_state"] = "poweredOff"
|
result["power_state"] = "poweredOff"
|
||||||
return result
|
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(
|
@mcp_tool(
|
||||||
name="export_vm_ovf",
|
name="export_vm_ovf",
|
||||||
description="Export a VM to OVF format on a datastore",
|
description="Export a VM to OVF format on a datastore",
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user