Documentation
¶
Overview ¶
Package vm implements the hyperv_vm resource (M4 minimal first slice). Wraps the vm/{get,new,set,remove}.ps1 contract via the typed hyperv.Client.
Excluded from this slice (each becomes its own follow-up PR):
- boot_order (gen1 BIOS / gen2 UEFI translation deserves its own design)
- dynamic memory (min_bytes / max_bytes / buffer / priority on the existing memory block)
- integration services map
- automatic start/stop actions
- checkpoint type/policy
- VM path overrides (defaults from Get-VMHost)
CPU and memory live in nested blocks per ADR-0001: `cpu = { count = N }` and `memory = { startup_bytes = N }`. The blocks exist as nested attributes (rather than flat top-level fields) so dynamic-CPU and dynamic-memory follow-ups attach to the same block without flattening more attribute names into the top namespace -- e.g. cpu.weight, cpu.reserve, memory.min_bytes, memory.max_bytes.
Index ¶
- Variables
- func BootOrderListFromSlice(ctx context.Context, slice []BootOrderEntryModel) (types.List, diag.Diagnostics)
- func DvdDriveListFromSlice(ctx context.Context, slice []DvdDriveModel) (types.List, diag.Diagnostics)
- func New() resource.Resource
- type BootOrderEntryModel
- type CPUModel
- type DvdDriveModel
- type HardDiskDriveModel
- type MemoryModel
- type Model
- type NetworkAdapterModel
- type Resource
- func (r *Resource) ConfigValidators(_ context.Context) []resource.ConfigValidator
- func (r *Resource) Configure(_ context.Context, req resource.ConfigureRequest, ...)
- func (r *Resource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse)
- func (r *Resource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse)
- func (r *Resource) ImportState(ctx context.Context, req resource.ImportStateRequest, ...)
- func (r *Resource) Metadata(_ context.Context, req resource.MetadataRequest, ...)
- func (r *Resource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse)
- func (r *Resource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse)
- func (r *Resource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse)
- func (r *Resource) UpgradeState(_ context.Context) map[int64]resource.StateUpgrader
- type StateModel
Constants ¶
This section is empty.
Variables ¶
var BootOrderEntryAttrTypes = map[string]attr.Type{ "type": types.StringType, "controller_type": types.StringType, "controller_number": types.Int64Type, "controller_location": types.Int64Type, "name": types.StringType, }
BootOrderEntryAttrTypes mirrors BootOrderEntryModel's tfsdk-tagged fields. Same role as DvdDriveAttrTypes for the boot_order list.
var BootOrderEntryListElementType = types.ObjectType{AttrTypes: BootOrderEntryAttrTypes}
BootOrderEntryListElementType is the Object element type the boot_order list holds.
var DvdDriveAttrTypes = map[string]attr.Type{ "iso_path": pathtype.Type, "controller_type": types.StringType, "controller_number": types.Int64Type, "controller_location": types.Int64Type, }
DvdDriveAttrTypes mirrors DvdDriveModel's tfsdk-tagged fields. Used by the Object element type that backs the dvd_drive list, by the helpers below for ListValueFrom / ElementsAs round-trips, and by schema.go's Default empty-list value.
var DvdDriveListElementType = types.ObjectType{AttrTypes: DvdDriveAttrTypes}
DvdDriveListElementType is the Object element type the dvd_drive list holds. Computed once so all helpers share the same instance.
Functions ¶
func BootOrderListFromSlice ¶ added in v0.3.0
func BootOrderListFromSlice(ctx context.Context, slice []BootOrderEntryModel) (types.List, diag.Diagnostics)
BootOrderListFromSlice builds a types.List from a typed slice. Nil slice -> null list; empty slice -> empty list. Same rationale as DvdDriveListFromSlice.
func DvdDriveListFromSlice ¶ added in v0.3.0
func DvdDriveListFromSlice(ctx context.Context, slice []DvdDriveModel) (types.List, diag.Diagnostics)
DvdDriveListFromSlice builds a types.List from a typed slice. A nil slice becomes a null list (matching the "DvdDrives not managed" semantics on a fresh VM); an empty slice becomes an empty list (the schema's Default value when the user omits the attribute).
Types ¶
type BootOrderEntryModel ¶
type BootOrderEntryModel struct {
Type types.String `tfsdk:"type"`
ControllerType types.String `tfsdk:"controller_type"`
ControllerNumber types.Int64 `tfsdk:"controller_number"`
ControllerLocation types.Int64 `tfsdk:"controller_location"`
Name types.String `tfsdk:"name"`
}
BootOrderEntryModel is one element of the `boot_order` list on a gen 2 hyperv_vm. Type discriminates between hard_disk_drive / dvd_drive entries (which carry the slot tuple) and network_adapter entries (which carry Name). Unused fields for a given Type are null.
Gen 1 BIOS startup order is a separate, deferred slice; the schema validator rejects boot_order on gen 1 at plan time.
type CPUModel ¶
CPUModel is the nested `cpu` block. Static count only in this slice; dynamic-CPU attributes (weight, reserve, limit) attach as additional fields here in a follow-up.
type DvdDriveModel ¶
type DvdDriveModel struct {
IsoPath pathtype.Path `tfsdk:"iso_path"`
ControllerType types.String `tfsdk:"controller_type"`
ControllerNumber types.Int64 `tfsdk:"controller_number"`
ControllerLocation types.Int64 `tfsdk:"controller_location"`
}
DvdDriveModel is one element of the `dvd_drive` list on hyperv_vm. Same slot-tuple shape as HardDiskDriveModel (controller_type, controller_number, controller_location), but IsoPath is Optional -- an empty DVD drive (no medium loaded) is a legitimate config.
IsoPath uses pathtype.Path for slash-style folding consistent with hyperv_image_file.destination_path and hyperv_vhd.path.
type HardDiskDriveModel ¶
type HardDiskDriveModel struct {
Path pathtype.Path `tfsdk:"path"`
ControllerType types.String `tfsdk:"controller_type"`
ControllerNumber types.Int64 `tfsdk:"controller_number"`
ControllerLocation types.Int64 `tfsdk:"controller_location"`
}
HardDiskDriveModel is one element of the `hard_disk_drive` nested set on hyperv_vm. Identifies an attached VHD (Path) at a specific controller slot (ControllerType + ControllerNumber + ControllerLocation).
Path uses pathtype.Path for slash/case folding consistent with hyperv_vhd.path and hyperv_image_file.destination_path -- a VHD path the user wrote with forward slashes round-trips through the bench's canonical backslash form without phantom diffs.
type MemoryModel ¶
type MemoryModel struct {
StartupBytes types.Int64 `tfsdk:"startup_bytes"`
Dynamic types.Bool `tfsdk:"dynamic"`
MinBytes types.Int64 `tfsdk:"min_bytes"`
MaxBytes types.Int64 `tfsdk:"max_bytes"`
}
MemoryModel is the nested `memory` block. StartupBytes is the only required field; Dynamic / MinBytes / MaxBytes opt in to Hyper-V's dynamic memory mode.
Dynamic is types.Bool (not pointer) because the framework's null representation handles both "user didn't manage" and "explicit false" cleanly via the wire-side *bool with omitempty: null on the wire means absent, which the script treats as static. MinBytes and MaxBytes follow the same Optional+Computed + UseStateForUnknown pattern as state.shutdown_mode (PR #33).
Buffer and Priority are deferred -- they're Hyper-V dynamic-memory niceties (~5% of users) and adding them later is a strict superset of the current schema.
type Model ¶
type Model struct {
ID types.String `tfsdk:"id"`
Name types.String `tfsdk:"name"`
Generation types.Int64 `tfsdk:"generation"`
CPU *CPUModel `tfsdk:"cpu"`
Memory *MemoryModel `tfsdk:"memory"`
HardDiskDrives []HardDiskDriveModel `tfsdk:"hard_disk_drive"`
NetworkAdapters []NetworkAdapterModel `tfsdk:"network_adapter"`
// DvdDrives and BootOrder are types.List rather than []Struct so the
// framework can represent unknown values cleanly. The framework's
// reflect path can't fit "the whole list is unknown" into a Go slice
// (no representation for it), and emits "Value Conversion Error /
// Suggested Type: basetypes.ListValue" when a config drives the
// attribute from a for_each variable that hasn't materialized at
// validate time. Same fix shape PR #70 applied to URLConfig on
// hyperv_image_file. Helpers below give resource code typed access
// to the underlying []DvdDriveModel / []BootOrderEntryModel slice
// when the value is known.
DvdDrives types.List `tfsdk:"dvd_drive"`
BootOrder types.List `tfsdk:"boot_order"`
SecureBoot types.Bool `tfsdk:"secure_boot"`
SecureBootTemplate types.String `tfsdk:"secure_boot_template"`
Notes types.String `tfsdk:"notes"`
State *StateModel `tfsdk:"state"`
IPAddresses types.List `tfsdk:"ip_addresses"`
Path types.String `tfsdk:"path"`
}
Model is the tfsdk-bound struct backing the resource state. Field tags align with schema.go attribute names; conversion to/from the typed hyperv.VM DTO lives in resource.go.
SecureBoot is types.Bool (not pointer-to-bool) -- the framework's null representation handles the gen-1 case where the host has no Secure Boot concept and the cmdlet returns null.
CPU and Memory are pointer-typed (not value) because at ImportState time the state passes through with just `name` set; the framework then calls Read to populate everything else, but in the brief window before Read runs, cpu/memory are null in state. A value type can't represent null and the framework errors with "Received null value, however the target type cannot handle null values." Pointers null cleanly; modelFromVM allocates them.
HardDiskDrives is a list of attachments stored canonically by slot tuple (controller_type, controller_number, controller_location). Schema-side ListNestedAttribute (rather than SetNestedAttribute) because terraform-plugin-framework v1.19's slice decode of nested-set attributes hits a reflect path that produces a "Target Type: []vm.HardDiskDriveModel, Suggested Type: basetypes.SetValue" error during req.Plan.Get. List + a canonical sort in modelFromVM gives the same user-visible behavior (HCL ordering matches canonical state on subsequent applies) with a simpler decode.
func (*Model) BootOrderEntries ¶ added in v0.3.0
func (m *Model) BootOrderEntries(ctx context.Context) ([]BootOrderEntryModel, diag.Diagnostics)
BootOrderEntries returns the typed slice underlying m.BootOrder, or nil if null/unknown. Same null-vs-empty rationale as DvdDriveModels.
func (*Model) DvdDriveModels ¶ added in v0.3.0
func (m *Model) DvdDriveModels(ctx context.Context) ([]DvdDriveModel, diag.Diagnostics)
DvdDriveModels returns the typed slice underlying m.DvdDrives, or nil if the list is null or unknown. Resource code that needs to distinguish "user explicitly set []" from "user did not set the attribute" should inspect m.DvdDrives directly via IsNull / IsUnknown -- a known-empty list returns an empty (but non-nil) slice here.
type NetworkAdapterModel ¶
type NetworkAdapterModel struct {
Name types.String `tfsdk:"name"`
SwitchName types.String `tfsdk:"switch_name"`
IPAddresses types.List `tfsdk:"ip_addresses"`
MacAddress mactype.MAC `tfsdk:"mac_address"`
VlanID types.Int64 `tfsdk:"vlan_id"`
}
NetworkAdapterModel is one element of the `network_adapter` list on hyperv_vm. Display Name is the slot key for diff/reconciliation (Hyper-V allows duplicate-named NICs at the cmdlet level, but the resource-layer schema validator enforces uniqueness within a VM's list at plan time, so the slot key is well-defined).
SwitchName binds the NIC to a hyperv_virtual_switch by name.
IPAddresses is the per-NIC slice of IPv4 / IPv6 addresses that Hyper-V's integration services have reported for this specific adapter. Computed -- populated on Read from the host. Unlike the VM-level flat `ip_addresses` list, the per-NIC view gives multi- homed VMs a stable reference to a specific NIC's IPs (order within a single NIC is host-driven but the NIC selector itself is keyed by the deterministic display Name).
MacAddress is Optional (not Computed). When the user sets it, the NIC uses a static MAC of that value. When unset, Hyper-V auto- assigns from its dynamic-MAC pool; Read writes null to state in that case (we don't store the dynamically-assigned MAC -- that would create a perpetual plan diff against the empty config). Optional-only is deliberate: with Computed, the framework copies state-value forward when config goes null, which masks intentional reverts to dynamic-MAC mode (omitted-vs-explicit-null are indistinguishable at the framework's config layer).
VlanID is Optional (not Computed) for the same reason. When set to 1-4094, the NIC is tagged with that VLAN ID in Access mode. When unset, the NIC carries untagged frames. Read populates from Get-VMNetworkAdapterVlan.AccessVlanId; an untagged NIC produces state value null (matching unset config to avoid perpetual diff).
type Resource ¶
type Resource struct {
// contains filtered or unexported fields
}
Resource implements hyperv_vm.
func (*Resource) ConfigValidators ¶
func (r *Resource) ConfigValidators(_ context.Context) []resource.ConfigValidator
ConfigValidators rejects mode/attribute combinations at plan time so the operator gets a clear, attribute-anchored diagnostic instead of the cmdlet's opaque error at apply time.
func (*Resource) Configure ¶
func (r *Resource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse)
Configure stashes the typed Hyper-V client built by the provider's Configure pass. Skips when ProviderData is nil (validate-time invocation before the provider has resolved its config).
func (*Resource) Create ¶
func (r *Resource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse)
Create runs new.ps1 with the plan's attributes and writes the post-create read shape back to state.
func (*Resource) Delete ¶
func (r *Resource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse)
Delete runs remove.ps1. ErrNotFound is treated as success (the VM is already gone). The script hard-stops the VM first if it's running -- this is the one place the PS layer drives a power transition. Hard stop (Stop-VM -Force -TurnOff) instead of graceful for the reasons documented in the resource MarkdownDescription: graceful shutdown hangs indefinitely on guests with absent / unresponsive integration services, and destroy semantics across IaC providers consistently match the "destroy means destroy" expectation.
func (*Resource) ImportState ¶
func (r *Resource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse)
ImportState lets `terraform import hyperv_vm.foo my-vm` work by treating the import ID as the VM name. Read populates the rest of the attributes via Get-VM on the immediately-following refresh.
func (*Resource) Metadata ¶
func (r *Resource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse)
Metadata sets the resource's TF type name.
func (*Resource) Read ¶
func (r *Resource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse)
Read fetches the current shape via get.ps1 and reconciles state.
ErrNotFound -> RemoveResource so Terraform plans recreate. Other errors -> AddError so a transient fault doesn't silently drop the resource from state.
func (*Resource) Schema ¶
func (r *Resource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse)
Schema returns the locked-in schema (see schema.go).
func (*Resource) Update ¶
func (r *Resource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse)
Update forwards only the fields that changed between state and plan to avoid hitting Set-VMMemory / Set-VMProcessor needlessly on a running VM (those cmdlets validate state by parameter set, not value semantics -- even a no-op call to Set-VMMemory on a running VM errors). Generation is always forwarded as the script's gen-2-only SecureBoot guard hint.
func (*Resource) UpgradeState ¶
UpgradeState bridges schema versions for hyperv_vm. Each entry maps from a SOURCE version directly to the current (v4) shape; the framework dispatches based on the on-disk version, NOT a chain.
Each upgradeNToM helper returns the current Model, so a v0 state file goes v0 -> Model in one hop (the helper handles every shape change between v0 and v4 inline). When a new schema version lands, add a new priorSchemaVX / priorModelVX / upgradeVXToV(X+1) triple here and update the existing helpers to populate any new fields with their defaulted-or-null v4 shape.
type StateModel ¶
type StateModel struct {
Desired types.String `tfsdk:"desired"`
Current types.String `tfsdk:"current"`
ShutdownMode types.String `tfsdk:"shutdown_mode"`
}
StateModel is the nested `state` block on hyperv_vm. Pointer-typed (Model.State is *StateModel) for the same reason as CPU and Memory: during ImportState the framework writes a partial Model with just `name` set, and a value-typed nested struct can't represent null.
Desired is the user-facing power-state input ("Off" | "Running"); a transition fires only when Desired differs from the host's actual state. Current is the Computed readback from Hyper-V; useful for downstream resources that key off the actual state ("provision the guest only when state.current = Running").
Saved and Paused are out of scope for this slice -- the schema validator on Desired rejects values outside {Off, Running}. Drift from those states surfaces verbatim in Current; the next Update will hard-power-off or start as configured.