From ad7bdfa66417781898fa82ff19d296b5c9d71650 Mon Sep 17 00:00:00 2001 From: Justin Cinkelj Date: Wed, 3 Dec 2025 19:54:18 +0100 Subject: [PATCH 1/2] Optionally wait on VM to get IP address Signed-off-by: Justin Cinkelj --- docs/resources/vm_power_state.md | 1 + .../hypercore_vm_power_state_resource.go | 31 ++++++++++-- .../tests/acceptance/setup/env-x11.txt | 8 +-- internal/utils/vm.go | 50 +++++++++++++++++++ local/assets/alma-10/meta-data | 6 +++ local/assets/alma-10/user-data | 10 ++++ local/main.tf | 18 +++++-- 7 files changed, 112 insertions(+), 12 deletions(-) create mode 100644 local/assets/alma-10/meta-data create mode 100644 local/assets/alma-10/user-data diff --git a/docs/resources/vm_power_state.md b/docs/resources/vm_power_state.md index 564ca20..07a814e 100644 --- a/docs/resources/vm_power_state.md +++ b/docs/resources/vm_power_state.md @@ -60,6 +60,7 @@ import { ### Optional - `force_shutoff` (Boolean) Set to `true` if you want to put the VM into the `SHUTOFF` state by force. This option will only be taken into account when `state` is set to `SHUTOFF`. Default is `false`. +- `wait_for_guest_net_timeout` (Number) Set to non-zero value to wait on guest OS to report guest IP address to hypervisor.
The guest OS needs to have guest tools installed (qemu-guest-agent). ### Read-Only diff --git a/internal/provider/hypercore_vm_power_state_resource.go b/internal/provider/hypercore_vm_power_state_resource.go index 0813440..6414eef 100644 --- a/internal/provider/hypercore_vm_power_state_resource.go +++ b/internal/provider/hypercore_vm_power_state_resource.go @@ -32,10 +32,11 @@ type HypercoreVMPowerStateResource struct { // HypercoreVMPowerStateResourceModel describes the resource data model. type HypercoreVMPowerStateResourceModel struct { - Id types.String `tfsdk:"id"` - VmUUID types.String `tfsdk:"vm_uuid"` - State types.String `tfsdk:"state"` - ForceSutoff types.Bool `tfsdk:"force_shutoff"` + Id types.String `tfsdk:"id"` + VmUUID types.String `tfsdk:"vm_uuid"` + State types.String `tfsdk:"state"` + ForceSutoff types.Bool `tfsdk:"force_shutoff"` + WaitForGuestNetTimeout types.Int32 `tfsdk:"wait_for_guest_net_timeout"` } func (r *HypercoreVMPowerStateResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { @@ -68,6 +69,12 @@ func (r *HypercoreVMPowerStateResource) Schema(ctx context.Context, req resource "This option will only be taken into account when `state` is set to `SHUTOFF`. Default is `false`.", Optional: true, }, + "wait_for_guest_net_timeout": schema.Int32Attribute{ + MarkdownDescription: "" + + "Set to non-zero value to wait on guest OS to report guest IP address to hypervisor.
" + + "The guest OS needs to have guest tools installed (qemu-guest-agent).", + Optional: true, + }, }, } } @@ -157,6 +164,14 @@ func (r *HypercoreVMPowerStateResource) Create(ctx context.Context, req resource return } + waitForGuestNetTimeout := data.WaitForGuestNetTimeout.ValueInt32() + waitForGuestNetFlag := waitForGuestNetTimeout > 0 && hc3PowerState == "RUNNING" + if waitForGuestNetFlag { + vm := &utils.VM{UUID: data.VmUUID.ValueString()} + wait_ok := vm.WaitGuestNetwork(waitForGuestNetTimeout, *r.client, ctx) + tflog.Debug(ctx, fmt.Sprintf("Waiting on guest OS IP address - wait_ok=%v", wait_ok)) + } + // save into the Terraform state. data.Id = types.StringValue(data.VmUUID.ValueString()) @@ -275,6 +290,14 @@ func (r *HypercoreVMPowerStateResource) Update(ctx context.Context, req resource return } + waitForGuestNetTimeout := data.WaitForGuestNetTimeout.ValueInt32() + waitForGuestNetFlag := waitForGuestNetTimeout > 0 && hc3PowerState == "RUNNING" + if waitForGuestNetFlag { + vm := &utils.VM{UUID: vmUUID} + wait_ok := vm.WaitGuestNetwork(waitForGuestNetTimeout, *r.client, ctx) + tflog.Debug(ctx, fmt.Sprintf("Waiting on guest OS IP address - wait_ok=%v", wait_ok)) + } + tflog.Info(ctx, fmt.Sprintf("TTRT HypercoreVMPowerStateResource: vm_uuid=%s, state=%s, action_performed=%s", vmUUID, hc3PowerState, actionType)) // Save updated data into Terraform state diff --git a/internal/provider/tests/acceptance/setup/env-x11.txt b/internal/provider/tests/acceptance/setup/env-x11.txt index c5b8581..2982848 100644 --- a/internal/provider/tests/acceptance/setup/env-x11.txt +++ b/internal/provider/tests/acceptance/setup/env-x11.txt @@ -1,9 +1,9 @@ HC_VM_SHUTDOWN_TIMEOUT=30 -SOURCE_VM_UUID="c6ce1356-7487-4fd3-97f4-bff1c3b3d86a" +SOURCE_VM_UUID="0ad1bb9b-470c-466f-ae88-797630118415" # testtf-ci-virtual-disk.img - https://dl-cdn.alpinelinux.org/alpine/v3.21/releases/cloud/generic_alpine-3.21.2-x86_64-bios-tiny-r0.qcow2 -EXISTING_VDISK_UUID="ac8c5105-6c42-474b-a445-61fc32e177e9" +EXISTING_VDISK_UUID="3ee7b833-4c7e-4410-82ab-43ddd8b101d6" SOURCE_VM_NAME="integration-test-vm" -SOURCE_NIC_UUID="e51c9076-b1ec-438a-8485-d653b627cdcb" -SOURCE_DISK_UUID="3c4db35c-c38c-4cdb-999a-46632058d29a" +SOURCE_NIC_UUID="36c8e4a8-9434-4549-bf24-d825cbbe7a06" +SOURCE_DISK_UUID="3a065638-c01b-43f4-b414-15d5ebf34b1e" diff --git a/internal/utils/vm.go b/internal/utils/vm.go index 349345a..b2156de 100644 --- a/internal/utils/vm.go +++ b/internal/utils/vm.go @@ -456,6 +456,56 @@ func (vc *VM) WaitShutdown(vmUUID string, shutdownTimeout int, restClient RestCl return false } +func (vc *VM) GetAllIpv4Addresses(restClient RestClient, ctx context.Context) []string { + vmUUID := (*vc).UUID + vmData := restClient.GetRecord( + fmt.Sprintf("/rest/v1/VirDomain/%s", vmUUID), + map[string]any{}, + true, + -1, + ) + + allIpv4Addresses := []string{} + if netDevs, ok := (*vmData)["netDevs"].([]any); ok { + for _, netDev := range netDevs { + // tflog.Debug(ctx, fmt.Sprintf("TTRT netDev=%v allIpv4Addresses=%v", netDev, allIpv4Addresses)) + // Safely assert that each item is a map + if device, ok := netDev.(map[string]any); ok { + // tflog.Debug(ctx, fmt.Sprintf("TTRT device=%v allIpv4Addresses=%v", device, allIpv4Addresses)) + if ipv4Addresses_any, ok := device["ipv4Addresses"].([]any); ok { + for _, ipv4Address_any := range ipv4Addresses_any { + if ipv4Address, ok := ipv4Address_any.(string); ok { + allIpv4Addresses = append(allIpv4Addresses, ipv4Address) + } + } + } + } + } + } + // tflog.Debug(ctx, fmt.Sprintf("TTRT VM=%v allIpv4Addresses=%v", vmUUID, allIpv4Addresses)) + return allIpv4Addresses +} + +/* +Wait until guest gets (at least one) IP address. +Return true if at least one IPv4 address is found. +*/ +func (vc *VM) WaitGuestNetwork(waitTimeout int32, restClient RestClient, ctx context.Context) bool { + startTime := time.Now().Unix() + for { + allIpv4Addresses := (*vc).GetAllIpv4Addresses(restClient, ctx) + if len(allIpv4Addresses) > 0 { + return true + } + + duration := time.Now().Unix() - startTime + if duration >= int64(waitTimeout) { + return false + } + time.Sleep(10 * time.Second) + } +} + func (vc *VM) ShutdownForced(vmUUID string, restClient RestClient, ctx context.Context) bool { vmFreshData := restClient.GetRecord( fmt.Sprintf("/rest/v1/VirDomain/%s", vmUUID), diff --git a/local/assets/alma-10/meta-data b/local/assets/alma-10/meta-data new file mode 100644 index 0000000..6342023 --- /dev/null +++ b/local/assets/alma-10/meta-data @@ -0,0 +1,6 @@ +#cloud-config +instance-id: +local-hostname: my-hostname +network-interfaces: | + auto eth0 + iface eth0 inet dhcp diff --git a/local/assets/alma-10/user-data b/local/assets/alma-10/user-data new file mode 100644 index 0000000..d1d0a0a --- /dev/null +++ b/local/assets/alma-10/user-data @@ -0,0 +1,10 @@ +#cloud-config + +# https://wiki.almalinux.org/cloud/Generic-cloud-on-local.html#create-a-snapshot-from-the-image + +ssh_pwauth: true # sshd service will be configured to accept password authentication method +password: testp # Set a password for almalinux +chpasswd: + expire: false # Don't ask for password reset after the first log-in +ssh_authorized_keys: # Add your ssh public key for publickey authentication + - ssh-ed25519 ... diff --git a/local/main.tf b/local/main.tf index b60e215..4dea360 100644 --- a/local/main.tf +++ b/local/main.tf @@ -10,8 +10,9 @@ terraform { } locals { - src_vm_name = "testtf-src-empty" - vm_name = "testtf-justin-affinity" + # Use image AlmaLinux-10-GenericCloud-10.1-20251125.0.x86_64_v2.qcow2 + src_vm_name = "testtf-src-alma-10" + vm_name = "testtf-wait-net" } provider "hypercore" {} @@ -24,8 +25,16 @@ resource "hypercore_vm" "myvm" { name = local.vm_name clone = { source_vm_uuid = data.hypercore_vms.srcvm.vms.0.uuid - user_data = "" - meta_data = "" + # meta_data = templatefile("assets/meta-data.ubuntu-22.04.yml.tftpl", { + # name = local.vm_name, + # }) + # user_data = templatefile("assets/user-data.ubuntu-22.04.yml.tftpl", { + # name = local.vm_name, + # ssh_authorized_keys = "", + # ssh_import_id = "justinc1", + # }) + user_data = file("assets/alma-10/user-data") + meta_data = file("assets/alma-10/meta-data") } # TODO - are computed, on HC3 side memory = 1024 @@ -42,4 +51,5 @@ resource "hypercore_vm_power_state" "myvm" { vm_uuid = hypercore_vm.myvm.id # state = "SHUTOFF" # available states are: SHUTOFF, RUNNING, PAUSED state = "RUNNING" # available states are: SHUTOFF, RUNNING, PAUSED + wait_for_guest_net_timeout = 120 } From 68af49f3cc2935a3256723e03e4778d0c16f14a9 Mon Sep 17 00:00:00 2001 From: Justin Cinkelj Date: Wed, 3 Dec 2025 22:05:36 +0100 Subject: [PATCH 2/2] Fix VM nice shutdown case Signed-off-by: Justin Cinkelj --- internal/utils/vm_power_state.go | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/internal/utils/vm_power_state.go b/internal/utils/vm_power_state.go index 4cfbf3e..3f25179 100644 --- a/internal/utils/vm_power_state.go +++ b/internal/utils/vm_power_state.go @@ -6,8 +6,10 @@ package utils import ( "context" "fmt" + "time" "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-log/tflog" ) var ALLOWED_POWER_STATES = map[string]bool{ @@ -59,9 +61,32 @@ func ModifyVMPowerState( taskTag.WaitTask(restClient, ctx) + // corner case. If actionType=SHUTDOWN, the taskTag is empty, and we need to manuall wait on state transition to happen. + // Say at most 300 seconds. + if actionType == "SHUTDOWN" { + waitVMPowerState(300, "SHUTOFF", vmUUID, restClient, ctx) + } + return nil } +func waitVMPowerState(waitTimeout int32, desiredPowerState string, vmUUID string, restClient RestClient, ctx context.Context) bool { + startTime := time.Now().Unix() + for { + vmPowerState, _ := GetVMPowerState(vmUUID, restClient) + if vmPowerState == desiredPowerState { + return true + } + tflog.Info(ctx, fmt.Sprintf("TTRT waitVMPowerState %v != %v", vmPowerState, desiredPowerState)) + + duration := time.Now().Unix() - startTime + if duration >= int64(waitTimeout) { + return false + } + time.Sleep(10 * time.Second) + } +} + func GetVMPowerState(vmUUID string, restClient RestClient) (string, diag.Diagnostic) { vm, err := GetOneVMWithError(vmUUID, restClient)