Repository: ansible-collections/community.general

Issue:
int() argument must be a string, a bytes-like object or a number, not 'NoneType'
### Summary

When I try to create a container on Proxmox, it fails saying that "fatal: [central.local]: FAILED! => {"changed": false, "msg": "creation of lxc VM 810 failed with exception: int() argument must be a string, a bytes-like object or a number, not 'NoneType'"}"

### Issue Type

Bug Report

### Component Name

community.general.proxmox

### Ansible Version

```console (paste below)
$ ansible --version
ansible [core 2.12.0]
  config file = /etc/ansible/ansible.cfg
  configured module search path = ['/root/.ansible/plugins/modules', '/usr/share/ansible/plugins/modules']
  ansible python module location = /usr/lib/python3.9/site-packages/ansible
  ansible collection location = /root/.ansible/collections:/usr/share/ansible/collections
  executable location = /usr/bin/ansible
  python version = 3.9.9 (main, Nov 20 2021, 21:30:06) [GCC 11.1.0]
  jinja version = 3.0.3
  libyaml = True
```


### Community.general Version

```console (paste below)
$ ansible-galaxy collection list community.general
# /usr/lib/python3.9/site-packages/ansible_collections
Collection        Version
----------------- -------
community.general 4.0.2  

# /root/.ansible/collections/ansible_collections
Collection        Version
----------------- -------
community.general 4.1.0 
```


### Configuration

```console (paste below)
$ ansible-config dump --only-changed
INVENTORY_ENABLED(/etc/ansible/ansible.cfg) = ['yaml', 'ini']
```


### OS / Environment

Arch Linux in LXC on Proxmox (although I get the exact same errors running from a bare-metal install of Arch Linux)

### Steps to Reproduce

<!--- Paste example playbooks or commands between quotes below -->
```yaml (paste below)

---Playbook extracted from proxmox documentation---

- name: Create new LXC container in Proxmox
  hosts: central.local
  tasks:
    - name: testing
      proxmox:
        vmid: 810
        node: central
        api_user: root@pam
        api_password: <password>
        api_host: central
        password: 123456
        hostname: example.org
        ostemplate: 'local:vztmpl/archlinux-base_20210420-1_amd64.tar.gz'


---Custom playbook---

- name: Create new LXC container in Proxmox
  hosts: 'central.local'
  tasks:
    - name: 'Create container'
      proxmox:
- name: Create new LXC container in Proxmox
  hosts: 'central.local'
  tasks:
    - name: 'Create container'
      proxmox:
        vmid: '810' # specifying container ID
        api_user: 'root@pam' # Proxmox user
        api_password: '' # password in plaintext
        api_host: 'central.local' # proxmox hostname
        password: 'password' # container password in plaintext
        hostname: 'CT-Prometheus-ArchLinux' # container hostname
        node: 'central' # name of proxmox host
        cores: '1'
        cpus: '2'
        cpuunits: '1000'
        ostemplate: 'local:vztmpl/archlinux-base_20210420-1_amd64.tar.gz'
        storage: 'VMs'
        disk: '16'
        memory: '512'
        nameserver: '8.8.8.8'
        netif: '{"net0":"name=eth0,ip=dhcp,bridge=vmbr10"}'
        state: 'present'


```


### Expected Results

I expect it to connect and create a container.



### Actual Results

```console (paste below)
ansible-playbook playbooks/vm_template_2.yml --user root@pam --ask-pass
SSH password: 

PLAY [Create new LXC container in Proxmox] *********************************************

TASK [Gathering Facts] *****************************************************************
ok: [central.local]

TASK [testing] *************************************************************************
fatal: [central.local]: FAILED! => {"changed": false, "msg": "creation of lxc VM 810 failed with exception: int() argument must be a string, a bytes-like object or a number, not 'NoneType'"}

PLAY RECAP *****************************************************************************
central.local              : ok=1    changed=0    unreachable=0    failed=1    skipped=0    rescued=0    ignored=0   

```


### Code of Conduct

