Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 54 additions & 0 deletions proxmox/resource_vm_qemu.go
100755 → 100644
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ func resourceVmQemu() *schema.Resource {
},
),
reboot.CustomizeDiff(),
validateVGAAndSerialDiff,
),

Schema: map[string]*schema.Schema{
Expand Down Expand Up @@ -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
}
232 changes: 232 additions & 0 deletions proxmox/resource_vm_qemu_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
})
}
}