diff --git a/docs/resources/vm.md b/docs/resources/vm.md index a5d16d5..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,10 +128,14 @@ Optional: ### Nested Schema for `clone` +Required: + +- `source_vm_uuid` (String) + Optional: - `meta_data` (String) -- `source_vm_uuid` (String) +- `preserve_mac_address` (Boolean) - `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 9d391d8..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" @@ -62,9 +63,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 { @@ -149,15 +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, + 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{ @@ -219,16 +232,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/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) diff --git a/internal/utils/vm.go b/internal/utils/vm.go index eabba4d..292cb5f 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,42 @@ 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{} + + 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"], + }) + } + } + } + // 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"]), clonePayload, @@ -180,33 +201,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 {