Repository: Telmate/terraform-provider-proxmox

Issue:
Clone uses wrong node when identical templates exist on multiple Proxmox nodes
**Versions:**
- Telmate/proxmox 3.0.2-rc03
- Terraform v1.9.8
- Proxmox VE 8.2.2

**Environment Setup:**
Proxmox cluster with two nodes:
- `proxmox1` with storage `proxmox1-1tb` (only available on `proxmox1`)
- `proxmox2` with storage `proxmox2-1tb` (only available on `proxmox2`)

On both nodes, I have created a template VM called "vm-template". The templates are identical in name, but each is stored on the local storage of its respective node:
On `proxmox1`: `vm-template` uses `proxmox1-1tb`
On `proxmox2`: `vm-template` uses `proxmox2-1tb`

**VM Configuration file:**
```
resource "proxmox_vm_qemu" "proxmox-vm" {
  target_nodes = [
    "proxmox2",
  ]
  clone = "vm-template"
  full_clone = true
  define_connection_info = true
  disk {
        backup = true
        discard = false
        emulatessd = false
        format = "raw"
        size = "250G"
        slot = "scsi0"
        storage = "proxmox2-1tb"
        type = "disk"
    }
...
}
```

**Problem:**
When using the `clone` parameter to create a new VM from a template, and the same template exists on multiple nodes (with identical names), the provider sometimes selects the wrong node to clone from - not the one specified in `target_nodes`.

This leads to errors if the storage backing the source template is not available on the destination node:
```
Error: storage migration failed: storage 'proxmox1-1tb' is not available on node 'proxmox2'
```

Even though the destination node (`target_nodes`) already has the exact same template available locally (with the same name), the provider attempts to clone from another node - which breaks the workflow.

**Expected Behavior:**
The provider should prefer cloning from the node specified in `target_nodes`, **if the template exists there**, instead of arbitrarily selecting another node. This would avoid unnecessary storage migration errors and match user intent.

**Possible Solution:**
- Introduce an optional `clone_node` argument, allowing the user to explicitly set the source node for cloning.
- Or, if `clone_node` is not set, always prefer the node in `target_nodes` if the template is found there.


Hints from issue comments:
@Tinyblargon: @vadym-replianchuk Would you be able to test with  #1398?@vadym-replianchuk: Of course! I'll write back when I check.

