diff --git a/proxmox/resource_vm_qemu.go b/proxmox/resource_vm_qemu.go old mode 100755 new mode 100644 index 1d7fde38..cb6bb13b --- a/proxmox/resource_vm_qemu.go +++ b/proxmox/resource_vm_qemu.go @@ -89,6 +89,7 @@ func resourceVmQemu() *schema.Resource { }, ), reboot.CustomizeDiff(), + validateVGAAndSerialDiff, ), Schema: map[string]*schema.Schema{ @@ -1521,3 +1522,56 @@ func mapToSDK_QemuGuestAgent(d *schema.ResourceData) *pveSDK.QemuGuestAgent { Enable: &tmpEnable, } } + +// validateVGAAndSerialDiff is a CustomizeDiff function that validates VGA and Serial +// configuration during terraform plan phase. +func validateVGAAndSerialDiff(ctx context.Context, d *schema.ResourceDiff, meta interface{}) error { + // Safely get VGA configuration + vgaRaw, vgaExists := d.GetOk("vga") + if !vgaExists { + return nil // No VGA configured, validation not needed + } + + // VGA is stored as a Set in the schema + vgaSet, ok := vgaRaw.(*schema.Set) + if !ok { + return nil // Invalid type, skip validation + } + + vgaList := vgaSet.List() + if len(vgaList) == 0 { + return nil // Empty VGA list, validation not needed + } + + // Extract VGA configuration + vgaMap, ok := vgaList[0].(map[string]interface{}) + if !ok { + return nil // Invalid format, skip validation + } + + // Check if VGA type is "serial" + vgaType, ok := vgaMap["type"].(string) + if !ok || vgaType != "serial" { + return nil // Not serial type, validation not needed + } + + // VGA type is "serial", now validate that serial devices exist + serialRaw, serialExists := d.GetOk("serial") + if !serialExists { + return fmt.Errorf("when vga.type is set to 'serial', at least one serial device must be configured in the 'serial' block") + } + + // Serial is stored as a Set in the schema + serialSet, ok := serialRaw.(*schema.Set) + if !ok || serialSet.Len() == 0 { + return fmt.Errorf("when vga.type is set to 'serial', at least one serial device must be configured in the 'serial' block") + } + + // Verify at least one serial device is properly configured + serialList := serialSet.List() + if len(serialList) == 0 { + return fmt.Errorf("when vga.type is set to 'serial', at least one serial device must be configured in the 'serial' block") + } + + return nil +} diff --git a/proxmox/resource_vm_qemu_test.go b/proxmox/resource_vm_qemu_test.go index f57fc230..2724604a 100644 --- a/proxmox/resource_vm_qemu_test.go +++ b/proxmox/resource_vm_qemu_test.go @@ -398,3 +398,235 @@ func TestAccProxmoxVmQemu_UpdateRebootRequired(t *testing.T) { }, }) } + +func TestValidateVGAAndSerial(t *testing.T) { + tests := []struct { + name string + vgaConfig []interface{} + serialConfig []interface{} + expectError bool + errorMsg string + }{ + { + name: "valid serial0 with matching serial device", + vgaConfig: []interface{}{ + map[string]interface{}{ + "type": "serial0", + }, + }, + serialConfig: []interface{}{ + map[string]interface{}{ + "id": 0, + "type": "socket", + }, + }, + expectError: false, + }, + { + name: "serial0 without matching serial device - should fail", + vgaConfig: []interface{}{ + map[string]interface{}{ + "type": "serial0", + }, + }, + serialConfig: []interface{}{}, + expectError: true, + errorMsg: "vga type 'serial0' requires serial device with id=0 to be defined", + }, + { + name: "serial1 with matching serial device", + vgaConfig: []interface{}{ + map[string]interface{}{ + "type": "serial1", + }, + }, + serialConfig: []interface{}{ + map[string]interface{}{ + "id": 1, + "type": "socket", + }, + }, + expectError: false, + }, + { + name: "serial1 without matching serial device - should fail", + vgaConfig: []interface{}{ + map[string]interface{}{ + "type": "serial1", + }, + }, + serialConfig: []interface{}{ + map[string]interface{}{ + "id": 0, + "type": "socket", + }, + }, + expectError: true, + errorMsg: "vga type 'serial1' requires serial device with id=1 to be defined", + }, + { + name: "serial2 with matching serial device", + vgaConfig: []interface{}{ + map[string]interface{}{ + "type": "serial2", + }, + }, + serialConfig: []interface{}{ + map[string]interface{}{ + "id": 2, + "type": "socket", + }, + }, + expectError: false, + }, + { + name: "serial3 with matching serial device", + vgaConfig: []interface{}{ + map[string]interface{}{ + "type": "serial3", + }, + }, + serialConfig: []interface{}{ + map[string]interface{}{ + "id": 3, + "type": "socket", + }, + }, + expectError: false, + }, + { + name: "non-serial vga type - should pass", + vgaConfig: []interface{}{ + map[string]interface{}{ + "type": "std", + }, + }, + serialConfig: []interface{}{}, + expectError: false, + }, + { + name: "cirrus vga type - should pass", + vgaConfig: []interface{}{ + map[string]interface{}{ + "type": "cirrus", + }, + }, + serialConfig: []interface{}{}, + expectError: false, + }, + { + name: "vmware vga type - should pass", + vgaConfig: []interface{}{ + map[string]interface{}{ + "type": "vmware", + }, + }, + serialConfig: []interface{}{}, + expectError: false, + }, + { + name: "no vga config - should pass", + vgaConfig: []interface{}{}, + serialConfig: []interface{}{}, + expectError: false, + }, + { + name: "serial0 with multiple serial devices including correct one", + vgaConfig: []interface{}{ + map[string]interface{}{ + "type": "serial0", + }, + }, + serialConfig: []interface{}{ + map[string]interface{}{ + "id": 1, + "type": "socket", + }, + map[string]interface{}{ + "id": 0, + "type": "socket", + }, + map[string]interface{}{ + "id": 2, + "type": "socket", + }, + }, + expectError: false, + }, + { + name: "serial0 with multiple serial devices but no matching id", + vgaConfig: []interface{}{ + map[string]interface{}{ + "type": "serial0", + }, + }, + serialConfig: []interface{}{ + map[string]interface{}{ + "id": 1, + "type": "socket", + }, + map[string]interface{}{ + "id": 2, + "type": "socket", + }, + }, + expectError: true, + errorMsg: "vga type 'serial0' requires serial device with id=0 to be defined", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Create a mock ResourceData + d := schema.TestResourceDataRaw(t, map[string]*schema.Schema{ + "vga": { + Type: schema.TypeList, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "type": { + Type: schema.TypeString, + Optional: true, + }, + }, + }, + }, + "serial": { + Type: schema.TypeList, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "id": { + Type: schema.TypeInt, + Required: true, + }, + "type": { + Type: schema.TypeString, + Optional: true, + }, + }, + }, + }, + }, map[string]interface{}{ + "vga": tt.vgaConfig, + "serial": tt.serialConfig, + }) + + // Call the validation function + err := validateVGAAndSerial(d) + + // Check the result + if tt.expectError { + if err == nil { + t.Errorf("expected error but got none") + } else if tt.errorMsg != "" && err.Error() != tt.errorMsg { + t.Errorf("expected error message '%s' but got '%s'", tt.errorMsg, err.Error()) + } + } else { + if err != nil { + t.Errorf("expected no error but got: %v", err) + } + } + }) + } +}