diff --git a/changelogs/fragments/1331-proxmox-info-modules.yml b/changelogs/fragments/1331-proxmox-info-modules.yml new file mode 100644 index 0000000000..c7510d30cc --- /dev/null +++ b/changelogs/fragments/1331-proxmox-info-modules.yml @@ -0,0 +1,3 @@ +--- +minor_changes: + - proxmox - extract common code and documentation (https://github.com/ansible-collections/community.general/pull/1331). diff --git a/plugins/doc_fragments/proxmox.py b/plugins/doc_fragments/proxmox.py new file mode 100644 index 0000000000..1d0490aa8b --- /dev/null +++ b/plugins/doc_fragments/proxmox.py @@ -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" ] +''' diff --git a/plugins/module_utils/proxmox.py b/plugins/module_utils/proxmox.py new file mode 100644 index 0000000000..666f87777a --- /dev/null +++ b/plugins/module_utils/proxmox.py @@ -0,0 +1,86 @@ +# -*- coding: utf-8 -*- +# +# Copyright: (c) 2020, Tristan Le Guern +# 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()) diff --git a/plugins/modules/cloud/misc/proxmox_domain_info.py b/plugins/modules/cloud/misc/proxmox_domain_info.py new file mode 100644 index 0000000000..fc7c37c613 --- /dev/null +++ b/plugins/modules/cloud/misc/proxmox_domain_info.py @@ -0,0 +1,133 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright: Tristan Le Guern (@Aversiste) +# 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() diff --git a/plugins/modules/cloud/misc/proxmox_group_info.py b/plugins/modules/cloud/misc/proxmox_group_info.py new file mode 100644 index 0000000000..063d28e559 --- /dev/null +++ b/plugins/modules/cloud/misc/proxmox_group_info.py @@ -0,0 +1,143 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright: Tristan Le Guern +# 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() diff --git a/plugins/modules/cloud/misc/proxmox_user_info.py b/plugins/modules/cloud/misc/proxmox_user_info.py new file mode 100644 index 0000000000..1de93e6009 --- /dev/null +++ b/plugins/modules/cloud/misc/proxmox_user_info.py @@ -0,0 +1,256 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright: Tristan Le Guern +# 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() diff --git a/plugins/modules/proxmox_domain_info.py b/plugins/modules/proxmox_domain_info.py new file mode 120000 index 0000000000..a14a61a33b --- /dev/null +++ b/plugins/modules/proxmox_domain_info.py @@ -0,0 +1 @@ +cloud/misc/proxmox_domain_info.py \ No newline at end of file diff --git a/plugins/modules/proxmox_group_info.py b/plugins/modules/proxmox_group_info.py new file mode 120000 index 0000000000..f9b760742b --- /dev/null +++ b/plugins/modules/proxmox_group_info.py @@ -0,0 +1 @@ +cloud/misc/proxmox_group_info.py \ No newline at end of file diff --git a/plugins/modules/proxmox_user_info.py b/plugins/modules/proxmox_user_info.py new file mode 120000 index 0000000000..a713ac8ddf --- /dev/null +++ b/plugins/modules/proxmox_user_info.py @@ -0,0 +1 @@ +cloud/misc/proxmox_user_info.py \ No newline at end of file diff --git a/tests/integration/targets/proxmox/aliases b/tests/integration/targets/proxmox/aliases new file mode 100644 index 0000000000..d5a5dcd139 --- /dev/null +++ b/tests/integration/targets/proxmox/aliases @@ -0,0 +1,4 @@ +unsupported +proxmox_domain_info +proxmox_group_info +proxmox_user_info diff --git a/tests/integration/targets/proxmox/tasks/main.yml b/tests/integration/targets/proxmox/tasks/main.yml new file mode 100644 index 0000000000..c615faf516 --- /dev/null +++ b/tests/integration/targets/proxmox/tasks/main.yml @@ -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 +# 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 }}"