**upd.** I couldn't check it@Tinyblargon: @vadym-replianchuk would you be able to test with `3.0.2-rc06`?@vadym-replianchuk: @Tinyblargon , ofc. But I [can't find](https://github.com/Telmate/terraform-provider-proxmox/tree/v3.0.2-rc06) that version, only `3.0.2-rc05`@Tinyblargon: @vadym-replianchuk Sorry, I got the numbers mixed up, meant `3.0.2-rc05`.@vadym-replianchuk: I checked my use case, the virtual machine was created without errors.
Thank you!

Solution patch:
diff --git a/go.mod b/go.mod
index 8de222f..908be2a 100644
--- a/go.mod
+++ b/go.mod
@@ -3,7 +3,7 @@ module github.com/Telmate/terraform-provider-proxmox/v2
 go 1.24
 
 require (
-	github.com/Telmate/proxmox-api-go v0.0.0-20250822215835-17334f99d611
+	github.com/Telmate/proxmox-api-go v0.0.0-20250909211818-e783370fc8ea
 	github.com/google/uuid v1.6.0
 	github.com/hashicorp/go-cty v1.5.0
 	github.com/hashicorp/terraform-plugin-sdk/v2 v2.37.0
diff --git a/go.sum b/go.sum
index bd4e650..0134ad3 100644
--- a/go.sum
+++ b/go.sum
@@ -4,8 +4,8 @@ github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERo
 github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
 github.com/ProtonMail/go-crypto v1.2.0 h1:+PhXXn4SPGd+qk76TlEePBfOfivE0zkWFenhGhFLzWs=
 github.com/ProtonMail/go-crypto v1.2.0/go.mod h1:9whxjD8Rbs29b4XWbB8irEcE8KHMqaR2e7GWU1R+/PE=
-github.com/Telmate/proxmox-api-go v0.0.0-20250822215835-17334f99d611 h1:F2yIhAUc89Mr0Bhhs5ttAjDcABmcJrFuqDntNH/INYU=
-github.com/Telmate/proxmox-api-go v0.0.0-20250822215835-17334f99d611/go.mod h1:HGPbpwiVsrpYYuQAygnnU04ZdGLYc8fdP9qhJIBw2Bg=
+github.com/Telmate/proxmox-api-go v0.0.0-20250909211818-e783370fc8ea h1:/g6SFJtTQO5H6RUbN2EirwY/uMFAG8w574RtQE4EizU=
+github.com/Telmate/proxmox-api-go v0.0.0-20250909211818-e783370fc8ea/go.mod h1:HGPbpwiVsrpYYuQAygnnU04ZdGLYc8fdP9qhJIBw2Bg=
 github.com/agext/levenshtein v1.2.3 h1:YB2fHEn0UJagG8T1rrWknE3ZQzWM06O8AMAatNn7lmo=
 github.com/agext/levenshtein v1.2.3/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558=
 github.com/apparentlymart/go-textseg/v12 v12.0.0/go.mod h1:S/4uRK2UtaQttw1GenVJEynmyUenKwP++x/+DdGV/Ec=
diff --git a/proxmox/Internal/resource/guest/node/sdk.go b/proxmox/Internal/resource/guest/node/sdk.go
index 71b686b..359cb56 100644
--- a/proxmox/Internal/resource/guest/node/sdk.go
+++ b/proxmox/Internal/resource/guest/node/sdk.go
@@ -33,6 +33,7 @@ func SdkUpdate(d *schema.ResourceData, current pveAPI.NodeName) (pveAPI.NodeName
 	return pveAPI.NodeName(nodes[randomIndex].(string)), nil
 }
 
+// SdkCreate selects a node for resource creation.
 func SdkCreate(d *schema.ResourceData) (pveAPI.NodeName, error) {
 	if node, ok := d.GetOk(RootNode); ok {
 		return pveAPI.NodeName(node.(string)), nil
diff --git a/proxmox/helper_guest.go b/proxmox/helper_guest.go
index 8eb6918..a7fde59 100644
--- a/proxmox/helper_guest.go
+++ b/proxmox/helper_guest.go
@@ -28,3 +28,55 @@ func guestDelete(ctx context.Context, d *schema.ResourceData, meta any, kind str
 	}
 	return nil
 }
+
+func guestGetSourceVmr(ctx context.Context, client *pveSDK.Client, name pveSDK.GuestName, id pveSDK.GuestID, preferredNode pveSDK.NodeName, guest pveSDK.GuestType) (*pveSDK.VmRef, error) {
+	if name != "" {
+		rawGuests, err := pveSDK.ListGuests(ctx, client)
+		if err != nil {
+			return nil, err
+		}
+		return guestGetSourceVmrByNode(rawGuests, name, preferredNode, guest)
+	} else if id != 0 {
+		rawGuests, err := pveSDK.ListGuests(ctx, client)
+		if err != nil {
+			return nil, err
+		}
+		rawGuest, err := rawGuests.SelectID(id)
+		if err != nil {
+			return nil, err
+		}
+		if rawGuest.GetType() != guest {
+			return nil, errors.New("guest with ID '" + id.String() + "' is not of type '" + string(guest) + "'")
+		}
+		guestRef := pveSDK.NewVmRef(rawGuest.GetID())
+		guestRef.SetNode(string(rawGuest.GetNode()))
+		guestRef.SetVmType(guest)
+		return guestRef, nil
+	}
+	return nil, errors.New("either 'clone' name or 'clone_id' must be specified")
+}
+
+func guestGetSourceVmrByNode(raw pveSDK.RawGuestResources, name pveSDK.GuestName, preferredNode pveSDK.NodeName, guest pveSDK.GuestType) (*pveSDK.VmRef, error) {
+	var guestRef *pveSDK.VmRef
+	for i := range raw {
+		if raw[i].GetName() == name {
+			if raw[i].GetType() == guest {
+				if node := raw[i].GetNode(); node == preferredNode { // Prefer source VM on the same node
+					guestRef = pveSDK.NewVmRef(raw[i].GetID())
+					guestRef.SetNode(string(node))
+					guestRef.SetVmType(guest)
+					return guestRef, nil
+				}
+				if guestRef == nil { // remember the first we find
+					guestRef = pveSDK.NewVmRef(raw[i].GetID())
+					guestRef.SetNode(string(raw[i].GetNode()))
+					guestRef.SetVmType(guest)
+				}
+			}
+		}
+	}
+	if guestRef == nil {
+		return nil, errors.New("no guest with name '" + name.String() + "' found")
+	}
+	return guestRef, nil
+}
diff --git a/proxmox/resource_cloud_init_disk.go b/proxmox/resource_cloud_init_disk.go
index 6fb7feb..f5565fd 100644
--- a/proxmox/resource_cloud_init_disk.go
+++ b/proxmox/resource_cloud_init_disk.go
@@ -8,7 +8,7 @@ import (
 	"io"
 	"strings"
 
-	"github.com/Telmate/proxmox-api-go/proxmox"
+	pveSDK "github.com/Telmate/proxmox-api-go/proxmox"
 	"github.com/kdomanski/iso9660"
 
 	"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
@@ -150,11 +150,7 @@ func resourceCloudInitDiskRead(ctx context.Context, d *schema.ResourceData, m in
 	client := pconf.Client
 
 	var isoFound bool
-	pveNode := d.Get("pve_node").(string)
-	vmRef := &proxmox.VmRef{}
-	vmRef.SetNode(pveNode)
-	vmRef.SetVmType("qemu")
-	storageContent, err := client.GetStorageContent(ctx, vmRef, d.Get("storage").(string))
+	storageContent, err := client.GetStorageContent(ctx, d.Get("storage").(string), pveSDK.NodeName(d.Get("pve_node").(string)))
 	if err != nil {
 		return diag.FromErr(err)
 	}
diff --git a/proxmox/resource_lxc.go b/proxmox/resource_lxc.go
index 52bd0e7..8756593 100644
--- a/proxmox/resource_lxc.go
+++ b/proxmox/resource_lxc.go
@@ -519,7 +519,7 @@ func resourceLxcCreate(ctx context.Context, d *schema.ResourceData, meta interfa
 	var vmr *pveSDK.VmRef
 	if d.Get("clone").(string) != "" { // Clone
 		var sourceVmr *pveSDK.VmRef
-		sourceVmr, err = getSourceVmr(ctx, client, pveSDK.GuestName(d.Get("clone").(string)), 0, targetNode)
+		sourceVmr, err = guestGetSourceVmr(ctx, client, pveSDK.GuestName(d.Get("clone").(string)), 0, targetNode, pveSDK.GuestLxc)
 		if err != nil {
 			return append(diags, diag.FromErr(err)...)
 		}
@@ -964,7 +964,7 @@ func processLxcDiskChanges(
 		}
 		params := map[string]interface{}{}
 		params["delete"] = strings.Join(deleteDiskKeys, ", ")
-		if vmr.GetVmType() == "lxc" {
+		if vmr.GetVmType() == pveSDK.GuestLxc {
 			if _, err := pconf.Client.SetLxcConfig(ctx, vmr, params); err != nil {
 				return err
 			}
@@ -995,7 +995,7 @@ func processLxcDiskChanges(
 		}
 	}
 	if len(newParams) > 0 {
-		if vmr.GetVmType() == "lxc" {
+		if vmr.GetVmType() == pveSDK.GuestLxc {
 			if _, err := pconf.Client.SetLxcConfig(ctx, vmr, newParams); err != nil {
 				return err
 			}
@@ -1014,7 +1014,7 @@ func processLxcDiskChanges(
 			// 2. Move disks with mismatching storage
 			newStorage, ok := newDisk["storage"].(string)
 			if ok && newStorage != prevDisk["storage"] {
-				if vmr.GetVmType() == "lxc" {
+				if vmr.GetVmType() == pveSDK.GuestLxc {
 					_, err := pconf.Client.MoveLxcDisk(ctx, vmr, diskSlotName(prevDisk), newStorage)
 					if err != nil {
 						return err
@@ -1107,7 +1107,7 @@ func processLxcNetworkChanges(ctx context.Context, prevNetworks []map[string]int
 		params := map[string]interface{}{
 			"delete": strings.Join(deleteNetKeys, ", "),
 		}
-		if vmr.GetVmType() == "lxc" {
+		if vmr.GetVmType() == pveSDK.GuestLxc {
 			if _, err := pconf.Client.SetLxcConfig(ctx, vmr, params); err != nil {
 				return err
 			}
diff --git a/proxmox/resource_lxc_disk.go b/proxmox/resource_lxc_disk.go
index 40ab061..e5577ef 100644
--- a/proxmox/resource_lxc_disk.go
+++ b/proxmox/resource_lxc_disk.go
@@ -123,7 +123,7 @@ func resourceLxcDiskCreate(ctx context.Context, d *schema.ResourceData, meta int
 
 	client := pconf.Client
 	vmr := pveSDK.NewVmRef(vmID)
-	vmr.SetVmType("lxc")
+	vmr.SetVmType(pveSDK.GuestLxc)
 	_, err = client.GetVmInfo(ctx, vmr)
 	if err != nil {
 		return diag.FromErr(err)
diff --git a/proxmox/resource_pool.go b/proxmox/resource_pool.go
index 8ce7852..0a450b9 100644
--- a/proxmox/resource_pool.go
+++ b/proxmox/resource_pool.go
@@ -85,20 +85,17 @@ func _resourcePoolRead(ctx context.Context, d *schema.ResourceData, meta interfa
 	logger, _ := CreateSubLogger("resource_pool_read")
 	logger.Info().Str("poolid", poolID).Msg("Reading configuration for poolid")
 
-	poolInfo, err := pool.Get(ctx, client)
+	rawPool, err := pool.Get(ctx, client)
 	if err != nil {
 		d.SetId("")
 		return nil
 	}
 
 	d.SetId(clusterResourceId("pools", poolID))
-	d.Set("comment", "")
-	if poolInfo.Comment != nil {
-		d.Set("comment", poolInfo.Comment)
-	}
+	d.Set("comment", rawPool.GetComment())
 
 	// DEBUG print the read result
-	logger.Debug().Str("poolid", poolID).Msgf("Finished pool read resulting in data: '%+v'", poolInfo)
+	logger.Debug().Str("poolid", poolID).Msgf("Finished pool read resulting in data: '%+v'", rawPool.Get())
 	return nil
 }
 
diff --git a/proxmox/resource_storage_iso.go b/proxmox/resource_storage_iso.go
index bb0393d..8488358 100644
--- a/proxmox/resource_storage_iso.go
+++ b/proxmox/resource_storage_iso.go
@@ -8,7 +8,7 @@ import (
 	"os"
 	"strings"
 
-	"github.com/Telmate/proxmox-api-go/proxmox"
+	pveSDK "github.com/Telmate/proxmox-api-go/proxmox"
 	"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
 	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
 )
@@ -115,11 +115,7 @@ func resourceStorageIsoRead(ctx context.Context, d *schema.ResourceData, meta in
 	client := pconf.Client
 
 	var isoFound bool
-	pveNode := d.Get("pve_node").(string)
-	vmRef := &proxmox.VmRef{}
-	vmRef.SetNode(pveNode)
-	vmRef.SetVmType(isoContentType)
-	storageContent, err := client.GetStorageContent(ctx, vmRef, d.Get("storage").(string))
+	storageContent, err := client.GetStorageContent(ctx, d.Get("storage").(string), pveSDK.NodeName(d.Get("pve_node").(string)))
 	if err != nil {
 		return diag.FromErr(err)
 	}
diff --git a/proxmox/resource_vm_qemu.go b/proxmox/resource_vm_qemu.go
index c9ca73d..48e6cfe 100755
--- a/proxmox/resource_vm_qemu.go
+++ b/proxmox/resource_vm_qemu.go
@@ -4,7 +4,6 @@ import (
 	"context"
 	"encoding/base64"
 	"encoding/json"
-	"errors"
 	"fmt"
 	"log"
 	"net"
@@ -536,27 +535,6 @@ func resourceVmQemu() *schema.Resource {
 	return thisResource
 }
 
-func getSourceVmr(ctx context.Context, client *pveSDK.Client, name pveSDK.GuestName, id pveSDK.GuestID, preferredNode pveSDK.NodeName) (*pveSDK.VmRef, error) {
-	if name != "" {
-		sourceVmrs, err := client.GetVmRefsByName(ctx, name)
-		if err != nil {
-			return nil, err
-		}
-		// Prefer source VM on the same node
-		sourceVmr := sourceVmrs[0]
-		for _, candVmr := range sourceVmrs {
-			if candVmr.Node() == preferredNode {
-				sourceVmr = candVmr
-			}
-		}
-		return sourceVmr, nil
-	} else if id != 0 {
-		return client.GetVmRefById(ctx, id)
-	}
-
-	return nil, errors.New("either 'clone' name or 'clone_id' must be specified")
-}
-
 func resourceVmQemuCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
 	// create a logger for this function
 	logger, _ := CreateSubLogger("resource_vm_create")
@@ -668,7 +646,7 @@ func resourceVmQemuCreate(ctx context.Context, d *schema.ResourceData, meta inte
 		// check if clone, or PXE boot
 		if d.Get("clone").(string) != "" || d.Get("clone_id").(int) != 0 { // Clone
 
-			sourceVmr, err := getSourceVmr(ctx, client, pveSDK.GuestName(d.Get("clone").(string)), pveSDK.GuestID(d.Get("clone_id").(int)), targetNode)
+			sourceVmr, err := guestGetSourceVmr(ctx, client, pveSDK.GuestName(d.Get("clone").(string)), pveSDK.GuestID(d.Get("clone_id").(int)), targetNode, pveSDK.GuestQemu)
 			if err != nil {
 				return append(diags, diag.FromErr(err)...)
 			}



Test patch:
diff --git a/proxmox/helper_guest_test.go b/proxmox/helper_guest_test.go
new file mode 100644
index 0000000..6e14dcb
--- /dev/null
+++ b/proxmox/helper_guest_test.go
@@ -0,0 +1,80 @@
+package proxmox
+
+import (
+	"errors"
+	"testing"
+
+	pveSDK "github.com/Telmate/proxmox-api-go/proxmox"
+	"github.com/stretchr/testify/require"
+)
+
+func Test_UserID_Validate(t *testing.T) {
+	newGuestRef := func(id pveSDK.GuestID, node pveSDK.NodeName, guest pveSDK.GuestType) *pveSDK.VmRef {
+		ref := pveSDK.NewVmRef(id)
+		ref.SetNode(string(node))
+		ref.SetVmType(guest)
+		return ref
+	}
+	guestBuilder := func(ID pveSDK.GuestID, Name pveSDK.GuestName, Node pveSDK.NodeName, Type pveSDK.GuestType) pveSDK.RawGuestResource {
+		return &pveSDK.RawGuestResourceMock{
+			GetIDFunc: func() pveSDK.GuestID {
+				return ID
+			},
+			GetNameFunc: func() pveSDK.GuestName {
+				return Name
+			},
+			GetNodeFunc: func() pveSDK.NodeName {
+				return Node
+			},
+			GetTypeFunc: func() pveSDK.GuestType {
+				return Type
+			}}
+	}
+	raw := []pveSDK.RawGuestResource{
+		guestBuilder(100, "test", "node1", pveSDK.GuestQemu),
+		guestBuilder(101, "test", "node1", pveSDK.GuestLxc),
+		guestBuilder(102, "test", "node2", pveSDK.GuestQemu),
+		guestBuilder(103, "test", "node2", pveSDK.GuestLxc),
+		guestBuilder(104, "test", "node3", pveSDK.GuestQemu),
+		guestBuilder(105, "test", "node3", pveSDK.GuestLxc),
+		guestBuilder(200, "single-node", "node3", pveSDK.GuestLxc),
+	}
+	type testInput struct {
+		guestType     pveSDK.GuestType
+		name          pveSDK.GuestName
+		preferredNode pveSDK.NodeName
+	}
+	tests := []struct {
+		name   string
+		input  testInput
+		output *pveSDK.VmRef
+		err    error
+	}{
+		{name: `no vm found`,
+			input: testInput{
+				guestType:     pveSDK.GuestQemu,
+				name:          "non-existing-vm",
+				preferredNode: "node1"},
+			output: nil,
+			err:    errors.New("no guest with name 'non-existing-vm' found")},
+		{name: `preferred node found`,
+			input: testInput{
+				guestType:     pveSDK.GuestQemu,
+				name:          "test",
+				preferredNode: "node2"},
+			output: newGuestRef(102, "node2", pveSDK.GuestQemu)},
+		{name: `preferred node not found, pick first`,
+			input: testInput{
+				guestType:     pveSDK.GuestLxc,
+				name:          "single-node",
+				preferredNode: "nodeX"},
+			output: newGuestRef(200, "node3", pveSDK.GuestLxc)},
+	}
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			vmref, err := guestGetSourceVmrByNode(raw, test.input.name, test.input.preferredNode, test.input.guestType)
+			require.Equal(t, test.output, vmref)
+			require.Equal(t, test.err, err)
+		})
+	}
+}