diff --git a/plugins/module_utils/proxmox_node_iface.py b/plugins/module_utils/proxmox_node_iface.py new file mode 100644 index 0000000..59bf17d --- /dev/null +++ b/plugins/module_utils/proxmox_node_iface.py @@ -0,0 +1,61 @@ +from typing import List, Tuple + +from ansible_collections.finallycoffee.proxmox.plugins.module_utils.common import _proxmox_request, ProxmoxAuthInfo + + +PM_ACCEPTED_IFACE_TYPES = [ + 'bridge', + 'bond', + 'eth', + 'alias', + 'vlan', + 'OVSBridge', + 'OVSBond', + 'OVSPort', + 'OVSIntPort', + 'unknown', +] + + +def get_interfaces(auth_info: ProxmoxAuthInfo, node: str) -> [dict]: + realm_answer = _proxmox_request('get', f"/nodes/{node}/network", auth_info).json() + return realm_answer['data'] + + +def set_interface_config(auth_info: ProxmoxAuthInfo, node: str, name: str, iface_type: str, config: dict[str, str], dry_run: bool, state: str = 'present') -> Tuple[dict, dict]: + existing_iface_data = None + iface_data = None + existing_iface = _proxmox_request('get', f"/nodes/{node}/network/{name}", auth_info) + if existing_iface.ok: + existing_iface_data = existing_iface.json()['data'] + if state == 'absent': + iface = _delete_iface(auth_info, node, name, dry_run) + return existing_iface_data, iface_data + else: + iface = _upsert_iface(auth_info, node, name, + iface_type, config, (existing_iface_data is not None), dry_run) + return existing_iface_data, iface + + +def _upsert_iface(auth_info: ProxmoxAuthInfo, node: str, name: str, iface_type: str, config: dict[str, str], update: bool = False, dry_run: bool = False) -> dict[str, str]: + extra_config = {'node': node, 'iface': name, 'type': iface_type} + full_iface_spec = config | extra_config + if not dry_run: + res = _proxmox_request('put' if update else 'post', f"/nodes/{node}/network", auth_info, full_iface_spec) + if res.ok: + return res.json()['data'] | extra_config + else: + res.raise_for_status() + else: + return full_iface_spec + + +def _delete_iface(auth_info: ProxmoxAuthInfo, node: str, name: str, dry_run: bool = False) -> dict[str, str]: + if not dry_run: + res = _proxmox_request('delete', f"/nodes/{node}/network/{name}", auth_info) + if res.ok: + return {} + else: + res.raise_for_status() + else: + return {} diff --git a/plugins/modules/node_interface.py b/plugins/modules/node_interface.py new file mode 100644 index 0000000..b8c8e6f --- /dev/null +++ b/plugins/modules/node_interface.py @@ -0,0 +1,170 @@ +#!/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_iface import * + +DOCUMENTATION = r''' +--- +module: node_interface +author: +- Johanna Dorothea Reichmann (transcaffeine@finally.coffee) +requirements: + - python >= 3.9 +short_description: Configures an authentication realm in a proxmox node +description: + - "Configue realm 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 + node: + description: Node to retrieve information about. + type: str + required: true + name: + description: Interface to retrieve information about. If left omitted, return all realms + type: str + required: false + config: + description: Configuration of the interface, see https://pve.proxmox.com/pve-docs/api-viewer/index.html#/nodes/{node}/network/{iface} for parameters + type: dict[str, str] + required: false + state: + description: If the realm should be present or absent + type: str + choices: [present, absent] + default: present + required: false +''' + +EXAMPLES = r''' +- name: Add and configure a new interface of type 'bond' on node 'hostname' + finallycoffee.proxmox.node_interface: + proxmox_instance: https://my.proxmox-node.local:8006 + promox_api_token_id: root@pam!token + proxmox_api_secret: supersecuretokencontent + node: hostname + name: my_bond_iface + type: bond + config: bond_mode: balance-rr + bond-primary: enp0s1 + config: + slaves: enp0s1 enp0s2 enp0s3 + +- name: Change CIDR range on interface 'vmbr0' on 'other_pm_host' + finallycoffee.proxmox.node_interface: + proxmox_instance: https://my.proxmox-node.local:8006 + promox_api_token_id: root@pam!token + proxmox_api_secret: supersecuretokencontent + node: other_pm_host + name: vmbr0 + type: bridge + config: + cidr: "1.2.3.4/8" +''' + +RETURN = r''' +interfaces: + description: The retrieved interfaces on the specified nodes + returned: When interfaces are present on a node + type: list + elements: 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=True, type='str'), + name=_(required_if=['state', 'present'], type='str'), + type=_(type='str', choices=PM_ACCEPTED_IFACE_TYPES), + config=_(required_if=['state', 'present'], type='dict'), + state=_(type='str', required=False, default='present', choices=['present', 'absent']) + ), + supports_check_mode=True + ) + + result = _( + changed=False, + diff={}, + message='' + ) + + realms = [] + try: + interfaces = set_interface_config(ProxmoxAuthInfo( + module.params['proxmox_instance'], + module.params['proxmox_api_token_id'], + module.params['proxmox_api_secret'], + module.params['proxmox_api_verify_cert'], + ), module.params['node'], module.params['name'], module.params['type'], module.params['config'], + module.check_mode, + module.params['state']) + except IOError as owie: + result['msg'] = owie + module.exit_json(**result) + + result['interfaces'] = interfaces + diff_rendered = _render_diff(interfaces[0], interfaces[1]) + result['diff'] = { + 'before_header': _render_diff_header(interfaces[0], module), + 'after_header': _render_diff_header(interfaces[1], module), + 'before': diff_rendered[0], + 'after': diff_rendered[1], + } + result['changed'] = interfaces[0] != interfaces[1] + + module.exit_json(**result) + + +def _render_diff_header(iface: dict[str, str], module: AnsibleModule) -> str: + return (f"{iface.get('name', module.params['name'])}@{iface.get('node', module.params['node'])}" + if iface else '') + + +def _render_diff(before: dict[str, str] | None, after: dict[str, str] | None) -> (str, str): + is_update: bool = (before and after) + after = after or {} + before = before or {} + diff_excluded_keys = {'name', 'digest', 'node', 'iface'} + + after_filtered = {k: v for k, v in after.items() if k not in diff_excluded_keys} + before_filtered = {k: v for k, v in before.items() if k not in diff_excluded_keys} + + before_clean = {k: v for k, v in before_filtered.items() if (k in after_filtered.keys() if is_update else True)} + + before_str = "\n".join([f"{k}: {v}" for k, v in sorted(before_clean.items(), key=lambda e: e[0])]) + after_str = "\n".join([f"{k}: {v}" for k, v in sorted(after_filtered.items(), key=lambda e: e[0])]) + return before_str + ("\n" if before_str else ''), after_str + ("\n" if after_str else '') + + +if __name__ == '__main__': + main() diff --git a/plugins/modules/node_interface_info.py b/plugins/modules/node_interface_info.py new file mode 100644 index 0000000..90d911e --- /dev/null +++ b/plugins/modules/node_interface_info.py @@ -0,0 +1,119 @@ +#!/usr/bin/python +# coding: utf-8 + +# (c) 2022, Johanna Dorothea Reichmann + +__metaclass__ = type + +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_iface import * + + +DOCUMENTATION = r''' +--- +module: node_interface_info +author: +- Johanna Dorothea Reichmann (transcaffeine@finally.coffee) +requirements: + - python >= 3.9 +short_description: Configures an authentication realm in a proxmox node +description: + - "Configue realm 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 + node: + description: Node to retrieve information about. + type: str + required: true + name: + description: Interface to retrieve information about. If left omitted, return all realms + type: str + required: false +''' + +EXAMPLES = r''' +- name: Retrieve all interfaces on node 'hostname' + finallycoffee.proxmox.node_interface_info: + proxmox_instance: https://my.proxmox-node.local:8006 + promox_api_token_id: root@pam!token + proxmox_api_secret: supersecuretokencontent + node: hostname + +- name: Retrieve interface information for 'vmbr0' on 'other_pm_host' + finallycoffee.proxmox.node_interface_info: + proxmox_instance: https://my.proxmox-node.local:8006 + promox_api_token_id: root@pam!token + proxmox_api_secret: supersecuretokencontent + node: other_pm_host + name: vmbr0 +''' + +RETURN = r''' +interfaces: + description: The retrieved interfaces on the specified nodes + returned: When interfaces are present on a node + type: list + elements: 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=True, type='str'), + name=_(required=False, type='str'), + ), + supports_check_mode=True + ) + + result = _( + changed=False, + diff={}, + message='' + ) + + realms = [] + try: + interfaces = get_interfaces(ProxmoxAuthInfo( + module.params['proxmox_instance'], + module.params['proxmox_api_token_id'], + module.params['proxmox_api_secret'], + module.params['proxmox_api_verify_cert'], + ), + module.params['node'], + ) + except IOError as owie: + result['msg'] = owie + module.exit_json(**result) + + result['interfaces'] = interfaces + + module.exit_json(**result) + + +if __name__ == '__main__': + main()