- [X] I agree to follow the Ansible Code of Conduct


Discussion:
Files identified in the description:
* `plugins/modules/cloud/misc/proxmox`

If these files are incorrect, please update the `component name` section of the description or use the `!component` bot command.

[click here for bot help](https://github.com/ansible/ansibullbot/blob/devel/ISSUE_HELP.md)
<!--- boilerplate: components_banner --->
cc @Ajpantuso @Thulium-Drake @joshainglis @karmab @tleguern
[click here for bot help](https://github.com/ansible/ansibullbot/blob/devel/ISSUE_HELP.md)
<!--- boilerplate: notify --->
This issue is resolved following a full system reboot of proxmox. Cannot say for other systems but it should be noted in the community docs if anyone else can replicate this error. 

EDIT: this was a false positive. System reboot did not restore. See below.
I spoke too soon. It appears incapable of creating a new container. 

If I use the following playbook:

- name: Create new LXC container in Proxmox
  hosts: central.local
  tasks:
    - name: testing
      proxmox:
        vmid: 810
        node: central
        api_user: root@pam
        api_password: <password>
        api_host: central
        password: 123456
        hostname: example.org 
        ostemplate: 'local:vztmpl/archlinux-base_20210420-1_amd64.tar.gz'

It will return "ok", HOWEVER a container at id 810 already exists on the proxmox installation. 

Which means two things:

1. this "ok" is a false positive
2. Nothing happens, no container is created

Altering this number exactly to any other number not yet assigned to a VM or container returns the error in OP. This example comes directly from the examples in your docs. 
Anyone going to address this? Not expecting an immediate fix, but this error renders Ansible completely useless in Proxmox. 
Nobody seems to be working on this. I **think** this is caused because you don't set the `onboot` parameter; the code handling it ignores that it can not be specified, and will likely (judging from glancing over it) produce this exact error when you don't specify it.

Changing line 608 to
```.py
                            onboot=int(module.params['onboot']) if module.params['onboot'] is not None else None,
```
should fix it (assuming my guess is correct).
Wouldn't it be slightly better to add `default=False` to the argument spec?
No, because we just spent quite some amount of work to get rid of that default.
> ```python
> onboot=int(module.params['onboot']) if module.params['onboot'] is not None else None
> ```

Could we do `is not None else 0` ? 
No, for exactly the same reason: that would re-introduce the default.
Oh right of course haha I totally read that wrong.

What about making `onboot` a mandatory value? That way there is no default. The user either defines it to boot on startup or defines it to not boot on startup. 
That's a breaking change and will not be accepted without a long deprecation period.
> That's a breaking change and will not be accepted without a long deprecation period.

Technically yes? But also isn't that kind of a moot point as this is a major bug which results in this value needing to be set as a workaround (tested and confirmed) regardless? Else it doesn't work at all?

Either way, software development protocol yadda yadda... all good, I gets how it be 😅

Ok cool, so should we define that as a workaround? For now we note that without defining this value, it throws this error and won't work.
If someone wants to touch the module, how about simply fixing the bug, instead of adding some other random restriction which will avoid the bug to happen? Fixing the bug requires less effort (considering possible deprecations, required documentation, explanation in changelog etc.) than all the other things proposed here.
> 

Happy to do that, but do you have a known solution? Do we know what the fundamental issue is under the hood?

If not, then I don't have the resources atm to commit for known unknowns. From my pov, it's simply easier to push through the deprecation cycle and stick with a workaround. 

Either way, the workaround exists in the interim.
I'm pretty sure https://github.com/ansible-collections/community.general/issues/3844#issuecomment-987037799 fixes the issue (since the fundamental reason is that `int(None)` gets called when you don't specify `onboot` as a module parameter), but I have no way to verify this because I don't use proxmox.
We seem to be going in circles. That solution is unfortunately out of the question, given the removal of default behaviour which I happen to agree with at the very least from a maintainer's pov. 

Making this flag mandatory has other benefits in terms of basic operations hygene and makes sense also from the Ansible/config mgmt pov. 

Does it boot? Does it not boot? Unacceptable unknown imo. Setting should be forced on the user.

Deprecation cycles aren't so bad, workarounds are very normal.
> We seem to be going in circles. That solution is unfortunately out of the question, given the removal of default behaviour which I happen to agree with at the very least from a maintainer's pov.

I don't understand what you are saying. The comment I linked to is handling the option correctly when it is not provided by the user.
> > We seem to be going in circles. That solution is unfortunately out of the question, given the removal of default behaviour which I happen to agree with at the very least from a maintainer's pov.
> 
> I don't understand what you are saying. The comment I linked to is handling the option correctly when it is not provided by the user.

Oh sorry, I'm really not reading properly today. 

Well no, coz the error is related to passing through `None` when the option isn't set in the first place right? If we then just give it None then we're not really fixing anything.
Please look closely. `None` is not passed to `int()`. But it is passed on as the value of `onboot` (which is correct and required) if the user provided `None`.
I can pick this up later today. I'll add the inverse operation of https://github.com/ansible-collections/community.general/blob/80d650f60a21ae2e6fc3e86f60e04e28baabfcf2/plugins/module_utils/proxmox.py#L52-L54 to the module_utils and wrap all the bool parameters in it. The other bool parameters have defaults so they are not affected currently, but obviously could be if defaults are removed.

EDIT: By inverse I mean True -> 1, False -> 0, None -> None.
> Please look closely. `None` is not passed to `int()`. But it is passed on as the value of `onboot` (which is correct and required) if the user provided `None`.

I was operating under the assumption that the value passed to `onboot` was the issue in question, not the value passed to `int()` but now I understand.

Resolving patch:
diff --git a/plugins/module_utils/proxmox.py b/plugins/module_utils/proxmox.py
--- a/plugins/module_utils/proxmox.py
+++ b/plugins/module_utils/proxmox.py
@@ -54,6 +54,17 @@ def proxmox_to_ansible_bool(value):
     return True if value == 1 else False
 
 
+def ansible_to_proxmox_bool(value):
+    '''Convert Ansible representation of a boolean to be proxmox-friendly'''
+    if value is None:
+        return None
+
+    if not isinstance(value, bool):
+        raise ValueError("%s must be of type bool not %s" % (value, type(value)))
+
+    return 1 if value else 0
+
+
 class ProxmoxAnsible(object):
     """Base class for Proxmox modules"""
     def __init__(self, module):
diff --git a/plugins/modules/cloud/misc/proxmox.py b/plugins/modules/cloud/misc/proxmox.py
--- a/plugins/modules/cloud/misc/proxmox.py
+++ b/plugins/modules/cloud/misc/proxmox.py
@@ -359,6 +359,10 @@
 from ansible.module_utils.basic import AnsibleModule, env_fallback
 from ansible.module_utils.common.text.converters import to_native
 
+from ansible_collections.community.general.plugins.module_utils.proxmox import (
+    ansible_to_proxmox_bool
+)
+
 
 VZ_TYPE = None
 
@@ -605,14 +609,14 @@ def main():
                             netif=module.params['netif'],
                             mounts=module.params['mounts'],
                             ip_address=module.params['ip_address'],
-                            onboot=int(module.params['onboot']),
+                            onboot=ansible_to_proxmox_bool(module.params['onboot']),
                             cpuunits=module.params['cpuunits'],
                             nameserver=module.params['nameserver'],
                             searchdomain=module.params['searchdomain'],
-                            force=int(module.params['force']),
+                            force=ansible_to_proxmox_bool(module.params['force']),
                             pubkey=module.params['pubkey'],
                             features=",".join(module.params['features']) if module.params['features'] is not None else None,
-                            unprivileged=int(module.params['unprivileged']),
+                            unprivileged=ansible_to_proxmox_bool(module.params['unprivileged']),
                             description=module.params['description'],
                             hookscript=module.params['hookscript'])
 


Test patch: