diff --git a/docs/resources/virtual_environment_vm.md b/docs/resources/virtual_environment_vm.md index 75ea8324..2f5bcf62 100644 --- a/docs/resources/virtual_environment_vm.md +++ b/docs/resources/virtual_environment_vm.md @@ -414,6 +414,9 @@ output "ubuntu_vm_public_key" { - `queues` - (Optional) The number of queues for VirtIO (1..64). - `rate_limit` - (Optional) The rate limit in megabytes per second. - `vlan_id` - (Optional) The VLAN identifier. + - `trunks` - (Optional) String containing a `;` separated list of VLAN trunks + ("10;20;30"). Note that the VLAN-aware feature need to be enabled on the PVE + Linux Bridge to use trunks. - `node_name` - (Required) The name of the node to assign the virtual machine to. - `on_boot` - (Optional) Specifies whether a VM will be started during system diff --git a/example/resource_virtual_environment_trunks.tf b/example/resource_virtual_environment_trunks.tf new file mode 100644 index 00000000..67cb06e0 --- /dev/null +++ b/example/resource_virtual_environment_trunks.tf @@ -0,0 +1,53 @@ +resource "proxmox_virtual_environment_vm" "trunks-example" { + name = "trunks-example" + node_name = data.proxmox_virtual_environment_nodes.example.names[0] + description = "Example of a VM using trunks to pass multiple VLANs on a single network interface." + + disk { + datastore_id = local.datastore_id + file_id = proxmox_virtual_environment_download_file.latest_debian_12_bookworm_qcow2_img.id + interface = "scsi0" + discard = "on" + cache = "writeback" + ssd = true + } + + initialization { + datastore_id = local.datastore_id + interface = "scsi4" + + dns { + servers = ["1.1.1.1", "8.8.8.8"] + } + + ip_config { + ipv4 { + address = "dhcp" + } + } + user_data_file_id = proxmox_virtual_environment_file.user_config.id + vendor_data_file_id = proxmox_virtual_environment_file.vendor_config.id + meta_data_file_id = proxmox_virtual_environment_file.meta_config.id + } + + memory { + dedicated = 1024 + } + + cpu { + cores = 2 + } + + agent { + enabled = true + } + + boot_order = ["scsi0"] + scsi_hardware = "virtio-scsi-pci" + + network_device { + model = "virtio" + bridge = "vmbr0" + trunks = "10;20;30" + } +} \ No newline at end of file diff --git a/fwprovider/tests/resource_vm_test.go b/fwprovider/tests/resource_vm_test.go index bac426d2..8db4976e 100644 --- a/fwprovider/tests/resource_vm_test.go +++ b/fwprovider/tests/resource_vm_test.go @@ -78,13 +78,11 @@ func TestAccResourceVM(t *testing.T) { } func TestAccResourceVMNetwork(t *testing.T) { - t.Skip("This test is hanging up") - tests := []struct { name string step resource.TestStep }{ - {"network interfaces mac", resource.TestStep{ + {"network interfaces", resource.TestStep{ Config: ` resource "proxmox_virtual_environment_file" "cloud_config" { content_type = "snippets" @@ -133,6 +131,7 @@ EOF } network_device { bridge = "vmbr0" + trunks = "10;20;30" } } @@ -144,8 +143,12 @@ EOF overwrite_unmanaged = true }`, Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr("proxmox_virtual_environment_vm.test_vm_network1", "ipv4_addresses.#", "2"), - resource.TestCheckResourceAttr("proxmox_virtual_environment_vm.test_vm_network1", "mac_addresses.#", "2"), + testResourceAttributes("proxmox_virtual_environment_vm.test_vm_network1", map[string]string{ + "ipv4_addresses.#": "2", + "mac_addresses.#": "2", + "network_device.0.bridge": "vmbr0", + "network_device.0.trunks": "10;20;30", + }), ), }}, } diff --git a/proxmoxtf/resource/vm/vm.go b/proxmoxtf/resource/vm/vm.go index 546f15e1..e6fb3962 100644 --- a/proxmoxtf/resource/vm/vm.go +++ b/proxmoxtf/resource/vm/vm.go @@ -100,6 +100,7 @@ const ( dvNetworkDeviceQueues = 0 dvNetworkDeviceRateLimit = 0 dvNetworkDeviceVLANID = 0 + dvNetworkDeviceTrunks = "" dvNetworkDeviceMTU = 0 dvOperatingSystemType = "other" dvPoolID = "" @@ -234,6 +235,7 @@ const ( mkNetworkDeviceQueues = "queues" mkNetworkDeviceRateLimit = "rate_limit" mkNetworkDeviceVLANID = "vlan_id" + mkNetworkDeviceTrunks = "trunks" mkNetworkDeviceMTU = "mtu" mkNetworkInterfaceNames = "network_interface_names" mkNodeName = "node_name" @@ -1129,6 +1131,11 @@ func VM() *schema.Resource { Optional: true, Default: dvNetworkDeviceVLANID, }, + mkNetworkDeviceTrunks: { + Type: schema.TypeString, + Optional: true, + Description: "List of VLAN trunks for the network interface", + }, mkNetworkDeviceMTU: { Type: schema.TypeInt, Description: "Maximum transmission unit (MTU)", @@ -2012,7 +2019,10 @@ func vmCreateClone(ctx context.Context, d *schema.ResourceData, m interface{}) d } if len(networkDevice) > 0 { - updateBody.NetworkDevices = vmGetNetworkDeviceObjects(d) + updateBody.NetworkDevices, err = vmGetNetworkDeviceObjects(d) + if err != nil { + return diag.FromErr(err) + } for i := 0; i < len(updateBody.NetworkDevices); i++ { if !updateBody.NetworkDevices[i].Enabled { @@ -2402,7 +2412,10 @@ func vmCreateCustom(ctx context.Context, d *schema.ResourceData, m interface{}) name := d.Get(mkName).(string) tags := d.Get(mkTags).([]interface{}) - networkDeviceObjects := vmGetNetworkDeviceObjects(d) + networkDeviceObjects, err := vmGetNetworkDeviceObjects(d) + if err != nil { + return diag.FromErr(err) + } nodeName := d.Get(mkNodeName).(string) @@ -3025,7 +3038,7 @@ func vmGetHostUSBDeviceObjects(d *schema.ResourceData) vms.CustomUSBDevices { return usbDeviceObjects } -func vmGetNetworkDeviceObjects(d *schema.ResourceData) vms.CustomNetworkDevices { +func vmGetNetworkDeviceObjects(d *schema.ResourceData) (vms.CustomNetworkDevices, error) { networkDevice := d.Get(mkNetworkDevice).([]interface{}) networkDeviceObjects := make(vms.CustomNetworkDevices, len(networkDevice)) @@ -3040,6 +3053,7 @@ func vmGetNetworkDeviceObjects(d *schema.ResourceData) vms.CustomNetworkDevices queues := block[mkNetworkDeviceQueues].(int) rateLimit := block[mkNetworkDeviceRateLimit].(float64) vlanID := block[mkNetworkDeviceVLANID].(int) + trunks := block[mkNetworkDeviceTrunks].(string) mtu := block[mkNetworkDeviceMTU].(int) device := vms.CustomNetworkDevice{ @@ -3068,6 +3082,23 @@ func vmGetNetworkDeviceObjects(d *schema.ResourceData) vms.CustomNetworkDevices device.Tag = &vlanID } + if trunks != "" { + splitTrunks := strings.Split(trunks, ";") + + var trunksAsInt []int + + for _, numStr := range splitTrunks { + num, err := strconv.Atoi(numStr) + if err != nil { + return nil, fmt.Errorf("error parsing trunks: %w", err) + } + + trunksAsInt = append(trunksAsInt, num) + } + + device.Trunks = trunksAsInt + } + if mtu != 0 { device.MTU = &mtu } @@ -3075,7 +3106,7 @@ func vmGetNetworkDeviceObjects(d *schema.ResourceData) vms.CustomNetworkDevices networkDeviceObjects[i] = device } - return networkDeviceObjects + return networkDeviceObjects, nil } func vmGetSerialDeviceList(d *schema.ResourceData) vms.CustomSerialDevices { @@ -4123,6 +4154,13 @@ func vmReadCustom( networkDevice[mkNetworkDeviceVLANID] = 0 } + if nd.Trunks != nil { + networkDevice[mkNetworkDeviceTrunks] = strings.Trim( + strings.Join(strings.Fields(fmt.Sprint(nd.Trunks)), ";"), "[]") + } else { + networkDevice[mkNetworkDeviceTrunks] = "" + } + if nd.MTU != nil { networkDevice[mkNetworkDeviceMTU] = nd.MTU } else { @@ -5148,7 +5186,7 @@ func vmUpdate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.D // Prepare the new memory configuration. if d.HasChange(mkMemory) { - memoryBlock, err := structure.GetSchemaBlock( + memoryBlock, er := structure.GetSchemaBlock( resource, d, []string{mkMemory}, @@ -5156,7 +5194,7 @@ func vmUpdate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.D true, ) if err != nil { - return diag.FromErr(err) + return diag.FromErr(er) } memoryDedicated := memoryBlock[mkMemoryDedicated].(int) @@ -5180,7 +5218,10 @@ func vmUpdate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.D // Prepare the new network device configuration. if d.HasChange(mkNetworkDevice) { - updateBody.NetworkDevices = vmGetNetworkDeviceObjects(d) + updateBody.NetworkDevices, err = vmGetNetworkDeviceObjects(d) + if err != nil { + return diag.FromErr(err) + } for i := 0; i < len(updateBody.NetworkDevices); i++ { if !updateBody.NetworkDevices[i].Enabled {