hyperv

package
v0.3.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 21, 2026 License: MPL-2.0 Imports: 22 Imported by: 0

Documentation

Overview

Package hyperv is the typed Go wrapper over the connection layer. It concatenates the embedded preamble to each script body, marshals Go DTOs into PowerShell input JSON, and unmarshals PowerShell output JSON back into typed structs. Errors from the structured envelope (Write-HypervError) are mapped to the typed errors in errors.go.

Resources never touch connection.Runner directly — they go through this Client.

Index

Constants

This section is empty.

Variables

View Source
var (
	ErrNotFound            = errors.New("hyperv: resource not found")
	ErrUnavailable         = errors.New("hyperv: resource temporarily unavailable")
	ErrUnauthorized        = errors.New("hyperv: permission denied")
	ErrInvalidParentPath   = errors.New("hyperv: invalid parent path")
	ErrChecksumMismatch    = errors.New("hyperv: image file checksum mismatch")
	ErrDecompressionFailed = errors.New("hyperv: image file decompression failed")
	ErrPSExecution         = errors.New("hyperv: powershell execution failed")
)

Sentinel errors. Resources match against these with `errors.Is(err, X)` to decide how to surface to the user (RemoveResource for ErrNotFound, AddAttributeError for ErrInvalidParentPath, etc.).

ErrNotFound vs ErrUnavailable is load-bearing: ErrNotFound means the object genuinely does not exist (PS ObjectNotFound) and a resource Read should RemoveResource so Terraform plans a recreate; ErrUnavailable means the object is known but temporarily inaccessible (PS ResourceUnavailable — vmms stopped, cluster node fenced, transport blip) and the resource MUST surface a transient error rather than dropping the resource from state.

Functions

func ComputeFileSHA256 added in v0.2.0

func ComputeFileSHA256(path string) (string, error)

ComputeFileSHA256 returns the lowercase-hex SHA-256 of the file at path. Streams via io.Copy so files of any size hash without buffering the whole payload in memory.

Exported because the resource layer's local_path-mode plan-time hashing reuses this -- both the typed-client method and the resource's ModifyPlan need the same function so the SHA the runner commits to at plan time is byte-identical to the one it sends on the wire at apply time.

Types

type AttachDvdDriveInput

type AttachDvdDriveInput struct {
	Name               string  `json:"name"`
	ControllerType     string  `json:"controller_type"`
	ControllerNumber   int     `json:"controller_number"`
	ControllerLocation int     `json:"controller_location"`
	IsoPath            *string `json:"iso_path,omitempty"`
}

