Repository: ansible-collections/community.general

Issue:
proxmox: allow to use a token for the API
##### SUMMARY
Instead of having to use a (security-sensitive) `api_password` parameter for the module, being able to use a token (cf https://pve.proxmox.com/wiki/User_Management#pveum_tokens) for the proxmox API would be nice. For now i resort to a dedicated cluster user with only `PVEVMAdmin` role..

the proxmoxer python module supports an API tokens since its 1.1.0 version

##### ISSUE TYPE
- Feature Idea

##### COMPONENT NAME
proxmox




Discussion:
Files identified in the description:
* [`plugins/modules/cloud/misc/virt_net.py`](https://github.com/ansible-collections/community.general/blob/master/plugins/modules/cloud/misc/virt_net.py)

If these files are inaccurate, 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/master/ISSUE_HELP.md)
<!--- boilerplate: components_banner --->
cc @drybjed @joshainglis @karmab
[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)
<!--- boilerplate: notify --->
!component plugins/modules/cloud/misc/proxmox.py
Is actually supported by proxmoxer, so code change is I believe limited. May I propose some code for this here, raw, untested, but want to help out as a non contributor. (remark that the variables of course need to be added to the overal code).


**Current auth code:**

```
if not api_password:
    try:
        api_password = os.environ['PROXMOX_PASSWORD']
    except KeyError as e:
        module.fail_json(msg='You should set api_password param or use PROXMOX_PASSWORD environment variable')

try:
    proxmox = ProxmoxAPI(api_host, user=api_user, password=api_password, verify_ssl=validate_certs)
    global PVE_MAJOR_VERSION
    PVE_MAJOR_VERSION = 3 if proxmox_version(proxmox) < LooseVersion('4.0') else 4
except Exception as e:
    module.fail_json(msg='authorization on proxmox cluster failed with exception: %s' % e)
```


**Proposal:**
```
if not (api_token_id and api_token_secret) and not api_password:
    try:
        api_password = os.environ['PROXMOX_PASSWORD']
    except KeyError as e:
        module.fail_json(msg='You should set api_password param, or use PROXMOX_PASSWORD environment variable, or provide api_token_id and api_token_secret')

if api_password:
    try:
        proxmox = ProxmoxAPI(api_host, user=api_user, password=api_password, verify_ssl=validate_certs)
        global PVE_MAJOR_VERSION
        PVE_MAJOR_VERSION = 3 if proxmox_version(proxmox) < LooseVersion('4.0') else 4
    except Exception as e:
        module.fail_json(msg='authorization on proxmox cluster failed with exception: %s' % e)

elif api_token_id:
    try:
        proxmox = ProxmoxAPI('api_host', user=api_user, token_name=api_token_id, token_value=api_token_secret)
        global PVE_MAJOR_VERSION
        PVE_MAJOR_VERSION = 3 if proxmox_version(proxmox) < LooseVersion('4.0') else 4
    except Exception as e:
        module.fail_json(msg='authorization on proxmox cluster failed with exception: %s' % e)
```
cc @Aversiste
[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)
<!--- boilerplate: notify --->
I will be able to test this solution next week only, sorry.
Ok, would be great. Keep us posted @Aversiste.
Seems good, I only had to make some slight adjustments to your proposal.
Do you want me to open a pull request on your behalf?
Yeah sure, thanks @Aversiste 

Resolving patch:
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
@@ -29,6 +29,16 @@
       - the password to authenticate with
       - you can use PROXMOX_PASSWORD environment variable
     type: str
+  api_token_id:
+    description:
+      - Specify the token ID.
+    type: str
+    version_added: 1.3.0
+  api_token_secret:
+    description:
+      - Specify the token secret.
+    type: str
+    version_added: 1.3.0
   vmid:
     description:
       - the instance id
@@ -496,8 +506,10 @@ def main():
     module = AnsibleModule(
         argument_spec=dict(
             api_host=dict(required=True),
-            api_user=dict(required=True),
             api_password=dict(no_log=True),
+            api_token_id=dict(no_log=True),
+            api_token_secret=dict(no_log=True),
+            api_user=dict(required=True),
             vmid=dict(required=False),
             validate_certs=dict(type='bool', default=False),
             node=dict(),
@@ -533,9 +545,11 @@ def main():
         module.fail_json(msg='proxmoxer required for this module')
 
     state = module.params['state']
-    api_user = module.params['api_user']
     api_host = module.params['api_host']
     api_password = module.params['api_password']
+    api_token_id = module.params['api_token_id']
+    api_token_secret = module.params['api_token_secret']
+    api_user = module.params['api_user']
     vmid = module.params['vmid']
     validate_certs = module.params['validate_certs']
     node = module.params['node']
@@ -570,18 +584,23 @@ def main():
             if module.params[param] is None:
                 module.params[param] = value
 
-    # If password not set get it from PROXMOX_PASSWORD env
-    if not api_password:
-        try:
-            api_password = os.environ['PROXMOX_PASSWORD']
-        except KeyError as e:
-            module.fail_json(msg='You should set api_password param or use PROXMOX_PASSWORD environment variable')
+    auth_args = {'user': api_user}
+    if not (api_token_id and api_token_secret):
+        # If password not set get it from PROXMOX_PASSWORD env
+        if not api_password:
+            try:
+                api_password = os.environ['PROXMOX_PASSWORD']
+            except KeyError as e:
+                module.fail_json(msg='You should set api_password param or use PROXMOX_PASSWORD environment variable')
+        auth_args['password'] = api_password
+    else:
+        auth_args['token_name'] = api_token_id
+        auth_args['token_value'] = api_token_secret
 
     try:
-        proxmox = ProxmoxAPI(api_host, user=api_user, password=api_password, verify_ssl=validate_certs)
+        proxmox = ProxmoxAPI(api_host, verify_ssl=validate_certs, **auth_args)
         global VZ_TYPE
         VZ_TYPE = 'openvz' if proxmox_version(proxmox) < LooseVersion('4.0') else 'lxc'
-
     except Exception as e:
         module.fail_json(msg='authorization on proxmox cluster failed with exception: %s' % e)
 
diff --git a/plugins/modules/cloud/misc/proxmox_kvm.py b/plugins/modules/cloud/misc/proxmox_kvm.py
--- a/plugins/modules/cloud/misc/proxmox_kvm.py
+++ b/plugins/modules/cloud/misc/proxmox_kvm.py
@@ -47,6 +47,16 @@
       - Specify the password to authenticate with.
       - You can use C(PROXMOX_PASSWORD) environment variable.
     type: str
+  api_token_id:
+    description:
+      - Specify the token ID.
+    type: str
+    version_added: 1.3.0
+  api_token_secret:
+    description:
+      - Specify the token secret.
+    type: str
+    version_added: 1.3.0
   autostart:
     description:
       - Specify if the VM should be automatically restarted after crash (currently ignored in PVE API).
@@ -920,8 +930,10 @@ def main():
             agent=dict(type='bool'),
             args=dict(type='str'),
             api_host=dict(required=True),
-            api_user=dict(required=True),
             api_password=dict(no_log=True),
+            api_token_id=dict(no_log=True),
+            api_token_secret=dict(no_log=True),
+            api_user=dict(required=True),
             autostart=dict(type='bool'),
             balloon=dict(type='int'),
             bios=dict(choices=['seabios', 'ovmf']),
@@ -999,9 +1011,11 @@ def main():
     if not HAS_PROXMOXER:
         module.fail_json(msg='proxmoxer required for this module')
 
-    api_user = module.params['api_user']
     api_host = module.params['api_host']
     api_password = module.params['api_password']
+    api_token_id = module.params['api_token_id']
+    api_token_secret = module.params['api_token_secret']
+    api_user = module.params['api_user']
     clone = module.params['clone']
     cpu = module.params['cpu']
     cores = module.params['cores']
@@ -1050,15 +1064,21 @@ def main():
     if module.params['format'] == 'unspecified':
         module.params['format'] = None
 
-    # If password not set get it from PROXMOX_PASSWORD env
-    if not api_password:
-        try:
-            api_password = os.environ['PROXMOX_PASSWORD']
-        except KeyError as e:
-            module.fail_json(msg='You should set api_password param or use PROXMOX_PASSWORD environment variable')
+    auth_args = {'user': api_user}
+    if not (api_token_id and api_token_secret):
+        # If password not set get it from PROXMOX_PASSWORD env
+        if not api_password:
+            try:
+                api_password = os.environ['PROXMOX_PASSWORD']
+            except KeyError as e:
+                module.fail_json(msg='You should set api_password param or use PROXMOX_PASSWORD environment variable')
+        auth_args['password'] = api_password
+    else:
+        auth_args['token_name'] = api_token_id
+        auth_args['token_value'] = api_token_secret
 
     try:
-        proxmox = ProxmoxAPI(api_host, user=api_user, password=api_password, verify_ssl=validate_certs)
+        proxmox = ProxmoxAPI(api_host, verify_ssl=validate_certs, **auth_args)
         global PVE_MAJOR_VERSION
         PVE_MAJOR_VERSION = 3 if proxmox_version(proxmox) < LooseVersion('4.0') else 4
     except Exception as e:
diff --git a/plugins/modules/cloud/misc/proxmox_template.py b/plugins/modules/cloud/misc/proxmox_template.py
--- a/plugins/modules/cloud/misc/proxmox_template.py
+++ b/plugins/modules/cloud/misc/proxmox_template.py
@@ -30,6 +30,16 @@
       - the password to authenticate with
       - you can use PROXMOX_PASSWORD environment variable
     type: str
+  api_token_id:
+    description:
+      - Specify the token ID.
+    type: str
+    version_added: 1.3.0
+  api_token_secret:
+    description:
+      - Specify the token secret.
+    type: str
+    version_added: 1.3.0
   validate_certs:
     description:
       - enable / disable https certificate verification
@@ -173,8 +183,10 @@ def main():
     module = AnsibleModule(
         argument_spec=dict(
             api_host=dict(required=True),
-            api_user=dict(required=True),
             api_password=dict(no_log=True),
+            api_token_id=dict(no_log=True),
+            api_token_secret=dict(no_log=True),
+            api_user=dict(required=True),
             validate_certs=dict(type='bool', default=False),
             node=dict(),
             src=dict(type='path'),
@@ -191,23 +203,33 @@ def main():
         module.fail_json(msg='proxmoxer required for this module')
 
     state = module.params['state']
-    api_user = module.params['api_user']
     api_host = module.params['api_host']
     api_password = module.params['api_password']
+    api_token_id = module.params['api_token_id']
+    api_token_secret = module.params['api_token_secret']
+    api_user = module.params['api_user']
     validate_certs = module.params['validate_certs']
     node = module.params['node']
     storage = module.params['storage']
     timeout = module.params['timeout']
 
-    # If password not set get it from PROXMOX_PASSWORD env
-    if not api_password:
-        try:
-            api_password = os.environ['PROXMOX_PASSWORD']
-        except KeyError as e:
-            module.fail_json(msg='You should set api_password param or use PROXMOX_PASSWORD environment variable')
+    auth_args = {'user': api_user}
+    if not (api_token_id and api_token_secret):
+        # If password not set get it from PROXMOX_PASSWORD env
+        if not api_password:
+            try:
+                api_password = os.environ['PROXMOX_PASSWORD']
+            except KeyError as e:
+                module.fail_json(msg='You should set api_password param or use PROXMOX_PASSWORD environment variable')
+        auth_args['password'] = api_password
+    else:
+        auth_args['token_name'] = api_token_id
+        auth_args['token_value'] = api_token_secret
 
     try:
-        proxmox = ProxmoxAPI(api_host, user=api_user, password=api_password, verify_ssl=validate_certs)
+        proxmox = ProxmoxAPI(api_host, verify_ssl=validate_certs, **auth_args)
+        # Used to test the validity of the token if given
+        proxmox.version.get()
     except Exception as e:
         module.fail_json(msg='authorization on proxmox cluster failed with exception: %s' % e)
 


Test patch: