Repository: ansible-collections/community.general

Issue:
proxmox_kvm unable to force-stop a VM quickly
### Summary

`state: stopped` won't stop a VM not responding to ACPI until 10 minutes, even with  `force: true`.

It will create a shutdown task, with the (proxmox VE) timeout of 10 minutes.
The task will finally halt the VM after 10 minutes but it's too long.

pve api `timeout` parameter should be exposed, to have proxmox kill the VM quicker.

Maybe it could be computed as the module's `timeout` parameter `- 1`?

### Issue Type

Bug Report

### Component Name

proxmox_kvm

### Ansible Version

```console (paste below)
$ ansible --version
ansible [core 2.12.1]
  config file = None
  configured module search path = ['/home/xxx/.ansible/plugins/modules', '/usr/share/ansible/plugins/modules']
  ansible python module location = /home/xxx/software/miniconda/envs/yyyy/lib/python3.9/site-packages/ansible
  ansible collection location = /home/xxx/.ansible/collections:/usr/share/ansible/collections
  executable location = /home/xxx/software/miniconda/envs/yyyyy/bin/ansible
  python version = 3.9.9 | packaged by conda-forge | (main, Dec 20 2021, 02:40:17) [GCC 9.4.0]
  jinja version = 3.0.3
  libyaml = True
```


### Community.general Version

```console (paste below)
$ ansible-galaxy collection list community.general
Collection        Version
----------------- -------
community.general 4.0.2 
```


### Configuration

N/A

### OS / Environment

_No response_

### Steps to Reproduce

<!--- Paste example playbooks or commands between quotes below -->
```yaml
---
- hosts: all
  gather_facts: false

  tasks:
    - name: Stop template VM Will timeout
      community.general.proxmox_kvm:
        api_user: "{{ pve_user }}"
        api_token_id: "{{ pve_token_id }}"
        api_token_secret: "{{ pve_token_secret }}"
        api_host: "{{ pve_api_host }}"
        vmid: "{{ pve_template_vm_id }}"
        state: stopped
        force: true
      delegate_to: localhost

    - name: Stop template VM will succeed after 10 minutes
      community.general.proxmox_kvm:
        api_user: "{{ pve_user }}"
        api_token_id: "{{ pve_token_id }}"
        api_token_secret: "{{ pve_token_secret }}"
        api_host: "{{ pve_api_host }}"
        vmid: "{{ pve_template_vm_id }}"
        state: stopped
        force: true
        timeout: 610
      delegate_to: localhost
```


### Expected Results

I expected to be able to specify a shorter timeout than 10 minutes for the force shutdown to apply.

### Actual Results

Proxmox will halt the VM after 10 minutes, but it's too long.


### Code of Conduct

- [X] I agree to follow the Ansible Code of Conduct


Discussion:
A `timeout` parameter should be added in [plugins/modules/proxmox_kvm.py#L1058](https://github.com/ansible-collections/community.general/blob/main/plugins/modules/proxmox_kvm.py#L1058)
cc @Ajpantuso @Thulium-Drake @helldorado @joshainglis @karmab @tleguern
[click here for bot help](https://github.com/ansible/ansibullbot/blob/devel/ISSUE_HELP.md)
<!--- boilerplate: notify --->
As far as I can see there is [no timeout option](https://pve.proxmox.com/pve-docs/api-viewer/#/nodes/{node}/qemu/{vmid}) for deleting QEMU virtual machine.

**EDIT**. My bad. We were talking about stopping VM, not deleting it. Then yeah, there is a timeout [option](https://pve.proxmox.com/pve-docs/api-viewer/#/nodes/{node}/qemu/{vmid}/status/stop).

Resolving patch:
diff --git a/plugins/modules/proxmox_kvm.py b/plugins/modules/proxmox_kvm.py
--- a/plugins/modules/proxmox_kvm.py
+++ b/plugins/modules/proxmox_kvm.py
@@ -444,7 +444,7 @@
       - Indicates desired state of the instance.
       - If C(current), the current state of the VM will be fetched. You can access it with C(results.status)
     type: str
-    choices: ['present', 'started', 'absent', 'stopped', 'restarted','current']
+    choices: ['present', 'started', 'absent', 'stopped', 'restarted', 'current']
     default: present
   storage:
     description:
@@ -480,6 +480,7 @@
   timeout:
     description:
       - Timeout for operations.
+      - When used with I(state=stopped) the option sets a graceful timeout for VM stop after which a VM will be forcefully stopped.
     type: int
     default: 30
   update:
@@ -889,6 +890,9 @@ def settings(self, vmid, node, **kwargs):
 
     def wait_for_task(self, node, taskid):
         timeout = self.module.params['timeout']
+        if self.module.params['state'] == 'stopped':
+            # Increase task timeout in case of stopped state to be sure it waits longer than VM stop operation itself
+            timeout += 10
 
         while timeout:
             if self.api_task_ok(node, taskid):
@@ -1058,10 +1062,10 @@ def start_vm(self, vm):
             return False
         return True
 
-    def stop_vm(self, vm, force):
+    def stop_vm(self, vm, force, timeout):
         vmid = vm['vmid']
         proxmox_node = self.proxmox_api.nodes(vm['node'])
-        taskid = proxmox_node.qemu(vmid).status.shutdown.post(forceStop=(1 if force else 0))
+        taskid = proxmox_node.qemu(vmid).status.shutdown.post(forceStop=(1 if force else 0), timeout=timeout)
         if not self.wait_for_task(vm['node'], taskid):
             self.module.fail_json(msg='Reached timeout while waiting for stopping VM. Last line in task before timeout: %s' %
                                   proxmox_node.tasks(taskid).log.get()[:1])
@@ -1293,7 +1297,7 @@ def main():
             elif proxmox.get_vmid(name, ignore_missing=True) and not (update or clone):
                 module.exit_json(changed=False, vmid=proxmox.get_vmid(name), msg="VM with name <%s> already exists" % name)
             elif not node:
-                module.fail.json(msg='node is mandatory for creating/updating VM')
+                module.fail_json(msg='node is mandatory for creating/updating VM')
             elif update and not any([vmid, name]):
                 module.fail_json(msg='vmid or name is mandatory for updating VM')
             elif not proxmox.get_node(node):
@@ -1409,7 +1413,7 @@ def main():
             if vm['status'] == 'stopped':
                 module.exit_json(changed=False, vmid=vmid, msg="VM %s is already stopped" % vmid, **status)
 
-            if proxmox.stop_vm(vm, force=module.params['force']):
+            if proxmox.stop_vm(vm, force=module.params['force'], timeout=module.params['timeout']):
                 module.exit_json(changed=True, vmid=vmid, msg="VM %s is shutting down" % vmid, **status)
         except Exception as e:
             module.fail_json(vmid=vmid, msg="stopping of VM %s failed with exception: %s" % (vmid, e), **status)


Test patch: