Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/resources/vm_power_state.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.<br>The guest OS needs to have guest tools installed (qemu-guest-agent).

### Read-Only

Expand Down
31 changes: 27 additions & 4 deletions internal/provider/hypercore_vm_power_state_resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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.<br>" +
"The guest OS needs to have guest tools installed (qemu-guest-agent).",
Optional: true,
},
},
}
}
Expand Down Expand Up @@ -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())

Expand Down Expand Up @@ -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
Expand Down
8 changes: 4 additions & 4 deletions internal/provider/tests/acceptance/setup/env-x11.txt
Original file line number Diff line number Diff line change
@@ -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"

50 changes: 50 additions & 0 deletions internal/utils/vm.go
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
25 changes: 25 additions & 0 deletions internal/utils/vm_power_state.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand Down Expand Up @@ -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)

Expand Down
6 changes: 6 additions & 0 deletions local/assets/alma-10/meta-data
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
#cloud-config
instance-id:
local-hostname: my-hostname
network-interfaces: |
auto eth0
iface eth0 inet dhcp
10 changes: 10 additions & 0 deletions local/assets/alma-10/user-data
Original file line number Diff line number Diff line change
@@ -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 ...
18 changes: 14 additions & 4 deletions local/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -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" {}
Expand All @@ -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
Expand All @@ -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
}
Loading