From 7344300126fe85668e7efe8c7547324c350da5b3 Mon Sep 17 00:00:00 2001 From: Dan Petersen Date: Tue, 31 Dec 2019 01:00:53 +0100 Subject: [PATCH] Add vga argument to VM resource --- CHANGELOG.md | 6 + README.md | 16 ++ proxmox/virtual_environment_vm_types.go | 52 ++++++- proxmoxtf/resource_virtual_environment_vm.go | 143 ++++++++++++++++++ .../resource_virtual_environment_vm_test.go | 18 +++ proxmoxtf/utils.go | 35 +++++ 6 files changed, 265 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6d4da919..412ab1d9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## 0.2.0 (UNRELEASED) + +ENHANCEMENTS: + +resource/virtual_environment_vm: Add `vga` argument + ## 0.1.0 FEATURES: diff --git a/README.md b/README.md index 2418c934..393836de 100644 --- a/README.md +++ b/README.md @@ -433,6 +433,22 @@ This resource doesn't expose any additional attributes. * `wxp` - Windows XP * `pool_id` - (Optional) The ID of a pool to assign the virtual machine to * `started` - (Optional) Whether to start the virtual machine (defaults to `true`) +* `vga` - (Optional) The VGA configuration + * `enabled` - (Optional) Whether to enable the VGA device (defaults to `true`) + * `memory` - (Optional) The VGA memory in megabytes (4-512 MB) + * `type` - (Optional) The VGA type + * `cirrus` - Cirrus (deprecated since QEMU 2.2) + * `qxl` - SPICE + * `qxl2` - SPICE Dual Monitor + * `qxl3` - SPICE Triple Monitor + * `qxl4` - SPICE Quad Monitor + * `serial0` - Serial Terminal 0 + * `serial1` - Serial Terminal 1 + * `serial2` - Serial Terminal 2 + * `serial3` - Serial Terminal 3 + * `std` - Standard VGA + * `virtio` - VirtIO-GPU + * `vmware` - VMware Compatible * `vm_id` - (Optional) The ID ###### Attributes diff --git a/proxmox/virtual_environment_vm_types.go b/proxmox/virtual_environment_vm_types.go index 22e3553d..6da1f8d0 100644 --- a/proxmox/virtual_environment_vm_types.go +++ b/proxmox/virtual_environment_vm_types.go @@ -165,8 +165,8 @@ type CustomUSBDevices []CustomUSBDevice // CustomVGADevice handles QEMU VGA device parameters. type CustomVGADevice struct { - Memory *int `json:"memory,omitempty" url:"memory,omitempty"` - Type string `json:"type" url:"type"` + Memory *int `json:"memory,omitempty" url:"memory,omitempty"` + Type *string `json:"type,omitempty" url:"type,omitempty"` } // CustomVirtualIODevice handles QEMU VirtIO device parameters. @@ -950,14 +950,16 @@ func (r CustomUSBDevices) EncodeValues(key string, v *url.Values) error { // EncodeValues converts a CustomVGADevice struct to a URL vlaue. func (r CustomVGADevice) EncodeValues(key string, v *url.Values) error { - values := []string{ - fmt.Sprintf("type=%s", r.Type), - } + values := []string{} if r.Memory != nil { values = append(values, fmt.Sprintf("memory=%d", *r.Memory)) } + if r.Type != nil { + values = append(values, fmt.Sprintf("type=%s", *r.Type)) + } + v.Add(key, strings.Join(values, ",")) return nil @@ -1355,3 +1357,43 @@ func (r *CustomStorageDevice) UnmarshalJSON(b []byte) error { return nil } + +// UnmarshalJSON converts a CustomVGADevice string to an object. +func (r *CustomVGADevice) UnmarshalJSON(b []byte) error { + var s string + + err := json.Unmarshal(b, &s) + + if err != nil { + return err + } + + if s == "" { + return nil + } + + pairs := strings.Split(s, ",") + + for _, p := range pairs { + v := strings.Split(strings.TrimSpace(p), "=") + + if len(v) == 1 { + r.Type = &v[0] + } else if len(v) == 2 { + switch v[0] { + case "memory": + m, err := strconv.Atoi(v[1]) + + if err != nil { + return err + } + + r.Memory = &m + case "type": + r.Type = &v[1] + } + } + } + + return nil +} diff --git a/proxmoxtf/resource_virtual_environment_vm.go b/proxmoxtf/resource_virtual_environment_vm.go index c2e068ae..2e73f0b2 100644 --- a/proxmoxtf/resource_virtual_environment_vm.go +++ b/proxmoxtf/resource_virtual_environment_vm.go @@ -51,6 +51,9 @@ const ( dvResourceVirtualEnvironmentVMOSType = "other" dvResourceVirtualEnvironmentVMPoolID = "" dvResourceVirtualEnvironmentVMStarted = true + dvResourceVirtualEnvironmentVMVGAEnabled = true + dvResourceVirtualEnvironmentVMVGAMemory = 0 + dvResourceVirtualEnvironmentVMVGAType = "std" dvResourceVirtualEnvironmentVMVMID = -1 mkResourceVirtualEnvironmentVMAgent = "agent" @@ -112,6 +115,10 @@ const ( mkResourceVirtualEnvironmentVMOSType = "os_type" mkResourceVirtualEnvironmentVMPoolID = "pool_id" mkResourceVirtualEnvironmentVMStarted = "started" + mkResourceVirtualEnvironmentVMVGA = "vga" + mkResourceVirtualEnvironmentVMVGAEnabled = "enabled" + mkResourceVirtualEnvironmentVMVGAMemory = "memory" + mkResourceVirtualEnvironmentVMVGAType = "type" mkResourceVirtualEnvironmentVMVMID = "vm_id" ) @@ -669,6 +676,49 @@ func resourceVirtualEnvironmentVM() *schema.Resource { Optional: true, Default: dvResourceVirtualEnvironmentVMStarted, }, + mkResourceVirtualEnvironmentVMVGA: &schema.Schema{ + Type: schema.TypeList, + Description: "The VGA configuration", + Optional: true, + DefaultFunc: func() (interface{}, error) { + defaultList := make([]interface{}, 1) + defaultMap := map[string]interface{}{} + + defaultMap[mkResourceVirtualEnvironmentVMVGAEnabled] = dvResourceVirtualEnvironmentVMVGAEnabled + defaultMap[mkResourceVirtualEnvironmentVMVGAMemory] = dvResourceVirtualEnvironmentVMVGAMemory + defaultMap[mkResourceVirtualEnvironmentVMVGAType] = dvResourceVirtualEnvironmentVMVGAType + + defaultList[0] = defaultMap + + return defaultList, nil + }, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + mkResourceVirtualEnvironmentVMVGAEnabled: { + Type: schema.TypeBool, + Description: "Whether to enable the VGA device", + Optional: true, + Default: dvResourceVirtualEnvironmentVMVGAEnabled, + }, + mkResourceVirtualEnvironmentVMVGAMemory: { + Type: schema.TypeInt, + Description: "The VGA memory in megabytes (4-512 MB)", + Optional: true, + Default: dvResourceVirtualEnvironmentVMVGAMemory, + ValidateFunc: getVGAMemoryValidator(), + }, + mkResourceVirtualEnvironmentVMVGAType: { + Type: schema.TypeString, + Description: "The VGA type", + Optional: true, + Default: dvResourceVirtualEnvironmentVMVGAType, + ValidateFunc: getVGATypeValidator(), + }, + }, + }, + MaxItems: 1, + MinItems: 0, + }, mkResourceVirtualEnvironmentVMVMID: { Type: schema.TypeInt, Description: "The VM identifier", @@ -769,6 +819,13 @@ func resourceVirtualEnvironmentVMCreate(d *schema.ResourceData, m interface{}) e osType := d.Get(mkResourceVirtualEnvironmentVMOSType).(string) poolID := d.Get(mkResourceVirtualEnvironmentVMPoolID).(string) started := proxmox.CustomBool(d.Get(mkResourceVirtualEnvironmentVMStarted).(bool)) + + vgaDevice, err := resourceVirtualEnvironmentVMGetVGADeviceObject(d, m) + + if err != nil { + return err + } + vmID := d.Get(mkResourceVirtualEnvironmentVMVMID).(int) if vmID == -1 { @@ -840,6 +897,7 @@ func resourceVirtualEnvironmentVMCreate(d *schema.ResourceData, m interface{}) e SharedMemory: memorySharedObject, StartOnBoot: &started, TabletDeviceEnabled: &tabletDeviceEnabled, + VGADevice: vgaDevice, VMID: &vmID, } @@ -1228,6 +1286,38 @@ func resourceVirtualEnvironmentVMGetNetworkDeviceObjects(d *schema.ResourceData, return networkDeviceObjects, nil } +func resourceVirtualEnvironmentVMGetVGADeviceObject(d *schema.ResourceData, m interface{}) (*proxmox.CustomVGADevice, error) { + resource := resourceVirtualEnvironmentVM() + + vgaBlock, err := getSchemaBlock(resource, d, m, []string{mkResourceVirtualEnvironmentVMVGA}, 0, true) + + if err != nil { + return nil, err + } + + vgaEnabled := proxmox.CustomBool(vgaBlock[mkResourceVirtualEnvironmentVMAgentEnabled].(bool)) + vgaMemory := vgaBlock[mkResourceVirtualEnvironmentVMVGAMemory].(int) + vgaType := vgaBlock[mkResourceVirtualEnvironmentVMVGAType].(string) + + vgaDevice := &proxmox.CustomVGADevice{} + + if vgaEnabled { + if vgaMemory > 0 { + vgaDevice.Memory = &vgaMemory + } + + vgaDevice.Type = &vgaType + } else { + vgaType = "none" + + vgaDevice = &proxmox.CustomVGADevice{ + Type: &vgaType, + } + } + + return vgaDevice, nil +} + func resourceVirtualEnvironmentVMRead(d *schema.ResourceData, m interface{}) error { config := m.(providerConfiguration) veClient, err := config.GetVEClient() @@ -1715,6 +1805,48 @@ func resourceVirtualEnvironmentVMRead(d *schema.ResourceData, m interface{}) err d.Set(mkResourceVirtualEnvironmentVMPoolID, *vmConfig.PoolID) } + // Compare the VGA configuration to the one stored in the state. + vga := map[string]interface{}{} + + if vmConfig.VGADevice != nil { + vgaEnabled := true + + if vmConfig.VGADevice.Type != nil { + vgaEnabled = *vmConfig.VGADevice.Type != "none" + } + + vga[mkResourceVirtualEnvironmentVMVGAEnabled] = vgaEnabled + + if vmConfig.VGADevice.Memory != nil { + vga[mkResourceVirtualEnvironmentVMVGAMemory] = *vmConfig.VGADevice.Memory + } else { + vga[mkResourceVirtualEnvironmentVMVGAMemory] = dvResourceVirtualEnvironmentVMVGAMemory + } + + if vgaEnabled { + if vmConfig.VGADevice.Type != nil { + vga[mkResourceVirtualEnvironmentVMVGAType] = *vmConfig.VGADevice.Type + } else { + vga[mkResourceVirtualEnvironmentVMVGAType] = dvResourceVirtualEnvironmentVMVGAType + } + } + } else { + vga[mkResourceVirtualEnvironmentVMVGAEnabled] = true + vga[mkResourceVirtualEnvironmentVMVGAMemory] = dvResourceVirtualEnvironmentVMVGAMemory + vga[mkResourceVirtualEnvironmentVMVGAType] = dvResourceVirtualEnvironmentVMVGAType + } + + currentVGA := d.Get(mkResourceVirtualEnvironmentVMVGA).([]interface{}) + + if len(currentVGA) > 0 || + vga[mkResourceVirtualEnvironmentVMVGAEnabled] != dvResourceVirtualEnvironmentVMVGAEnabled || + vga[mkResourceVirtualEnvironmentVMVGAMemory] != dvResourceVirtualEnvironmentVMVGAMemory || + vga[mkResourceVirtualEnvironmentVMVGAType] != dvResourceVirtualEnvironmentVMVGAType { + d.Set(mkResourceVirtualEnvironmentVMVGA, []interface{}{vga}) + } else { + d.Set(mkResourceVirtualEnvironmentVMVGA, make([]interface{}, 0)) + } + // Determine the state of the virtual machine in order to update the "started" argument. status, err := veClient.GetVMStatus(nodeName, vmID) @@ -2006,6 +2138,17 @@ func resourceVirtualEnvironmentVMUpdate(d *schema.ResourceData, m interface{}) e rebootRequired = true } + // Prepare the new VGA configuration. + if d.HasChange(mkResourceVirtualEnvironmentVMVGA) { + body.VGADevice, err = resourceVirtualEnvironmentVMGetVGADeviceObject(d, m) + + if err != nil { + return err + } + + rebootRequired = true + } + // Update the configuration now that everything has been prepared. err = veClient.UpdateVM(nodeName, vmID, body) diff --git a/proxmoxtf/resource_virtual_environment_vm_test.go b/proxmoxtf/resource_virtual_environment_vm_test.go index 87876140..e0de5f32 100644 --- a/proxmoxtf/resource_virtual_environment_vm_test.go +++ b/proxmoxtf/resource_virtual_environment_vm_test.go @@ -327,4 +327,22 @@ func TestResourceVirtualEnvironmentVMSchema(t *testing.T) { schema.TypeFloat, schema.TypeInt, }) + + vgaSchema := testNestedSchemaExistence(t, s, mkResourceVirtualEnvironmentVMVGA) + + testOptionalArguments(t, vgaSchema, []string{ + mkResourceVirtualEnvironmentVMVGAEnabled, + mkResourceVirtualEnvironmentVMVGAMemory, + mkResourceVirtualEnvironmentVMVGAType, + }) + + testSchemaValueTypes(t, vgaSchema, []string{ + mkResourceVirtualEnvironmentVMVGAEnabled, + mkResourceVirtualEnvironmentVMVGAMemory, + mkResourceVirtualEnvironmentVMVGAType, + }, []schema.ValueType{ + schema.TypeBool, + schema.TypeInt, + schema.TypeString, + }) } diff --git a/proxmoxtf/utils.go b/proxmoxtf/utils.go index d02db38d..086309d5 100644 --- a/proxmoxtf/utils.go +++ b/proxmoxtf/utils.go @@ -175,6 +175,41 @@ func getSchemaBlock(r *schema.Resource, d *schema.ResourceData, m interface{}, k return resourceBlock, nil } +func getVGAMemoryValidator() schema.SchemaValidateFunc { + return func(i interface{}, k string) ([]string, []error) { + v, ok := i.(int) + + if !ok { + return []string{}, []error{fmt.Errorf("expected type of %s to be []interface{}", k)} + } + + if v == 0 { + return []string{}, []error{} + } + + validator := validation.IntBetween(4, 512) + + return validator(i, k) + } +} + +func getVGATypeValidator() schema.SchemaValidateFunc { + return validation.StringInSlice([]string{ + "cirrus", + "qxl", + "qxl2", + "qxl3", + "qxl4", + "serial0", + "serial1", + "serial2", + "serial3", + "std", + "virtio", + "vmware", + }, false) +} + func getVLANIDsValidator() schema.SchemaValidateFunc { return func(i interface{}, k string) (ws []string, es []error) { min := 1