Repository: ansible-collections/community.general

Issue:
Proxmox token authentication broken
### Summary

Token authentication of proxmox modules is broken.

https://github.com/ansible-collections/community.general/commit/e5dc69788799168c0f53c1da636262c81d8fdc5b introduced a version check for proxmoxer, but actually makes an api call to PVE itself, and this before the api is readily connected.

Reverting this change fixes the token authentication.

### Issue Type

Bug Report

### Component Name

/plugins/module_utils/proxmox.py

### Ansible Version

```
ansible [core 2.14.5]
  config file = /home/user/.ansible.cfg
  configured module search path = ['/home/user/.ansible/plugins/modules', '/usr/share/ansible/plugins/modules']
  ansible python module location = /usr/lib/python3.11/site-packages/ansible
  ansible collection location = /home/user/.ansible/collections:/usr/share/ansible/collections
  executable location = /usr/lib/python-exec/python3.11/ansible
  python version = 3.11.3 (main, Jun  9 2023, 16:40:48) [GCC 12.2.1 20230428] (/usr/bin/python3.11)
  jinja version = 3.1.2
  libyaml = True

```


### Community.general Version

```
# /home/user/.ansible/collections/ansible_collections
Collection        Version
----------------- -------
community.general 7.2.0
```


### Configuration

```
CACHE_PLUGIN(/home/user/.ansible.cfg) = jsonfile
CACHE_PLUGIN_CONNECTION(/home/user/.ansible.cfg) = ~/.ansible/cache_facts
CACHE_PLUGIN_TIMEOUT(/home/user/.ansible.cfg) = 3600
CONFIG_FILE() = /home/user/.ansible.cfg
DEFAULT_BECOME(/home/user/.ansible.cfg) = False
DEFAULT_FILTER_PLUGIN_PATH(/home/user/.ansible.cfg) = ['/home/user/ansible/filter_plugins', '/home/user/.ansible/plugins/filter', '/usr/share/ansible/plugins/filter']
DEFAULT_FORKS(/home/user/.ansible.cfg) = 10
DEFAULT_HOST_LIST(/home/user/.ansible.cfg) = ['/home/user/ansible/hosts']
DEFAULT_MANAGED_STR(/home/user/.ansible.cfg) = Ansible managed
DEFAULT_REMOTE_USER(/home/user/.ansible.cfg) = root
DEFAULT_ROLES_PATH(/home/user/.ansible.cfg) = ['/home/user/ansible/roles', '/home/user/.ansible/roles', '/usr/share/ansible/roles', '/etc/ansible/roles']
DEFAULT_TIMEOUT(/home/user/.ansible.cfg) = 2
DEFAULT_VAULT_PASSWORD_FILE(/home/user/.ansible.cfg) = /home/user/.ansible/vault.passwd
```


### OS / Environment

Gentoo stable

### Steps to Reproduce

<!--- Paste example playbooks or commands between quotes below -->
```yaml (paste below)
- name: Create VM
  community.general.proxmox_kvm:
    api_user: root@pam
    api_host: 127.0.0.1
    api_token_id: ansible
    api_token_secret: 12345678-1234-1234-1234-123456789012
    name: vm1
    node: node1
```


### Expected Results

Creation of a minimal VM on node1

### Actual Results

```
The full traceback is:
  File "/tmp/ansible_community.general.proxmox_kvm_payload_v872i0zh/ansible_community.general.proxmox_kvm_payload.zip/ansible_collections/community/general/plugins/module_utils/proxmox.py", line 115, in version
    apiversion = self.proxmox_api.version.get()
                 ^^^^^^^^^^^^^^^^
fatal: [verse]: FAILED! => {
    "changed": false,

    "invocation": {                                                                                                                                                                                                               [35/21179]
        "module_args": {
            "acpi": null,
            "agent": null,
            "api_host": "127.0.0.1",
            "api_password": null,
            "api_token_id": "ansible",
            "api_token_secret": "VALUE_SPECIFIED_IN_NO_LOG_PARAMETER",
            "api_user": "root@pam",
            "archive": null,
            "args": null,
            "autostart": null,
            "balloon": null,
            "bios": null,
            "boot": null,
            "bootdisk": null,
            "cicustom": null,
            "cipassword": null,
            "citype": null,
            "ciuser": null,
            "clone": null,
            "cores": null,
            "cpu": null,
            "cpulimit": null,
            "cpuunits": null,
            "delete": null,
            "description": null,
            "digest": null,
            "efidisk0": null,
            "force": null,
            "format": null,
            "freeze": null,
            "full": true,
            "hostpci": null,
            "hotplug": null,
            "hugepages": null,
            "ide": null,
            "ipconfig": null,
            "keyboard": null,
            "kvm": null,
            "localtime": null,
            "lock": null,
            "machine": null,
            "memory": null,
            "migrate": false,
            "migrate_downtime": null,
            "migrate_speed": null,
            "name": "vm1",
            "nameservers": null,
            "net": null,
            "newid": null,
            "node": "node1",
            "numa": null,
            "numa_enabled": null,
            "onboot": null,
            "ostype": null,
            "parallel": null,
            "pool": null,
            "protection": null,
            "proxmox_default_behavior": "no_defaults",
            "reboot": null,
            "revert": null,
            "sata": null,
            "scsi": null,
            "scsihw": null,
            "searchdomains": null,
            "serial": null,
            "shares": null,
            "skiplock": null,
            "smbios": null,
            "snapname": null,
            "sockets": null,
            "sshkeys": null,
            "startdate": null,
            "startup": null,
            "state": "present",
            "storage": null,
            "tablet": null,
            "tags": null,
            "target": null,
            "tdf": null,
            "template": null,
            "timeout": 30,
            "tpmstate0": null,
            "update": false,
            "validate_certs": false,
            "vcpus": null,
            "vga": null,
            "virtio": null,
            "vmid": null,
            "watchdog": null
        }
    },
    "msg": "Unable to retrieve Proxmox VE version: 'ProxmoxKvmAnsible' object has no attribute 'proxmox_api'"
```


