From d226b59e2e37ffebc737e27cc9bb0182d4bda993 Mon Sep 17 00:00:00 2001 From: Pavel Boldyrev <627562+bpg@users.noreply.github.com> Date: Mon, 30 Sep 2024 11:14:55 -0400 Subject: [PATCH] feat(vm): add support for `watchdog` (#1556) Signed-off-by: Pavel Boldyrev <627562+bpg@users.noreply.github.com> --- docs/resources/virtual_environment_vm.md | 12 ++ fwprovider/test/resource_vm_test.go | 56 ++++++ proxmox/nodes/vms/custom_watchdog_device.go | 2 +- proxmoxtf/resource/vm/vm.go | 178 ++++++++++++++++++++ 4 files changed, 247 insertions(+), 1 deletion(-) diff --git a/docs/resources/virtual_environment_vm.md b/docs/resources/virtual_environment_vm.md index 39997a22..fb8d750b 100644 --- a/docs/resources/virtual_environment_vm.md +++ b/docs/resources/virtual_environment_vm.md @@ -555,6 +555,18 @@ output "ubuntu_vm_public_key" { - `clipboard` - (Optional) Enable VNC clipboard by setting to `vnc`. See the [Proxmox documentation](https://pve.proxmox.com/pve-docs/pve-admin-guide.html#qm_virtual_machines_settings) section 10.2.8 for more information. - `vm_id` - (Optional) The VM identifier. - `hook_script_file_id` - (Optional) The identifier for a file containing a hook script (needs to be executable, e.g. by using the `proxmox_virtual_environment_file.file_mode` attribute). +- `watchdog` - (Optional) The watchdog configuration. Once enabled (by a guest action), the watchdog must be periodically polled by an agent inside the guest or else the watchdog will reset the guest (or execute the respective action specified). + - `enabled` - (Optional) Whether the watchdog is enabled (defaults to `false`). + - `model` - (Optional) The watchdog type to emulate (defaults to `i6300esb`). + - `i6300esb` - Intel 6300ESB. + - `ib700` - iBase IB700. + - `action` - (Optional) The action to perform if after activation the guest fails to poll the watchdog in time (defaults to `none`). + - `debug` + - `none` + - `pause` + - `poweroff` + - `reset` + - `shutdown` ## Attribute Reference diff --git a/fwprovider/test/resource_vm_test.go b/fwprovider/test/resource_vm_test.go index e0b4a357..3e4bf2da 100644 --- a/fwprovider/test/resource_vm_test.go +++ b/fwprovider/test/resource_vm_test.go @@ -217,6 +217,62 @@ func TestAccResourceVM(t *testing.T) { ), }}, }, + { + "update watchdog block", []resource.TestStep{{ + Config: te.RenderConfig(` + resource "proxmox_virtual_environment_vm" "test_vm" { + node_name = "{{.NodeName}}" + started = false + + watchdog { + enabled = "true" + } + }`), + Check: resource.ComposeTestCheckFunc( + ResourceAttributes("proxmox_virtual_environment_vm.test_vm", map[string]string{ + "watchdog.0.model": "i6300esb", + "watchdog.0.action": "none", + }), + ), + }, { + Config: te.RenderConfig(` + resource "proxmox_virtual_environment_vm" "test_vm" { + node_name = "{{.NodeName}}" + started = false + + watchdog { + enabled = "true" + model = "ib700" + action = "reset" + } + }`), + Check: resource.ComposeTestCheckFunc( + ResourceAttributes("proxmox_virtual_environment_vm.test_vm", map[string]string{ + "watchdog.0.model": "ib700", + "watchdog.0.action": "reset", + }), + ), + }, { + Config: te.RenderConfig(` + resource "proxmox_virtual_environment_vm" "test_vm" { + node_name = "{{.NodeName}}" + started = false + + watchdog { + enabled = "false" + model = "ib700" + action = "reset" + } + }`), + Check: resource.ComposeTestCheckFunc( + ResourceAttributes("proxmox_virtual_environment_vm.test_vm", map[string]string{ + "watchdog.0.enabled": "false", + "watchdog.0.model": "ib700", + "watchdog.0.action": "reset", + }), + ), + }}, + }, } for _, tt := range tests { diff --git a/proxmox/nodes/vms/custom_watchdog_device.go b/proxmox/nodes/vms/custom_watchdog_device.go index b1b5071a..48e54098 100644 --- a/proxmox/nodes/vms/custom_watchdog_device.go +++ b/proxmox/nodes/vms/custom_watchdog_device.go @@ -22,7 +22,7 @@ type CustomWatchdogDevice struct { // EncodeValues converts a CustomWatchdogDevice struct to a URL value. func (r *CustomWatchdogDevice) EncodeValues(key string, v *url.Values) error { values := []string{ - fmt.Sprintf("model=%+v", r.Model), + fmt.Sprintf("model=%s", *r.Model), } if r.Action != nil { diff --git a/proxmoxtf/resource/vm/vm.go b/proxmoxtf/resource/vm/vm.go index 72e11513..ef9f1783 100644 --- a/proxmoxtf/resource/vm/vm.go +++ b/proxmoxtf/resource/vm/vm.go @@ -129,6 +129,8 @@ const ( dvSCSIHardware = "virtio-scsi-pci" dvStopOnDestroy = false dvHookScript = "" + dvWatchdogModel = "i6300esb" + dvWatchdogAction = "none" maxResourceVirtualEnvironmentVMAudioDevices = 1 maxResourceVirtualEnvironmentVMSerialDevices = 4 @@ -278,6 +280,11 @@ const ( mkSCSIHardware = "scsi_hardware" mkHookScriptFileID = "hook_script_file_id" mkStopOnDestroy = "stop_on_destroy" + mkWatchdog = "watchdog" + // a workaround for the lack of proper support of default and undefined values in SDK. + mkWatchdogEnabled = "enabled" + mkWatchdogModel = "model" + mkWatchdogAction = "action" ) // VM returns a resource that manages VMs. @@ -1457,6 +1464,56 @@ func VM() *schema.Resource { Optional: true, Default: dvStopOnDestroy, }, + mkWatchdog: { + Type: schema.TypeList, + Description: "The watchdog configuration", + Optional: true, + DefaultFunc: func() (interface{}, error) { + return []interface{}{ + map[string]interface{}{ + mkWatchdogAction: dvWatchdogAction, + mkWatchdogEnabled: false, + mkWatchdogModel: dvWatchdogModel, + }, + }, nil + }, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + mkWatchdogAction: { + Type: schema.TypeString, + Description: "The watchdog action", + Optional: true, + Default: dvWatchdogAction, + ValidateDiagFunc: validation.ToDiagFunc(validation.StringInSlice([]string{ + "debug", + "none", + "pause", + "poweroff", + "reset", + "shutdown", + }, true)), + }, + mkWatchdogEnabled: { + Type: schema.TypeBool, + Description: "Whether the watchdog is enabled", + Optional: true, + Default: false, + }, + mkWatchdogModel: { + Type: schema.TypeString, + Description: "The watchdog model", + Optional: true, + Default: dvWatchdogModel, + ValidateDiagFunc: validation.ToDiagFunc(validation.StringInSlice([]string{ + "i6300esb", + "ib700", + }, true)), + }, + }, + }, + MaxItems: 1, + MinItems: 0, + }, } structure.MergeSchema(s, disk.Schema()) @@ -1801,6 +1858,7 @@ func vmCreateClone(ctx context.Context, d *schema.ResourceData, m interface{}) d protection := types.CustomBool(d.Get(mkProtection).(bool)) template := types.CustomBool(d.Get(mkTemplate).(bool)) vga := d.Get(mkVGA).([]interface{}) + watchdog := d.Get(mkWatchdog).([]interface{}) updateBody := &vms.UpdateRequestBody{ AudioDevices: audioDevices, @@ -2075,6 +2133,23 @@ func vmCreateClone(ctx context.Context, d *schema.ResourceData, m interface{}) d del = append(del, "hookscript") } + if len(watchdog) > 0 && watchdog[0] != nil { + watchdogBlock := watchdog[0].(map[string]interface{}) + + watchdogEnabled := types.CustomBool( + watchdogBlock[mkWatchdogEnabled].(bool), + ) + if watchdogEnabled { + watchdogAction := watchdogBlock[mkWatchdogAction].(string) + watchdogModel := watchdogBlock[mkWatchdogModel].(string) + + updateBody.WatchdogDevice = &vms.CustomWatchdogDevice{ + Action: &watchdogAction, + Model: &watchdogModel, + } + } + } + updateBody.Delete = del e = vmAPI.UpdateVM(ctx, updateBody) @@ -2521,6 +2596,32 @@ func vmCreateCustom(ctx context.Context, d *schema.ResourceData, m interface{}) scsiHardware := d.Get(mkSCSIHardware).(string) + var watchdogObject *vms.CustomWatchdogDevice + + watchdogBlock, err := structure.GetSchemaBlock( + resource, + d, + []string{mkWatchdog}, + 0, + true, + ) + if err != nil { + return diag.FromErr(err) + } + + watchdogEnabled := types.CustomBool( + watchdogBlock[mkWatchdogEnabled].(bool), + ) + if watchdogEnabled { + watchdogAction := watchdogBlock[mkWatchdogAction].(string) + watchdogModel := watchdogBlock[mkWatchdogModel].(string) + + watchdogObject = &vms.CustomWatchdogDevice{ + Action: &watchdogAction, + Model: &watchdogModel, + } + } + createBody := &vms.CreateRequestBody{ ACPI: &acpi, Agent: &vms.CustomAgent{ @@ -2563,6 +2664,7 @@ func vmCreateCustom(ctx context.Context, d *schema.ResourceData, m interface{}) USBDevices: usbDeviceObjects, VGADevice: vgaDevice, VMID: vmID, + WatchdogDevice: watchdogObject, CustomStorageDevices: diskDeviceObjects, } @@ -4325,6 +4427,51 @@ func vmReadCustom( } } + watchdog := map[string]interface{}{} + + if vmConfig.WatchdogDevice != nil { + watchdog[mkWatchdogEnabled] = true + + if vmConfig.WatchdogDevice.Action != nil { + watchdog[mkWatchdogAction] = *vmConfig.WatchdogDevice.Action + } else { + watchdog[mkWatchdogAction] = dvWatchdogAction + } + + if vmConfig.WatchdogDevice.Model != nil { + watchdog[mkWatchdogModel] = *vmConfig.WatchdogDevice.Model + } else { + watchdog[mkWatchdogModel] = dvWatchdogModel + } + } else { + watchdog[mkWatchdogEnabled] = false + watchdog[mkWatchdogAction] = dvWatchdogAction + watchdog[mkWatchdogModel] = dvWatchdogModel + } + + currentWatchdog := d.Get(mkWatchdog).([]interface{}) + currentWatchdogEnabled := len(currentWatchdog) > 0 && + currentWatchdog[0] != nil && currentWatchdog[0].(map[string]interface{})[mkWatchdogEnabled].(bool) + currentWatchdogDisabled := len(currentWatchdog) > 0 && + currentWatchdog[0] != nil && !currentWatchdog[0].(map[string]interface{})[mkWatchdogEnabled].(bool) + + switch { + case len(clone) > 0 && len(currentWatchdog) > 0: + err := d.Set(mkWatchdog, []interface{}{watchdog}) + diags = append(diags, diag.FromErr(err)...) + case currentWatchdogEnabled || + watchdog[mkWatchdogEnabled] != false || + watchdog[mkWatchdogAction] != dvWatchdogAction || + watchdog[mkWatchdogModel] != dvWatchdogModel: + err := d.Set(mkWatchdog, []interface{}{watchdog}) + diags = append(diags, diag.FromErr(err)...) + case currentWatchdogDisabled && vmConfig.WatchdogDevice == nil: + // do nothing + default: + err := d.Set(mkWatchdog, []interface{}{}) + diags = append(diags, diag.FromErr(err)...) + } + vmAPI := client.Node(nodeName).VM(vmID) started := d.Get(mkStarted).(bool) @@ -5160,6 +5307,37 @@ func vmUpdate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.D } } + // Prepare the new watchdog configuration. + if d.HasChange(mkWatchdog) { + watchdogBlock, err := structure.GetSchemaBlock( + resource, + d, + []string{mkWatchdog}, + 0, + true, + ) + if err != nil { + return diag.FromErr(err) + } + + watchdogEnabled := types.CustomBool( + watchdogBlock[mkWatchdogEnabled].(bool), + ) + if watchdogEnabled { + watchdogAction := watchdogBlock[mkWatchdogAction].(string) + watchdogModel := watchdogBlock[mkWatchdogModel].(string) + + updateBody.WatchdogDevice = &vms.CustomWatchdogDevice{ + Action: &watchdogAction, + Model: &watchdogModel, + } + } else { + del = append(del, "watchdog") + } + + rebootRequired = true + } + // Update the configuration now that everything has been prepared. updateBody.Delete = del