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
42 changes: 42 additions & 0 deletions proxmox/resource_vm_qemu.go
Original file line number Diff line number Diff line change
Expand Up @@ -612,6 +612,10 @@ func resourceVmQemuCreate(ctx context.Context, d *schema.ResourceData, meta inte
config.EFIDisk = qemuEfiDisks[0]
}

if err := validateVGAAndSerial(d); err != nil {
return diag.FromErr(err)
}

var vmr *pveSDK.VmRef
if guestID := vmID.Get(d); guestID != 0 { // Manually set vmID
log.Print("[DEBUG][QemuVmCreate] checking if vmId: " + guestID.String() + " already exists")
Expand Down Expand Up @@ -871,6 +875,10 @@ func resourceVmQemuUpdate(ctx context.Context, d *schema.ResourceData, meta inte
return diags
}

if err := validateVGAAndSerial(d); err != nil {
return diag.FromErr(err)
}

logger.Debug().Int(vmID.Root, int(resourceID.ID)).Msgf("Updating VM with the following configuration: %+v", config)

var rebootRequired bool
Expand Down Expand Up @@ -1521,3 +1529,37 @@ func mapToSDK_QemuGuestAgent(d *schema.ResourceData) *pveSDK.QemuGuestAgent {
Enable: &tmpEnable,
}
}

func validateVGAAndSerial(d *schema.ResourceData) error {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My advise would be to implement the logic by parsing the config defined by:

config := pveSDK.ConfigQemu{

// Get VGA configuration
vgaList := d.Get("vga").([]interface{})
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This panics when "vga" is not defined.


if len(vgaList) > 0 {
vga := vgaList[0].(map[string]interface{})
vgaType := vga["type"].(string)

// Check if vga type is serial0, serial1, serial2, or serial3
if strings.HasPrefix(vgaType, "serial") {
// Extract serial number (0-3)
serialNum := strings.TrimPrefix(vgaType, "serial")

// Check if corresponding serial device is defined
serialList := d.Get("serial").([]interface{})
serialFound := false

for _, s := range serialList {
serial := s.(map[string]interface{})
if fmt.Sprintf("%d", serial["id"].(int)) == serialNum {
serialFound = true
break
}
}

if !serialFound {
return fmt.Errorf("vga type '%s' requires serial device with id=%s to be defined", vgaType, serialNum)
}
}
}

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)
}
}
})
}
}