1
0
Fork 0
mirror of https://github.com/ansible-collections/community.general.git synced 2024-09-14 20:13:21 +02:00

proxmox: create a common base (#1331) (#1378)

* proxmox: create a common base

Add a doc_fragment to share the documentation regarding authentication
parameters (api_host, api_user, api_password, api_token_id,
api_token_secret as well as the lone validate_certs).

Add a module_utils to hold common code such as the argument spec (again
related to authentication paramters), a helper function to convert from
Proxmox boolean representation to python and the base class
ProxmoxAnsible.
For now it only handles the connection to Proxmox VE API but more can be
added in the future.

To check if everything is well in place add three new modules:
proxmox_{domain,group,user}_info.

And finaly tests these new modules.

* Apply suggestions from code review

Co-authored-by: Felix Fontein <felix@fontein.de>

* Add tests/integration/targets/proxmox/aliases

Co-authored-by: Felix Fontein <felix@fontein.de>
(cherry picked from commit 51a08ea398)

Co-authored-by: Tristan Le Guern <tleguern@bouledef.eu>
This commit is contained in:
patchback[bot] 2020-11-24 17:56:50 +01:00 committed by GitHub
parent b1d1391be5
commit d5c24e67e8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 784 additions and 0 deletions

View file

@ -0,0 +1,3 @@
---
minor_changes:
- proxmox - extract common code and documentation (https://github.com/ansible-collections/community.general/pull/1331).

View file

@ -0,0 +1,45 @@
# -*- coding: utf-8 -*-
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
class ModuleDocFragment(object):
# Common parameters for Proxmox VE modules
DOCUMENTATION = r'''
options:
api_host:
description:
- Specify the target host of the Proxmox VE cluster.
type: str
required: true
api_user:
description:
- Specify the user to authenticate with.
type: str
required: true
api_password:
description:
- 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
validate_certs:
description:
- If C(no), SSL certificates will not be validated.
- This should only be used on personally controlled sites using self-signed certificates.
type: bool
default: no
requirements: [ "proxmoxer", "requests" ]
'''

View file

@ -0,0 +1,86 @@
# -*- coding: utf-8 -*-
#
# Copyright: (c) 2020, Tristan Le Guern <tleguern at bouledef.eu>
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import absolute_import, division, print_function
__metaclass__ = type
import atexit
import time
import re
import traceback
PROXMOXER_IMP_ERR = None
try:
from proxmoxer import ProxmoxAPI
HAS_PROXMOXER = True
except ImportError:
HAS_PROXMOXER = False
PROXMOXER_IMP_ERR = traceback.format_exc()
from ansible.module_utils.basic import env_fallback, missing_required_lib
def proxmox_auth_argument_spec():
return dict(
api_host=dict(type='str',
required=True,
fallback=(env_fallback, ['PROXMOX_HOST'])
),
api_user=dict(type='str',
required=True,
fallback=(env_fallback, ['PROXMOX_USER'])
),
api_password=dict(type='str',
no_log=True,
fallback=(env_fallback, ['PROXMOX_PASSWORD'])
),
api_token_id=dict(type='str',
no_log=False
),
api_token_secret=dict(type='str',
no_log=True
),
validate_certs=dict(type='bool',
default=False
),
)
def proxmox_to_ansible_bool(value):
'''Convert Proxmox representation of a boolean to be ansible-friendly'''
return True if value == 1 else False
class ProxmoxAnsible(object):
"""Base class for Proxmox modules"""
def __init__(self, module):
self.module = module
self.proxmox_api = self._connect()
# 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:
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())

View file

@ -0,0 +1,133 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
#
# Copyright: Tristan Le Guern (@Aversiste) <tleguern at bouledef.eu>
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import absolute_import, division, print_function
__metaclass__ = type
DOCUMENTATION = '''
---
module: proxmox_domain_info
short_description: Retrieve information about one or more Proxmox VE domains
version_added: 1.3.0
description:
- Retrieve information about one or more Proxmox VE domains.
options:
domain:
description:
- Restrict results to a specific authentication realm.
aliases: ['realm', 'name']
type: str
author: Tristan Le Guern (@Aversiste)
extends_documentation_fragment: community.general.proxmox.documentation
'''
EXAMPLES = '''
- name: List existing domains
community.general.proxmox_domain_info:
api_host: helldorado
api_user: root@pam
api_password: "{{ password | default(omit) }}"
api_token_id: "{{ token_id | default(omit) }}"
api_token_secret: "{{ token_secret | default(omit) }}"
register: proxmox_domains
- name: Retrieve information about the pve domain
community.general.proxmox_domain_info:
api_host: helldorado
api_user: root@pam
api_password: "{{ password | default(omit) }}"
api_token_id: "{{ token_id | default(omit) }}"
api_token_secret: "{{ token_secret | default(omit) }}"
domain: pve
register: proxmox_domain_pve
'''
RETURN = '''
proxmox_domains:
description: List of authentication domains.
returned: always, but can be empty
type: list
elements: dict
contains:
comment:
description: Short description of the realm.
returned: on success
type: str
realm:
description: Realm name.
returned: on success
type: str
type:
description: Realm type.
returned: on success
type: str
digest:
description: Realm hash.
returned: on success, can be absent
type: str
'''
from ansible.module_utils.basic import AnsibleModule, missing_required_lib
from ansible_collections.community.general.plugins.module_utils.proxmox import (
proxmox_auth_argument_spec, ProxmoxAnsible, HAS_PROXMOXER, PROXMOXER_IMP_ERR)
class ProxmoxDomainInfoAnsible(ProxmoxAnsible):
def get_domain(self, realm):
try:
domain = self.proxmox_api.access.domains.get(realm)
except Exception:
self.module.fail_json(msg="Domain '%s' does not exist" % realm)
domain['realm'] = realm
return domain
def get_domains(self):
domains = self.proxmox_api.access.domains.get()
return domains
def proxmox_domain_info_argument_spec():
return dict(
domain=dict(type='str', aliases=['realm', 'name']),
)
def main():
module_args = proxmox_auth_argument_spec()
domain_info_args = proxmox_domain_info_argument_spec()
module_args.update(domain_info_args)
module = AnsibleModule(
argument_spec=module_args,
required_one_of=[('api_password', 'api_token_id')],
required_together=[('api_token_id', 'api_token_secret')],
supports_check_mode=True
)
result = dict(
changed=False
)
if not HAS_PROXMOXER:
module.fail_json(msg=missing_required_lib('proxmoxer'), exception=PROXMOXER_IMP_ERR)
proxmox = ProxmoxDomainInfoAnsible(module)
domain = module.params['domain']
if domain:
domains = [proxmox.get_domain(realm=domain)]
else:
domains = proxmox.get_domains()
result['proxmox_domains'] = domains
module.exit_json(**result)
if __name__ == '__main__':
main()

View file

@ -0,0 +1,143 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
#
# Copyright: Tristan Le Guern <tleguern at bouledef.eu>
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import absolute_import, division, print_function
__metaclass__ = type
DOCUMENTATION = '''
---
module: proxmox_group_info
short_description: Retrieve information about one or more Proxmox VE groups
version_added: 1.3.0
description:
- Retrieve information about one or more Proxmox VE groups
options:
group:
description:
- Restrict results to a specific group.
aliases: ['groupid', 'name']
type: str
author: Tristan Le Guern (@Aversiste)
extends_documentation_fragment: community.general.proxmox.documentation
'''
EXAMPLES = '''
- name: List existing groups
community.general.proxmox_group_info:
api_host: helldorado
api_user: root@pam
api_password: "{{ password | default(omit) }}"
api_token_id: "{{ token_id | default(omit) }}"
api_token_secret: "{{ token_secret | default(omit) }}"
register: proxmox_groups
- name: Retrieve information about the admin group
community.general.proxmox_group_info:
api_host: helldorado
api_user: root@pam
api_password: "{{ password | default(omit) }}"
api_token_id: "{{ token_id | default(omit) }}"
api_token_secret: "{{ token_secret | default(omit) }}"
group: admin
register: proxmox_group_admin
'''
RETURN = '''
proxmox_groups:
description: List of groups.
returned: always, but can be empty
type: list
elements: dict
contains:
comment:
description: Short description of the group.
returned: on success, can be absent
type: str
groupid:
description: Group name.
returned: on success
type: str
users:
description: List of users in the group.
returned: on success
type: list
elements: str
'''
from ansible.module_utils.basic import AnsibleModule, missing_required_lib
from ansible_collections.community.general.plugins.module_utils.proxmox import (
proxmox_auth_argument_spec, ProxmoxAnsible, HAS_PROXMOXER, PROXMOXER_IMP_ERR)
class ProxmoxGroupInfoAnsible(ProxmoxAnsible):
def get_group(self, groupid):
try:
group = self.proxmox_api.access.groups.get(groupid)
except Exception:
self.module.fail_json(msg="Group '%s' does not exist" % groupid)
group['groupid'] = groupid
return ProxmoxGroup(group)
def get_groups(self):
groups = self.proxmox_api.access.groups.get()
return [ProxmoxGroup(group) for group in groups]
class ProxmoxGroup:
def __init__(self, group):
self.group = dict()
# Data representation is not the same depending on API calls
for k, v in group.items():
if k == 'users' and type(v) == str:
self.group['users'] = v.split(',')
elif k == 'members':
self.group['users'] = group['members']
else:
self.group[k] = v
def proxmox_group_info_argument_spec():
return dict(
group=dict(type='str', aliases=['groupid', 'name']),
)
def main():
module_args = proxmox_auth_argument_spec()
group_info_args = proxmox_group_info_argument_spec()
module_args.update(group_info_args)
module = AnsibleModule(
argument_spec=module_args,
required_one_of=[('api_password', 'api_token_id')],
required_together=[('api_token_id', 'api_token_secret')],
supports_check_mode=True
)
result = dict(
changed=False
)
if not HAS_PROXMOXER:
module.fail_json(msg=missing_required_lib('proxmoxer'), exception=PROXMOXER_IMP_ERR)
proxmox = ProxmoxGroupInfoAnsible(module)
group = module.params['group']
if group:
groups = [proxmox.get_group(group=group)]
else:
groups = proxmox.get_groups()
result['proxmox_groups'] = [group.group for group in groups]
module.exit_json(**result)
if __name__ == '__main__':
main()

View file

@ -0,0 +1,256 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
#
# Copyright: Tristan Le Guern <tleguern at bouledef.eu>
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import absolute_import, division, print_function
__metaclass__ = type
DOCUMENTATION = '''
---
module: proxmox_user_info
short_description: Retrieve information about one or more Proxmox VE users
version_added: 1.3.0
description:
- Retrieve information about one or more Proxmox VE users
options:
domain:
description:
- Restrict results to a specific authentication realm.
aliases: ['realm']
type: str
user:
description:
- Restrict results to a specific user.
aliases: ['name']
type: str
userid:
description:
- Restrict results to a specific user ID, which is a concatenation of a user and domain parts.
type: str
author: Tristan Le Guern (@Aversiste)
extends_documentation_fragment: community.general.proxmox.documentation
'''
EXAMPLES = '''
- name: List existing users
community.general.proxmox_user_info:
api_host: helldorado
api_user: root@pam
api_password: "{{ password | default(omit) }}"
api_token_id: "{{ token_id | default(omit) }}"
api_token_secret: "{{ token_secret | default(omit) }}"
register: proxmox_users
- name: List existing users in the pve authentication realm
community.general.proxmox_user_info:
api_host: helldorado
api_user: root@pam
api_password: "{{ password | default(omit) }}"
api_token_id: "{{ token_id | default(omit) }}"
api_token_secret: "{{ token_secret | default(omit) }}"
domain: pve
register: proxmox_users_pve
- name: Retrieve information about admin@pve
community.general.proxmox_user_info:
api_host: helldorado
api_user: root@pam
api_password: "{{ password | default(omit) }}"
api_token_id: "{{ token_id | default(omit) }}"
api_token_secret: "{{ token_secret | default(omit) }}"
userid: admin@pve
register: proxmox_user_admin
- name: Alternative way to retrieve information about admin@pve
community.general.proxmox_user_info:
api_host: helldorado
api_user: root@pam
api_password: "{{ password | default(omit) }}"
api_token_id: "{{ token_id | default(omit) }}"
api_token_secret: "{{ token_secret | default(omit) }}"
user: admin
domain: pve
register: proxmox_user_admin
'''
RETURN = '''
proxmox_users:
description: List of users.
returned: always, but can be empty
type: list
elements: dict
contains:
comment:
description: Short description of the user.
returned: on success
type: str
domain:
description: User's authentication realm, also the right part of the user ID.
returned: on success
type: str
email:
description: User's email address.
returned: on success
type: str
enabled:
description: User's account state.
returned: on success
type: bool
expire:
description: Expiration date in seconds since EPOCH. Zero means no expiration.
returned: on success
type: int
firstname:
description: User's first name.
returned: on success
type: str
groups:
description: List of groups which the user is a member of.
returned: on success
type: list
elements: str
keys:
description: User's two factor authentication keys.
returned: on success
type: str
lastname:
description: User's last name.
returned: on success
type: str
tokens:
description: List of API tokens associated to the user.
returned: on success
type: list
elements: dict
contains:
comment:
description: Short description of the token.
returned: on success
type: str
expire:
description: Expiration date in seconds since EPOCH. Zero means no expiration.
returned: on success
type: int
privsep:
description: Describe if the API token is further restricted with ACLs or is fully privileged.
returned: on success
type: bool
tokenid:
description: Token name.
returned: on success
type: str
user:
description: User's login name, also the left part of the user ID.
returned: on success
type: str
userid:
description: Proxmox user ID, represented as user@realm.
returned: on success
type: str
'''
from ansible.module_utils.basic import AnsibleModule, missing_required_lib
from ansible_collections.community.general.plugins.module_utils.proxmox import (
proxmox_auth_argument_spec, ProxmoxAnsible, proxmox_to_ansible_bool, HAS_PROXMOXER, PROXMOXER_IMP_ERR)
class ProxmoxUserInfoAnsible(ProxmoxAnsible):
def get_user(self, userid):
try:
user = self.proxmox_api.access.users.get(userid)
except Exception:
self.module.fail_json(msg="User '%s' does not exist" % userid)
user['userid'] = userid
return ProxmoxUser(user)
def get_users(self, domain=None):
users = self.proxmox_api.access.users.get(full=1)
users = [ProxmoxUser(user) for user in users]
if domain:
return [user for user in users if user.user['domain'] == domain]
return users
class ProxmoxUser:
def __init__(self, user):
self.user = dict()
# Data representation is not the same depending on API calls
for k, v in user.items():
if k == 'enable':
self.user['enabled'] = proxmox_to_ansible_bool(user['enable'])
elif k == 'userid':
self.user['user'] = user['userid'].split('@')[0]
self.user['domain'] = user['userid'].split('@')[1]
self.user[k] = v
elif k in ['groups', 'tokens'] and (v == '' or v is None):
self.user[k] = []
elif k == 'groups' and type(v) == str:
self.user['groups'] = v.split(',')
elif k == 'tokens' and type(v) == list:
for token in v:
if 'privsep' in token:
token['privsep'] = proxmox_to_ansible_bool(token['privsep'])
self.user['tokens'] = v
elif k == 'tokens' and type(v) == dict:
self.user['tokens'] = list()
for tokenid, tokenvalues in v.items():
t = tokenvalues
t['tokenid'] = tokenid
if 'privsep' in tokenvalues:
t['privsep'] = proxmox_to_ansible_bool(tokenvalues['privsep'])
self.user['tokens'].append(t)
else:
self.user[k] = v
def proxmox_user_info_argument_spec():
return dict(
domain=dict(type='str', aliases=['realm']),
user=dict(type='str', aliases=['name']),
userid=dict(type='str'),
)
def main():
module_args = proxmox_auth_argument_spec()
user_info_args = proxmox_user_info_argument_spec()
module_args.update(user_info_args)
module = AnsibleModule(
argument_spec=module_args,
required_one_of=[('api_password', 'api_token_id')],
required_together=[('api_token_id', 'api_token_secret')],
mutually_exclusive=[('user', 'userid'), ('domain', 'userid')],
supports_check_mode=True
)
result = dict(
changed=False
)
if not HAS_PROXMOXER:
module.fail_json(msg=missing_required_lib('proxmoxer'), exception=PROXMOXER_IMP_ERR)
proxmox = ProxmoxUserInfoAnsible(module)
domain = module.params['domain']
user = module.params['user']
if user and domain:
userid = user + '@' + domain
else:
userid = module.params['userid']
if userid:
users = [proxmox.get_user(userid=userid)]
else:
users = proxmox.get_users(domain=domain)
result['proxmox_users'] = [user.user for user in users]
module.exit_json(**result)
if __name__ == '__main__':
main()

View file

@ -0,0 +1 @@
cloud/misc/proxmox_domain_info.py

View file

@ -0,0 +1 @@
cloud/misc/proxmox_group_info.py

View file

@ -0,0 +1 @@
cloud/misc/proxmox_user_info.py

View file

@ -0,0 +1,4 @@
unsupported
proxmox_domain_info
proxmox_group_info
proxmox_user_info

View file

@ -0,0 +1,111 @@
####################################################################
# WARNING: These are designed specifically for Ansible tests #
# and should not be used as examples of how to write Ansible roles #
####################################################################
# Copyright: (c) 2020, Tristan Le Guern <tleguern at bouledef.eu>
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
- name: List domains
proxmox_domain_info:
api_host: "{{ api_host }}"
api_user: "{{ user }}@{{ domain }}"
api_password: "{{ api_password | default(omit) }}"
api_token_id: "{{ api_token_id | default(omit) }}"
api_token_secret: "{{ api_token_secret | default(omit) }}"
validate_certs: "{{ validate_certs }}"
register: results
- assert:
that:
- results is not changed
- results.proxmox_domains is defined
- name: Retrieve info about pve
proxmox_domain_info:
api_host: "{{ api_host }}"
api_user: "{{ user }}@{{ domain }}"
api_password: "{{ api_password | default(omit) }}"
api_token_id: "{{ api_token_id | default(omit) }}"
api_token_secret: "{{ api_token_secret | default(omit) }}"
validate_certs: "{{ validate_certs }}"
domain: pve
register: results
- assert:
that:
- results is not changed
- results.proxmox_domains is defined
- results.proxmox_domains|length == 1
- results.proxmox_domains[0].type == 'pve'
- name: List groups
proxmox_group_info:
api_host: "{{ api_host }}"
api_user: "{{ user }}@{{ domain }}"
api_password: "{{ api_password | default(omit) }}"
api_token_id: "{{ api_token_id | default(omit) }}"
api_token_secret: "{{ api_token_secret | default(omit) }}"
validate_certs: "{{ validate_certs }}"
register: results
- assert:
that:
- results is not changed
- results.proxmox_groups is defined
- name: List users
proxmox_user_info:
api_host: "{{ api_host }}"
api_user: "{{ user }}@{{ domain }}"
api_password: "{{ api_password | default(omit) }}"
api_token_id: "{{ api_token_id | default(omit) }}"
api_token_secret: "{{ api_token_secret | default(omit) }}"
validate_certs: "{{ validate_certs }}"
register: results
- assert:
that:
- results is not changed
- results.proxmox_users is defined
- name: Retrieve info about api_user using name and domain
proxmox_user_info:
api_host: "{{ api_host }}"
api_user: "{{ user }}@{{ domain }}"
api_password: "{{ api_password | default(omit) }}"
api_token_id: "{{ api_token_id | default(omit) }}"
api_token_secret: "{{ api_token_secret | default(omit) }}"
validate_certs: "{{ validate_certs }}"
user: "{{ user }}"
domain: "{{ domain }}"
register: results_user_domain
- assert:
that:
- results_user_domain is not changed
- results_user_domain.proxmox_users is defined
- results_user_domain.proxmox_users|length == 1
- results_user_domain.proxmox_users[0].domain == "{{ domain }}"
- results_user_domain.proxmox_users[0].user == "{{ user }}"
- results_user_domain.proxmox_users[0].userid == "{{ user }}@{{ domain }}"
- name: Retrieve info about api_user using userid
proxmox_user_info:
api_host: "{{ api_host }}"
api_user: "{{ user }}@{{ domain }}"
api_password: "{{ api_password | default(omit) }}"
api_token_id: "{{ api_token_id | default(omit) }}"
api_token_secret: "{{ api_token_secret | default(omit) }}"
validate_certs: "{{ validate_certs }}"
userid: "{{ user }}@{{ domain }}"
register: results_userid
- assert:
that:
- results_userid is not changed
- results_userid.proxmox_users is defined
- results_userid.proxmox_users|length == 1
- results_userid.proxmox_users[0].domain == "{{ domain }}"
- results_userid.proxmox_users[0].user == "{{ user }}"
- results_userid.proxmox_users[0].userid == "{{ user }}@{{ domain }}"