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/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)
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
}