### Code of Conduct

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


Discussion:
Files identified in the description:
* [`plugins/module_utils/proxmox.py`](https://github.com/ansible-collections/community.general/blob/main/plugins/module_utils/proxmox.py)

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 --->
This is related to this part:

```
def __init__(self, module):
        if not HAS_PROXMOXER:
            module.fail_json(msg=missing_required_lib('proxmoxer'), exception=PROXMOXER_IMP_ERR)

        self.module = module
        self.proxmox_api = self._connect()
        self.proxmoxer_version = proxmoxer_version
        # Test token validity
        try:
            self.proxmox_api.version.get()
        except Exception as e:
            module.fail_json(msg='%s' % e, exception=traceback.format_exc())

    def _connect(self):
        api_host = self.module.params['api_host']
        api_user = self.module.params['api_user']
        api_password = self.module.params['api_password']
        api_token_id = self.module.params['api_token_id']
        api_token_secret = self.module.params['api_token_secret']
        validate_certs = self.module.params['validate_certs']
        
        auth_args = {'user': api_user}
        if api_password:
            auth_args['password'] = api_password
        else:
            if self.version() < LooseVersion('1.1.0'):
                self.module.fail_json('Using "token_name" and "token_value" require proxmoxer>=1.1.0')
            auth_args['token_name'] = api_token_id
            auth_args['token_value'] = api_token_secret
        
        try:
            return ProxmoxAPI(api_host, verify_ssl=validate_certs, **auth_args)
        except Exception as e:
            self.module.fail_json(msg='%s' % e, exception=traceback.format_exc())

    def version(self):
        try:
            apiversion = self.proxmox_api.version.get()
            return LooseVersion(apiversion['version'])
        except Exception as e:
            self.module.fail_json(msg='Unable to retrieve Proxmox VE version: %s' % e)
```

`self.init()` defines `self.proxmox_api` by calling `self._connect()`, which calls `self.version()`, which finally looks at `self.proxmox_api` object.

related to #6974 
CC @russoz

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
@@ -18,6 +18,7 @@
 PROXMOXER_IMP_ERR = None
 try:
     from proxmoxer import ProxmoxAPI
+    from proxmoxer import __version__ as proxmoxer_version
     HAS_PROXMOXER = True
 except ImportError:
     HAS_PROXMOXER = False
@@ -79,6 +80,7 @@ def __init__(self, module):
             module.fail_json(msg=missing_required_lib('proxmoxer'), exception=PROXMOXER_IMP_ERR)
 
         self.module = module
+        self.proxmoxer_version = proxmoxer_version
         self.proxmox_api = self._connect()
         # Test token validity
         try:
@@ -98,7 +100,7 @@ def _connect(self):
         if api_password:
             auth_args['password'] = api_password
         else:
-            if self.version() < LooseVersion('1.1.0'):
+            if self.proxmoxer_version < LooseVersion('1.1.0'):
                 self.module.fail_json('Using "token_name" and "token_value" require proxmoxer>=1.1.0')
             auth_args['token_name'] = api_token_id
             auth_args['token_value'] = api_token_secret


Test patch:
diff --git a/tests/unit/plugins/modules/test_proxmox_snap.py b/tests/unit/plugins/modules/test_proxmox_snap.py
--- a/tests/unit/plugins/modules/test_proxmox_snap.py
+++ b/tests/unit/plugins/modules/test_proxmox_snap.py
@@ -8,6 +8,13 @@
 
 import json
 import pytest
+import sys
+
+proxmoxer = pytest.importorskip('proxmoxer')
+mandatory_py_version = pytest.mark.skipif(
+    sys.version_info < (2, 7),
+    reason='The proxmoxer dependency requires python2.7 or higher'
+)
 
 from ansible_collections.community.general.tests.unit.compat.mock import MagicMock, patch
 from ansible_collections.community.general.plugins.modules import proxmox_snap
diff --git a/tests/unit/plugins/modules/test_proxmox_tasks_info.py b/tests/unit/plugins/modules/test_proxmox_tasks_info.py
--- a/tests/unit/plugins/modules/test_proxmox_tasks_info.py
+++ b/tests/unit/plugins/modules/test_proxmox_tasks_info.py
@@ -12,6 +12,13 @@
 
 import pytest
 import json
+import sys
+
+proxmoxer = pytest.importorskip('proxmoxer')
+mandatory_py_version = pytest.mark.skipif(
+    sys.version_info < (2, 7),
+    reason='The proxmoxer dependency requires python2.7 or higher'
+)
 
 from ansible_collections.community.general.plugins.modules import proxmox_tasks_info
 import ansible_collections.community.general.plugins.module_utils.proxmox as proxmox_utils
diff --git a/tests/unit/requirements.txt b/tests/unit/requirements.txt
--- a/tests/unit/requirements.txt
+++ b/tests/unit/requirements.txt
@@ -43,4 +43,8 @@ dataclasses ; python_version == '3.6'
 elastic-apm ; python_version >= '3.6'
 
 # requirements for scaleway modules
-passlib[argon2]
\ No newline at end of file
+passlib[argon2]
+
+# requirements for the proxmox modules
+proxmoxer < 2.0.0 ; python_version >= '2.7' and python_version <= '3.6'
+proxmoxer ; python_version > '3.6'