diff --git a/plugins/module_utils/proxmox_datacenter_acl.py b/plugins/module_utils/proxmox_datacenter_acl.py new file mode 100644 index 0000000..4e293be --- /dev/null +++ b/plugins/module_utils/proxmox_datacenter_acl.py @@ -0,0 +1,77 @@ +import dataclasses +from dataclasses import dataclass, field +from typing import List, Tuple + +from ansible_collections.finallycoffee.proxmox.plugins.module_utils.common import _proxmox_request, ProxmoxAuthInfo + + +@dataclass(frozen=True) +class ProxmoxACL: + path: str + role: str + propagate: bool = True + user: str = None + group: str = None + token: str = None + + +def get_acls(auth_info: ProxmoxAuthInfo) -> List['ProxmoxACL']: + acl_answer = _proxmox_request('get', '/access/acl', auth_info).json() + return list(map( + lambda r: ProxmoxACL(r['path'], r['roleid'], r['propagate'], + (r['ugid'] if (r['type'] == 'user') else None), + (r['ugid'] if (r['type'] == 'group') else None), + (r['ugid'] if (r['type'] == 'token') else None)), + acl_answer['data'] + )) + + +def set_acl(auth_info: ProxmoxAuthInfo, acl: ProxmoxACL, dry_run: bool, state: str = 'present') -> Tuple[dict, dict]: + existing_acls = get_acls(auth_info) + # find existing acl matching the specified one + try: + existing_acl = [x for x in existing_acls if x == acl][0] + except: + existing_acl = None + + if state == 'present': + acl_spec = _acl_to_dict(acl) | { + 'propagate': "1" if acl.propagate else "0" + } + if not dry_run: + acl_res = _proxmox_request('put', '/access/acl', auth_info, data=acl_spec) + acl_res.raise_for_status() + if acl_res.ok: + new_acl = acl + else: + new_acl = acl + elif state == 'exact': + # Also match on propagate and remove ACLs which match in all fields except propagation + pass + else: + # ACL to be removed + if not dry_run: + acl_spec = _acl_to_dict(acl) | { + 'delete': 1 + } + acl_res = _proxmox_request('put', '/access/acl', auth_info, data=acl_spec) + acl_res.raise_for_status() + if acl_res.ok: + new_acl = None + else: + new_acl = None + return dataclasses.asdict(existing_acl) if existing_acl else None, dataclasses.asdict(new_acl) if new_acl else None + + +def _acl_to_dict(acl: ProxmoxACL) -> dict: + acl_spec = { + 'path': acl.path, + 'roles': acl.role, + } + if acl.user: + acl_spec['users'] = acl.user + if acl.group: + acl_spec['groups'] = acl.group + if acl.token: + acl_spec['tokens'] = acl.token + return acl_spec diff --git a/plugins/module_utils/proxmox_datacenter_group.py b/plugins/module_utils/proxmox_datacenter_group.py new file mode 100644 index 0000000..8ce358d --- /dev/null +++ b/plugins/module_utils/proxmox_datacenter_group.py @@ -0,0 +1,19 @@ +from dataclasses import dataclass, field +from typing import List, Tuple + +from ansible_collections.finallycoffee.proxmox.plugins.module_utils.common import _proxmox_request, ProxmoxAuthInfo + + +@dataclass(frozen=True) +class ProxmoxGroup: + name: str + members: List = field(default_factory=lambda: []) + comment: str = None + + +def get_groups(auth_info: ProxmoxAuthInfo) -> List['ProxmoxGroups']: + group_answer = _proxmox_request('get', '/access/groups', auth_info).json() + return list(map( + lambda r: ProxmoxGroup(r['groupid'], r['users'].split(','), r.get('comment', '')), + group_answer['data'] + )) diff --git a/plugins/module_utils/proxmox_datacenter_realm.py b/plugins/module_utils/proxmox_datacenter_realm.py index 3bb19f0..be06ce9 100644 --- a/plugins/module_utils/proxmox_datacenter_realm.py +++ b/plugins/module_utils/proxmox_datacenter_realm.py @@ -28,7 +28,9 @@ def set_realm_data(auth_info: ProxmoxAuthInfo, realm: str, config: dict[str, str if existing_realm.ok: existing_realm_data = existing_realm.json()['data'] if not dry_run: - realm_res = _proxmox_request('put', f"/access/domains/{realm}", auth_info, data=config) + put_args_denylist = ['type'] + put_config = {k: v for k, v in config.items() if k not in put_args_denylist} + realm_res = _proxmox_request('put', f"/access/domains/{realm}", auth_info, data=put_config) realm_res.raise_for_status() else: if not dry_run: @@ -53,3 +55,8 @@ def set_realm_data(auth_info: ProxmoxAuthInfo, realm: str, config: dict[str, str new_realm_data = None return existing_realm_data, new_realm_data + +def do_realm_sync(auth_info: ProxmoxAuthInfo, realm: str, config: dict[str, str], dry_run: bool): + sync_config = config | {"dry-run": dry_run} + realm_sync = _proxmox_request('post', f"/access/domains/{realm}/sync", auth_info, data=sync_config) + return realm_sync.ok diff --git a/plugins/module_utils/proxmox_datacenter_role.py b/plugins/module_utils/proxmox_datacenter_role.py new file mode 100644 index 0000000..2bde0f7 --- /dev/null +++ b/plugins/module_utils/proxmox_datacenter_role.py @@ -0,0 +1,63 @@ +from dataclasses import dataclass, field +from typing import List, Tuple + +from ansible_collections.finallycoffee.proxmox.plugins.module_utils.common import _proxmox_request, ProxmoxAuthInfo + + +@dataclass(frozen=True) +class ProxmoxRole: + name: str + privileges: List = field(default_factory=lambda: []) + built_in: bool = False + + +def get_roles(auth_info: ProxmoxAuthInfo) -> List['ProxmoxRoles']: + role_answer = _proxmox_request('get', '/access/roles', auth_info).json() + return list(map( + lambda r: ProxmoxRole(r['roleid'], r['privs'].split(','), bool(r.get('special', False)) is True), + role_answer['data'] + )) + + +def set_role_data(auth_info: ProxmoxAuthInfo, role: str, privileges: list[str], dry_run: bool, state: str = 'present') -> Tuple[dict, dict]: + existing_role = _proxmox_request('get', f"/access/roles/{role}", auth_info) + existing_role_data = existing_role.json()['data'] + new_role_data = '' + if state == 'present' or state == 'exact': + if existing_role.ok: + # When state is present, we only append all listed privs to existing roles, + # otherwise we set the role's permission list directly + changeset = { + 'privs': ','.join(privileges), + 'append': False if (state == 'exact') else True + } + if not dry_run: + role_res = _proxmox_request('put', f"/access/roles/{role}", auth_info, data=changeset) + role_res.raise_for_status() + else: + # For new roles, state being exact or present is the same thing + if not dry_run: + role_res = _proxmox_request('post', '/access/roles', auth_info, data=({ + 'roleId': role, + 'privs': ','.join(privileges) + })) + role_res.raise_for_status() + if not dry_run: + if role_res.ok: + new_role_data = _proxmox_request('get', f"/access/roles/{role}", auth_info).json()['data'] + else: + new_role_data = {'role': role, 'privs': privileges} + else: + if existing_role.ok: + existing_role_data = existing_role.json()['data'] + if not dry_run: + role_res = _proxmox_request('delete', f"/access/role/{role}", auth_info) + role_res.raise_for_status() + if role_res.ok: + new_role_data = None + else: + new_role_data = None + else: + new_role_data = None + return existing_role_data, new_role_data + diff --git a/plugins/module_utils/proxmox_node.py b/plugins/module_utils/proxmox_node.py new file mode 100644 index 0000000..4bda14d --- /dev/null +++ b/plugins/module_utils/proxmox_node.py @@ -0,0 +1,11 @@ +from ansible_collections.finallycoffee.proxmox.plugins.module_utils.common import _proxmox_request, ProxmoxAuthInfo + + +def get_nodes(auth_info: ProxmoxAuthInfo) -> [dict]: + node_answer = _proxmox_request('get', f"/nodes", auth_info).json() + return node_answer['data'] + + +def get_node(auth_info: ProxmoxAuthInfo, node: str) -> [dict]: + node_answer = _proxmox_request('get', f"/nodes/{node}", auth_info).json() + return node_answer['data'] diff --git a/plugins/module_utils/proxmox_pool.py b/plugins/module_utils/proxmox_pool.py new file mode 100644 index 0000000..f3c4ec9 --- /dev/null +++ b/plugins/module_utils/proxmox_pool.py @@ -0,0 +1,13 @@ +from typing import List, Tuple + +from ansible_collections.finallycoffee.proxmox.plugins.module_utils.common import _proxmox_request, ProxmoxAuthInfo + + +def get_pools(auth_info: ProxmoxAuthInfo) -> [str]: + pool_answer = _proxmox_request('get', f"/pools", auth_info).json() + return pool_answer['data'] + + +def get_pool(auth_info: ProxmoxAuthInfo, pool: str) -> [dict]: + pool_answer = _proxmox_request('get', f"/pools/{pool}", auth_info).json() + return pool_answer['data'] diff --git a/plugins/modules/acl.py b/plugins/modules/acl.py new file mode 100644 index 0000000..5e4c810 --- /dev/null +++ b/plugins/modules/acl.py @@ -0,0 +1,185 @@ +#!/usr/bin/python +# coding: utf-8 + +# (c) 2022, Johanna Dorothea Reichmann + +__metaclass__ = type + +import traceback + +from ansible.module_utils.basic import AnsibleModule + +from ansible_collections.finallycoffee.proxmox.plugins.module_utils.common import * +from ansible_collections.finallycoffee.proxmox.plugins.module_utils.proxmox_datacenter_acl import * + + +LIB_IMP_ERR = None +try: + from ansible_collections.finallycoffee.proxmox.plugins.module_utils.proxmox_datacenter_acl import set_acl + from ansible_collections.finallycoffee.proxmox.plugins.module_utils.proxmox_datacenter_acl import ProxmoxACL + HAS_LIB = True +except: + HAS_LIB = False + LIB_IMP_ERR = traceback.format_exc() + + +DOCUMENTATION = r''' +--- +module: acl +author: +- Johanna Dorothea Reichmann (transcaffeine@finally.coffee) +requirements: + - python >= 3.9 +short_description: Configures an access control list in a proxmox node +description: + - "Configue access control lists in a proxmox instance" +options: + proxmox_instance: + description: Location of the proxmox API with scheme, domain name/ip and port, e.g. https://localhost:8006 + type: str + required: true + proxmox_api_token_id: + description: The token ID containing username, realm and token name (format: user@realm!name) + type: str + required: true + proxmox_api_secret: + description: The secret + type: str + required: true + proxmox_api_verify_cert: + description: If the certificate presented for `proxmox_instance_url` should be verified + type: bool + required: false + default: true + path: + description: Path to which access should be granted. Must be a valid proxmox access control path + type: str + required: true + role: + description: The role which the user/group/token should be added to when they match the ACL + type: str + required: true + group: + description: The group to match on the ACL. Must be a valid proxmox group id + type: str + required: false + user: + description: The user to match on the ACL. Must be a valid proxmox user id + type: str + required: false + token: + description: The token to match on the ACL. Must be a valid proxmox token id + type: str + required: false + propagate: + description: Wether to allow inheriting permissions or not. + type: bool + default: true + required: false + state: + description: >- + If the access control list should be present, absent or exact. When state is exact, + the value of `propagate` is also considered, and if an existing access control rule is found + which only differs in the value of `propagate`, the old rule is removed by the module, + essentially replacing it. + type: str + choices: [present, exact, absent] + default: present + required: false +''' + +EXAMPLES = r''' +- name: Configure an admin group from LDAP as proxmox global admins + finallycoffee.proxmox.acl: + proxmox_instance: https://my.proxmox-node.local:8006 + promox_api_token_id: root@pam!token + proxmox_api_secret: supersecuretokencontent + path: / + roles: Administrator + groups: proxmox-admins-openldap + propagate: true + state: present +- name: Configure an LDAP user to use their own VM + finallycoffee.proxmox.acl: + proxmox_instance: https://my.proxmox-node.local:8006 + promox_api_token_id: root@pam!token + proxmox_api_secret: supersecuretokencontent + path: /vms/1000 + roles: PVEVMUser + groups: myusername-openldap + state: present +''' + +RETURN = r''' +acl: + description: The updated/created access control list + returned: always + type: complex + +''' + + +def main(): + _ = dict + module = AnsibleModule( + argument_spec=_( + proxmox_instance=_(required=True, type='str'), + proxmox_api_token_id=_(required=True, type='str'), + proxmox_api_secret=_(type='str', required=True, no_log=True), + proxmox_api_verify_cert=_(type='bool', required=False, default=True), + path=_(required=True, type='str'), + role=_(required=True, type='str'), + group=_(required=False, type='str'), + user=_(required=False, type='str'), + token=_(required=False, type='str'), + propagate=_(required=False, default=True, type='bool'), + state=_(type='str', required=False, default='present', choices=['present', 'exact', 'absent']) + ), + supports_check_mode=True + ) + + result = _( + changed=False, + diff={}, + message='' + ) + + realms = [] + try: + acl_spec = ProxmoxACL( + module.params['path'], + module.params['role'], + module.params['propagate'], + module.params.get('user', None), + module.params.get('group', None), + module.params.get('token', None), + ) + acl_change = set_acl(ProxmoxAuthInfo( + module.params['proxmox_instance'], + module.params['proxmox_api_token_id'], + module.params['proxmox_api_secret'], + module.params['proxmox_api_verify_cert'], + ), + acl_spec, + module.check_mode, + module.params['state']) + except IOError as owie: + result['msg'] = owie + module.exit_json(**result) + + diff_excluded_keys = [] + result['acl'] = acl_change[1] + result['diff'] = { + 'before_header': f"{acl_change[0].get('role', module.params['role'])}@{acl_change[0].get('path', module.params['path'])}" if acl_change[0] else acl_change[0], + 'after_header': f"{acl_change[1].get('role', module.params['role'])}@{acl_change[1].get('path', module.params['path'])}" if acl_change[1] else acl_change[1], + 'before': {k: v for k, v in acl_change[0].items() if k not in diff_excluded_keys} if acl_change[0] else acl_change[0], + 'after': {k: v for k, v in acl_change[1].items() if k not in diff_excluded_keys} if acl_change[1] else acl_change[1], + } + # TODO: Needs special handling for state=exact + result['changed'] = acl_change[0] != acl_change[1] + + module.exit_json(**result) + + +if __name__ == '__main__': + main() diff --git a/plugins/modules/group_info.py b/plugins/modules/group_info.py new file mode 100644 index 0000000..8c4bdb7 --- /dev/null +++ b/plugins/modules/group_info.py @@ -0,0 +1,127 @@ +#!/usr/bin/python +# coding: utf-8 + +# (c) 2022, Johanna Dorothea Reichmann + +__metaclass__ = type + +import dataclasses +import traceback + +from ansible.module_utils.basic import AnsibleModule + +from ansible_collections.finallycoffee.proxmox.plugins.module_utils.common import * +from ansible_collections.finallycoffee.proxmox.plugins.module_utils.proxmox_datacenter_group import * + + +DOCUMENTATION = r''' +--- +module: group_info +author: +- Johanna Dorothea Reichmann (transcaffeine@finally.coffee) +requirements: + - python >= 3.9 +short_description: Get all groups of a proxmox +description: + - "Lists all groups in proxmos" +options: + proxmox_instance: + description: Location of the proxmox API with scheme, domain name/ip and port, e.g. https://localhost:8006 + type: str + required: true + proxmox_api_token_id: + description: The token ID containing username, realm and token name (format: user@realm!name) + type: str + required: true + proxmox_api_secret: + description: The secret + type: str + required: true + proxmox_api_verify_cert: + description: If the certificate presented for `proxmox_instance_url` should be verified + type: bool + required: false + default: true + group: + description: Group to retrieve information about. If left omitted, return all groups + type: str + required: false +''' + +EXAMPLES = r''' +- name: Retrieve all groups + finallycoffee.proxmox.group_info: + proxmox_instance: https://my.proxmox-node.local:8006 + promox_api_token_id: root@pam!token + proxmox_api_secret: supersecuretokencontent +- name: Retrieve group information for group developers-realm + finallycoffee.proxmox.group_info: + proxmox_instance: https://my.proxmox-node.local:8006 + promox_api_token_id: root@pam!token + proxmox_api_secret: supersecuretokencontent + group: developers-realm +''' + +RETURN = r''' +groups: + description: The retrieved groups + returned: When groups were found (matching the filter) + type: list + elements: dict[str, list[str]] +group: + description: The groups's name (`.name`) and members (`.members`) + returned: When a single group was queried + type: dict[str, list[str]] + +''' + + +def main(): + _ = dict + module = AnsibleModule( + argument_spec=_( + proxmox_instance=_(required=True, type='str'), + proxmox_api_token_id=_(required=True, type='str'), + proxmox_api_secret=_(type='str', required=True, no_log=True), + proxmox_api_verify_cert=_(type='bool', required=False, default=True), + group=_(required=False, type='str'), + ), + supports_check_mode=True + ) + + result = _( + changed=False, + diff={}, + message='' + ) + + groups = [] + try: + groups = get_groups(ProxmoxAuthInfo( + module.params['proxmox_instance'], + module.params['proxmox_api_token_id'], + module.params['proxmox_api_secret'], + module.params['proxmox_api_verify_cert'], + )) + except IOError as owie: + result['msg'] = owie + module.exit_json(**result) + + result['groups'] = list( + map(lambda r: dataclasses.asdict(r), + filter(lambda r: r.name == module.params['group'] + if module.params['group'] is not None + else True, + groups) + ) + ) + if module.params['group'] is not None: + result['group'] = dataclasses.asdict( + list(filter(lambda r: r.name == module.params['group'], groups))[0] + ) + + module.exit_json(**result) + + +if __name__ == '__main__': + main() diff --git a/plugins/modules/node_info.py b/plugins/modules/node_info.py new file mode 100644 index 0000000..20af2f4 --- /dev/null +++ b/plugins/modules/node_info.py @@ -0,0 +1,115 @@ +#!/usr/bin/python +# coding: utf-8 + +# (c) 2022, Johanna Dorothea Reichmann + +__metaclass__ = type + +import json + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.finallycoffee.proxmox.plugins.module_utils.common import * +from ansible_collections.finallycoffee.proxmox.plugins.module_utils.proxmox_node import * + +DOCUMENTATION = r''' +--- +module: node_info +author: +- Johanna Dorothea Reichmann (transcaffeine@finally.coffee) +requirements: + - python >= 3.9 +short_description: Get information about all proxmox cluster nodes +description: + - "Equivalent to /api2/json/nodes, returns all available top-level information about all visible nodes" +options: + proxmox_instance: + description: Location of the proxmox API with scheme, domain name/ip and port, e.g. https://localhost:8006 + type: str + required: true + proxmox_api_token_id: + description: The token ID containing username, realm and token name (format: user@realm!name) + type: str + required: true + proxmox_api_secret: + description: The secret + type: str + required: true + proxmox_api_verify_cert: + description: If the certificate presented for `proxmox_instance_url` should be verified + type: bool + required: false + default: true + node: + description: Node to retrieve information about. + type: str + required: true +''' + +EXAMPLES = r''' +- name: Retrieve all nodes + finallycoffee.proxmox.node_info: + proxmox_instance: https://my.proxmox-node.local:8006 + promox_api_token_id: root@pam!token + proxmox_api_secret: supersecuretokencontent + +- name: Retrieve info about node called "my_pm_host" + finallycoffee.proxmox.node_info: + proxmox_instance: https://my.proxmox-node.local:8006 + promox_api_token_id: root@pam!token + proxmox_api_secret: supersecuretokencontent + node: my_pm_hst +''' + +RETURN = r''' +nodes: + description: All nodes present in the datacenter + returned: When nodes are visible to the API + type: list + elements: dict[str, str] +node: + description: Specific node information when queried for a single node + returned: When a single node was queried and found + type: dict[str, str] +''' + + +def main(): + _ = dict + module = AnsibleModule( + argument_spec=_( + proxmox_instance=_(required=True, type='str'), + proxmox_api_token_id=_(required=True, type='str'), + proxmox_api_secret=_(type='str', required=True, no_log=True), + proxmox_api_verify_cert=_(type='bool', required=False, default=True), + node=_(required=False, type='str'), + ), + supports_check_mode=True + ) + + result = _( + changed=False, + diff={}, + message='' + ) + + nodes = [] + try: + nodes = get_nodes(ProxmoxAuthInfo( + module.params['proxmox_instance'], + module.params['proxmox_api_token_id'], + module.params['proxmox_api_secret'], + module.params['proxmox_api_verify_cert'], + )) + except IOError as owie: + result['msg'] = owie + module.exit_json(**result) + + result['nodes'] = nodes + if module.params['node'] is not None: + result['node'] = list(filter(lambda n: n.name == module.params['node'], nodes))[0] + + module.exit_json(**result) + + +if __name__ == '__main__': + main() diff --git a/plugins/modules/pool_info.py b/plugins/modules/pool_info.py new file mode 100644 index 0000000..ddcc018 --- /dev/null +++ b/plugins/modules/pool_info.py @@ -0,0 +1,96 @@ +#!/usr/bin/python +# coding: utf-8 + +# (c) 2022, Johanna Dorothea Reichmann + +__metaclass__ = type + +import json + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.finallycoffee.proxmox.plugins.module_utils.common import * +from ansible_collections.finallycoffee.proxmox.plugins.module_utils.proxmox_pool import * + +DOCUMENTATION = r''' +--- +module: pool_info +author: +- Johanna Dorothea Reichmann (transcaffeine@finally.coffee) +requirements: + - python >= 3.9 +short_description: Get information about all proxmox cluster nodes +description: + - "Equivalent to /api2/json/nodes, returns all available top-level information about all visible nodes" +options: + proxmox_instance: + description: Location of the proxmox API with scheme, domain name/ip and port, e.g. https://localhost:8006 + type: str + required: true + proxmox_api_token_id: + description: The token ID containing username, realm and token name (format: user@realm!name) + type: str + required: true + proxmox_api_secret: + description: The secret + type: str + required: true + proxmox_api_verify_cert: + description: If the certificate presented for `proxmox_instance_url` should be verified + type: bool + required: false + default: true +''' + +EXAMPLES = r''' +- name: Retrieve all resource pools + finallycoffee.proxmox.pool_info: + proxmox_instance: https://my.proxmox-node.local:8006 + promox_api_token_id: root@pam!token + proxmox_api_secret: supersecuretokencontent +''' + +RETURN = r''' +pools: + description: All resource pools present in the datacenter + returned: When pools are visible (token has atleast Pool.Audit on each pool) + type: list + elements: str +''' + + +def main(): + _ = dict + module = AnsibleModule( + argument_spec=_( + proxmox_instance=_(required=True, type='str'), + proxmox_api_token_id=_(required=True, type='str'), + proxmox_api_secret=_(type='str', required=True, no_log=True), + proxmox_api_verify_cert=_(type='bool', required=False, default=True), + ), + supports_check_mode=True + ) + + result = _( + changed=False, + diff={}, + message='' + ) + + pools = [] + try: + pools = get_pools(ProxmoxAuthInfo( + module.params['proxmox_instance'], + module.params['proxmox_api_token_id'], + module.params['proxmox_api_secret'], + module.params['proxmox_api_verify_cert'], + )) + except IOError as owie: + result['msg'] = owie + module.exit_json(**result) + + result['pools'] = pools + module.exit_json(**result) + + +if __name__ == '__main__': + main() diff --git a/plugins/modules/realm_sync.py b/plugins/modules/realm_sync.py new file mode 100644 index 0000000..0530770 --- /dev/null +++ b/plugins/modules/realm_sync.py @@ -0,0 +1,135 @@ +#!/usr/bin/python +# coding: utf-8 + +# (c) 2022, Johanna Dorothea Reichmann + +__metaclass__ = type + +import traceback + +from ansible.module_utils.basic import AnsibleModule + +from ansible_collections.finallycoffee.proxmox.plugins.module_utils.common import * +from ansible_collections.finallycoffee.proxmox.plugins.module_utils.proxmox_datacenter_realm import * + + +LIB_IMP_ERR = None +try: + from ansible_collections.finallycoffee.proxmox.plugins.module_utils.proxmox_datacenter_realm import do_realm_sync + HAS_LIB = True +except: + HAS_LIB = False + LIB_IMP_ERR = traceback.format_exc() + + +DOCUMENTATION = r''' +--- +module: realm_sync +author: +- Johanna Dorothea Reichmann (transcaffeine@finally.coffee) +requirements: + - python >= 3.9 +short_description: Ensures a realm is synchronized +description: + - "Allows synchronizing users, groups or both using the realm sync mechanism" +options: + proxmox_instance: + description: Location of the proxmox API with scheme, domain name/ip and port, e.g. https://localhost:8006 + type: str + required: true + proxmox_api_token_id: + description: The token ID containing username, realm and token name (format: user@realm!name) + type: str + required: true + proxmox_api_secret: + description: The secret + type: str + required: true + proxmox_api_verify_cert: + description: If the certificate presented for `proxmox_instance_url` should be verified + type: bool + required: false + default: true + name: + description: Realm to schedule synchronize job for + type: str + required: true + config: + description: >- + Configuration for the synchronization. See + https://pve.proxmox.com/pve-docs/api-viewer/index.html#/access/domains/{realm}/sync + for a list of parameters + type: dict[str, str] + required: true +''' + +EXAMPLES = r''' +- name: Sync only users on realm 'org_ldap', deleting users who can't be found amymore + finallycoffee.proxmox.realm_sync: + proxmox_instance: https://my.proxmox-node.local:8006 + promox_api_token_id: root@pam!token + proxmox_api_secret: supersecuretokencontent + realms: org_ldap + config: + enable-new: true + remove-vanished: entry + scope: users + +- name: Sync groups and users and delete all ACLs on vanished groups and/or users + finallycoffee.proxmox.realm_sync: + proxmox_instance: https://my.proxmox-node.local:8006 + promox_api_token_id: root@pam!token + proxmox_api_secret: supersecuretokencontent + realm: org_ldap + config: + enable-new: true + remove-vanished: acl;properties;entry + scope: both +''' + +RETURN = r''' + +''' + + +def main(): + _ = dict + module = AnsibleModule( + argument_spec=_( + proxmox_instance=_(required=True, type='str'), + proxmox_api_token_id=_(required=True, type='str'), + proxmox_api_secret=_(type='str', required=True, no_log=True), + proxmox_api_verify_cert=_(type='bool', required=False, default=True), + name=_(required=True, type='str'), + config=_(required=True, type='dict') + ), + supports_check_mode=True + ) + + result = _( + changed=False, + diff={}, + message='' + ) + + try: + do_realm_sync(ProxmoxAuthInfo( + module.params['proxmox_instance'], + module.params['proxmox_api_token_id'], + module.params['proxmox_api_secret'], + module.params['proxmox_api_verify_cert'], + ), + module.params['name'], + module.params['config'], + module.check_mode) + except IOError as owie: + result['msg'] = owie + module.exit_json(**result) + + result['changed'] = True + + module.exit_json(**result) + + +if __name__ == '__main__': + main() diff --git a/plugins/modules/role_info.py b/plugins/modules/role_info.py new file mode 100644 index 0000000..1cd7207 --- /dev/null +++ b/plugins/modules/role_info.py @@ -0,0 +1,127 @@ +#!/usr/bin/python +# coding: utf-8 + +# (c) 2022, Johanna Dorothea Reichmann + +__metaclass__ = type + +import dataclasses +import traceback + +from ansible.module_utils.basic import AnsibleModule + +from ansible_collections.finallycoffee.proxmox.plugins.module_utils.common import * +from ansible_collections.finallycoffee.proxmox.plugins.module_utils.proxmox_datacenter_role import * + + +DOCUMENTATION = r''' +--- +module: role_info +author: +- Johanna Dorothea Reichmann (transcaffeine@finally.coffee) +requirements: + - python >= 3.9 +short_description: Get configured and builtin roles of a proxmox node +description: + - "Lists all configured roles in a proxmox instance" +options: + proxmox_instance: + description: Location of the proxmox API with scheme, domain name/ip and port, e.g. https://localhost:8006 + type: str + required: true + proxmox_api_token_id: + description: The token ID containing username, realm and token name (format: user@realm!name) + type: str + required: true + proxmox_api_secret: + description: The secret + type: str + required: true + proxmox_api_verify_cert: + description: If the certificate presented for `proxmox_instance_url` should be verified + type: bool + required: false + default: true + role: + description: Role to retrieve information about. If left omitted, return all roles + type: str + required: false +''' + +EXAMPLES = r''' +- name: Retrieve all roles + finallycoffee.proxmox.role_info: + proxmox_instance: https://my.proxmox-node.local:8006 + promox_api_token_id: root@pam!token + proxmox_api_secret: supersecuretokencontent +- name: Retrieve role information for 'PVEAdmin' + finallycoffee.proxmox.role_info: + proxmox_instance: https://my.proxmox-node.local:8006 + promox_api_token_id: root@pam!token + proxmox_api_secret: supersecuretokencontent + role: PVEAdmin +''' + +RETURN = r''' +roles: + description: The retrieved roles + returned: When roles were found (matching the filter) + type: list + elements: dict[str, dict[str], bool] +role: + description: The role's name (`.name`) and privileges (`.privileges`) + returned: When a single role was queried + type: dict[str, list[str]] + +''' + + +def main(): + _ = dict + module = AnsibleModule( + argument_spec=_( + proxmox_instance=_(required=True, type='str'), + proxmox_api_token_id=_(required=True, type='str'), + proxmox_api_secret=_(type='str', required=True, no_log=True), + proxmox_api_verify_cert=_(type='bool', required=False, default=True), + role=_(required=False, type='str'), + ), + supports_check_mode=True + ) + + result = _( + changed=False, + diff={}, + message='' + ) + + roles = [] + try: + roles = get_roles(ProxmoxAuthInfo( + module.params['proxmox_instance'], + module.params['proxmox_api_token_id'], + module.params['proxmox_api_secret'], + module.params['proxmox_api_verify_cert'], + )) + except IOError as owie: + result['msg'] = owie + module.exit_json(**result) + + result['roles'] = list( + map(lambda r: dataclasses.asdict(r), + filter(lambda r: r.name == module.params['role'] + if module.params['role'] is not None + else True, + roles) + ) + ) + if module.params['role'] is not None: + result['role'] = dataclasses.asdict( + list(filter(lambda r: r.name == module.params['role'], roles))[0] + ) + + module.exit_json(**result) + + +if __name__ == '__main__': + main()