AttachDvdDriveInput is the stdin JSON shape for vm/add-dvd-drive.ps1. IsoPath is *string so the wire JSON drops it cleanly when the user wants an empty drive (script's "if not empty" guard then omits -Path from the cmdlet call).

type AttachHardDiskInput

type AttachHardDiskInput struct {
	Name               string `json:"name"`
	ControllerType     string `json:"controller_type"`
	ControllerNumber   int    `json:"controller_number"`
	ControllerLocation int    `json:"controller_location"`
	Path               string `json:"path"`
}

AttachHardDiskInput is the stdin JSON shape for vm/add-hard-disk-drive.ps1. All fields are required; the script's ValidateSet on ControllerType is the second line of defense against typos that the resource-layer schema validator should catch first.

type AttachNetworkAdapterInput

type AttachNetworkAdapterInput struct {
	Name       string `json:"name"`
	VMName     string `json:"vm_name"`
	SwitchName string `json:"switch_name"`

	// MacAddress is optional. Empty string means "let Hyper-V auto-
	// assign from its dynamic pool" -- the script omits the
	// -StaticMacAddress flag in that case. Any non-empty value is
	// passed through verbatim; the schema validator at the resource
	// layer pre-screens the format.
	MacAddress string `json:"mac_address,omitempty"`

	// VlanID is optional. 0 means untagged (the script issues
	// `Set-VMNetworkAdapterVlan -Untagged` after the Add). 1-4094 sets
	// access-mode VLAN. The schema validator pre-screens the range.
	VlanID int `json:"vlan_id,omitempty"`
}

AttachNetworkAdapterInput is the stdin JSON shape for vm/add-network-adapter.ps1. Name / VMName / SwitchName are required; MacAddress and VlanID are optional. Uniqueness of Name within a VM is enforced by the resource-layer schema validator (Hyper-V itself doesn't enforce it).

type BootOrderEntry

type BootOrderEntry struct {
	Type               string `json:"Type"`
	ControllerType     string `json:"ControllerType"`
	ControllerNumber   int    `json:"ControllerNumber"`
	ControllerLocation int    `json:"ControllerLocation"`
	Name               string `json:"Name"`
}

BootOrderEntry is the per-entry shape vm/get.ps1 emits inside VM.BootOrder. Type discriminates between hard_disk_drive / dvd_drive (which carry the ControllerType + ControllerNumber + ControllerLocation slot tuple) and network_adapter (which carries Name). Unused fields for a given Type are zero values; consumers branch on Type.

Gen 1 VMs always emit []BootOrderEntry{} (the script doesn't fetch firmware for them; gen 1 BIOS StartupOrder is a separate, deferred schema slice).

type Client

type Client struct {
	// contains filtered or unexported fields
}

Client is the typed wrapper resources use to invoke Hyper-V cmdlets. One instance per provider configuration; passed via the framework's resp.ResourceData / resp.DataSourceData.

httpClient is consumed by the runner-pipelined image_file fetch; other methods route entirely through the PowerShell connection layer and don't observe it.

netNatMu serializes calls to every NetNat-touching method (nat_static_mapping CRUD plus the NAT branches of vswitch CRUD). Windows' NetNat is a host-singleton with a persistent-store backing file; under terraform's default parallelism=10 (or higher), parallel Add-NetNatStaticMapping calls race the same file handle and surface as ERROR_SHARING_VIOLATION ("The process cannot access the file because it is being used by another process") on the loser. The PS-side _retry helper retries these as defense in depth, but eliminating the contention here is the real fix.

RWMutex (not Mutex) so concurrent reads parallelize. The race is specifically between WRITERS racing the NetNat backing file's exclusive-write handle -- Add-NetNatStaticMapping is the offender named in the bug report. Get-NetNatStaticMapping (the Read path) opens the file with shared-read access per the Windows file API convention and doesn't conflict with other readers. So:

  • Get* methods take RLock -- N parallel terraform refreshes of nat_static_mapping resources run in O(1) wall time, not O(N).
  • New / Set / Remove methods take Lock (exclusive) -- writers serialize against each other AND against any in-flight reader.

One RWMutex per Client is correct because NetNat is host-singleton: there's exactly one ordering of NetNat writes per host.

func NewClient

func NewClient(r connection.Runner, opts ...ClientOption) *Client

NewClient wraps a connection.Runner. The Runner abstraction is what lets unit tests substitute a fake without standing up a real PowerShell host.

The default *http.Client is configured with a non-zero ResponseHeaderTimeout to bound the "TCP open, server stalled before flushing headers" failure mode that http.DefaultClient leaves unbounded. Overall request timeout is intentionally left unset -- large image downloads at low bandwidth are legitimate and shouldn't trip a fixed Client.Timeout; the caller's context bounds in-flight progress.

func (*Client) AttachDvdDrive

func (c *Client) AttachDvdDrive(ctx context.Context, in AttachDvdDriveInput) error

AttachDvdDrive adds a DVD drive to a VM at a specific controller slot via Add-VMDvdDrive, optionally loading an ISO. IsoPath=nil produces an empty drive (the medium tray exists but no ISO is loaded); IsoPath=&"path" loads the ISO at attach time.

func (*Client) AttachHardDisk

func (c *Client) AttachHardDisk(ctx context.Context, in AttachHardDiskInput) error

AttachHardDisk wires an existing VHD to a VM at a specific controller slot via Add-VMHardDiskDrive. Slot semantics:

  • (ControllerType, ControllerNumber, ControllerLocation) identifies the slot uniquely. Two attachments at the same slot is an error (Hyper-V's InvalidArgument -> ErrPSExecution).
  • The Path argument is the existing VHD's location -- this method does NOT create the VHD; pair with hyperv_vhd or hyperv_image_file for that.
  • ControllerType=IDE on a gen 2 VM errors at the cmdlet layer with a clear "cannot attach IDE devices to a generation 2 virtual machine" -- the resource-layer schema validator should catch this at plan time, but the script-side ValidateSet is defense in depth.

Returns ErrNotFound if the VM is missing (resource Read should have reconciled before this is reachable, but the path exists for safety). Other errors map to ErrPSExecution and surface verbatim.

func (*Client) AttachNetworkAdapter

func (c *Client) AttachNetworkAdapter(ctx context.Context, in AttachNetworkAdapterInput) error

AttachNetworkAdapter adds a new NIC to a VM and binds it to the named virtual switch via Add-VMNetworkAdapter. The display name is the slot key used by the resource-layer Update reconciliation.

Returns ErrNotFound if the VM is missing. Switch-not-found surfaces as ErrPSExecution (Hyper-V's InvalidArgument category isn't routed to a typed sentinel for this cmdlet).

func (*Client) DetachDvdDrive

func (c *Client) DetachDvdDrive(ctx context.Context, in DetachDvdDriveInput) error

DetachDvdDrive removes a DVD drive from a VM at a specific controller slot via Remove-VMDvdDrive. Same slot-keyed semantics as DetachHardDisk -- whatever ISO (if any) was loaded gets implicitly ejected.

func (*Client) DetachHardDisk

func (c *Client) DetachHardDisk(ctx context.Context, in DetachHardDiskInput) error

DetachHardDisk removes a VHD attachment from a VM at a specific controller slot via Remove-VMHardDiskDrive. The slot tuple alone identifies the attachment (Path is not part of the wire payload).

"Slot already empty" surfaces as ObjectNotFound from the cmdlet -> ErrNotFound on the Go side. The resource-layer reconciliation in Update treats ErrNotFound as a no-op (desired state is "empty", already met). Other errors map to ErrPSExecution.

func (*Client) DetachNetworkAdapter

func (c *Client) DetachNetworkAdapter(ctx context.Context, in DetachNetworkAdapterInput) error

DetachNetworkAdapter removes a NIC from a VM by display name via Remove-VMNetworkAdapter. Missing VM and "no NIC by that name" both surface as ObjectNotFound -> ErrNotFound; the resource-layer Update reconciliation treats ErrNotFound as a no-op (desired state is "no NIC by that name", already met).

func (*Client) GetImageFile

func (c *Client) GetImageFile(ctx context.Context, path string) (*ImageFile, error)

GetImageFile reads metadata + SHA-256 for a file on the host. Returns ErrNotFound when the file is absent (resource Read should call RemoveResource), or ErrUnauthorized for permission errors. SHA-256 is recomputed on every call as intentional drift detection.

func (*Client) GetNatStaticMapping added in v0.3.0

func (c *Client) GetNatStaticMapping(ctx context.Context, in GetNatStaticMappingInput) (*NatStaticMapping, error)

GetNatStaticMapping fetches a NAT static netnat-static-mapping mapping by its (nat_name, protocol, external_ip, external_port) lookup tuple and joins the optional companion firewall rule into the read shape.

Returns ErrNotFound when no mapping matches the tuple (resource Read should call RemoveResource), or ErrUnavailable when the underlying service is transiently unreachable.

func (*Client) GetVHD

func (c *Client) GetVHD(ctx context.Context, path string) (*VHD, error)

GetVHD reads a VHD's metadata + parent/format/attached flags. Returns ErrNotFound when the file is absent (resource Read should call RemoveResource), or ErrUnauthorized for permission errors.

func (*Client) GetVM

func (c *Client) GetVM(ctx context.Context, name string) (*VM, error)

GetVM fetches a VM by name. Returns ErrNotFound when the VM doesn't exist (resource Read should call RemoveResource), or ErrUnauthorized for permission errors. SecureBootEnabled in the returned VM is *bool because gen 1 VMs report null (no Secure Boot concept).

func (*Client) GetVMHost

func (c *Client) GetVMHost(ctx context.Context) (*VMHost, error)

GetVMHost returns the Hyper-V host info. The cmdlet stays inline; when M1c lands the vswitch scripts they'll move to internal/scripts/<resource>/<verb>.ps1 with Pester coverage.

func (*Client) GetVMSwitch

func (c *Client) GetVMSwitch(ctx context.Context, name, natName string) (*VMSwitch, error)

GetVMSwitch fetches a virtual switch by name. Returns ErrNotFound when the switch doesn't exist (resource Read should call RemoveResource), or ErrUnavailable when vmms is stopped / cluster node fenced (transient).

natName is optional. When non-empty, the script joins Get-NetNat + Get-NetIPAddress with the underlying VMSwitch read and synthesizes SwitchType="NAT" -- callers managing NAT-typed resources pass the nat_name from state so Read round-trips correctly. Empty natName returns the bare six-field shape with empty NAT fields and SwitchType reflecting Hyper-V's underlying enum (External/Internal/Private).

func (*Client) ListVHDsByPrefix added in v0.3.0

func (c *Client) ListVHDsByPrefix(ctx context.Context, parentDir, prefix string) ([]VHDPath, error)

ListVHDsByPrefix returns paths of all VHD/VHDX (and avhd/avhdx) files under parentDir whose filename starts with the given prefix. Unlike ListVMsByPrefix which enumerates host-globally via Get-VM, VHDs are path-addressable -- there's no "list all VHDs on the host" Hyper-V cmdlet -- so the caller must supply the directory to scan. The acctest sweeper threads HYPERV_TEST_VHD_DIR as parentDir.

A missing parentDir is a normal empty return ([]VHDPath{}, nil), not an error -- a fresh bench legitimately has no fixture directory yet. Other errors (permission denied, etc.) propagate.

Backed by vhd/list.ps1. Read-only.

func (*Client) ListVMSwitchesByPrefix added in v0.3.0

func (c *Client) ListVMSwitchesByPrefix(ctx context.Context, prefix string) ([]VMSwitchName, error)

ListVMSwitchesByPrefix returns the names of all virtual switches whose Name begins with the given prefix (typically "tfacc-" for the acceptance-test sweeper). Empty result is a normal return ([]VMSwitchName{}, nil); the caller can distinguish "no matches" from "fault" without checking err.

Backed by vswitch/list.ps1. Read-only operation; no side effects. Doesn't take the netNatMu (vs RLock on GetVMSwitch's NAT branch) because Get-VMSwitch enumeration without name-narrowing doesn't touch the NetNat backing file at all -- the NAT join lives in the caller-supplied-natName path of GetVMSwitch, not here.

func (*Client) ListVMsByPrefix added in v0.3.0

func (c *Client) ListVMsByPrefix(ctx context.Context, prefix string) ([]VMName, error)

ListVMsByPrefix returns the names of all VMs whose Name begins with the given prefix (typically "tfacc-" for the acceptance-test sweeper). Empty result is a normal return ([]VMName{}, nil) -- the caller can distinguish "no matches" from "fault" without checking err.

Backed by vm/list.ps1 -- see that script's header for the wire contract. Read-only operation; no power transitions or other side effects.

func (*Client) NewImageFileFromBytes added in v0.3.0

func (c *Client) NewImageFileFromBytes(ctx context.Context, in NewImageFileFromBytesInput) (*ImageFile, error)

NewImageFileFromBytes lands a literal byte payload at DestinationPath via the same wire path as local_path mode. Writes Bytes to a runner- side tmpfile, hashes it, picks a sibling .part staging path on the host, streams via Connection.StreamFile, and dispatches new.ps1 in source_mode=local_path for the verify-and-rename. The host-side contract is identical to local_path mode -- the script can't tell whether the staged bytes came from a runner-side file or an in-memory payload, and doesn't need to.

Returns ErrChecksumMismatch when the streamed bytes don't hash to the runner-computed value (transport corruption between runner and host). Memory cost: the payload is held twice briefly (in `in.Bytes` and in the runner tmpfile) which is fine for the sub-MiB seed-ISO workloads this method targets; for multi-GiB files prefer NewImageFileFromLocalPath or NewImageFileFromURL instead.

func (*Client) NewImageFileFromHostPath

func (c *Client) NewImageFileFromHostPath(ctx context.Context, destinationPath string) (*ImageFile, error)

NewImageFileFromHostPath verifies a file the user attests already exists at destinationPath and returns its metadata. No copy, no fetch. Returns ErrNotFound if the file is absent. For host_path-mode resources, Delete is a no-op on the Go side -- the user did not ask the provider to put the file there, so removing it on destroy would surprise them.

func (*Client) NewImageFileFromLocalPath added in v0.2.0

func (c *Client) NewImageFileFromLocalPath(ctx context.Context, in NewImageFileFromLocalPathInput) (*ImageFile, error)

NewImageFileFromLocalPath streams the runner-local file at LocalPath to the host, then asks new.ps1 to verify the staged bytes against the runner-computed SHA-256 and atomic-rename to DestinationPath. Three transport-distinct stages, all driven from this one call:

  1. Compute the SHA-256 of the local file (one os.Open + io.Copy into sha256.New). The bytes leave the runner once for hashing and once more for streaming -- the kernel's page cache makes the second read effectively free for files that fit in RAM.
  2. Pick a deterministically-shaped staging path -- DestinationPath plus a `.part-<8-hex>` suffix, sibling to the destination so the PS-side Move-Item lands on the same NTFS volume and stays atomic.
  3. Stream local -> staging via Connection.StreamFile, then invoke new.ps1 with source_mode=local_path so the host-side script verifies the SHA matches expectation and renames into place.

Returns ErrChecksumMismatch when the bytes that landed don't hash to the expected value -- a transport-level corruption signal the caller surfaces back to the user. Returns ErrNotFound only if the staging file was absent at the moment new.ps1 ran (StreamFile claimed success but the file was deleted between then and the script's Test-Path); in normal flow this can't happen.

func (*Client) NewImageFileFromURL

func (c *Client) NewImageFileFromURL(ctx context.Context, in NewImageFileFromURLInput) (*ImageFile, error)

NewImageFileFromURL fetches a file by URL. With Compression="" the host-side new.ps1 url-mode path runs: HttpClient streams to a sibling .part file in the destination directory, the host verifies the SHA-256 against in.ExpectedSha256, and Move-Item atomic-renames into place. With Compression set (currently only "gz"/"gzip"), the call delegates to the runner-pipelined flow in newImageFileFromCompressedURL -- fetching and decompressing happen on the runner because PS 5.1 has no built-in xz/zst/bz2 decompressors and shipping host-side third-party modules defeats the §5 PS 5.1 floor that exists so Hyper-V hosts need no extra installs.

Returns ErrChecksumMismatch when the downloaded bytes don't hash to the expected value (the .part is cleaned up; no half-baked file lingers at the canonical destination). Returns ErrDecompressionFailed from the runner-pipelined path when the gzip stream is corrupt.

func (*Client) NewNatStaticMapping added in v0.3.0

func (c *Client) NewNatStaticMapping(ctx context.Context, in NewNatStaticMappingInput) (*NatStaticMapping, error)

NewNatStaticMapping creates a static NAT mapping plus the optional inbound firewall allow rule. The script-side rollback handles the partial-failure case (mapping landed, firewall rule failed); see nat_static_mapping/new.ps1 for the catch path.

Cross-resource: nat_name must resolve to an existing NetNat instance on the host. The script's Get-NetNat precondition surfaces a clear "missing NetNat" error if it doesn't, mapped here through the typed-error path.

func (*Client) NewVHDDifferencing

func (c *Client) NewVHDDifferencing(ctx context.Context, in NewVHDDifferencingInput) (*VHD, error)

NewVHDDifferencing creates a child that reads from in.ParentPath and writes new blocks locally. Returns ErrInvalidParentPath when the parent path is missing or invalid; the mapping comes from New-VHD's "InvalidParameter,Microsoft.Vhd.*" error envelope, classified in errors.go.

Recovers from connection.ErrSessionDropped -- see NewVHDFixed. The recovery's expectedVHD passes SizeBytes=0 ("skip size check") because differencing disks inherit size from the parent and the typed-client method does not read the parent ahead of time. Path + VhdType match is the load-bearing guard for this variant.

func (*Client) NewVHDDynamic

func (c *Client) NewVHDDynamic(ctx context.Context, in NewVHDDynamicInput) (*VHD, error)

NewVHDDynamic creates a sparse VHD/VHDX. Initial on-disk size is minimal; the file grows as the guest writes blocks, up to SizeBytes. Recovers from connection.ErrSessionDropped via verify-on-drop -- see NewVHDFixed.

func (*Client) NewVHDFixed

func (c *Client) NewVHDFixed(ctx context.Context, in NewVHDFixedInput) (*VHD, error)

NewVHDFixed creates a pre-allocated (full-sized on disk) VHD/VHDX. Slow create, no runtime expansion. Returns the post-create read shape.

Recovers from connection.ErrSessionDropped -- same root cause as NewVMSwitch's recovery: an External-switch NIC rebind on the same SSH path can blink the session before the cmdlet's exit status reaches the runner. New-VHD itself is not the cmdlet rebinding the NIC, but its session is collateral damage when a parallel resource (e.g. hyperv_virtual_switch.main running concurrently) does the rebinding. Verify-on-drop polls GetVHD: if a matching VHD lands at the requested path with the requested VhdType + SizeBytes, the cmdlet succeeded and we adopt it into state; if mismatched, surface the drop with the mismatch detail so the operator can sweep before retry.

func (*Client) NewVM

func (c *Client) NewVM(ctx context.Context, in NewVMInput) (*VM, error)

NewVM creates a VM and returns the canonical read shape. The script-side sequence is New-VM (with -NoVHD -BootDevice None -- no auto-attach of storage or boot device) followed by Set-VMMemory (static, with DynamicMemoryEnabled=$false in the same call), Set-VMProcessor, and the optional Set-VMFirmware (gen 2 + SecureBoot) and Set-VM (Notes) tail.

func (*Client) NewVMSwitch

func (c *Client) NewVMSwitch(ctx context.Context, in NewVMSwitchInput) (*VMSwitch, error)

NewVMSwitch creates a virtual switch and returns the canonical read shape. The script-side guard rejects Private + AllowManagementOS with a clear error before invoking the cmdlet (see new.ps1).

Recovers from connection.ErrSessionDropped via a verify-on-drop loop -- symmetric to RemoveVMSwitch's recovery, same physical root cause: New-VMSwitch -NetAdapterName <NIC> on the very NIC the SSH session traverses makes Hyper-V re-bind that NIC for a few seconds, blinking the session before the cmdlet's exit status reaches the runner. The cmdlet itself almost always succeeded -- the response just got stranded. The recovery polls GetVMSwitch and on the first hit returns that switch's read shape (Hyper-V refuses to create over an existing same-name switch, so a post-drop Get-found switch is the one this call just created). The collateral-damage twin -- another resource running concurrently whose session shared the blinking link -- is out of scope here; that resource's apply needs a separate retry, which terraform's normal failure-and-rerun cycle covers.

If the verify ultimately can't confirm the switch exists (Get keeps failing with transport errors, returns NotFound, or ctx cancels), the original ErrSessionDropped surfaces -- the operator can re-run terraform apply once the host is healthy and the resource's Read path will reconcile.

func (*Client) RemoveImageFile

func (c *Client) RemoveImageFile(ctx context.Context, path string, force bool) error

`force` opts into the detach-then-retry escape hatch in remove.ps1: when the initial Remove-Item hits a sharing violation whose holders are Hyper-V DVDs, the host script detaches each slot via Set-VMDvdDrive -Path $null and retries the delete once. Used by the resource Delete when the `force_destroy` attribute is set on the hyperv_image_file resource. Default (false) keeps the safer behavior: surface the locked-file diagnostic and let the operator resolve the holder explicitly.

func (*Client) RemoveNatStaticMapping added in v0.3.0

func (c *Client) RemoveNatStaticMapping(ctx context.Context, in RemoveNatStaticMappingInput) error

RemoveNatStaticMapping deletes the static mapping and the companion firewall rule. Resource Delete should treat ErrNotFound as success (the mapping is already gone). Best-effort destroy: a missing firewall rule doesn't fail Delete.

func (*Client) RemoveVHD

func (c *Client) RemoveVHD(ctx context.Context, path string) error

RemoveVHD deletes the VHD file. Resource Delete should treat ErrNotFound as success (already gone). The cmdlet errors loudly when the file is attached to a running VM (open file handle); that surfaces as ErrPSExecution rather than being swallowed.

func (*Client) RemoveVM

func (c *Client) RemoveVM(ctx context.Context, name string) error

RemoveVM deletes a VM. Resource Delete should treat ErrNotFound as success (the VM is already gone). The script stops the VM first if it's running (Remove-VM errors on a running VM); this is the one place the PS layer drives a power transition, justified because destroy is destructive by definition.

func (*Client) RemoveVMSwitch

func (c *Client) RemoveVMSwitch(ctx context.Context, name, natName string) error

RemoveVMSwitch deletes a virtual switch by name. Resource Delete should treat ErrNotFound as success (the switch is already gone).

External switches with AllowManagementOS=true get a two-step destroy: first Set-VMSwitch -AllowManagementOS $false to migrate the host's IP off the vNIC and back to the physical NIC, then Remove-VMSwitch -Force once the host is on a stable connection. The naive single-step Remove-VMSwitch on the same kind of switch causes a NIC rebind concurrent with the cmdlet's own teardown, and the resulting SSH- session blink can land mid-destroy -- leaving the switch in a transitional state Hyper-V's Get-VMSwitch still reports as "exists" and a subsequent terraform-destroy retry re-triggers from scratch. Splitting the operation lets the destabilizing event (IP migration) happen in a small property-toggle that the bench-side cmdlet completes quickly, then runs the destructive Remove against an already-stabilized host.

Internal / Private switches and External switches with AllowManagementOS=false skip the pre-step -- there's no IP migration concern. The dispatch reads the bench's actual state (not Terraform's last-known state) so a drifted switch type still gets the right path.

natName is forwarded so remove.ps1 can run the multi-phase NAT teardown (Remove-NetNat -> Remove-NetIPAddress -> Remove-VMSwitch). Empty natName runs the bare Remove-VMSwitch path. Each NAT step tolerates ObjectNotFound -- best-effort destroy.

Recovers from connection.ErrSessionDropped on the actual Remove via the existing recoverVMSwitchRemoveOnDrop verify-loop. After the pre-step the SSH path is on the physical NIC and Remove typically completes cleanly without triggering recovery; the recovery is belt-and-suspenders for transient drops unrelated to the migration.

func (*Client) ResizeVHD

func (c *Client) ResizeVHD(ctx context.Context, path string, sizeBytes int64) (*VHD, error)

ResizeVHD changes the declared size of an existing VHD. The cmdlet errors on shrink-without-compaction (run Optimize-VHD first) and on fixed-format resize while the disk is attached to a running VM; both surface as ErrPSExecution to the resource layer.

func (*Client) SetBootOrder

func (c *Client) SetBootOrder(ctx context.Context, in SetBootOrderInput) error

SetBootOrder replaces the boot device sequence on a gen 2 VM via Set-VMFirmware -BootOrder. Wholesale replacement: each call sets the full order; the script resolves each entry's slot/name to the underlying device handle the cmdlet expects. Gen 1 isn't supported in this slice -- the resource-layer schema validator should reject boot_order on gen 1 at plan time.

func (*Client) SetNatStaticMapping added in v0.3.0

func (c *Client) SetNatStaticMapping(ctx context.Context, in SetNatStaticMappingInput) (*NatStaticMapping, error)

SetNatStaticMapping applies a partial update. internal_ip/internal_port changes are Remove + Add under the hood (NatStaticMapping has no in-place edit); firewall.* changes go through Set-NetFirewallRule. Returns the post-mutation read shape -- the StaticMappingID may change because Hyper-V re-numbers mappings on Add.

func (*Client) SetVM

func (c *Client) SetVM(ctx context.Context, in SetVMInput) (*VM, error)

SetVM applies a partial update and returns the post-mutation read shape (set.ps1 follows the Set-* sequence with a Get-VM read-back so the emitted shape matches GetVM exactly).

Callers should populate in.Generation from prior state so set.ps1's gen-2-only SecureBoot guard fires at the script layer; the Go-side Update should never let SecureBoot through for a gen 1 VM (the ConfigValidator catches it at plan time), but the script-layer guard is defense in depth.

Mutations on a running VM may error: vcpu, memory_bytes, and secure_boot generally require the VM to be Off. The script surfaces those errors verbatim -- the operator drives power transitions via hyperv_vm_state.

func (*Client) SetVMState

func (c *Client) SetVMState(ctx context.Context, in SetVMStateInput) (*VM, error)

SetVMState transitions the VM's power state via Start-VM (Desired= 'Running') or Stop-VM -TurnOff -Force (Desired='Off'). Returns the post-transition VM read so callers can refresh state without a separate GetVM round-trip. Idempotent at the cmdlet level: setting Desired='Running' on an already-Running VM is a no-op.

func (*Client) SetVMSwitch

func (c *Client) SetVMSwitch(ctx context.Context, in SetVMSwitchInput) (*VMSwitch, error)

SetVMSwitch applies a partial update and returns the post-mutation read shape (set.ps1 follows Set-VMSwitch with a Get-VMSwitch read-back so the emitted shape matches GetVMSwitch exactly).

Callers should populate in.SwitchType from prior state so set.ps1's Private + AllowManagementOS guard can fire at the script layer; without it, the cmdlet's opaque "parameter is not applicable" error surfaces instead.

func (*Client) SweepImageFiles added in v0.3.0

func (c *Client) SweepImageFiles(ctx context.Context, parentDir, prefix string) ([]string, error)

SweepImageFiles removes orphan files under parentDir whose name starts with prefix and whose extension is NOT in the VHD family (.vhd / .vhdx / .avhd / .avhdx -- those are the hyperv_vhd sweeper's territory). Empty result is a normal return; callers don't need to special-case it. Backed by image_file/sweep.ps1.

func (*Client) SweepNetNats added in v0.3.0

func (c *Client) SweepNetNats(ctx context.Context, prefix string) ([]string, error)

SweepNetNats removes every NetNat instance on the host whose Name starts with the given prefix (typically "tfacc-" for the acceptance-test sweeper) and returns the names that were removed. Multiple NetNats can coexist on a host, so the script returns a list and this method's return type is []string.

Empty result is a normal return ([]string{}, nil); the caller can distinguish "no orphans" from "fault" without checking err.

Takes the package netNatMu write lock for the same reason RemoveVMSwitch's NAT branch does: Remove-NetNat mutates the host's NetNat persistent-store backing file under an exclusive-write handle and races every nat_static_mapping or vswitch NAT writer otherwise. Sweep only runs from the acceptance-test sweeper, which executes against an idle bench, so the lock is belt-and-suspenders rather than load- bearing -- but the cost is one uncontended Lock+Unlock and the consistency story is worth more than that.

Backed by netnat/sweep.ps1.

type ClientOption added in v0.3.0

type ClientOption func(*Client)

ClientOption customizes a Client at construction time. Functional- options shape rather than a constructor variant so adding future knobs (per-call timeouts, retry policy, ...) doesn't ripple out into every NewClient call site.

func WithHTTPClient added in v0.3.0

func WithHTTPClient(hc *http.Client) ClientOption

WithHTTPClient overrides the default *http.Client the runner-pipelined image_file fetch uses. Tests pass a transport pointed at httptest.Server; integrators with non-default proxy / TLS requirements pass their tuned client. nil restores the default (no panic surface for callers passing a maybe-nil value).

type DetachDvdDriveInput

type DetachDvdDriveInput struct {
	Name               string `json:"name"`
	ControllerType     string `json:"controller_type"`
	ControllerNumber   int    `json:"controller_number"`
	ControllerLocation int    `json:"controller_location"`
}

DetachDvdDriveInput mirrors DetachHardDiskInput -- slot tuple identifies the DVD to remove, no Path needed.

type DetachHardDiskInput

type DetachHardDiskInput struct {
	Name               string `json:"name"`
	ControllerType     string `json:"controller_type"`
	ControllerNumber   int    `json:"controller_number"`
	ControllerLocation int    `json:"controller_location"`
}

DetachHardDiskInput is the stdin JSON shape for vm/remove-hard-disk-drive.ps1. Path is intentionally omitted -- the slot tuple identifies the attachment, not the underlying VHD.

type DetachNetworkAdapterInput

type DetachNetworkAdapterInput struct {
	Name   string `json:"name"`
	VMName string `json:"vm_name"`
}

DetachNetworkAdapterInput is the stdin JSON shape for vm/remove-network-adapter.ps1. Name + VMName identify the NIC to detach; the cmdlet would happily remove ALL NICs sharing the same Name, but the schema-level uniqueness validator means there's only ever one match in our state.

type DvdDrive

type DvdDrive struct {
	Path               string `json:"Path"`
	ControllerType     string `json:"ControllerType"`
	ControllerNumber   int    `json:"ControllerNumber"`
	ControllerLocation int    `json:"ControllerLocation"`
}

DvdDrive is the per-attachment shape vm/get.ps1 emits inside VM.DvdDrives. Same slot-tuple identity as HardDiskDrive (ControllerType + ControllerNumber + ControllerLocation), but Path may be empty -- a DVD drive without an ISO loaded is a legitimate state (the drive exists, the medium tray is empty).

type GetNatStaticMappingInput added in v0.3.0

type GetNatStaticMappingInput struct {
	NatName           string `json:"nat_name"`
	Protocol          string `json:"protocol"`
	ExternalIPAddress string `json:"external_ip"`
	ExternalPort      int    `json:"external_port"`
	FirewallName      string `json:"firewall_name"`
}

GetNatStaticMappingInput is the stdin JSON shape for nat_static_mapping/get.ps1 and nat_static_mapping/remove.ps1. The lookup tuple uniquely identifies a mapping; firewall_name is needed alongside because the firewall rule is keyed by DisplayName, not derived from the mapping itself.

type HardDiskDrive

type HardDiskDrive struct {
	Path               string `json:"Path"`
	ControllerType     string `json:"ControllerType"`
	ControllerNumber   int    `json:"ControllerNumber"`
	ControllerLocation int    `json:"ControllerLocation"`
}

HardDiskDrive is the per-attachment shape vm/get.ps1 emits inside VM.HardDiskDrives. The (ControllerType, ControllerNumber, ControllerLocation) tuple identifies the slot uniquely on a given VM; Path identifies the underlying VHD/VHDX. The same VHD attached at two different slots produces two HardDiskDrive entries.

type ImageFile

type ImageFile struct {
	Path      string `json:"Path"`
	SizeBytes int64  `json:"SizeBytes"`
	Sha256    string `json:"Sha256"`
}

ImageFile is the canonical read shape emitted by image_file/{get,new}.ps1. Sha256 is lowercase hex (the wire contract); SizeBytes is int64 because VHDX/ISO files routinely exceed 2^31 bytes.

type NatStaticMapping added in v0.3.0

type NatStaticMapping struct {
	ID                  string `json:"Id"`
	StaticMappingID     int    `json:"StaticMappingId"`
	NatName             string `json:"NatName"`
	Protocol            string `json:"Protocol"`
	ExternalIPAddress   string `json:"ExternalIPAddress"`
	ExternalPort        int    `json:"ExternalPort"`
	InternalIPAddress   string `json:"InternalIPAddress"`
	InternalPort        int    `json:"InternalPort"`
	FirewallRulePresent bool   `json:"FirewallRulePresent"`
	FirewallRuleName    string `json:"FirewallRuleName"`
	FirewallRuleProfile string `json:"FirewallRuleProfile"`
}

NatStaticMapping is the canonical eleven-field read shape emitted by nat_static_mapping/{get,new,set}.ps1. Composite Id encodes the lookup tuple (NatName:Protocol:ExternalIPAddress:ExternalPort) lowercase for stable cross-tool interop; Protocol on the rest of the struct is uppercase (TCP / UDP) because that's what Get-NetNatStaticMapping reports natively. StaticMappingID is the Hyper-V-assigned opaque identifier; it changes whenever the mapping is recreated (Set on internal_ip/internal_port is Remove + Add under the hood).

type NatStaticMappingFirewallInput added in v0.3.0

type NatStaticMappingFirewallInput struct {
	Enabled bool   `json:"enabled"`
	Name    string `json:"name"`
	Profile string `json:"profile"`
}

NatStaticMappingFirewallInput is the nested firewall block's wire shape for new.ps1 / set.ps1. Defaulting (enabled=true, derived name, profile=Any) lives on the resource layer; this struct carries the already-resolved values to the script.

type NetworkAdapter

type NetworkAdapter struct {
	Name        string   `json:"Name"`
	SwitchName  string   `json:"SwitchName"`
	IPAddresses []string `json:"IPAddresses"`

	// MacAddress is the active MAC. Only populated by the script when
	// DynamicMacAddressEnabled is false (i.e. user-set static MAC); for
	// dynamic MACs the script emits an empty string so the resource
	// layer's flatten can store state value as null without conflating
	// "user picked no MAC" with "user picked Hyper-V's auto-assigned
	// pool value of the moment".
	MacAddress string `json:"MacAddress"`

	// VlanID is the access-mode VLAN ID. 0 means untagged. The resource
	// layer flattens 0 to a null state value so unset config matches
	// unset state on round-trip.
	VlanID int `json:"VlanID"`
}

NetworkAdapter is the per-NIC shape vm/get.ps1 emits inside VM.NetworkAdapters. Display Name is the slot key the resource-layer reconciliation uses to diff plan vs state. SwitchName identifies which hyperv_virtual_switch the NIC is bound to (or empty when unbound -- Hyper-V allows that, though it's rare).

IPAddresses is populated by Hyper-V's integration services running in the guest -- empty when the VM is Off, when integration services haven't loaded yet, or when the guest doesn't ship them. The resource layer flattens IPAddresses across all NICs into a top- level ip_addresses Computed attribute.

type NewImageFileFromBytesInput added in v0.3.0

type NewImageFileFromBytesInput struct {
	DestinationPath     string `json:"destination_path"`
	Bytes               []byte `json:"-"`
	ReplaceWhileMounted bool   `json:"-"`
}

NewImageFileFromBytesInput is the public input shape for the literal_bytes source mode. The runner writes Bytes to a tmpfile, hashes it, streams to a sibling .part of DestinationPath, and asks new.ps1 to verify-and-rename via the same wire path local_path mode uses. The wire shape on the host stays identical -- new.ps1 cannot tell whether the staged bytes came from the runner's filesystem (local_path) or from an in-memory payload (literal_bytes).

ReplaceWhileMounted has the same semantics as on the local_path input. Callers that synthesize iso_volume bytes for a DVD-mountable destination set it true; literal_bytes for a fresh path leaves it false (default).

type NewImageFileFromLocalPathInput added in v0.2.0

type NewImageFileFromLocalPathInput struct {
	DestinationPath string `json:"destination_path"`
	LocalPath       string `json:"-"`
	// ReplaceWhileMounted opts the host-side Move-Item into a swap-via-
	// pivot dance that handles the case where DestinationPath is currently
	// mounted as a DVD on a running VM. Off by default; set true for
	// callers that may stream over a destination some VM holds open
	// (cidata seeds, autounattend ISOs).
	ReplaceWhileMounted bool `json:"-"`
}

NewImageFileFromLocalPathInput is the public input shape for the local_path source mode of image_file/new.ps1. The runner-local source (LocalPath) is JSON-skipped because it never crosses the wire -- the typed-client method opens it on the runner side, computes the SHA-256, streams the bytes to a sibling .part of DestinationPath via Connection.StreamFile, and asks new.ps1 to verify-and-rename. The discriminator (source_mode) and the computed staging_path / expected_sha256 fields are added internally by the method, so callers can't pass the wrong values for the mode they invoke.

type NewImageFileFromURLInput

type NewImageFileFromURLInput struct {
	DestinationPath string `json:"destination_path"`
	URL             string `json:"url"`
	ExpectedSha256  string `json:"expected_sha256"`
	Compression     string `json:"-"`
}

NewImageFileFromURLInput is the public input shape for the URL source mode of image_file/new.ps1. The discriminator field (source_mode) is not on the public struct -- the typed-client method sets it internally so callers can't pass the wrong value for the method they invoke.

Compression is a canonical decompressor identifier (currently "gz" only; "" means no compression). When set, the typed client switches from the host-direct fetch flow to a runner-pipelined flow: download via the runner's net/http stack, decompress in-process, stream the decompressed bytes to a sibling .part of DestinationPath via Connection.StreamFile, and dispatch new.ps1 in local_path mode. ExpectedSha256 is always the hash of the *compressed* bytes the publisher signs (this is what users copy from a SHA256SUMS file). The runner-computed *decompressed* SHA is what the host script receives for staging-bytes verification; the wire shape stays identical to the existing local_path mode.

type NewNatStaticMappingInput added in v0.3.0

type NewNatStaticMappingInput struct {
	NatName           string                        `json:"nat_name"`
	Protocol          string                        `json:"protocol"`
	ExternalIPAddress string                        `json:"external_ip"`
	ExternalPort      int                           `json:"external_port"`
	InternalIPAddress string                        `json:"internal_ip"`
	InternalPort      int                           `json:"internal_port"`
	Firewall          NatStaticMappingFirewallInput `json:"firewall"`
}

NewNatStaticMappingInput is the stdin JSON shape for nat_static_mapping/new.ps1. All mapping fields are required; firewall is required as a nested object (resource defaults populate it before reaching the wire).

type NewVHDDifferencingInput

type NewVHDDifferencingInput struct {
	Path       string `json:"path"`
	ParentPath string `json:"parent_path"`
}

NewVHDDifferencingInput is the public input shape for the Differencing creation mode. SizeBytes and BlockSizeBytes are inherited from the parent and rejected by Hyper-V if supplied; the typed-client method omits them from the wire payload.

type NewVHDDynamicInput

type NewVHDDynamicInput struct {
	Path           string `json:"path"`
	SizeBytes      int64  `json:"size_bytes"`
	BlockSizeBytes *int64 `json:"block_size_bytes,omitempty"`
}

NewVHDDynamicInput is the public input shape for the Dynamic creation mode. Same field set as fixed -- the discriminator is what differs.

type NewVHDFixedInput

type NewVHDFixedInput struct {
	Path           string `json:"path"`
	SizeBytes      int64  `json:"size_bytes"`
	BlockSizeBytes *int64 `json:"block_size_bytes,omitempty"`
}

NewVHDFixedInput is the public input shape for the Fixed creation mode. BlockSizeBytes is *int64 + omitempty so absent leaves the cmdlet default. The discriminator (vhd_type) is set internally by the typed client method, not on the public struct.

type NewVMInput

type NewVMInput struct {
	Name               string  `json:"name"`
	Generation         int     `json:"generation"`
	Vcpu               int     `json:"vcpu"`
	MemoryBytes        int64   `json:"memory_bytes"`
	DynamicMemory      *bool   `json:"dynamic_memory,omitempty"`
	MinMemoryBytes     *int64  `json:"min_memory_bytes,omitempty"`
	MaxMemoryBytes     *int64  `json:"max_memory_bytes,omitempty"`
	SecureBoot         *bool   `json:"secure_boot,omitempty"`
	SecureBootTemplate *string `json:"secure_boot_template,omitempty"`
	Notes              *string `json:"notes,omitempty"`
}

NewVMInput is the stdin JSON shape for vm/new.ps1.

Required fields: Name, Generation, Vcpu, MemoryBytes (startup). Optionals use pointer types so missing-vs-explicit-false round-trips correctly through the wire contract: the entry block in new.ps1 treats absent keys and explicit null as equivalent (both skip the corresponding Set-*), so omitempty + nil pointer yields the "use cmdlet default" behavior.

Dynamic memory: DynamicMemory opts in to Hyper-V's dynamic memory mode. MinMemoryBytes / MaxMemoryBytes are the minimum and maximum bounds and are only meaningful when DynamicMemory is true (the script gates forwarding accordingly). When DynamicMemory is nil, the script defaults to static memory (DynamicMemoryEnabled=$false), preserving the v2-and- prior behavior for callers that don't manage dynamic memory.

type NewVMSwitchInput

type NewVMSwitchInput struct {
	Name                     string   `json:"name"`
	SwitchType               string   `json:"switch_type"`
	NetAdapterNames          []string `json:"net_adapter_names,omitempty"`
	AllowManagementOS        *bool    `json:"allow_management_os,omitempty"`
	Notes                    *string  `json:"notes,omitempty"`
	NatName                  string   `json:"nat_name,omitempty"`
	NatInternalAddressPrefix string   `json:"nat_internal_address_prefix,omitempty"`
	NatHostAddress           string   `json:"nat_host_address,omitempty"`
}

NewVMSwitchInput is the stdin JSON shape for vswitch/new.ps1.

Required fields: Name, SwitchType. Optional fields use pointer types so missing-vs-explicit-false round-trips correctly through the wire contract: the entry block in new.ps1 treats absent keys and explicit null as equivalent (both skip the splat), so omitempty + nil pointer yields the "use cmdlet default" behavior.

NAT fields are required when SwitchType == "NAT" and rejected otherwise (the resource-layer validator enforces this; the script trusts the validation already happened).

type RemoveNatStaticMappingInput added in v0.3.0

type RemoveNatStaticMappingInput struct {
	NatName           string `json:"nat_name"`
	Protocol          string `json:"protocol"`
	ExternalIPAddress string `json:"external_ip"`
	ExternalPort      int    `json:"external_port"`
	FirewallName      string `json:"firewall_name"`
}

RemoveNatStaticMappingInput mirrors GetNatStaticMappingInput -- destroy needs the same lookup tuple plus the firewall display name.

type SetBootOrderEntryInput

type SetBootOrderEntryInput struct {
	Type               string `json:"type"`
	ControllerType     string `json:"controller_type"`
	ControllerNumber   int    `json:"controller_number"`
	ControllerLocation int    `json:"controller_location"`
	Name               string `json:"name"`
}

SetBootOrderEntryInput is the per-entry shape inside SetBootOrderInput.BootOrder. Same discriminator pattern as BootOrderEntry: Type drives which subset of fields the script reads.

All fields are emitted unconditionally (no omitempty). Reason: PowerShell's Set-StrictMode 3.0 throws on access of an absent property on a PSCustomObject. The script reads $entry.controller_* for HDD/DVD entries and $entry.name for NIC entries; whichever fields are unused for a given Type still need to be present on the wire (zero values are fine -- the script's switch ignores them). Specifically, omitempty on `int` would drop controller_number=0, which is the most common slot index and would break the resolver.

type SetBootOrderInput

type SetBootOrderInput struct {
	Name      string                   `json:"name"`
	BootOrder []SetBootOrderEntryInput `json:"boot_order"`
}

SetBootOrderInput is the stdin JSON shape for vm/set-boot-order.ps1. BootOrder is the new desired sequence; the script replaces the VM's current order wholesale (Set-VMFirmware -BootOrder is not additive). Per-entry shape mirrors BootOrderEntry above with snake_case keys.

type SetNatStaticMappingInput added in v0.3.0

type SetNatStaticMappingInput struct {
	NatName           string                        `json:"nat_name"`
	Protocol          string                        `json:"protocol"`
	ExternalIPAddress string                        `json:"external_ip"`
	ExternalPort      int                           `json:"external_port"`
	InternalIPAddress string                        `json:"internal_ip"`
	InternalPort      int                           `json:"internal_port"`
	Firewall          NatStaticMappingFirewallInput `json:"firewall"`
}

SetNatStaticMappingInput is the stdin JSON shape for nat_static_mapping/set.ps1. Same shape as NewNatStaticMappingInput -- set.ps1 looks up the existing mapping by tuple (nat_name + protocol + external_ip + external_port) then mutates internal_ip/internal_port via Remove + Add and the firewall via Set-NetFirewallRule. The lookup tuple is RequiresReplace at the schema layer; Update only fires when internal_* or firewall.* changes.

type SetVMInput

type SetVMInput struct {
	Name           string  `json:"name"`
	Generation     int     `json:"generation"`
	Vcpu           *int    `json:"vcpu,omitempty"`
	MemoryBytes    *int64  `json:"memory_bytes,omitempty"`
	DynamicMemory  *bool   `json:"dynamic_memory,omitempty"`
	MinMemoryBytes *int64  `json:"min_memory_bytes,omitempty"`
	MaxMemoryBytes *int64  `json:"max_memory_bytes,omitempty"`
	SecureBoot     *bool   `json:"secure_boot,omitempty"`
	Notes          *string `json:"notes,omitempty"`
}

SetVMInput is the stdin JSON shape for vm/set.ps1.

Same pattern as NewVMInput, with two differences:

  • Vcpu and MemoryBytes are *int / *int64 because Set is a partial update -- only changed fields are forwarded; nil drops them from the JSON (omitempty) so the script's "key present?" check skips the corresponding Set-* cmdlet.
  • Generation is OPTIONAL on the schema but ALWAYS forwarded by the Update path; it's a validation hint for set.ps1's gen-2-only SecureBoot guard, not a mutation.

type SetVMStateInput

type SetVMStateInput struct {
	Name         string `json:"name"`
	Desired      string `json:"desired"`
	ShutdownMode string `json:"shutdown_mode,omitempty"`
}

SetVMStateInput is the stdin JSON shape for vm/set-state.ps1.

Desired is the primary mutation: 'Off' invokes Stop-VM, 'Running' invokes Start-VM. Other Hyper-V states (Saved, Paused) are out of scope for this slice -- the script's ValidateSet on Desired rejects them.

ShutdownMode is optional and only governs the Stop dispatch:

  • "" or "turn_off" (default): Stop-VM -TurnOff -Force (hard power-off, matches destroy semantics, no integration-services dependency).
  • "graceful": Stop-VM -Force without -TurnOff (ACPI shutdown via integration services; hangs on guests without them).

`omitempty` keeps the wire shape stable for callers that don't care about the mode -- the script defaults to turn_off when the field is absent or empty.

type SetVMSwitchInput

type SetVMSwitchInput struct {
	Name              string   `json:"name"`
	SwitchType        string   `json:"switch_type,omitempty"`
	NetAdapterNames   []string `json:"net_adapter_names,omitempty"`
	AllowManagementOS *bool    `json:"allow_management_os,omitempty"`
	Notes             *string  `json:"notes,omitempty"`
	NatName           string   `json:"nat_name,omitempty"`
}

SetVMSwitchInput is the stdin JSON shape for vswitch/set.ps1.

Same pattern as NewVMSwitchInput, with two differences:

  • SwitchType is OPTIONAL here -- it's a validation hint, not a mutation. The Update path should populate it from prior state so set.ps1's Private + AllowManagementOS guard fires with a clear error.
  • Only keys present in the input get forwarded to Set-VMSwitch (see set.ps1's wire contract). Sending nil/null for an attribute means "leave it alone"; sending a value means "set it to this".

NatName is forwarded for NAT switches so set.ps1 can route the read-back through Get-NetNat + Get-NetIPAddress and synthesize SwitchType=NAT. Every NAT-specific input on the resource is RequiresReplace; the only in-place mutation that reaches Update for a NAT switch is Notes.

type VHD

type VHD struct {
	Path           string `json:"Path"`
	VhdType        string `json:"VhdType"`
	SizeBytes      int64  `json:"SizeBytes"`
	FileSizeBytes  int64  `json:"FileSizeBytes"`
	BlockSizeBytes int64  `json:"BlockSizeBytes"`
	ParentPath     string `json:"ParentPath"`
	Format         string `json:"Format"`
	Attached       bool   `json:"Attached"`
}

VHD is the canonical read shape emitted by vhd/{get,new,set}.ps1. SizeBytes is the declared logical size; FileSizeBytes is the actual on-disk size (smaller than SizeBytes for dynamic and differencing). ParentPath is empty unless VhdType is "Differencing".

type VHDPath added in v0.3.0

type VHDPath struct {
	Path string `json:"Path"`
}

VHDPath is the minimal shape vhd/list.ps1 emits per result. Path-only because the sweeper's RemoveVHD call only needs the path; bigger shape means slower enumeration on a directory with many files and a wider blast radius for script-Go contract drift.

type VM

type VM struct {
	Name                 string           `json:"Name"`
	ID                   string           `json:"Id"`
	Generation           int              `json:"Generation"`
	ProcessorCount       int              `json:"ProcessorCount"`
	MemoryStartupBytes   int64            `json:"MemoryStartupBytes"`
	MemoryAssignedBytes  int64            `json:"MemoryAssignedBytes"`
	MemoryDynamicEnabled bool             `json:"MemoryDynamicEnabled"`
	MemoryMinimumBytes   *int64           `json:"MemoryMinimumBytes"`
	MemoryMaximumBytes   *int64           `json:"MemoryMaximumBytes"`
	State                string           `json:"State"`
	Notes                string           `json:"Notes"`
	Path                 string           `json:"Path"`
	SecureBootEnabled    *bool            `json:"SecureBootEnabled"`
	SecureBootTemplate   string           `json:"SecureBootTemplate"`
	HardDiskDrives       []HardDiskDrive  `json:"HardDiskDrives"`
	NetworkAdapters      []NetworkAdapter `json:"NetworkAdapters"`
	DvdDrives            []DvdDrive       `json:"DvdDrives"`
	BootOrder            []BootOrderEntry `json:"BootOrder"`
}

VM is the canonical read shape emitted by vm/{get,new,set}.ps1. SecureBootEnabled is *bool because gen 1 VMs return null (BIOS-based, no Secure Boot concept); gen 2 always returns a real bool.

HardDiskDrives, NetworkAdapters, and DvdDrives are always (possibly empty) slices -- the script-side @() wrapper guarantees JSON array shape even when nothing is attached, so a freshly-created VM with no attachments round-trips as "[]" rather than null.

type VMHost

type VMHost struct {
	ComputerName          string `json:"ComputerName"`
	LogicalProcessorCount int64  `json:"LogicalProcessorCount"`
	MemoryCapacity        int64  `json:"MemoryCapacity"`
	VirtualMachinePath    string `json:"VirtualMachinePath"`
	VirtualHardDiskPath   string `json:"VirtualHardDiskPath"`
}

VMHost mirrors the subset of Get-VMHost output the provider exposes. Field tags match the PowerShell property names emitted by Get-VMHost.

type VMName added in v0.3.0

type VMName struct {
	Name string `json:"Name"`
}

VMName is the minimal shape vm/list.ps1 emits per result. Only Name is carried because the sweeper (the sole caller today) only needs the name to call RemoveVM. Adding more fields means slower enumeration on hosts with many VMs and a wider blast radius for script-Go contract drift; if a future caller needs richer shape, add a separate verb rather than fattening this one.

type VMSwitch

type VMSwitch struct {
	Name                           string `json:"Name"`
	SwitchType                     string `json:"SwitchType"`
	AllowManagementOS              bool   `json:"AllowManagementOS"`
	NetAdapterInterfaceDescription string `json:"NetAdapterInterfaceDescription"`
	Notes                          string `json:"Notes"`
	ID                             string `json:"Id"`
	NatName                        string `json:"NatName"`
	NatInternalAddressPrefix       string `json:"NatInternalAddressPrefix"`
	NatHostAddress                 string `json:"NatHostAddress"`
}

VMSwitch is the canonical read shape emitted by vswitch/{get,new,set}.ps1. Field tags use PascalCase to match Get-VMSwitch's native output (the stdin convention is snake_case per the wire contract; stdout is the raw cmdlet shape consumed by the typed client).

SwitchType reads as "External", "Internal", "Private", or the synthesized "NAT" -- Hyper-V's underlying enum has no NAT type, so the script reports SwitchType=NAT only when the caller passes nat_name and the matching NetNat + NetIPAddress are both present on the host. NAT fields are empty strings for non-NAT switches.

type VMSwitchName added in v0.3.0

type VMSwitchName struct {
	Name string `json:"Name"`
}

VMSwitchName is the minimal shape vswitch/list.ps1 emits per result. Only Name is carried because the sweeper (the sole caller today) passes name + empty natName to RemoveVMSwitch; the existing acctest bar uses Private + Internal switches only, so empty natName is correct for everything the sweeper currently encounters. NAT-switch sweep support (which would need NatName carried alongside) is a follow-up when NAT acctests land. Symmetric with VMName.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL