From 2e92a80ddde6dc05a1dd5a4b8993a513c0ec51e3 Mon Sep 17 00:00:00 2001 From: Domen Date: Thu, 15 May 2025 16:04:01 +0200 Subject: [PATCH 1/4] Added preserve mac address logic for VM clone --- internal/provider/hypercore_vm_resource.go | 17 ++++--- internal/utils/vm.go | 53 ++++++++++++++-------- 2 files changed, 45 insertions(+), 25 deletions(-) diff --git a/internal/provider/hypercore_vm_resource.go b/internal/provider/hypercore_vm_resource.go index 9d391d8..b4abd6c 100644 --- a/internal/provider/hypercore_vm_resource.go +++ b/internal/provider/hypercore_vm_resource.go @@ -62,9 +62,10 @@ type ImportModel struct { } type CloneModel struct { - SourceVMUUID types.String `tfsdk:"source_vm_uuid"` - UserData types.String `tfsdk:"user_data"` - MetaData types.String `tfsdk:"meta_data"` + SourceVMUUID types.String `tfsdk:"source_vm_uuid"` + UserData types.String `tfsdk:"user_data"` + MetaData types.String `tfsdk:"meta_data"` + PreserveMacAddress types.Bool `tfsdk:"preserve_mac_address"` } type AffinityStrategyModel struct { @@ -155,9 +156,10 @@ The provider will currently try to shutdown VM only before VM delete.`, "`user_data` and `meta_data` are used for the cloud init data.", Optional: true, AttributeTypes: map[string]attr.Type{ - "source_vm_uuid": types.StringType, - "user_data": types.StringType, - "meta_data": types.StringType, + "source_vm_uuid": types.StringType, + "user_data": types.StringType, + "meta_data": types.StringType, + "preserve_mac_address": types.BoolType, }, }, "affinity_strategy": schema.ObjectAttribute{ @@ -219,16 +221,19 @@ func (r *HypercoreVMResource) Configure(ctx context.Context, req resource.Config func getVMStruct(data *HypercoreVMResourceModel, vmDescription *string, vmTags *[]string) *utils.VM { // Gets VM structure from Utils.VM, sends parameters based on which VM create logic is being called sourceVMUUID, userData, metaData := "", "", "" + preserveMacAddress := false if data.Clone != nil { sourceVMUUID = data.Clone.SourceVMUUID.ValueString() userData = data.Clone.UserData.ValueString() metaData = data.Clone.MetaData.ValueString() + preserveMacAddress = data.Clone.PreserveMacAddress.ValueBool() } vmStruct := utils.GetVMStruct( data.Name.ValueString(), sourceVMUUID, userData, metaData, + preserveMacAddress, vmDescription, vmTags, data.VCPU.ValueInt32Pointer(), diff --git a/internal/utils/vm.go b/internal/utils/vm.go index eabba4d..c0d8221 100644 --- a/internal/utils/vm.go +++ b/internal/utils/vm.go @@ -84,6 +84,7 @@ func GetVMStruct( _sourceVMUUID string, userData string, metaData string, + preserveMacAddress bool, _description *string, _tags *[]string, _vcpu *int32, @@ -101,7 +102,7 @@ func GetVMStruct( UUID: "", VMName: _VMName, sourceVMUUID: _sourceVMUUID, - preserveMacAddress: false, + preserveMacAddress: preserveMacAddress, cloudInit: map[string]any{ "userData": userDataB64, "metaData": metaDataB64, @@ -146,8 +147,8 @@ func (vc *VM) SendFromScratchRequest(restClient RestClient) *TaskTag { return taskTag } -func (vc *VM) FromScratch(restClient RestClient, ctx context.Context) (bool, string) { - task := vc.SendFromScratchRequest(restClient) +func (vmNew *VM) FromScratch(restClient RestClient, ctx context.Context) (bool, string) { + task := vmNew.SendFromScratchRequest(restClient) task.WaitTask(restClient, ctx) taskStatus := task.GetStatus(restClient) @@ -156,22 +157,36 @@ func (vc *VM) FromScratch(restClient RestClient, ctx context.Context) (bool, str } if state, ok := (*taskStatus)["state"]; ok && state == "COMPLETE" { - vc.UUID = task.CreatedUUID - return true, fmt.Sprintf("Virtual machine create complete to - %s.", vc.VMName) + vmNew.UUID = task.CreatedUUID + return true, fmt.Sprintf("Virtual machine create complete to - %s.", vmNew.VMName) } panic("There was a problem during VM create.") } -func (vc *VM) SendCloneRequest(restClient RestClient, sourceVM map[string]any) *TaskTag { - // Clone payload +func (vmNew *VM) SendCloneRequest(restClient RestClient, sourceVM map[string]any) *TaskTag { clonePayload := map[string]any{ "template": map[string]any{ - "name": vc.VMName, - "cloudInitData": vc.cloudInit, + "name": vmNew.VMName, + "cloudInitData": vmNew.cloudInit, }, } - + // User wants to preserve net devices from the source VM + if vmNew.preserveMacAddress { + netDevicesNewVM := []map[string]any{} + // Loop through each network device in source VM + if sourceVM["netDevs"] != nil { + for _, netDeviceSourceVM := range sourceVM["netDevs"].([]any) { + device := netDeviceSourceVM.(map[string]any) + netDevicesNewVM = append(netDevicesNewVM, map[string]any{ + "type": device["type"], + "macAddress": device["macAddress"], + "vlan": device["vlan"], + }) + } + } + clonePayload["template"].(map[string]any)["netDevs"] = netDevicesNewVM + } taskTag, _, _ := restClient.CreateRecord( fmt.Sprintf("/rest/v1/VirDomain/%s/clone", sourceVM["uuid"]), clonePayload, @@ -180,33 +195,33 @@ func (vc *VM) SendCloneRequest(restClient RestClient, sourceVM map[string]any) * return taskTag } -func (vc *VM) Clone(restClient RestClient, ctx context.Context) (bool, string) { - vm := GetVM(map[string]any{"name": vc.VMName}, restClient) +func (vmNew *VM) Clone(restClient RestClient, ctx context.Context) (bool, string) { + vm := GetVM(map[string]any{"name": vmNew.VMName}, restClient) if len(vm) > 0 { - vc.UUID = AnyToString(vm[0]["uuid"]) - return false, fmt.Sprintf("Virtual machine %s already exists.", vc.VMName) + vmNew.UUID = AnyToString(vm[0]["uuid"]) + return false, fmt.Sprintf("Virtual machine %s already exists.", vmNew.VMName) } sourceVM := GetOneVM( - vc.sourceVMUUID, + vmNew.sourceVMUUID, restClient, ) sourceVMName, _ := sourceVM["name"].(string) // Clone payload - task := vc.SendCloneRequest(restClient, sourceVM) + task := vmNew.SendCloneRequest(restClient, sourceVM) task.WaitTask(restClient, ctx) taskStatus := task.GetStatus(restClient) if taskStatus != nil { if state, ok := (*taskStatus)["state"]; ok && state == "COMPLETE" { - vc.UUID = task.CreatedUUID - return true, fmt.Sprintf("Virtual machine - %s %s - cloning complete to - %s.", sourceVMName, vc.sourceVMUUID, vc.VMName) + vmNew.UUID = task.CreatedUUID + return true, fmt.Sprintf("Virtual machine - %s %s - cloning complete to - %s.", sourceVMName, vmNew.sourceVMUUID, vmNew.VMName) } } - panic(fmt.Sprintf("There was a problem during cloning of %s %s, cloning failed.", sourceVMName, vc.sourceVMUUID)) + panic(fmt.Sprintf("There was a problem during cloning of %s %s, cloning failed.", sourceVMName, vmNew.sourceVMUUID)) } func (vc *VM) SendImportRequest(restClient RestClient, source map[string]any) *TaskTag { From b5f522c995a55ae067ae5e7135dfa3d7539c5c66 Mon Sep 17 00:00:00 2001 From: Domen Date: Thu, 15 May 2025 16:13:01 +0200 Subject: [PATCH 2/4] Lint and docs update --- docs/resources/vm.md | 1 + internal/utils/vm.go | 26 ++++++++++++++++---------- 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/docs/resources/vm.md b/docs/resources/vm.md index a5d16d5..054f86e 100644 --- a/docs/resources/vm.md +++ b/docs/resources/vm.md @@ -130,6 +130,7 @@ Optional: Optional: - `meta_data` (String) +- `preserve_mac_address` (Boolean) - `source_vm_uuid` (String) - `user_data` (String) diff --git a/internal/utils/vm.go b/internal/utils/vm.go index c0d8221..292cb5f 100644 --- a/internal/utils/vm.go +++ b/internal/utils/vm.go @@ -174,18 +174,24 @@ func (vmNew *VM) SendCloneRequest(restClient RestClient, sourceVM map[string]any // User wants to preserve net devices from the source VM if vmNew.preserveMacAddress { netDevicesNewVM := []map[string]any{} - // Loop through each network device in source VM - if sourceVM["netDevs"] != nil { - for _, netDeviceSourceVM := range sourceVM["netDevs"].([]any) { - device := netDeviceSourceVM.(map[string]any) - netDevicesNewVM = append(netDevicesNewVM, map[string]any{ - "type": device["type"], - "macAddress": device["macAddress"], - "vlan": device["vlan"], - }) + + if sourceNetDevs, ok := sourceVM["netDevs"].([]any); ok { + for _, netDeviceSource := range sourceNetDevs { + // Safely assert that each item is a map + if device, ok := netDeviceSource.(map[string]any); ok { + netDevicesNewVM = append(netDevicesNewVM, map[string]any{ + "type": device["type"], + "macAddress": device["macAddress"], + "vlan": device["vlan"], + }) + } } } - clonePayload["template"].(map[string]any)["netDevs"] = netDevicesNewVM + + // Safely assert the "template" field is a map + if tmpl, ok := clonePayload["template"].(map[string]any); ok { + tmpl["netDevs"] = netDevicesNewVM + } } taskTag, _, _ := restClient.CreateRecord( fmt.Sprintf("/rest/v1/VirDomain/%s/clone", sourceVM["uuid"]), From f616bb7c8d1d65d05e64582e6bcf3def19d59a2f Mon Sep 17 00:00:00 2001 From: Domen Date: Thu, 15 May 2025 16:26:04 +0200 Subject: [PATCH 3/4] Clone acceptance test update --- .../tests/acceptance/hypercore_vm_resource_clone_acc_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/internal/provider/tests/acceptance/hypercore_vm_resource_clone_acc_test.go b/internal/provider/tests/acceptance/hypercore_vm_resource_clone_acc_test.go index 5777ed5..ad8d9bd 100644 --- a/internal/provider/tests/acceptance/hypercore_vm_resource_clone_acc_test.go +++ b/internal/provider/tests/acceptance/hypercore_vm_resource_clone_acc_test.go @@ -125,6 +125,7 @@ resource "hypercore_vm" "test" { source_vm_uuid = %[2]q user_data = "" meta_data = "" + preserve_mac_address = false } } `, vm_name, source_vm_uuid, requested_power_state) From d9bc0ef2d947feee020da9c77d729787e1dd7802 Mon Sep 17 00:00:00 2001 From: Domen Date: Mon, 19 May 2025 10:35:05 +0200 Subject: [PATCH 4/4] Changed clone schema - Changed to SingleNestedAttribute - Added Defaults, Optional and Required attributes --- docs/resources/vm.md | 8 +++++-- examples/resources/hypercore_vm/resource.tf | 1 + internal/provider/hypercore_vm_resource.go | 23 +++++++++++++++------ 3 files changed, 24 insertions(+), 8 deletions(-) diff --git a/docs/resources/vm.md b/docs/resources/vm.md index 054f86e..6999ad8 100644 --- a/docs/resources/vm.md +++ b/docs/resources/vm.md @@ -67,6 +67,7 @@ resource "hypercore_vm" "myvm" { ssh_authorized_keys = "", ssh_import_id = "", }) + preserve_mac_address = true # User wants to preserve mac address from the source machine (Default is false) } } @@ -102,7 +103,7 @@ output "vm_uuid" { ### Optional - `affinity_strategy` (Object) VM node affinity. (see [below for nested schema](#nestedatt--affinity_strategy)) -- `clone` (Object) Clone options if the VM is being created as a clone. The `source_vm_uuid` is the UUID of the VM used for cloning,
`user_data` and `meta_data` are used for the cloud init data. (see [below for nested schema](#nestedatt--clone)) +- `clone` (Attributes) Clone options if the VM is being created as a clone. The `source_vm_uuid` is the UUID of the VM used for cloning,
`user_data` and `meta_data` are used for the cloud init data. (see [below for nested schema](#nestedatt--clone)) - `description` (String) Description of this VM - `import` (Attributes) Options for importing a VM through a SMB server or some other HTTP location.
Use server, username, password for SMB or http_uri for some other HTTP location. Parameters path and file_name are always **required** (see [below for nested schema](#nestedatt--import)) - `memory` (Number) Memory (RAM) size in `MiB`: If the cloned VM was already created
and it's memory was modified, the cloned VM will be rebooted (either gracefully or forcefully) @@ -127,11 +128,14 @@ Optional: ### Nested Schema for `clone` +Required: + +- `source_vm_uuid` (String) + Optional: - `meta_data` (String) - `preserve_mac_address` (Boolean) -- `source_vm_uuid` (String) - `user_data` (String) diff --git a/examples/resources/hypercore_vm/resource.tf b/examples/resources/hypercore_vm/resource.tf index cc30b8a..aded50a 100644 --- a/examples/resources/hypercore_vm/resource.tf +++ b/examples/resources/hypercore_vm/resource.tf @@ -36,6 +36,7 @@ resource "hypercore_vm" "myvm" { ssh_authorized_keys = "", ssh_import_id = "", }) + preserve_mac_address = true # User wants to preserve mac address from the source machine (Default is false) } } diff --git a/internal/provider/hypercore_vm_resource.go b/internal/provider/hypercore_vm_resource.go index b4abd6c..d30392c 100644 --- a/internal/provider/hypercore_vm_resource.go +++ b/internal/provider/hypercore_vm_resource.go @@ -16,6 +16,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault" "github.com/hashicorp/terraform-plugin-framework/resource/schema/objectdefault" "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" @@ -150,16 +151,26 @@ The provider will currently try to shutdown VM only before VM delete.`, }, }, }, - "clone": schema.ObjectAttribute{ + "clone": schema.SingleNestedAttribute{ MarkdownDescription: "" + "Clone options if the VM is being created as a clone. The `source_vm_uuid` is the UUID of the VM used for cloning,
" + "`user_data` and `meta_data` are used for the cloud init data.", Optional: true, - AttributeTypes: map[string]attr.Type{ - "source_vm_uuid": types.StringType, - "user_data": types.StringType, - "meta_data": types.StringType, - "preserve_mac_address": types.BoolType, + Attributes: map[string]schema.Attribute{ + "source_vm_uuid": schema.StringAttribute{ + Required: true, + }, + "user_data": schema.StringAttribute{ + Optional: true, + }, + "meta_data": schema.StringAttribute{ + Optional: true, + }, + "preserve_mac_address": schema.BoolAttribute{ + Optional: true, + Computed: true, + Default: booldefault.StaticBool(false), + }, }, }, "affinity_strategy": schema.ObjectAttribute{