"""VM clone + create + reconfigure via SOAP.
REST (``/api/vcenter/vm``) covers basic VM lifecycle but lacks the
hardware-customization depth Terraform users expect (clone from
template with full vCPU/memory/disk/network override, guest OS
customization specs, etc.). This module exposes the SOAP path:
- ``vim.VirtualMachine.CloneVM_Task`` for template/source clones
- ``vim.Folder.CreateVM_Task`` for from-scratch VMs
- ``vim.VirtualMachine.ReconfigVM_Task`` for hardware-level edits
Disk/NIC management has its own dedicated modules (``vim_vm_disk`` and
``vim_vm_nic``) since each has a richer surface.
"""
from pyVmomi import vim
from saltext.vcf.utils import vim as soap
# ---------------------------------------------------------------------------
# Lookup helpers
# ---------------------------------------------------------------------------
def _vm(opts, vm_id_or_name, profile=None):
content = soap.content(opts, profile=profile)
container = content.viewManager.CreateContainerView(
content.rootFolder, [vim.VirtualMachine], True
)
try:
for vm in container.view:
if vm_id_or_name in (vm._moId, vm.name): # noqa: SLF001
return vm
finally:
container.Destroy()
raise LookupError(f"VM {vm_id_or_name!r} not found")
def _find_by_type(opts, vim_type, name_or_id, profile=None):
content = soap.content(opts, profile=profile)
container = content.viewManager.CreateContainerView(content.rootFolder, [vim_type], True)
try:
for entity in container.view:
if name_or_id in (entity._moId, entity.name): # noqa: SLF001
return entity
finally:
container.Destroy()
raise LookupError(f"{vim_type.__name__} {name_or_id!r} not found")
# ---------------------------------------------------------------------------
# Clone
# ---------------------------------------------------------------------------
[docs]
def clone(
opts,
source,
name,
*,
folder=None,
datastore=None,
host=None,
resource_pool=None,
cluster=None,
template=False,
power_on=False,
customization=None,
cpu_count=None,
memory_mb=None,
annotation=None,
profile=None,
):
"""Clone *source* (VM or template) into a new VM named *name*.
All target placement fields accept either an MoID or a name. *folder*
and *datastore* are required; *resource_pool* defaults to the root pool
on *cluster* (or *host*) when omitted.
"""
src = _vm(opts, source, profile=profile)
target_folder = _find_by_type(opts, vim.Folder, folder, profile=profile)
target_datastore = _find_by_type(opts, vim.Datastore, datastore, profile=profile)
relocate = vim.vm.RelocateSpec(datastore=target_datastore)
if host:
relocate.host = _find_by_type(opts, vim.HostSystem, host, profile=profile)
if resource_pool:
relocate.pool = _find_by_type(opts, vim.ResourcePool, resource_pool, profile=profile)
elif cluster:
cluster_obj = _find_by_type(opts, vim.ClusterComputeResource, cluster, profile=profile)
relocate.pool = cluster_obj.resourcePool
spec = vim.vm.CloneSpec(
location=relocate,
powerOn=bool(power_on),
template=bool(template),
)
if any(v is not None for v in (cpu_count, memory_mb, annotation)):
config = vim.vm.ConfigSpec()
if cpu_count is not None:
config.numCPUs = int(cpu_count)
if memory_mb is not None:
config.memoryMB = int(memory_mb)
if annotation is not None:
config.annotation = annotation
spec.config = config
if customization is not None:
spec.customization = customization
task = src.CloneVM_Task(folder=target_folder, name=name, spec=spec)
return task._moId # noqa: SLF001
# ---------------------------------------------------------------------------
# Create-from-scratch
# ---------------------------------------------------------------------------
[docs]
def create(
opts,
name,
folder,
datastore,
*,
cpu_count=1,
memory_mb=1024,
guest_id="otherGuest64",
cluster=None,
host=None,
resource_pool=None,
annotation="",
profile=None,
):
"""Create a bare VM (no disks, no NICs)."""
target_folder = _find_by_type(opts, vim.Folder, folder, profile=profile)
target_datastore = _find_by_type(opts, vim.Datastore, datastore, profile=profile)
if resource_pool:
pool = _find_by_type(opts, vim.ResourcePool, resource_pool, profile=profile)
elif cluster:
cluster_obj = _find_by_type(opts, vim.ClusterComputeResource, cluster, profile=profile)
pool = cluster_obj.resourcePool
elif host:
host_obj = _find_by_type(opts, vim.HostSystem, host, profile=profile)
pool = host_obj.parent.resourcePool
else:
raise ValueError("provide cluster, host, or resource_pool for VM placement")
host_obj = _find_by_type(opts, vim.HostSystem, host, profile=profile) if host else None
config = vim.vm.ConfigSpec(
name=name,
memoryMB=int(memory_mb),
numCPUs=int(cpu_count),
guestId=guest_id,
annotation=annotation,
files=vim.vm.FileInfo(vmPathName=f"[{target_datastore.name}]"),
)
task = target_folder.CreateVM_Task(config=config, pool=pool, host=host_obj)
return task._moId # noqa: SLF001
# ---------------------------------------------------------------------------
# Reconfigure (hardware + metadata)
# ---------------------------------------------------------------------------
[docs]
def get_advanced_settings(opts, vm_id_or_name, profile=None):
"""Return the VM's ``extraConfig`` as a flat ``{key: value}`` dict."""
vm = _vm(opts, vm_id_or_name, profile=profile)
return {opt.key: opt.value for opt in (vm.config.extraConfig or [])}
# ---------------------------------------------------------------------------
# Destroy
# ---------------------------------------------------------------------------
[docs]
def destroy(opts, vm_id_or_name, profile=None):
"""Power off (if needed) and destroy the VM. Returns task moId."""
vm = _vm(opts, vm_id_or_name, profile=profile)
if vm.runtime.powerState == "poweredOn":
vm.PowerOffVM_Task()
task = vm.Destroy_Task()
return task._moId # noqa: SLF001
def mark_as_template(opts, vm_id_or_name, profile=None):
vm = _vm(opts, vm_id_or_name, profile=profile)
vm.MarkAsTemplate()
def mark_as_virtual_machine(opts, template_id_or_name, resource_pool, host=None, profile=None):
template = _vm(opts, template_id_or_name, profile=profile)
pool = _find_by_type(opts, vim.ResourcePool, resource_pool, profile=profile)
host_obj = _find_by_type(opts, vim.HostSystem, host, profile=profile) if host else None
template.MarkAsVirtualMachine(pool=pool, host=host_obj)
# ---------------------------------------------------------------------------
# Instant clone (memory-state + disk-delta from a running source)
# ---------------------------------------------------------------------------
[docs]
def instant_clone(
opts,
source,
name,
*,
folder=None,
datastore=None,
host=None,
resource_pool=None,
extra_config=None,
profile=None,
):
"""Instant-clone *source* (must be powered on) into a new VM *name*.
Returns task moId. Source must be running; the new VM inherits memory
state. Far cheaper than a regular clone but defaults to same-host
placement.
*extra_config* — optional ``{key: value}`` dict applied as VM extraConfig
options (e.g. ``{"guestinfo.role": "worker"}``).
"""
src = _vm(opts, source, profile=profile)
spec = vim.vm.InstantCloneSpec(name=name)
location = vim.vm.RelocateSpec()
if folder is not None:
location.folder = _find_by_type(opts, vim.Folder, folder, profile=profile)
if datastore is not None:
location.datastore = _find_by_type(opts, vim.Datastore, datastore, profile=profile)
if host is not None:
location.host = _find_by_type(opts, vim.HostSystem, host, profile=profile)
if resource_pool is not None:
location.pool = _find_by_type(opts, vim.ResourcePool, resource_pool, profile=profile)
spec.location = location
if extra_config:
spec.config = [vim.option.OptionValue(key=k, value=v) for k, v in extra_config.items()]
task = src.InstantClone_Task(spec=spec)
return task._moId # noqa: SLF001
# ---------------------------------------------------------------------------
# Move to folder
# ---------------------------------------------------------------------------
[docs]
def move_to_folder(opts, vm_id_or_name, folder, profile=None):
"""Reparent *vm_id_or_name* under *folder*. Returns task moId."""
vm = _vm(opts, vm_id_or_name, profile=profile)
target_folder = _find_by_type(opts, vim.Folder, folder, profile=profile)
task = target_folder.MoveIntoFolder_Task(list=[vm])
return task._moId # noqa: SLF001
# ---------------------------------------------------------------------------
# Register / unregister existing VMX
# ---------------------------------------------------------------------------
[docs]
def register(
opts,
vmx_path,
name,
folder,
*,
resource_pool=None,
cluster=None,
host=None,
as_template=False,
profile=None,
):
"""Register an existing .vmx as a new VM. Returns task moId.
*vmx_path* is the datastore path (e.g. ``[ds1] vm/vm.vmx``). One of
*resource_pool*, *cluster*, or *host* is required.
"""
target_folder = _find_by_type(opts, vim.Folder, folder, profile=profile)
if resource_pool:
pool = _find_by_type(opts, vim.ResourcePool, resource_pool, profile=profile)
elif cluster:
cluster_obj = _find_by_type(opts, vim.ClusterComputeResource, cluster, profile=profile)
pool = cluster_obj.resourcePool
elif host:
host_obj = _find_by_type(opts, vim.HostSystem, host, profile=profile)
pool = host_obj.parent.resourcePool
else:
raise ValueError("provide cluster, host, or resource_pool for VM placement")
host_obj = _find_by_type(opts, vim.HostSystem, host, profile=profile) if host else None
task = target_folder.RegisterVM_Task(
path=vmx_path,
name=name,
asTemplate=bool(as_template),
pool=pool,
host=host_obj,
)
return task._moId # noqa: SLF001
[docs]
def unregister(opts, vm_id_or_name, profile=None):
"""Remove the VM from inventory but leave its files on disk."""
vm = _vm(opts, vm_id_or_name, profile=profile)
vm.UnregisterVM()
return True